namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


kennen:operator

Operatoren

Ausdrücke sind eine Folge von Operatoren und Operanden, die eine Berechnung bewirken. Ausdrücke können einen Wert ergeben und können Nebenwirkungen (Seiteneffekte) hervorrufen.
— C++ Standard ISO 14882, Kap. 5 Abs. 1

C und C++ sind reich an Operatoren. Nicht alle sind unmittelbar einsichtig, u.a. deshalb wirkt C so kryptisch auf Sprachfremde. Operatoren werden auf Operanden (Variablen, Konstanten, Ausdrücke) angewendet und verknüpfen diese:

punkte[mannschaft] += gewonnen ? 2 : 0
(c=getchar()) != EOF
(*d++ = *s++) != 0

Die Wirkung von Operatoren wird unten erklärt. Die Verknüpfung geschieht in einer bestimmten Rangfolge. Bei arithmetischen Ausdrücken wie

  x = 2.1 + 3*4/5 - 6.3

erfolgt Punktrechnung vor Strichrechnung (höherer Rang). Die rechte Seite (Rechtswert, rvalue) der Zuweisung muss vollständig berechnet sein, ehe deren Wert an die links stehende Variable (Linkswert, lvalue) zugewiesen wird. Die Änderung eines gespeicherten Variablenwertes ist ein Seiteneffekt. Leerzeichen neben den Operatoren ändern die Bedeutung nicht, können aber die Lesbarkeit des Quelltextes erhöhen.

Der Wert von 12/5 ist nicht 2.4, sondern die Ganzzahl 2, da beide Operanden ganzzahlig sind. 12.0/5 ergibt hingegen 2.4 als gebrochene Zahl, genauer: einen Wert, der dieser Zahl möglichst nahe kommt.

Die Bindungsrichtung (Assoziativität) legt fest, in welcher Richtung gleichrangige Operationen gruppiert werden:

  z = 10 - 5 - 2

wird linksassoziativ als (10-5)-2 behandelt und nicht als 10-(5-2). Klammern erzwingen eine Gruppierung:

  std::cout << (2<=3) << '\n';

Nur wenige Operatoren erzwingen eine bestimmte Auswertefolge der Teilausdrücke. Compiler können

  i = 1;
  a[i] = i++; 

unterschiedlich auswerten: Es kann a[1] = 1 oder a[2] = 1 bedeuten.1)

Rangfolge

Effektives Schreiben (oder Lesen) in C ohne Kenntnis dieser Regeln ist unmöglich. Bitte studiere die Rangtabelle jeden Abend beim Zähneputzen.
RangBindungZweckOperator
1L Bereichsauflösung ::
2L Postfix-Inkrement/-Dekrement
Funktionsaufruf,Typumwandlung
Zugriff auf Element
++ --
Typ()
[] . ->
3R Präfix-Inkrement/-Dekrement
Einstellige Operatoren
Typecast, Speicher
++ --
! ~ - + * &
(Typ) new delete
4L Elementzeiger .* ->*
5L Punktrechnung * / %
6L Strichrechnung + -
7L Bitschieben << >>
8L 3-Wege-Vergleich (C++2a) <=>
9
10
L Vergleiche < <= >= >
== !=
11
12
13
L bitweise UND
XOR
ODER
&
^
|
14
15
L logisch UND
ODER
&&
||
16R Entscheidungsoperator, Zuweisung
mit + - * / % << >> & | ^ als op
? : = op=
17L Liste ,
L linksassoziativ: Gruppierung von links nach rechts a+b+c bedeutet (a+b)+c
R rechtsassoziativ: Gruppierung von rechts nach links a=b=c bedeutet a=(b=c)

Wirkung

Bereichsauflösung

Manche Bezeichner gehören einem bestimmten Namensbereich oder einer Klasse an. Sie werden mit Namensbereich::Bezeichner qualifiziert.

std::cout
std::istream::get()
::globaler_Name

