Axel Rogat
Objektorientiertes Programmieren mit C++ und JAVA
 
4: Ausdrücke Kapitel 4 4.12: Typumwandlungen 
 
  Ausdrücke 4.1 - 4.11  
 

4.1 Lvalues

Für die genaue Beschreibung der Wirkungsweise einiger Operatoren brauchen wir den Begriff Lvalue (L-Wert). Ein Lvalue ist ein Ausdruck, der ein Objekt bezeichnet und dadurch mit einer bestimmten Speicheradresse verbunden ist.

Literalkonstanten als Ausdrücke (2) sind damit keine Lvalues, der Ausdruck 2+a auch nicht (er könnte höchstens ein temporäres Objekt bezeichnen). Variablen und Konstanten sind dagegen Lvalues, genauso wie referenzierte Pointer *P oder P[2].

Ist ein Lvalue nicht konstant (durch eine const-Deklaration), heißt er "modifiable lvalue" (modifizierbarer L-Wert).

Von den Objekten, die durch Lvalues (modifizierbar oder nicht) bezeichnet werden, kann man mit dem Adreßoperator die Adresse bestimmen: &2 geht nicht, &pow(x,y) auch nicht, &sin schon.

Der Name stammt aus einer frühen C-Grammatik, in der ein Lvalue alles bezeichnete, was auf der linken Seite einer Zuweisung stehen konnte. Damit kann man sich bei der Bestimmung " modifiable Lvalue oder nicht?" immer noch helfen: 2=a; ist nicht möglich, "axl"[0]='A' schon (auch wenn es nicht besonders viel Sinn macht und zu einem Segmentierungsfehler führt).

4.2 Grundrechenarten

Die Rechnungsweise der Operatoren + - * / sollte offensichtlich sein. Sie sind für ganzzahlige und Gleitkomma-Operanden definiert. Bei gemischter Verwendung wird der ganzzahlige Operand zuerst ins Gleitkommaformat konvertiert (genauer später). Bei zwei ganzzahligen Operanden ist auch das Ergebnis ganzzahlig, also 5/4=1, 5.0/4=1.25.

+ und - haben außerdem in Verbindung mit Pointern als Operanden spezielle Bedeutungen, dazu im entsprechenden Abschnitt.

Der Operator % steht für Modulo (den ganzzahligen Rest bei Division). Er ist nur auf ganze Zahlen anwendbar. Sein Verhalten bei negativen Operanden ist implementationsabhängig und manchmal etwas ungewöhnlich:
7%3=1,   (-7)%3=(-7)%(-3)=-1 (meistens),   7%(-3)=1 (meistens).

4.3 Vergleichsoperatoren

Die Operatoren == und != stehen für gleich und ungleich und können beliebige Objekte miteinander vergleichen. Sie liefern Werte vom Typ bool oder, wenn der Zusammenhang ganze Zahlen verlangt, int, nämlich 1 statt true und 0 statt false.

Die Operatoren < <= > >= verhalten sich bei den Werten genauso, sind aber nur auf ganze Zahlen, Gleitkommazahlen und Pointer anwendbar. Bei Pointern werden die Adressen aus der internen Darstellung miteinander verglichen.

Achtung: Die mathematische Schreibweise a<b<c wird nicht als a<b && b<c, sondern (wegen der Linksassoziativität) als (a<b)<c interpretiert.

Dadurch daß == und != eine niedrigere Priorität haben als die anderen vier Operatoren, können deren Ergebnisse miteinander verglichen werden:

if ( a<b == c<d ) ...
Das ist dann eine Abkürzung für
if ( (a<b && c<d) || (a>=b && c>=d) ) ...

4.4 Bedingte Ausdrücke

