Software Enginering

Programmier-Fallstricke

Ein großer Teil der Programmier­fehler geht darauf zurück, dass Programmierer die Semantik der verwendeten Programmier­sprache nicht genau beachten. Dies ist einerseits Schuld der Programmierer, andererseits vielfach aber auch Schuld der Entwickler der Programmier­sprache, die Miss­verständnisse sozusagen "vor­programmiert" haben.

Die folgenden Beispiele ver­deut­lichen dies.

Die Zahl 231

Probieren Sie aus, welchen Wert folgendes Java-Programm ausgibt:

public class Test
{
    public static void main(String[] args)
    {
        System.out.println(023l);
    }
}

Die Zahl 231 wird ausgegeben. So sollte man denken. Tatsächlich jedoch ist das Ergebnis 19. Der Computer hat nichts falsch gemacht, sondern einerseits der Programmierer, der statt einer 1 versehentlich ein kleines "L" geschrieben hat. Angehängt an die Zahl 023 wird es als Typ­kennzeichner für den Typ long aufgefasst. Und andererseits die Entwickler der Programmier­sprache, die entschieden haben, dass ein kleines "L" zulässig ist und, schlimmer noch, dass eine führende 0 anzeigt, dass die Zahl als Oktalzahl, also als Zahl zur Basis 8 (hier 2·8 + 3·1 = 19), zu inter­pretieren ist.1)

Von der voran­gestellten 0 abgeleitet ist wohl auch die sonderbare Kenn­zeichnung für hexadezimal dargestellte Zahlen­kostanten durch "0x", also "0 extended".

Python

Tatsächlich hat sich die voran­gestellte 0 sogar noch in Python bis zu den Versionen 2.x hinübergeschleppt. Erst in Python 3 hat man das Naheliegende umgesetzt, nämlich Oktalzahlen durch Voranstellen von "0o" zu kennzeichnen, analog zu "0x" für Hexa­dezimal­zahlen. Leider aber ist auch "0O" zulässig, mit dem Groß­buchstaben O. Was wird in Python 3 ausgegeben?

print(0O00023)

Richtig. 19.

Die Zeichen +, -, * und /

Die meisten Programmierer gehen davon aus, dass die Zeichen +, -, * und / die vier Grund­rechenarten Addition, Subtraktion, Multi­plikation und Division bedeuten. Natürlich ist dies meistens richtig, manchmal aber auch falsch. In C++ und Java bedeutet etwa das Zeichen + bei int-Werten keine normale Addition, sondern eine Addition modulo 232. Das Ent­sprechende gilt für - und *.

 

Niemand erwartet, dass die Multi­plikation der beiden positiven Zahlen 46000 und 47000 eine negative Zahl ergibt. Es gibt keine Fehler­meldung "Überlauf des Zahlen­bereichs", sondern es wird modulo 232 gerechnet, das Ergebnis ist -2132967296.

public class TestMult
{
    public static void main(String[] args)
    {
        long a=46000*47000;
        System.out.println(a);
    }
}

Die Zahlen 46000 und 47000 werden in Java als int-Werte aufgefasst; das Ergebnis der Multi­plikation ist dementsprechend wieder vom Typ int. Weil aber das Ergebnis größer als 231-1 ist, wird es als negative Zahl inter­pretiert. Erst danach wird es in den Typ long der Variablen a umgewandelt.

 

Das Zeichen / bedeutet bei int-Werten keine normale Division, sondern eine ganzzahlige Division mit Rest. In folgendem Programm soll die double-Variable a den Wert 0.5 erhalten, aber das Ergebnis ist 0.0.

public class TestDiv
{
    public static void main(String[] args)
    {
        double a=1/2;
        System.out.println(a);
    }
}

Die Zahlen 1 und 2 sind vom Typ int und somit auch das Ergebnis des Ausdrucks 1/2. Die ganze Zahl 1 wird ganzzahlig durch 2 geteilt; das Ergebnis ist 0 Rest 1. Das Ergebnis 0 wird in die double-Zahl 0.0 umgewandelt.

 

