C++: Die Basics - eps1.12_Operatoren_p2
Hallo und herzlich willkommen. Da wir mit dem Thema Operatoren letztens nicht fertig geworden sind, schließen wir ohne großes Vorwort direkt daran an.
Zusammengesetzte Zuweisungsoperatoren
Zusätzlich zu den bisher vorgestellten Operatoren bietet C++ viel spezifischere Operationen, um Variable zu verändern. Zusammengesetzte Zuweisungsoperatoren kombinieren Operationen anderer Operatoren, können selbst aber nicht in Anweisungen mit anderen Operatoren kombiniert werden.
Mit dem Operator +=
wird zum Beispiel die Summe der zwei Operanden gebildet und diese dem ersten Operanden zugewiesen.
Während ein Ausdruck aus einer Reihenfolge beliebig vieler Operanden und Operatoren bestehen kann, hat ein Zusammengesetzter Zuweisungsoperator nur einen Operand als l-value und einen weiteren Operanden als r-value. Dem l-value wird das Ergebnis der Operation, die auf die beiden Operanden angewandt wird, zugewiesen.
Operator | Operation | |
---|---|---|
+= |
value1 = value1 + value2 | |
-= |
value1 = value1 - value2 | |
*= |
value1 = value1 * value2 | |
/= |
value1 = value1 / value2 | |
%= |
value1 = value1 % value2 | |
&= |
value1 = value1 & value2 | |
|= |
value1 = value1 | value2 |
^= |
value1 = value1 ^ value2 | |
<<= |
value1 = value1 « value2 | |
>>= |
value1 = value1 » value2 |
Ähnlich wie der Inkrement oder Dekrement-Operator fügen Zusammengesetzte Zuweisungsoperatoren keine fundamental neuen Operationen der Programmiersprache hinzu. Stellen aber eine komfortable Alternative dar, um häufig vorkommenden Anweisungen kürzer und übersichtlicher zu schreiben.
// listing1: compound assignment operators
#include <iostream>
#include <bitset>
int main() {
int operand1 = 7;
int operand2 = 4;
// +=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 + operand2: " << (operand1 + operand2) << std::endl;
operand1 += operand2;
std::cout << " operand1 += operand2: " << operand1 << std::endl << std::endl;
// -=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 - operand2: " << (operand1 - operand2) << std::endl;
operand1 -= operand2;
std::cout << "operand1 -= operand2: " << operand1 << std::endl << std::endl;
// *=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 * operand2: " << (operand1 * operand2) << std::endl;
operand1 *= operand2;
std::cout << "operand1 *= operand2: " << operand1 << std::endl << std::endl;
// /=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 / operand2: " << (operand1 / operand2) << std::endl;
operand1 /= operand2;
std::cout << " operand1 /= operand2: " << operand1 << std::endl << std::endl;
// %=
std::cout << "operand1: " << operand1 << " operand2: " << operand2 << std::endl;
std::cout << "operand1 = operand1 % operand2: " << (operand1 % operand2) << std::endl;
operand1 %= operand2;
std::cout << "operand1 %= operand2: " << operand1 << std::endl << std::endl;
// &=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 & operand2: " << std::bitset<8>(operand1 & operand2) << std::endl;
operand1 &= operand2;
std::cout << "operand1 &= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// |=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 | operand2: " << std::bitset<8>(operand1 | operand2) << std::endl;
operand1 |= operand2;
std::cout << "operand1 |= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
operand1 = 5;
operand2 = 2;
// ^=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 ^ operand2: " << std::bitset<8>(operand1 ^ operand2) << std::endl;
operand1 ^= operand2;
std::cout << "operand1 ^= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// <<=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 << operand2: " << std::bitset<8>(operand1 << operand2) << std::endl;
operand1 <<= operand2;
std::cout << "operand1 <<= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
// >>=
std::cout << "operand1: " << std::bitset<8>(operand1) << " operand2: " << std::bitset<8>(operand2) << std::endl;
std::cout << "operand1 = operand1 >> operand2: " << std::bitset<8>(operand1 >> operand2) << std::endl;
operand1 >>= operand2;
std::cout << "operand1 >>= operand2: " << std::bitset<8>(operand1) << std::endl << std::endl;
return 0;
}
Tabelle von Operatoren
Wir haben in dieser und der letzten Episode der Beitragsreihe einige Operatoren kennengelernt. Für einen besseren Überblick habe ich alle in einer Tabelle zusammengefasst.
Da haben wir ein ordentliches Portfolio an Operatoren aufgebaut. Jetzt sind wir sehr gut ausgerüstet für kommende Aufgaben.
An dieser Stelle kannst du sagen: „Mehr brauche ich nicht zu wissen! Ich weiß jetzt genug von Operatoren.“ Trotzdem möchte ich dir noch ergänzende Informationen zu Operatoren geben. Du wirst diese vielleicht im ersten Moment nicht benötigen, doch im Laufe der Reise werden sie sich als sehr nützlich zeigen.
Unary Operatoren
Zur Vollständigkeit und damit du mit dem Begriff etwas anfangen kannst, möchte ich kurz auf Unary eingehen. Hin und wieder stößt du in Fachliteratur auf dieses Wort. So wie es dort eingesetzt wird, habe ich mir komplizierte Definitionen und eine spektakuläre Funktionsweise vorgestellt.
Vielleicht ging es nur mir so, aber ich hatte großen Respekt vor diesem Fachbegriff. Das lag aber hauptsächlich an meinem Unwissen. Es steckt nämlich gar nicht viel dahinter.
Ein unärer Operator bezieht sich einfach nur auf einen Operanden. Schau dir den Negations Operator !
an. Da ist es sehr anschaulich. !
nimmt den Wert einer boolschen Variablen und kehrt ihn um. Er bezieht sich nur auf diese Variable und beachtet keinen weiteren Operanden.
Mehr steckt nicht hinter dem Begriff Unary. Wir haben auch schon einige unäre Operatoren in unserer Werkzeugkiste.
Operator | Operation |
---|---|
+x |
unary plus |
−x |
unary minus |
++x |
Inkrement |
--x |
Derement |
!x |
Logisches Nicht |
~x |
Komplement |
sizeof | Größe bestimmen |
In anderen Beiträgen haben wir mit sizeof
die Größe in Byte von Variablen und Datentypen bestimmt. Es scheint auf den ersten Blick wie eine Funktion, ist aber im C Standard als unärer Operator beschrieben. Nur als kleiner theoretischer Fakt am Rande.
Du weißt nun, was ein Unary ist. Um die Abgrenzung zu anderen Operatoren noch deutlicher zu machen, haben diese eine analoge Bezeichnung dazu. Hat ein Operator zwei Operanden, wie beispielsweise der arithmetische Multiplikation Operator *
, wird er Binary oder binärer Operator genannt.
Du merkst schon, es wurden sich sehr viele Gedanken bei der Entwicklung von Programmiersprachen gemacht und auch die kleinsten Details haben Beachtung gefunden. Und über die Sinnhaftigkeit vieler Details wird in Fachkreisen heftig diskutiert.
Prefix oder Postfix
Als Prefix werden alle Operatoren bezeichnet, die vor den Operanden gesetzt werden. Die Negation !
ist beispielsweise ein Prefix Operator.
Der Postfix Operator wird an den Operanden angehängt. Wir haben bisher zwei Postfix-Operatoren kennengelernt, aber diese als Prefix verwendet. Ich spreche vom Inkrement-Operator ++
und Dekrement-Operator --
.
Du kannst also entweder ++x
oder x++
schreiben. Doch wozu soll das gut sein?
Zunächst müssen wir die Unterschiede verstehen.
Der Prefix funktioniert so, wie du es wahrscheinlich als natürlich empfindest. In der Anweisung y = ++x;
wird x
um 1
erhöht und anschließend y
zugewiesen. Der r-value wird inkrementiert und der l-value bekommt den inkrementierten Wert.
Der Postfix verhält sich etwas anders. Dort wird erst der Wert des r-values dem l-value zugewiesen und danach erst der r-value inkrementiert. Der l-value hält also den alten Wert des r-values.
Lass uns das an einem Codebeispiel ausprobieren.
// listing2: prefix and postfix
#include <iostream>
int main()
{
int operand1 = 0;
int operand2 = 4;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// prefix increment
operand1 = ++operand2;
std::cout << "after prefix increment" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// prefix decrement
operand1 = --operand2;
std::cout << "after prefix decrement" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// postfix increment
operand1 = operand2++;
std::cout << "after postfix increment" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
// postfix decrement
operand1 = operand2--;
std::cout << "after postfix decrement" << std::endl;
std::cout << "operand1 = " << operand1 << " operand2 = " << operand2 << std::endl;
return 0;
}
Wie du siehst, macht es einen gewaltigen Unterschied für deine Anweisungen, ob du dich für Prefix oder Postfix entscheidest. Den Inkrement- oder Dekrementoperationen ist es egal. Der Operand erfährt die Veränderung in beiden Varianten zum selben Zeitpunkt. Aber die Zuweisung findet entweder beim Postfix mit dem alten Wert oder beim Prefix mit dem neuen Wert statt.
Ein weiterer Unterschied wirkt sich auf die Performance deines Programms aus. Während beim Prefix der Wert einfach nach der Operation zugewiesen wird, muss bei einem Postfix Operator für die Zuweisung eine temporäre Kopie des alten Werts gemacht werden.
Dieser Leistungsunterschied mag zwar sehr theoretisch klingen und bei Integern nicht auffallen, doch kann bei manchen Klassen oder Datentypen zum Argument werden. Gerade bei wenig performanten Embedded Systemen.
Prioritäten der Operator
Du stimmst mir sicherlich zu, dass die Grundrechenregeln aus der Mathematik jedem bekannt sind. Auch wenn sie oft unterbewusst angewendet werden. An Rechenregeln, wie den Inhalt von Klammern zuerst ausrechnen, Potenzen haben Vorrang, Punktrechnung kommt vor Strichrechnung, halten wir uns ohne groß darüber nachdenken zu müssen. Und ganz selbstverständlich wird von rechts nach links gelesen.
Wir haben das alles einmal gelernt. Doch berücksichtigt ein Computer die selben Regeln?
Ausdrücke werden schon von rechts nach links ausgewertet. Zuweisungen allerdings nicht. Hier wird der rechte Ausdruck (r-value) dem linken Operanden (l-value) zugewiesen.
Gibt es noch weitere Abweichungen zu den mathematischen Rechenregeln? Oder haben Operatoren sogar völlig andere Prioritäten?
Welcher Wert wird nach Auswertung des Ausdrucks der Variablen result
in listing3 zugewiesen?
// listing3: operator precedence
#include <iostream>
int main()
{
int result = 11*3+20-5*5 << 1;
std::cout << "11*3+20-5*5 << 1 =" << result << std::endl;
result = 33+20-25 << 1;
std::cout << "33+20-25 << 1 =" << result << std::endl;
result = 28 << 1;
std::cout << "28 << 1 =" << result << std::endl;
result = (11*3)+20-(5*5) << 1;
std::cout << "(11*3)+20-(5*5) << 1 =" << result << std::endl;
return 0;
}
Das Ergebnis der Zuweisung ist kein Zufall. Die Reihenfolge, in der die verschiedenen Operatoren aufgerufen werden, ist sehr genau festgelegt. Und genau das ist mit Priorität der Operatoren gemeint.
Die Ausführungspriorität und Assoziativität der C++-Operatoren sind im ISO-Standard genau beschrieben.
Hier ein kleiner Ausschnitt, um die Auswertung des Ausrucks in listing3 nachvollziehen zu können.
Rang | Name | Operator |
---|---|---|
2 | Klammer | () |
5 | Multiplizieren | * |
6 | Addieren | + |
6 | Subtrahieren | - |
7 | Bitshift | << |
16 | Zuweisung | = |
Operatoren mit dem höchsten Rang werden zuerst ausgewertet. Eine Folge von Operatoren mit selbem Rang wird von links nach rechts abgearbeitet.
Schau dir mit diesem Wissen das Beispiel aus listing3 nochmal an. Jetzt kannst du jeden Schritt anhand der Ausführungspriorität nachvollziehen.
Den höchsten Rang im Ausdruck 11*3+20–5*5 << 1
haben die Multiplikationsoperatoren. Daraus ergeben sich die Zwischenwerte 33+20–25 << 1
. Danach kommen die Addition und Subtraktion. Es wird also mit 28 << 1
weitergearbeitet. Darauf folgt vor der abschließenden Zuweisung der Bitshift und der Ausdruck steht für den Wert 56
.
Als kleinen Tipp empfehle ich dir den Einsatz von Klammern. Ich benutze sie gerne, um für mich und andere Programmierer die Prioritäten zu verdeutlichen.
(11*3)+20–(5*5) << 1
So kann ich schneller erkennen, welcher Wert sich aus einem Ausdruck ergibt. Zusätzlich bin ich mir sicher, dass meine Operationsreihenfolge eingehalten wird, da Klammern einen hohen Rang haben. Du kannst somit die Prioritäten einzelner Operationen erhöhen.
Das bleibt hängen
Wir sind am Ende des zweiten Teils zu Operatoren in C++ angelangt. Zuerst waren noch die Zusammengesetzten Operatoren offen. Sie kombinieren verschiedene Operatoren mit einem Zuweisungsoperator. So lassen sich Anweisungen kürzer fassen und dein Code wird übersichtlicher.
Danach haben wir alle besprochenen Operatoren aus beiden Teilen in einer Tabelle übersichtlich aufgelistet.
Die erste Ergänzung zu Operatoren war die Begriffserklärung zu Unary. Dieser bezeichnet Operatoren, die sich nur auf einen Operanden beziehen.
Des Weiteren haben wir uns angschaut, welchen Unterschied es macht, den Inkrement- oder Dekrementoperator als Prefix oder Postfix zu verwenden. Wir bemerkten, dass es sich in erster Linie auf die Zuweisung ausübt. Denn bei einem Postfix wird erst zugewiesen und danach die Operation ausgeführt.
Abschließend haben wir mathematische Rechenregeln mit den Ausführungsprioritäten der Operatoren verglichen. Die Prioritäten bauen zwar auf den Rechenregeln auf, haben aber hier und dort kleine Abweichungen und Eigenheiten.
Mit dem Wissen können wir eine Menge Operationen auf unsere bekannten Datentypen ausführen. Doch mit Operatoren lässt sich sogar der gesammte Programmablauf gestalten. Dazu aber beim nächsten Mal mehr.
Ich wünsche dir maximalen Erfolg!
Quellen
- [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
- [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
- [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017