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 sehen, dass Rollschuhlaufen
und Eisessen verboten ist, 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 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 |
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))
{
...