Alle Funktionen, Klassen, Objekte der Standard-Bibiothek sind im Namensbereich std definiert. Der globale Namensbereich enthält die Bezeichner außerhalb von geschweiften Klammern. Der Bereichsauflösungsoperator vor ::globaler_name erlaubt auf den globalen Namen zuzugreifen selbst dann, wenn er durch einen lokalen gleichnamigen Bezeichner verdeckt ist.

Zugriff

Komponenten, Attribute und Methoden von Strukturvariablen und Objekten werden mit variable.komponente ausgewählt.

Der Ausdruck zeiger->komponente mit dem Pfeiloperator ist zu (*zeiger).komponente gleichwertig.

  struct Punkt { int x, y; };
  Punkt a; 
  a.x = 2;
 
  Punkt* a_ptr = &a;
  a_ptr->y = 3;

Mit eckigen Klammern wird auf das Element feld[index] eines Feldes zugegriffen (Feldoperator oder Indexoperator).

  int feld[10];
  feld[0] = 1;

Runde Klammern () gruppieren den eingeschlossenen Ausdruck. Als Funktionsoperator schliessen sie die Funktionsparameter (Argumente) ein.

  x = (2+3)*5;
  y = sqrt(2); 

Die Postfix-Operatoren ++ -- sind arithmetische Operatoren. Sie haben höheren Rang als andere einstellige Operatoren:

  *d++ = *s++;  // erhöht Zeiger s, nicht Wert *s

Der Schlüsselwort-Operator typeid ist zur Laufzeit-Typabfrage vorgesehen.

  std::cout << typeid(Punkt).name() << '\n';  // #include <typeinfo>

Einstellige Operatoren

Die Präfix-Operatoren ++ -- und die Vorzeichen + - sind arithmetische Operatoren. Negieren kann je nach Ziel mit

Der Adressoperator & ermittelt die Adresse der folgenden Variable. Der Inhaltsoperator * greift auf den Wert zu, auf dessen Adresse der dahinterstehende Zeiger-Ausdruck verweist. Er dereferenziert den Zeiger. Adressoperator und Inhaltsoperator sind zueinander invers.

  int  i = 1;
  int* p = &i;
  *p = 2; 

Mit den Operatoren new und delete wird dynamischer Speicher angefordert bzw. wieder freigegeben.

  p = new int[n];
  // ... benutze Feld mit n int-Werten
  delete [] p;

Werden mit new mehrere Objekte als Feld angefordert, muss die Freigabe mit delete [] gekennzeichnet werden. Bei Einzelobjekten ist weder bei new noch bei delete eine eckige Klammer zu setzen. Die Nichtbeachtung führt zu schwerwiegenden Fehlern in der Speicherverwaltung.

Der Ausdruck sizeof(x) gibt den Speicherbedarf des Operanden in Byte an.

Elementzeiger

Methodenzeiger (und Attributzeiger) Typ::*methode auf Methoden (Attribute) einer Struktur / Klasse sind eigentlich keine Zeiger, sondern "nur" Adress-Offsets innerhalb eines Objekttyps. Sie ermöglichen die Auswahl einer Methode (eines Attributs), ohne schon ein Objekt benennen zu müssen. Die Ausführung (der Zugriff) erfolgt dann über variable.*methode bzw. objektzeiger->*methode (Beispiel).

Das Eintragen (Registrieren) von Zeigern auf Funktionen für einen späteren Aufruf wird als Callback-Technik bezeichnet. Diese Technik findet sich häufig in ereignisgesteuerten Programmen (Bedienoberflächen). Ein solcher Callback-Mechanismus für Objekte wird in der mem_fn()-Schablone der Standardbibliothek ausgenutzt.

Arithmetische Operatoren

Die arithmetischen Operatoren + - * / haben die gewohnte mathematische Bedeutung, mit einer Ausnahme: Das Ergebnis von 13/5 ist die Ganzzahl 2, da beide Operanden ganzzahlig sind, nicht 2.6. Operationen führen nicht aus dem Zahlbereich der Operanden heraus. Ist mindestens ein Operand von einem gebrochenzahligen Typ (float oder double), so ist es auch das Ergebnis. Sind die Typen der Operanden unterschiedlich, wird vor der Operation der "kleinere" Typ an den "größeren" Typ angeglichen.

  std::cout << 13/5   << '\n';  // 13 div 5 ergibt 2
  std::cout << 13.0/5 << '\n';  // 13.0/5.0 ergibt 2.3