Das +-Zeichen wird in Java sowohl für die Addition von Zahlen als auch für die String-Verkettung verwendet. Java wertet die unten­stehenden Ausdrücke von links nach rechts aus. Wenn einer der Operanden ein String ist, wird der andere Operand in einen String umgewandelt und das +-Zeichen als Verkettungsoperator inter­pretiert.

public class TestPlus
{
    public static void main(String[] args)
    {
        int a=3, b=4;
        System.out.println("Ergebnis: "+a+b);     // Ergebnis: 34
        System.out.println(a+b+" kommt raus");    // 7 kommt raus
    }
}

Semikolon

Das folgende Java-Programm soll die ersten zehn Quadrat­zahlen auf dem Bildschirm ausgeben, probieren Sie es einmal aus:

public class TestQuadrate
{
    public static void main(String[] args)
    {
        int i=0;
        while (i<10);
        {
            i=i+1;
            System.out.println(i*i);
        }
    }
}

Das Programm gerät in eine Endlos­schleife. Ursache ist das kleine unscheinbare Semikolon hinter der While-Bedingung. Da in Java fast alle Zeilen mit einem Semikolon enden, fällt es nicht weiter auf.

Ein Semikolon schließt aber nicht eine Zeile ab, sondern eine Anweisung. Hier wird eine leere Anweisung durch das Semikolon abge­schlossen. Der Computer denkt also, er solle solange wie i < 10 gilt die leere Anweisung ausführen.

In der Programm-Entwicklungs­umgebung Eclipse ist es möglich, das Auf­treten einer leeren Anweisung durch eine Warnung zu kennzeichnen. Leider ist dies aber nicht standard­mäßig so eingestellt.

Rundung

Bei Rechnungen mit dem Datentyp double ist es wichtig zu berück­sichtigen, dass es zu Rundungs­fehlern kommen kann. Insbesondere ist ein Vergleich if (x==0) äußerst problematisch, denn wenn x als Ergebnis einer Berechnung zustande gekommen ist, so ist es meist aufgrund von Rundungs­fehlern keineswegs gewähr­leistet, dass x exakt gleich Null ist, auch wenn es dies eigentlich sein sollte. So kann x zum Beispiel gleich 1.1E-16 = 0.00000000000000011 sein, also praktisch gleich Null, aber nicht exakt gleich Null.

Nie sollte man also so programmieren:

public class TestRundung
{
    public static void main(String[] args)
    {
        double a, b;
        a=2;
        b=0.4;
        while (a!=0)
            a=a-b;
    }
}

Das Programm­stück sieht harmlos aus: b wird von a fünfmal subtrahiert, dann ist der Wert 0 erreicht und die Schleife sollte abbrechen. In Wirklichkeit handelt es sich um eine Endlos­schleife, denn a wird nicht exakt gleich Null. Das Programm bleibt also in dieser Schleife hängen und ist damit "abgestürzt".

Daher dürfen double-Werte niemals mit den Vergleichs­operatoren == und != miteinander verglichen werden.

Mit b = 0.5 funktioniert das obige Programm aber doch. Warum?

Gleichheitszeichen

In C++ und Java ist das Zeichen = das Wert­zuweisungszeichen. Für einen Vergleich zweier Werte wird das doppelte Gleichheits­zeichen == verwendet. Probieren Sie folgendes C++-Programm aus:

void main()
{
    int x=1;
    while (x=1)
        x=x-1;
    cout<<x<<endl;
}

Das Programm gerät in eine Endlos­schleife. Ursache ist eine ganze Kette von Fehlern, von denen nur einer vom Programmierer gemacht worden ist; die anderen Fehler liegen in der Programmier­sprache C++ selbst begründet:

In der While-Bedingung wird der Variablen x der Wert 1 zugewiesen; das Ergebnis dieses Ausdrucks ist die Zahl 1; diese wird als Wahrheits­wert true inter­pretiert; der Schleifen­körper wird ausgeführt. Zwar hat danach x den Wert 0, aber beim erneuten Prüfen der While-Bedingung wird x wieder auf 1 gesetzt und die Schleife wird noch einmal ausgeführt usw.

In Java sind als Bedingungen in If-Anweisungen und While-Schleifen nur Ausdrücke erlaubt, die einen Wahrheits­wert (Typ boolean) ergeben; der Fehler wird hier also erkannt.

Initialisierung von Variablen

Das folgende C++-Programm soll die Zahlen von 1 bis 10 zusammenzählen. Das Ergebnis ist -858993405. Es wurde vergessen, die Variable s mit 0 zu initialisieren.

void main()
{
    int i, s;
    for (i=1; i<=10; i++)
        s=s+i;
    cout<<s<<endl;
}

Array-Grenzen

In C++ wird nicht überprüft, ob die Grenzen eines Arrays über­schritten werden:

void main()
{
    int* a=new int[2];
    a[0]=1;
    a[1]=1;
    int i, s=0;
    for (i=0; i<=2; i++)
        s=s+a[i];
    cout<<s<<endl;
}

Das Programm gibt den Wert -33686017 aus, weil auf das nicht vorhandene Array-Element a[2] zugegriffen wird.

Seiteneffekte

Eine Funktion soll normaler­weise aus den Argumenten einen Wert berechnen und nichts anderes nebenbei tun. Von diesem Prinzip weichen Programmierer jedoch immer wieder ab, etwa indem sie in der Funktion globale Variablen verändern. Dies wird dann als Seiteneffekt der Funktion bezeichnet.

Die folgenden beiden C++-Funktionen s1() und s2() geben als Seiteneffekt die Strings "1" bzw. "2" aus. Aufgerufen werden die Funktionen in dem Ausdruck s1()+s2() in der Funktion main(). Merk­würdiger­weise wird dieser Ausdruck in C++ nicht von links nach rechts ausgewertet, sondern von rechts nach links, sodass zuerst der Seiteneffekt von s2() auf­tritt und dann der von s1().

In Java wird der Ausdruck korrekt von links nach rechts ausgewertet.

#include <iostream>
#include <string>
using namespace std;

string s1()
{
    cout<<1;
    return "1";
}

string s2()
{
    cout<<2;
    return "2";
}

void main()
{
    string y=s1()+s2();
    cout<<endl<<y<<endl;
}

 


1)  Wahrscheinlich wurde dies schon beim Entwurf einer Vorläufer­sprache von C festgelegt, denn Oktalzahlen wurden nur in der Frühzeit der Daten­verarbeitung benutzt. Man kann sich förmlich vorstellen, wie es dazu gekommen ist. Irgendjemand wollte unbedingt oktal dargestellte Zahlen­konstanten haben. Ein voran­gestellter Buchstabe zur Kenn­zeichnung der oktalen Darstellung, vielleicht ein kleines "o", wäre am deutlichsten gewesen, aber dann wäre z.B. o23 als Variablen­name o23 zu inter­pretieren gewesen. Ein nach­gestelltes Zeichen ging auch nicht, da der Compiler die Zahl schon als Dezimalzahl ausgewertet hat, wenn er auf das nach­gestellte Zeichen trifft. Die Sonder­zeichen waren schon alle für Operatoren verbraucht, also blieb nur eine Ziffer. Und so kam es zu der "Vereinbarung" (der ich als Programmierer nie zugestimmt hätte), dass eine führende 0, die ja an ein "O" wie oktal erinnert, die Inter­pretation der Zahl als Oktalzahl anzeigt.

 

Weiter mit:   [up]

 


H.W. Lang   mail@hwlang.de   Impressum   Datenschutz
Created: 06.04.2005   Updated: 18.02.2023
Diese Webseiten sind während meiner Lehrtätigkeit an der Hochschule Flensburg entstanden