Boolesche Ausdrücke in C++
Willemers Informatik-Ecke

»Was ist Wahrheit?«
Pontius Pilatus (Joh. 18, 38)

In der if-Abfrage und im Fragezeichen-Ausdruck wurden jeweils bereits Bedingungen verwendet. Bedingungen werden auch in den Schleifen verwendet, so dass es an der Zeit ist, sie näher zu betrachten.

Erbschaft

Das Ergebnis einer Bedingung kann zwei Zustände annehmen: Sie kann wahr oder falsch sein. Für den Zustand wahr verwendet C++ das Schlüsselwort true, und für falsch lautet es false. Die Sprache C kannte diese beiden Schlüsselwörter nicht, sondern interpretierte den Zahlenwert 0 als falsch und jeden anderen Zahlenwert als richtig. Dieses Verhalten hat C++ geerbt, so dass sowohl false also auch 0 für falsch steht.

Variablen und Konstanten

Typ bool

Da C++ eigene Konstanten für die Wahrheitswerte kennt, ist es auch nahe liegend, dass es einen eigenen Datentyp dafür gibt. Dieser trägt den Namen bool und kann die Werte true oder false annehmen. Obwohl für eine solche Variable theoretisch nur ein Bit benötigt wird, belegt sie in der Praxis mindestens ein Byte.
bool Kleiner = true;
Kleiner = a < b;
if (Kleiner)
{
   ....
Auch in C-Programmen findet man manchmal boolesche Typen und Konstanten. Im Windows-Bereich sind vor allem BOOL, TRUE und FALSE verbreitet. Dies sind allerdings keine Sprachelemente, sondern sie wurden mit Hilfe von Makros definiert. Dabei steckt hinter dem FALSE eine Null, hinter TRUE meist eine Eins und hinter BOOL ein char oder gar ein int.

Operatoren

Größer und kleiner

Die einfachste Art, eine Bedingung zu formulieren, ist es, zwei Zahlenwerte miteinander zu vergleichen. Um zu erfragen, ob die erste Zahl kleiner als die zweite ist, wird ein Kleiner-Zeichen (<) zwischen die Operanden gestellt. Als Eselsbrücke können Sie sich vorstellen, dass das Kleiner-Zeichen aus einem Gleichheitszeichen geformt wurde, indem links (in Leserichtung am Anfang) der Abstand der beiden Striche verkleinert wurde. Eine andere Eselsbrücke funktioniert über die deutsche Tastatur. Größer- und Kleiner-Zeichen liegen auf der gleichen Taste. Um das Größer-Zeichen zu bekommen, müssen Sie die Taste drücken, mit der Sie auch Großbuchstaben erzeugen.

if (a < b)
Die nachfolgende Anweisung wird nur ausgeführt, wenn der Inhalt der Variablen a kleiner als der Inhalt von b ist.

Durch Anhängen eines Gleichheitszeichens kann aus dem Kleiner-Zeichen ein Kleiner-gleich-Zeichen gemacht werden, das abfragt, ob der linke Wert »kleiner oder gleich« dem rechten Wert ist.

if (a <= b)
Um das Größer-gleich-Zeichen zu erzeugen, muss erst das Größer-Zeichen und dann das Gleichheitszeichen gesetzt werden. Zwischen beiden darf kein Leerzeichen stehen.

gleich

Um die Gleichheit zwischen zwei Werten abzufragen, verwendet C++ zwei direkt aufeinander folgende Gleichheitszeichen. Das ist erforderlich, um es von dem Zuweisungszeichen zu unterscheiden.
if (a == b)
Die nachfolgende Anweisung wird nur ausgeführt, wenn der Inhalt der Variablen a und der Inhalt von b gleich ist. Leider lauert hier eine böse Falle. Sehen Sie sich den folgenden Code an:
if (a=1) // Vorsicht, Falle!
{
    a=5;
}
Auf den ersten Blick sieht es so aus, als würde der Variablen a genau dann eine 5 zugewiesen werden, wenn der Inhalt der Variablen a gleich 1 wäre. Tatsächlich handelt es sich bei a=1 aber um eine Zuweisung. Die Variable a enthält also nach dem Durchlaufen der if-Anweisung den Wert 1. Das war vermutlich nicht im Sinne des Programmierers. Darüber hinaus ist das Ergebnis des Klammerinhalts ebenfalls 1, weil ja der Zuweisungswert immer nach links weitergereicht wird. Da aber eine Zahl ungleich 0 immer als wahr interpretiert wird, ist die Bedingung immer wahr, und die Zuweisung a=5 wird in jedem Fall ausgeführt. Korrekt muss es also heißen:
if (a==1)
{
    a=5;
}
Aufgrund der besonderen Heimtücke dieser Situation reagieren die meisten Compiler mit einer Warnung, wenn in der Bedingung eine Zuweisung steht. Ein Fehler ist es nicht, denn auf Grund seines C-Erbes ist es in C++ durchaus zulässig, Zahlenwerte auch als boolesche Werte zu verwenden. Manche Programmierer haben es sich zur Gewohnheit gemacht, bei Abfragen die Konstante zuerst zu nennen, denn a==1 ist das Gleiche wie 1==a. Wenn man allerdings doch einmal das zweite Gleichheitszeichen vergisst, ergibt 1=a sofort einen Compiler-Fehler. Auf der anderen Seite kann man davon ausgehen, dass ein Programmierer, der daran denkt, die Abfrage in dieser Form aufzuschreiben, auch daran denkt, ein zweites Gleichheitszeichen zu setzen.

Ungleich

Das Zeichen für Ungleichheit ist ein Ausrufezeichen, gefolgt von einem Gleichheitszeichen. Das Ausrufezeichen stellt das Zeichen für die Verneinung dar.

Vergleich numerischer Werte

Operator Bedeutung
a == b a gleich b?
a != b a ungleich b?
a > b a größer als b?
a >= b a größer oder gleich b?
a < b a kleiner als b?
a <= b a kleiner oder gleich b?

Sie können immer nur zwei Operanden miteinander vergleichen. Wenn Sie prüfen wollen, ob der Wert der Variable a zwischen 5 und 10 liegt, muss dies in zwei Vergleichen stattfinden. Die folgende Abfrage wird vom Compiler zwar zugelassen, ergibt aber eine völlig andere Interpretation:

if (5<=a<=10) // funktioniert nicht!
{
    // Tu irgendetwas;
}
Zunächst wird der Compiler den Ausdruck 5<=a auswerten. (Für die Experten: Die Vergleichsoperatoren sind linksassoziativ.) Dies kann wahr oder falsch sein, ergibt also 0 oder 1. Dieser Wert wird nun mit <=10 weiterverknüpft. Da 0 und 1 kleiner als 10 sind, wird der obere Ausdruck als Ganzes immer wahr sein. Korrekt muss verglichen werden, ob a größer oder gleich 5 ist und ob a kleiner oder 10 ist.
if (5<=a)
{
    if (a<=10)
    {
        // Tu irgendetwas;
    }
}
Die Ausdrücke sind nun so verknüpft, dass beide Ausdrücke zutreffen müssen, damit der innere Block erreicht wird.

Verknüpfung von booleschen Ausdrücken

UND

Wenn zwei Bedingungen zutreffen müssen, damit die Gesamtbedingung wahr ist, dann spricht man von einer UND-Verknüpfung. Beispiel: Wenn ein Wert innerhalb eines Intervalls liegen soll, muss er zwei Bedingungen erfüllen: Der Wert muss mindestens das Minimum erreichen und darf das Maximum nicht übersteigen. Beide Teilbedingungen müssen erfüllt sein, damit die Gesamtbedingung wahr ist. Diese Verknüpfung heißt UND und wird in C++ mit zwei kaufmännischen Und-Zeichen dargestellt.
if (5<=a && a<=10)
{
    // Tu irgendetwas;
}
Ein anderes Beispiel: Eine Firma zahlt vielleicht nur dann Weihnachtsgeld aus, wenn der Mitarbeiter fest angestellt und bereits seit einem halben Jahr in der Firma ist. Auch hier müssen beide Bedingungen erfüllt sein. Es handelt sich also auch um eine UND-Verknüpfung. Die folgende Wahrheitstabelle zeigt alle denkbaren Kombinationen der beiden Operanden und welches Ergebnis die UND-Verknüpfung liefert:

a b a && b
true true true
true false false
false true false
false false false

ODER

Eine andere Sache ist es, wenn Sie prüfen wollen, ob ein Wert außerhalb eines Intervalls liegt. Der Wert kann schlecht sowohl größer als das Maximum als auch kleiner als das Minimum sein. Hier ist die UND-Verknüpfung Unsinn. Es wird ausreichen, dass eine von beiden Bedingungen zutrifft. Ist der Wert kleiner als das Minimum des Intervalls, liegt er bereits außerhalb. Auch die Teilbedingung, dass der Wert über dem Maximum liegt, reicht aus, damit der Wert außerhalb des Intervalls ist. Eine Verknüpfung, die wahr ist, wenn eine der Teilbedingungen wahr ist, heißt ODER-Verknüpfung und wird durch zwei senkrechte Striche dargestellt. (In den meisten Zeichensätzen haben diese senkrechten Striche etwa in der Mitte eine Lücke.)
if (5>a || a>10)
{
    // Tu irgendetwas;
}
Zu unserem anderen Beispiel: Eine etwas großzügigere Firma könnte der Regelung folgen, dass sowohl Angestellte ab dem ersten Tag ihrer Betriebszugehörigkeit als auch Aushilfen, die schon seit mehr als zwei Jahren dabei sind, Weihnachtsgeld bekommen. In diesem Fall würde bereits das Erfüllen einer Teilbedingung ausreichen, um die Gesamtbedingung zu erfüllen. Hier wird die ODER-Verknüpfung eingesetzt.

Wahrheitstabelle ODER

a b a || b
true true true
true false true
false true true
false false false

NOT

Der dritte logische Operator negiert einen booleschen Wert. Das NOT oder NICHT wirkt auf einen Operanden und wird in C++ durch das Ausrufezeichen dargestellt. Ist der Ausdruck, vor dem das Ausrufezeichen steht, wahr, so ist der Gesamtausdruck falsch.

Wahrheitstabelle NICHT

a !a
true false
false true

Sprachliche Ungenauigkeit

Achten Sie genau auf die Definition der beiden Verknüpfungen UND und ODER. UND ist erfüllt, wenn alle Teilbedingungen wahr sind. ODER ist erfüllt, wenn mindestens eine der Teilbedingungen erfüllt ist. Die sprachliche Verwendung ist da oft sehr viel schlampiger. Wenn Sie beispielsweise in einem Museum auf einem Schild steht "Rollschuhlaufen und Eisessen verboten", dürften Sie aus Sicht der booleschen Algebra durchaus Rollschuh laufen. Sie dürften auch Eis essen. Sie dürften nur nicht beides tun. Allerdings werden Sie damit vermutlich keinen der Aufseher überzeugen können. Rechnen Sie lieber damit, dass deren mathematischen Kenntnisse nicht so ausgeprägt sind, ihnen aber dafür die Angst um die Ausstellungsstücke im Nacken sitzt. Im umgangssprachlichen Bereich wird auch schnell die ODER-Verknüpfung zu einem Entweder-Oder verschärft. Der Unterschied liegt darin, dass ODER wahr ist, wenn mindestens ein Teilausdruck wahr ist. Entweder-Oder sagt dagegen aus, dass genau ein Teilausdruck wahr ist.

XOR

C++ stellt keinen eigenen booleschen Operator für die Entweder-Oder-Verknüpfung zur Verfügung. Eine solche Verknüpfung wird oft auch XOR genannt. Natürlich ist es möglich, mit Hilfe von UND und ODER diesen Operator nachzubilden. Dazu erstellen Sie zunächst eine Tabelle, in der sie festhalten, wann die Verknüpfung wahr und wann sie falsch wird:

Wahrheitstabelle Entweder Oder

a b a XOR b
true true false
true false true
false true true
false false false

Es gibt zwei Ideen, wie Sie die Lösung finden können. Die erste Idee besteht darin, alle Zeilen die wahr sind nachzubilden und diese Nachbildungen mit ODER zu verknüpfen. Im Beispiel sind das die beiden mittleren Zeilen. Die erste Zeile lautet »a UND NICHT b«, da a wahr und b falsch ist. Die zweite Zeile ist »NICHT a UND b«. Zuletzt werden die beiden Ausdrücke durch ODER verbunden. Zur Kontrolle können Sie eine Wahrheitstabelle erstellen:

Wahrheitstabelle (a UND NICHT b) ODER (NICHT a UND b)

a b a && !b !a && b (a && !b) || (!a && b)
true true false false false
true false true false true
false true false true true
false false false false false

Die zweite Idee versucht, alle wahren Zeilen zusammenzufassen. Werden dabei ein paar falsche Zeilen eingeschlossen, muss man sie halt wieder ausschließen. Im Beispiel würde »a ODER b« die oberen drei Zeilen umfassen. Nur die erste Zeile passt nicht auf »a XOR b«, also schließt man sie mit »UND NICHT« aus. Der Ausdruck lautet dann: »(a OR b) UND NICHT (a UND b)«. Auch hier beweist die Wahrheitstabelle, ob der Ausdruck stimmt:

Wahrheitstabelle (a OR b) UND NICHT (a UND b)

a b a || b a && b (a || b) && !(a && b)
true true true true false
true false true false true
false true true false true
false false false false false

Und dann gibt es noch praktische Ansätze, einen XOR-Operator zu simulieren. Als boolescher Operator bietet sich der Ungleich-Operator != an. Alternativ kann der binäre Operator ^ verwendet werden. Da die booleschen Ausdrücke mit Zahlen kompatibel ist, funktioniert dieser auch wie ein boolescher Operator bei der Verknüpfung boolescher Ausdrücke.

De Morgan

Ausdrücke negieren

Bei der Negation verknüpfter Ausdrücke können einige Überraschungen auftreten. Es gelten nämlich nach dem Gesetz von De Morgan folgende Regeln:
! ( a && b ) ist äquivalent zu !a || !b 
! ( a || b ) ist äquivalent zu !a && !b
Als Beispiel soll eine Abfrage negiert werden, die prüft, ob ein Wert innerhalb eines Intervalls liegt. Das Ergebnis der Negation wäre also eine Abfrage, ob der Wert außerhalb des Intervalls liegt. Da Sie beide Abfragen schon gesehen haben, können Sie den Schritt der Negation an diesem Beispiel leicht prüfen.
if (a>=2 && a<=5)
Die einfachste Form, die Negation zu bilden, besteht darin, eine Klammer um den Gesamtausdruck zu legen und ihren Inhalt durch das Ausrufezeichen zu negieren.
if (!(a>=2 && a<=5))
Das mag unsportlich wirken, ist aber ohne Frage korrekt. Darum sollten Sie lieber eine solche, wenig elegante Lösung verwenden als eine elegante, die den Schönheitsfehler hat, falsch zu sein. Falsch wäre es beispielsweise, einfach nur die Vergleichsoperatoren umzukehren:
if (a<2 && a>5)) // schnell, aber falsch!
Hier würde geprüft, ob die Variable a sowohl kleiner als 2 als auch größer als 5 ist. Naheliegenderweise kann ein Wert nicht beides sein. Wenn Sie dagegen die Regel von De Morgan anwenden, werden die Teilausdrücke negiert, und UND wird durch ODER ersetzt:
if (!(a>=2) || !(a<=5))
Nun können die Teilausdrücke vereinfacht werden: !(a>=2) ist leicht in a<2 zu überführen. Genauso ist !(a<=5) äquivalent zu a>5. Also lautet die korrekt umgesetzte Bedingung:
if (a<2 || a>5)) // richtige Negation!
Die Variable a liegt außerhalb des Intervalls, wenn sie kleiner als 2 oder größer als 5 ist. Wenn eine der Aussagen wahr ist, ist der Gesamtausdruck wahr. Liegt a doch im Intervall, sind beide Teilausdrücke falsch, und damit ist auch der Gesamtausdruck falsch. Daraus ergibt sich, dass eine UND-Verknüpfung negiert wird, indem jeder Teilausdruck negiert wird und das UND durch ein ODER ersetzt wird. Das Gleiche gilt umgekehrt: Um eine ODER-Verknüpfung zu negieren, werden die Teilausdrücke negiert und das ODER durch ein UND ersetzt.