Der Divisionsrest lässt sich mit dem Modulo-Operator % nur bilden, wenn beide Operanden ganzzahlig sind:

 std::cout << 13%5 << '\n';  // 13 mod 5 ergibt Rest 3

Die Operatoren + und - finden sich

  • einstellig als Vorzeichen von Zahlwerten,
  • zweistellig für Summen- und Differenzen von Zahlen und
  • zur Adressrechnung in der Zeigerarithmetik.
  int feld[10];
  int* p = feld+5;             // Adresse von feld[5]
  std::cout << p-feld << '\n'; // 5 Elemente Distanz

Inkrement ++ und Dekrement erhöhen bzw. senken einen Variablenwert (Zahl oder Zeiger) um eins. In zusammengesetzten Ausdrücken ist die Stellung der Operatoren ++ und zum Operanden wichtig:

Postfix j = i++; j erhält den alten Wert von i
Präfix j = ++i; j erhält den neuen Wert von i

Bitweise Operatoren

Bitoperationen erfordern ganzzahlige Typen (char, short, int, long, unsigned). Zur Illustration werden 8 bit dargestellt mit dem höchstwertigen Bit (Wert 128) auf der linken Seite.

Das Bit-Komplement (Einer-Komplement, compl) kehrt alle Bits um:

  10001011   i
  01110100  ~i

Schiebeoperationen verrücken die Bits vom Wert des linken Operanden um die rechts angegebene Anzahl von Stellen in der angegebenen Pfeilrichtung. In Schieberichtung fallen Bits heraus, auf der anderen Seite werden Nullen aufgefüllt. Achtung! Bei vorzeichenbehafteten Typen ist das Rechtsschieben maschinenabhängig. Einige Maschinen füllen Einsen auf, wenn das Vorzeichenbit gesetzt ist.

  10001011   i
  01011000   i << 3 (Linksschieben)
  00100010   i >> 2 (Rechtsschieben)

Bitweises UND (bit_and) setzt im Ergebnis nur die Stellen auf 1, die in beiden Operanden auf 1 stehen:

  00001111   i
  10101010   j
  00001010   i & j  

Bitweises ausschließendes ODER (Exklusiv-ODER, xor) setzt im Ergebnis jene Stellen auf 1, deren Wert sich in beiden Operanden unterscheidet:

  00001111   i
  10101010   j
  10100101   i ^ j  

Bitweises ODER (bit_or) setzt im Ergebnis all die Stellen auf 1, die in mindestens einem Operanden auf 1 stehen:

  00001111   i
  10101010   j
  10101111   i | j  

Vergleiche

Vergleiche liefern einen Wahrheitswert. Die Ordnungsrelationen < <= >= > sind selbsterklärend. Der Test auf Gleichheit muss mit zwei Gleichheitszeichen == geschrieben werden, weil das einzelne = eine Zuweisung auslöst. Ungleichheit != kann als "nicht gleich" gelesen werden.

Logische Operatoren

Wahrheitswerte (bool) und Ausdrücke lassen sich logisch verknüpfen und liefern wieder einen Wahrheitswert. Ausdrücke ungleich Null gelten als wahr (), nur 0 als falsch (). Weitere logische Verknüpfungen lassen sich durch Kombination bilden (Beispiel bool).

not and or Antivalenz (XOR) Äquivalenz Folge A ⇒ B
A B !B A&&B A||B bool(A)!=bool(B) bool(A)==bool(B) B||!A
0 0 1 0 0 0 1 1
0 1 0 0 1 1 0 1
1 0 0 1 1 0 0
1 1 1 1 0 1 1

