Operatoren
Please study the accompanying table while you brush your teeth every night.
– W.H. Press et. al.: Numerical Recipes in C. 2nd edn (1992), p. 25
Zuweisung, Test auf Gleichheit
Typen legen fest, welche Operationen mit den Werten ausführbar sind. C++ nutzt eine Fülle an Operatoren (siehe Tabelle). Anfangswertbelegung, Zuweisung und Tests auf Gleichheit sind (fast) allen Typen eigen.
Die Zuordnung von Anfangswerten bei der Definition wird trotz Verwendung des Gleichheitszeichens nicht als Zuweisung bezeichnet, sondern als Initialisierung.
Der Ausdruck a = 42 weist einer Variable aden Wert 42 zu. Das doppelte Gleichheitszeichen == bildet den Operator für den Test auf Gleichheit, die Zeichenkombination != steht für ungleich. Das Ergebnis von Vergleichen ist entweder false oder true:
//> cpp_basics/assign.cpp
import std;
int main()
{
// Initialisierung
auto theorie = 0.1 + 0.2 == 0.3;
auto praxis = 0.1 + 0.2 != 0.3;
std::println("{}", theorie == praxis);
std::println("{}", theorie != praxis);
// Zuweisung
theorie = theorie == praxis;
praxis = theorie != praxis;
std::println("{}", theorie); // false
std::println("{}", praxis); // true
}Aufgaben
- Ersetze die Zehntel im Programm durch ihr Zehnfaches. Was ändert sich an den Ausgaben des Programms und warum?
- Wieso ergeben dann im zweiten Teil sowohl Gleichheit als auch Ungleichheit
false?- Wieso ergibt
0.1 + 0.2nicht0.3? Stelle0.1als Dualbruch, also im Stellenwertsystem zur Basis 2 dar. Entwickle sie dazu als Summe von Kehrwerten von Zweierpotenzen:
Grundrechenarten und Vergleiche
Von ganzen, rationalen und reellen Zahlen sind uns die Grundrechenarten vertraut. Runde Klammernpaare erlauben uns, von der eingebauten Punkt-vor-Strich-Regel abzuweichen. Die Endlichkeit des Speichers zwingt der Verarbeitung von Ganzzahlen und gebrochenen Zahlen zusätzliche Regeln auf, die wir beachten müssen.
Ganzzahlige Operanden liefern ein ganzzahliges Ergebnis, auch bei Division. Im Ausdruck 22/7 erfolgt eine ganzzahlige Division mit Rest: Ergebnis 3. Der Divisionsrest wird mit dem Prozentzeichen als Operator ermittelt: 22%7 ergibt 1.
Ist mindestens ein Operand eine Gleitkommazahl, so ist das Ergebnis der Operation ebenfalls vom Gleitkommatyp, auch wenn ein ganzzahliger Wert angezeigt wird:
//> cpp_basics/arithmetic.cpp
import std;
int main()
{
// Ganzzahlen
std::println("(2 + 5) * 6 = {}", (2 + 5) * 6);
std::println("22 / 7 = {}", 22 / 7);
std::println("22 % 7 = {}", 22 % 7);
// Gleitkommazahlen
std::println("22.0 / 7 = {}", 22.0 / 7);
std::println("1.0 / 7 * 7 = {}", 1.0 / 7 * 7);
std::println("1.0 / 49 * 49 = {}", 1.0 / 49 * 49);
std::println("1.0 / 49 * 49 - 1 = {}", 1.0 / 49 * 49 - 1);
}Gleitkommazahlen bilden nur einen Teil der rationalen Zahlen. Ihr Wertebereich ist eingeschränkt. Zudem werden rationale Zahlen wie 0.1, deren Nenner nicht als Zweierpotenz darstellbar ist, durch Näherungswerte repräsentiert. Ergebnisse von Rechenoperationen werden zu darstellbaren Werten gerundet. Der Abstand zwischen zwei benachbarten Gleitkommazahlen wird als unit of least precision (ulp) bezeichnet und variiert mit der Größe der Zahlen. Solche Abweichungen lassen manchen Test auf Gleichheit mit Gleitkommazahlen scheitern. Der Standard IEEE 754 schreibt für Gleitkommazahlen Regeln vor, die zunächst verwundern, bei genauer Überlegung aber sinnvoll erscheinen.1
Überschreiten Gleitkommaoperationen den Wertebereich (Wertebereichsüberlauf), wird das Ergebnis als unendlich behandelt. Werte mit zu kleinem Betrag werden zu 0.0 (Wertebereichsunterlauf). Die Division durch Null ist im Sinne eines Grenzwertes erlaubt. Es existiert auch eine „negative Null“. die aber nicht kleiner als 0.0 ist, wie die anschließenden Vergleichsoperationen zeigen:
//> cpp_basics/zero_to_infinity.cpp
import std;
auto const infinity = 1.0/0.0;
auto const smallest = std::nextafter(0.0, infinity);
auto const biggest = std::nextafter(infinity, 0.0);
auto const minus_zero = -1.0/infinity;
auto const epsilon = std::nextafter(1.0, infinity) - 1.0;
int main()
{
std::println("Wertebereich Gleitkommazahlen:");
std::print("{} {}...{} {}==", -infinity, -biggest, -smallest, minus_zero);
std::println("{} {}...{} {}", 0.0, smallest, biggest, infinity);
std::println("ulp(1.0): {}\n", epsilon);
// Vergleiche
std::println("{} == {}? {}", minus_zero, 0.0, minus_zero == 0.0);
std::println("{} != {}? {}", 0.0, smallest, 0.0 != smallest);
std::println("{} < {}? {}", 0.0, smallest, 0.0 < smallest);
std::println("{} <= {}? {}", 0.0, smallest, 0.0 <= smallest);
std::println("{} >= {}? {}", infinity, biggest, infinity >= biggest);
std::println("{} > {}? {}", infinity, biggest, infinity > biggest);
// NaN = not a number
auto const nan = 0.0/0.0;
auto const nan2 = 0.0 * infinity;
auto const nan3 = infinity - infinity;
auto const nan4 = std::sqrt(-1.0);
std::println("\n0.0/0.0 : {}", nan);
std::println("0.0 * ∞ : {}", nan2);
std::println("∞ - ∞ : {}", nan3);
std::println("Wurzel(-1.0): {}", nan4);
std::println("NaN == NaN? {}", nan == nan);
}Einige Werte des Typs double sind keine Zahlen (NaN = not a number). Sie können entstehen, wenn das Ergebnis einer Operation nicht definiert ist. Welche Zeichen für NaNs ausgegeben werden, hängt von der Implementierung der Standardbibliothek ab. NaNs sind unvergleichlich: Sämtliche Vergleiche mit NaNs liefern false, sogar mit sich selbst.
Kurzschreibweisen
Die Programmiersprachen der C-Familie erlauben Kurzschreibweisen, die einer Erklärung bedürfen. Bei sogenannten Kettenrechnungen gibt es Aufgaben dieser Art: Sei die Zahl x zunächst gleich 2, dann x wird um 5 erhöht, danach wird x versechsfacht. Die Verbundzuweisung x += 5 könnte als x = x+5geschrieben werden, sinngemäß für die anderen zweistelligen Operatoren:
//> cpp_basics/verbundzuweisung.cpp
import std;
int main()
{
int x = 2;
x += 5;
x *= 6;
std::println("x = {}", x);
}Der Inkrement-Operator ++ entspricht x += 1. Das Dekrement -- vermindert entsprechend die Variable um 1. In zusammengesetzten Ausdrücken ist die Stellung des Operators wichtig. Der Postfix-Operator x++ nutzt den alten Wert, der Präfix-Operator ++x den neuen Wert der Variable in der weiteren Auswertung. Als Merkregel hilft: Sieht man beim Lesen des Ausdrucks zuerst die Variable, ist es der alte Wert; liest man zuerst ++bzw. --, so ist es der neue Wert:
//> cpp_basics/prefix_postfix_increment.cpp
import std;
int main()
{
int x = 1, y = 9;
std::println(" x: {}, y: {}", x, y);
std::println("++x: {}, --y: {}", ++x, --y);
std::println(" x: {}, y: {}", x, y);
std::println("x++: {}, y--: {}", x++, y--);
std::println(" x: {}, y: {}", x, y);
}Merkregel Nr. 2: Sei nicht zu clever. Wenn man erst darüber nachdenken muss, ist es zu kompliziert!
Modularithmetik und Zweierkomplement bei Ganzzahlen
In einem Ganzzahltyp mit Bit lassen sich nur verschiedene Werte darstellen. Dadurch kommt man bei fortlaufender Zählung nach Schritten wieder beim Ausgangswert an, ähnlich der Stundenzahl einer Uhr. Dieses Verhalten wird deshalb manchmal Uhrenarithmetik genannt.Die in Bit nicht darstellbare Zahl wird als Modul bezeichnet. Überschreitet eine Rechnung den möglichen Wertebereich, wird in den Bits der Rest der Zahl modulo gespeichert.
Negative Ganzzahlen s tragen im Zweierkomplement ein gesetztes höchstwertiges Bit. Sei u die zum Bitmuster gehörende vorzeichenlose Zahl und der Betrag der mit Vorzeichen behafteten Zahl s, so gilt für alle . Sie ergänzen sich also zur Zweierpotenz .
Das folgende Programm
//> cpp_basics/modulo_arithmetic.cpp
import std;
int main()
{
auto u = std::uint8_t{0};
std::println("Bits | ohne | mit Vorzeichen");
do
{
auto s = static_cast<std::int8_t>(u);
std::println("{0:08b} {0:>6} {1:>6}", u, s);
} while (++u > 0);
auto s = static_cast<std::int8_t>(u);
std::println("{0:08b} {0:>6} {1:>6}", u, s);
}stellt die Bitmuster aller 8 Bit umfassenden Ganzzahlen ohne und mit Vorzeichen dar und endet wieder beim Startwert (leicht gekürzt):
Bits | ohne | mit Vorzeichen
00000000 0 0
00000001 1 1
00000010 2 2
00000011 3 3
...
01111111 127 127
10000000 128 -128
10000001 129 -127
...
11111101 253 -3
11111110 254 -2
11111111 255 -1
00000000 0 0
Rang und Auswertungsreihenfolge von Operationen
In Ausdrücken, die mehrere Operatoren enthalten, ist festgelegt, in welcher Reihenfolge die Operationen ausführen sind. Aus der Mathematik ist uns die Regel „Punkt vor Strich“ geläufig. Daher ergibt 2+4*5 den Wert 42. Die Multiplikation hat einen höheren Rang. Bei gleichrangigen Operationen kommt die Auswertungsrichtung (Assoziativität) hinzu. Der Term 8/4*2 ergibt den Wert 4, weil von links nach rechts ausgewertet wird (linksassoziativ). Mit Klammern erzwingt man eine davon abweichende Reihenfolge: 8/(4*2) ergibt 1.
Die Rangfolge und Auswertungsrichtung kann man in der Operatortabelle nachsehen.
Aufgaben
- Finde komplexe Ausdrücke mit den bisher besprochenen Operatoren. Bestimme die Werte der Ausdrücke im Kopf und überprüfe deine Ergebnisse mittels selbst geschriebener Programme.
Anmerkungen
-
Siehe Wikipedia, in Kurzfassung oder ausführlich Goldberg, David: What every computer scientist should know about floating-point arithmetic. ↩