Kurzschluss

Einer für alle

Das Programm muss bei der Abarbeitung von verknüpften Bedingungen nicht in jedem Fall alle Teilbedingungen auswerten. Scheitert zum Beispiel bei einer UND-Verknüpfung bereits die erste Teilbedingung, so ist es nicht erforderlich, die anderen Teilbedingungen zu prüfen, denn der Gesamtausdruck kann nur noch falsch sein. Das Gleiche gilt, wenn sich bei einer ODER-Verknüpfung bereits der erste Teilausdruck als wahr erweist. Dann brauchen die anderen Teilbedingungen nicht mehr geprüft werden. Der Gesamtausdruck muss wahr sein.

Diese Erkenntnis wird in C++ effizienzsteigernd umgesetzt. Das Programm wird in einer solchen Situation die weitere Analyse abbrechen und sofort seine Schlüsse ziehen. In den allermeisten Fällen wird dieses Verhalten Sie kaum berühren. Den geringfügigen Geschwindigkeitsgewinn werden Sie vermutlich nicht einmal bemerken. Schwieriger wird es jedoch, wenn der zweite Teilausdruck neben der Wahrheitsfindung noch etwas anderes tut.

if ((Gehalt<2000) && (PersonalNr++>10))
{
    ...
Normalerweise wird nach dem Durchlaufen dieser Abfrage die Variable PersonalNr um eins erhöht. Tatsächlich ist dies aber keineswegs der Fall, wenn das Gehalt kleiner als 2000 ist. Sollte also beabsichtigt sein, dass auch bei kleinen Gehältern die Variable PersonalNr inkrementiert wird, muss die Reihenfolge der Bedingungen umgedreht werden.
if ((PersonalNr++>10) && (Gehalt<2000))
{
    ...
Wesentlich sinnvoller ist es allerdings, solche Situationen grundsätzlich zu vermeiden. Innerhalb einer Bedingung sollten Abfragen stehen und deren Verknüpfungen stehen. Arithmetische und andere Operationen sollten Sie sicherheitshalber vor oder hinter die Bedingung setzen.
PersonalNr++;
if ((PersonalNr>10) && (Gehalt<2000))
{
    ...