Logische Ausdrücke werden garantiert von links nach rechts ausgewertet, jedoch nur soweit, bis das Ergebnis feststeht (Kurzschlussverfahren, engl. short circuit evaluation):

  if (n && z/n == 0) // bei Nenner 0 wird Division nicht ausgeführt
  {
    std::cout << "echter Bruch" << '\n';
  }

Entscheidungsoperator

Der bedingte Ausdruck bedingung ? dann : sonst enthält den einzigen dreistelligen Operator. Ist der erste Teilausdruck wahr, wird der folgende Dann-Zweig ausgewertet, andernfalls der Wert des Sonst-Zweiges berechnet. Der Dann-Ausdruck und der Sonst-Ausdruck müssen typgleich sein. Das Ergebnis kann (sollte) weiterverwendet werden:

  x = a < b ? a : b;  // Minimum von a und b

Zuweisung

Zuweisungen ändern Variablenwerte. Die linke Seite der Zuweisung (das Ziel der Zuweisung) muss nicht unbedingt eine Variable sein. Es kann ein komplizierterer Ausdruck sein, der einen veränderbaren Speicherplatz (lvalue) anspricht. Die Zuweisung erfolgt von rechts nach links.

  punkte[mannschaft] = 0;  
  x = y = z = 1;

In Zuweisungsketten haben am Ende haben alle links stehenden Variablen den rechten Wert angenommen. Das ist möglich, weil eine Zuweisung einen Wert erhält, nämlich den neuen Wert der linken Variable. Zuweisungen können Teil eines Ausdrucks sein.

  while ((c = std::cin.get()) != '\n' && std::cin) // bis zum Zeilenende auffressen
  {
  }

Statt x = x op (y) kann x op= y geschrieben werden, wenn op ein zweistelliger arithmetischer (+ - * / %) oder Bit-Operator (<< >> & ^ |) ist.

  x += 10;  // x wird um 10 erhöht,
  x -= 5;   // x wird um 5 vermindert,
  x *= 2;   // x wird verdoppelt,
  x /= y+2; // x wird geteilt durch y+2, usw.

Die Kurzschrift (Verbundzuweisung) kommt der natürlichen Sprechweise näher als

  x = x+10; // Ermittle die Summe von x und 10 und weise diese x zu !:-)

Listenoperator

Das Komma steht zwischen Parametern / Argumenten von Funktionen, auch zwischen den Werten einer Aufzählungs- oder Initialisiererliste. Hinter einem Typ können mehrere Bezeichner vereinbart werden, durch Komma getrennt. (Aber: Jedem Funktionsparameter muss sein eigener Typ vorangestellt sein.)

  int i = 1, j = 2;
  int folge[] = { 1, 2, 3 };
  double winkel = atan2(y, x);

In Zählschleifen mit mehreren Steuervariablen kann das Komma auftreten:

  for (i = 0, j = strlen(s)-1; i < j; i++, j--) // Zeichenkette s umdrehen
  {
    swap(s[i], s[j]);
  }

Ausdrücke, durch Komma getrennt, werden streng von links nach rechts ausgewertet. Ergebnistyp und Ergebniswert sind die des rechtesten Operanden. Alle vorhergehenden Ergebnisse werden bis auf Seiteneffekte verworfen. Aber: In welcher Reihenfolge die Argumentwerte vor einem Funktionsaufruf berechnet und abgelegt werden, ist nicht definiert, sprich: compilerabhängig.

Operatoren überladen

Für die eingebauten Typen ist die Wirkung der Operatoren festgelegt. Im Zusammenhang mit neuen Typen (Aufzählungen, Klassen und Strukturen) kann für Operatoren eine neue Bedeutung eingeführt (überladen) werden. Als Einschränkungen gelten:

  • Es können keine neuen Operatoren "erfunden" werden.
  • Rangfolge und Parameteranzahl der Operatoren sind nicht änderbar.
  • Die Operatoren :: . .* ?: sind gar nicht, = [] () -> nur als Operatormethode überladbar.
  • Überladene Operatoren sollten ihrer gewohnten Bedeutung nahekommen (Etikette: keine Überraschungen), z.B.:
    • Der Zeigeroperator -> sollte einen Zeiger auf einen zusammengesetzten Typ liefern.
    • Vergleiche sollten Wahrheitswerte liefern.