Man hat sehr oft Anweisungskombinationen der folgenden Art:
if (condition) a=expression_1; else a=expression_2;
Zum Zweck, dies abzukürzen, gibt es in C++ den bedingten Ausdruck, der mit dem ternären (dreistelligen) Konditional-Operator ?: realisiert wird. Die Kombination von oben ist damit äquivalent zu
a = condition ? expression_1 : expression_2;
Bedingte Ausdrücke sind natürlich nicht nur direkt in Zuweisungen anwendbar, sondern können in größere Ausdrücke eingebaut oder als Funktionsparameter verwendet werden:
PlotPoint( x, x==0.0 ? 0.0 : x*sin(1.0/x) );
Der Operator ?: ist rechtsassoziativ. Das bedeutet, daß ein :-Teil dem letzten noch freien ? zugeordnet wird. Damit verhalten sich verschachtelte bedingte Ausdrücke wie verschachtelte if-else-Anweisungen.

4.5 Zuweisungsoperatoren

Zusätzlich zum normalen Zuweisungsoperator = gibt es für alle arithmetischen und Bit-Operatoren einen kombinierten Berechnungs- und Zuweisungsoperator (vorletzte Gruppe in der Tabelle).

Dabei ist a+=b äquivalent zu a=a+b, etc. Zum einen wird so der Optimierungsstufe des Compilers teilweise die Arbeit abgenommen, mehrfach vorkommende Teilausdrücke zu erkennen. Vor allem ist -- wenn man sich einmal daran gewöhnt hat -- diese Schreibweise suggestiver und trägt oft zum Verständnis der Semantik der Berechnung bei.

Besonders deutlich wird das bei komplexeren Konstruktionen auf der linken Seite, etwa

a[23*i+j][k+l]+=3
Der linke Operand bei Zuweisungsoperatoren muß ein Lvalue sein. Er bezeichnet das Objekt, das durch die Operation einen neuen Wert erhält. (Es werden noch einige weitere Zuweisungsoperatoren folgen, bei denen wir das nicht explizit erwähnen werden.)

Eine Zuweisung ist in C++ keine Anweisung, sondern wird wieder als Ausdruck aufgefaßt, der als Teilausdruck in einen größeren eingebaut werden kann. Der Wert dieses Ausdrucks ist der Wert, der zugewiesen wurde:

a=2+(b+=3+c);
Zunächst wird der Ausdruck 3+c ausgewertet und sein Wert auf b aufaddiert. Auf den sich ergebenden Wert wird noch 2 addiert und das Ergebnis a zugewiesen. Erst durch das abschließende Semikolon wird aus dem ganzen eine Anweisung.

Man sagt, daß Ausdrücke, die solche Zuweisungen enthalten, Seiteneffekte haben. Aus Gründen der Lesbarkeit sollte man sie nur sehr eingeschränkt verwenden.

Man beachte, daß die Zuweisungsoperatoren (im Gegensatz zu den arithmetischen Operatoren) sinnvollerweise rechtsassoziativ sind:

a+b-c entspricht (a+b)-c, aber a+=b+=c entspricht a+=(b+=c).

4.6 Inkrement- und Dekrement-Operatoren

Bei den Operatoren ++ und -- handelt es sich um spezielle Zuweisungsoperatoren. Es gibt sie jeweils in einer Präfix- und einer Postfix-Version. Alleinstehend entsprechen beide Versionen +=1 bzw. -=1:

++a; und a++; entsprechen beide a+=1;

Die Operatoren sind auf ganzzahlige und Pointer-Typen anwendbar und sehr gebräuchlich in Zählschleifen (int) und Arbeiten in Arrays (Pointer). Der Operand muß natürlich ein Lvalue sein.

Werden Ausdrücke mit diesen Operatoren als Teilausdrücke in größere eingebaut, kommt es auf die Stellung des Operators vor oder nach dem Operanden dagegen sehr wohl an.

4.7 Shift-Operatoren

Die Operatoren << und >> verschieben den ersten Operanden um soviele Binärstellen nach links bzw. rechts, wie der zweite Operand angibt. Beide Operanden sind ganzzahlig.

Bei Verschieben nach links werden von rechts immer binäre Nullen eingefügt:

unsigned char a=50; a=(00110010)2=50, a<<2=(11001000)2=200
Deshalb entspricht offensichtlich a<<b der Multiplikation a·2b (bis auf Überlauf).

Bei der Verschiebung nach rechts (also zu den niederwertigen Bits hin) kommt es auf den Typ des ersten Operanden an: Bei signed-Typen wird von links das Vorzeichen-Bit eingeschoben, bei unsigned-Typen Nullen:

unsigned char a=50; a = (00110010)2 = 50 a>>2 = (00001100)2 = 12
unsigned char a=-50; a = (11001110)2 = 206 a>>2 = (00110011)2 = 51
char a=50; a = (00110010)2 = 50 a>>2 = (00001100)2 = 12
char a=-50; a = (11001110)2 = -50 a>>2 = (11110011)2 = -13

Damit entspricht das Rechts-Schieben einer Division durch die entsprechende Zweierpotenz, allerdings immer mit Rundung nach -unendlich.

Die Shift-Operatoren gibt es auch mit einer Zuweisung kombiniert: >>= und <<=.

Beispiel: Für Integerzahlen kann der Zweierlogarithmus wie folgt bestimmt werden:

int n,l=0; cin >> n; while ((n>>=1)>0) ++l; cout << l;
Beispiel: Eine Integerzahl kann man auf die untenstehende Weise im Dualsystem ausgeben. Dabei wird ausgenutzt, daß in der internen Darstellung (im Zweierkomplement) das oberste Bit genau dann gesetzt ist, wenn die Zahl negativ ist.

int n; cin >> n; for ( int i=8*sizeof(int) ; i>0 ; --i , n<<=1 ) cout << ( n<0?'1':'0' );
Die Operatoren sind überladen beispielsweise für Ein-/Ausgabe-Streams (wie wir auch in diesem Beispiel sehen). Dort haben sie natürlich eine andere Bedeutung.

4.8 Bitweise Operatoren

Mit den binären Operatoren & | ^ können ganzzahlige Objekte bitweise mit den logischen Operationen AND, OR, XOR (exklusiv-ODER) verknüpft werden. Der unäre Operator ~ steht für die bitweise Inversion einer ganzen Zahl, also die Bildung ihres Einer-Komplements.

Es folgen die Wahrheitstabellen der vier Operationen für je ein Bit:

a ~a
0 1
1 0
a b a&b
0 0 0
0 1 0
1 0 0
1 1 1
a b a|b
0 0 0
0 1 1
1 0 1
1 1 1
a b a^b
0 0 0
0 1 1
1 0 1
1 1 0

Auch für die (binären) bitweisen Operatoren gibt es kombinierte Zuweisungsoperatoren: &= |= ^=

Beispiel: Als Ersatz für gepackte Felder von Wahrheitswerten wird gerne mit Integer-Zahlen gearbeitet, in deren Darstellung gerade genau ein Bit gesetzt ist.

Das Verhalten eines Edit-Felds in Windows® wird mit einem long-Wort gesteuert, dessen einzelne Bits ("Flags") jeweils eine Steuerfunktion haben ("ES" steht für "Edit Style"):