Überladene Operatoren werden als Funktionen mit besonderen Namen definiert. Ihr Name beginnt mit dem Schlüsselwort operator, gefolgt von dem Operator-Symbol. Operatoren können als globale Funktion oder als Methoden überladen werden (s. Beispiel Operatoren überladen).

Besondere Regeln gilt es zu beachten bei Ein- und Ausgabeoperatoren, Zuweisungsoperator, Inkrement und Dekrement, Feldindex, Funktionsklammern und Typumwandlung.

Operatormethoden

  u += v;     // nutzt Methode  u.operator+=(v);

Operatormethoden erhalten den ersten Operanden implizit (*this). Bei zweistelligen Operanden muss nur das rechte Argument ("right hand side" = rhs) als Parameter übergeben werden.

struct Vec
{
  Vec& Vec::operator+=(const Vec& rhs);
  // ...
};

Operatoren, die Zugriff auf (private) Daten eines Linkswertes (lvalue) benötigen, z.B. +=, lassen sich meist besser als Methode implementieren.

Operatorfunktionen

  w = u + v;  // nutzt Funktion w = operator+(u, v);

Operatorfunktionen werden außerhalb von Klassen (global) vereinbart. Benötigt eine Operatorfunktion Zugriff auf private Klassenbereiche, muss die Klasse sie zur Freundin erklären (Schlüsselwort friend). In diesem Beispiel ist das unnötig, da ein zugehöriger Verbundoperator exisiert:

Vec operator+(const Vec u, constVec& v)
{                        // ausführlich: Vec sum = u;
  return Vec(u) += v;    //              sum += v;  
}                        //              return sum;

Die paarweise Bereitstellung von Operatorfunktion + und Verbundoperator += führt zu effizientem Code und gehört zur Etikette (Prinzip der minimierten Überraschung). Interessanterweise basiert die Operatorfunktion auf dem Verbundoperator und nicht umgekehrt!

Zweistellige Operatorfunktionen arbeiten mit gleichberechtigten linken und rechten Operanden. Damit sind implizite Konversionen des linken Operanden möglich:

  std::string s  = "def";
  std::string s2 = "abc" + s;  // Konversion std::string("abc");

Ein- und Ausgabeoperatoren

C++ nutzt die Schiebeoperatoren >> und << als Ein- und Ausgabeoperatoren für die Datenströme istream und ostream. Für nutzerdefinierte Typen müssen diese als Operatorfunktionen mit dem Datenstrom als linksseitigem Argument überladen werden und den Strom nach der Operation zurückliefern, damit die Verkettung möglich ist.

std::ostream& operator<<(std::ostream& os, const Vec& v)
{
  return os << v.x << ' ' << v.y << ' ' << v.z;
}
 
std::istream& operator>>(std::istream& is, Vec& v)
{ 
  // ... sinngemäß
  return is;
}

Der Eingabeoperator muss das Objekt als Referenz übernehmen (Rückgabe!). Er sollte dessen Werte erst ändern, wenn alle notwendigen Daten erfolgreich gelesen wurden (Etikette), damit Objekte nicht nicht in einen ungültigen Zustand geraten.

Zuweisungsoperator

Wenn nicht anders definiert, wird jeder zusammengesetzte Typ als Byte-Block kopiert (sog. flache Kopie). Zuweisungsoperatoren sollten dann definiert werden, wenn auch ein Kopierkonstruktor erforderlich ist. Zumeist verwaltet eine Klasse dann dynamischen Speicher oder andere Ressourcen (Beispiel tiefes Kopieren). Zuweisungsoperatoren werden nicht vererbt.

Inkrement und Dekrement

Erhöhen und Absenken können als vorangestellte (Präfix-) und nachgestellte (Postfix-) Operatoren definiert werden:

class Date
{
public:
  Date& operator++();    // Präfix:  ++date;
  Date  operator++(int); // Postfix: date++;
 
  // ...
};

Durch ein "überflüssiges" int-Argument wird der Operator als Postfixvariante markiert. Üblicherweise sollte diese den alten Wert als Kopie (rvalue) liefern.

Feldindex

  Element& Klasse::operator[](Typ index);

Eckige Klammern machen eine Klasse konzeptionell zu etwas Feldähnlichem (Beispiel: Array-Schablone). Die Feldindex-Operatormethode kann nur einen Parameter aufnehmen. Allerdings muss der Index keine Ganzzahl sein. Auch sogenannte "assoziative" (zuordnende) Felder sind möglich:

  std::map<std::string, int> telefonbuch;
  telefonbuch["Richter"] = 19;                 // merken: Richter TelNr. 19
  std::cout << telefonbuch["Richter"] << '\n'; // 19

Funktionsklammern

  Ergebnistyp Klasse::operator()( Parameterliste );

Die Funktionsklammer kann beliebig viele Parameter aufnehmen und lässt Objekte wie Funktionen agieren (Beispiel Funktionsobjekt). Im mathematischen Sinne ist das Funktionsobjekt ein Operator, der auf die Argumente angewendet wird.

Funktionsobjekte (Funktoren) können sich Einstellungen (Parameter) merken und können ihren inneren Zustand ändern. Dadurch wird die Anzahl der Argumente beim Aufruf verringert. Funktoren kommen u.a. in den Algorithmen der Standard-Bibliothek zum Einsatz.

Typumwandlung

Umwandlungsmethoden erlauben, Objekte in einen anderen zuvor definierten Typ zu konvertieren. Der Ergebnistyp steht hinter dem Schlüsselwort operator. Der Typkonverter Bruch::operator double() ist das Gegenstück (die Umkehrung) zu einem Konstruktor Bruch::Bruch(double x).

struct Bruch
{
  long z, n;
 
  operator double() const // umwandeln in Dezimalbruch
  { 
    return double(z)/n; 
  }
};
 
Bruch  b = { 1, 2 };
double x = b;             // 0.5, implizite Typumwandlung
double y = double(b);     //      explizite Typumwandlung 

Vorsicht! Durch ein Zuviel schlägt die beabsichtigte Vereinfachung ins Gegenteil um. Durch Konstruktoren und Typkonverter sollten keine Zyklen im Typ-Umwandlungs-Graphen entstehen. Auch mehrere Typumwandlungen in ähnliche Typen schaffen Probleme. Der Compiler versucht bei Nichtübereinstimmung automatisch (implizit) Typen zu konvertieren, indem er Konstruktoren und Typkonverter untersucht. Findet der Compiler Mehrdeutigkeiten, muss der Anwender durch ausdrückliche (explizite) Typkonversion nachhelfen.

Benutzerdefinierte Konstanten

Im C++-Standard 2011 als "user-defined literals" bezeichnete Konstanten mit frei wählbaren Anhängseln werden durch Definition besonderer Operatoren festlegt, deren Ergebnis einen beliebigen, vom Nutzer definierten Typ haben kann. Auf diese Weise werden einfach lesbare Anfangswerte z.B. für Maßeinheiten möglich:

struct Length { double value; };
 
Length operator "" _cm (long double x)
{
  return { double(x) / 100 };
}
 
Length length = 1.0_cm;

Als Argumente sind nur die Typen unsigned long long int, long double, char und (const char*, size_t) zugelassen. Bei Zeichenkettenliteralen gibt der zweite Parameter die Anzahl der Zeichen ohne Ende-Null an. Literal-Anhängsel ohne Unterstrich sind für künftige Standardisierungen reserviert.

Vorsicht: Wie bei jedem neuen Merkmal besteht die Gefahr des übermäßigen Gebrauchs.

1)
Ab C++17 ist die Reihenfolge festgelegt: Alle Seiteneffekte rechts der Zuweisung werden zuerst ausgewertet. Dennoch ist es schlechter Programmierstil, wenn man darüber nachdenken muss.
kennen/operator.txt · Zuletzt geändert: 2019-01-13 14:19 von rrichter