enum { ES_LEFT=0, ES_CENTER=1, ES_RIGHT=2, ES_MULTILINE=4, ES_UPPERCASE=8, ES_LOWERCASE=16, ES_PASSWORD=32, ES_AUTOVSCROLL=64, ES_AUTOHSCROLL=128 // ... weitere weggelassen };
Ein Steuerwort kann dann mit Hilfe des OR-Operators aus den einzelnen Flags zusammengesetzt werden:
long editstyle=ES_MULTILINE|ES_UPPERCASE|ES_AUTOVSCROLL;
Danach hat editstyle den Wert 4|8|64 = (0000100)2 | (0001000)2 | (1000000)2 = (1001100)2 = 76. (Hier könnte auch + verwendet werden).

Ein Flag wird gesetzt mit OR (hier kann kein + verwendet werden, da das entsprechende Bit ja schon gesetzt sein könnte):

editstyle|=ES_AUTOHSCROLL;
Ein einzelnes Flag wird mit AND abgefragt:
if (editstyle&ES_MULTILINE) ...
Mit XOR kann der Wert eines einzelnen Bits "umgekippt" werden:
editstyle^=ES_LOWERCASE;
Mit ~a wird ein Bitmuster gesetzt, in dem das einzige gesetzte Bit aus a gerade das einzige nicht gesetzte ist. Deshalb kann mit NOT und AND ein einzelnes Flag gelöscht werden:
editstyle&=~ES_AUTOVSCROLL;

4.9 Logische Operatoren

Die drei logischen Operatoren ! && || sind auf ganzzahlige Ausdrücke (insbesondere bool) anwendbar.

In solchen Ausdrücken stehen ganzzahlige Werte !=0 für true und der Wert 0 für false -- in alter C-Tradition, wo es den Typ bool noch nicht gab.

Für den Typ bool folgen die bekannten Wahrheitstabellen für die drei logischen Operationen NOT, AND, OR, für die es in C++ direkt Operatoren gibt:

a !a
false true
true false
a b a&&b
false false false
false true false
true false false
true true 1
a b a||b
false false false
false true true
true false true
true true true

Bemerkenswert bei && und || ist, daß bei ihnen eine Kurzschlußauswertung (short cut evaluation) stattfindet.

Manchmal liegt der Wert des logischen Ausdrucks bereits durch den ersten Operanden fest:

false && a = false  unabhängig von a
true || a = true  unabhängig von a

In solchen Fällen wird die Auswertung des ganzen Ausdrucks dort abgebrochen, d.h. der zweite Operand wird nicht ausgewertet.

Durch die Linksassoziativität der Operatoren läßt sich das auf mehrere Operanden erweitern:

a && b && c ...
Hier wird die Auswertung nach dem ersten Operanden abgebrochen, der den Wert false hat.

Das ist meistens aus Effizienzgründen natürlich erwünscht. Falls im zweiten Operanden allerdings Seiteneffekte auftauchen, ist Vorsicht geboten:

while ( ++a<b && ++c<d ) ... while ( ++a<b || ++c<d ) ...
Links wird c einmal weniger erhöht als a, nämlich im letzten Schleifendurchgang nicht. Rechts wird c so lange nicht erhöht, wie noch a<b ist.

Oft kann man sich die Kurzschlußauswertung aber auch zunutze machen:

if ( i<MAXINDEX && a[i]!=0 ) ...

Ein beliebter Fehler ist die Verwechslung der Operatoren & und &&, bzw. | und ||. Wenn immer nur mit 0 und 1 gearbeitet wird, ist die Interpretation der Wahrheitswerte identisch, die Operanden dürfen bei den bitweisen Operatoren aber in beliebiger Reihenfolge ausgewertet werden, und es findet keine Kurzschlußauswertung statt. Bei anderen Werten als 1 für "wahr" kann es natürlich zu Fehlinterpretationen kommen (z.B. 1&2=0, 1&&2=1).

4.10 Der Komma-Operator

Man kann mehrere Ausdrücke zu einem zusammenfassen, indem man sie durch Kommas getrennt hintereinander schreibt. Sie werden dann von links nach rechts ausgewertet, und der gesamte Ausdruck hat den Wert des letzten Ausdrucks. Die Werte der anderen Ausdrücke werden nicht verwendet, sie sind nur sinnvoll, wenn sie Seiteneffekte haben.

Sinn macht diese Konstruktion vor allem in Schleifen (siehe dazu später). Beispielsweise können so mehrere Variablen in einer for-Schleife initialisiert, bzw. verändert werden:

int i; char *P; for ( i=0,P=start ; i<MAX ; ++i,++P ) ...

Außerdem können Aktionen, die die Auswertung der Schleifenbedingung erst ermöglichen, in die Bedingung eingebaut werden:

while ( cin >> c , c!=',' ) cout << c;

4.11 Konstante Ausdrücke

An einigen Stellen der C++-Grammatik werden konstante Ausdrücke gefordert, z.B. bei der Festlegung von Array-Größen und in Mehrfach-Fallunterscheidungen.

Ein konstanter Ausdruck muß bereits zur Compilationszeit ausgewertet werden können. Daher müssen alle Operanden konstant sein (also Literale oder benannte Konstanten), es dürfen keine Funktionen aufgerufen werden, und es sind keine Seiteneffekte erlaubt.

 
4: Ausdrücke Startseite 4.12: Typumwandlungen 
 

© 1998 Axel Rogat