Hallo und herzlich willkommen. Nenne mir die drei O’s eines Ausdrucks! Schon vergessen? Na gut, ausnahmsweise gebe ich dir eine kurze Zusammenfassung.

Ein Ausdruck besteht aus O peranden, die mit O peratoren verbunden sind. Dabei sind Operatoren die symbolische Beschreibung einer O peration. Operanden sind Variablen oder Literale aller möglichen Datentypen. Und zu jedem Datentyp gehört auch ein Satz von Operatoren.

Um es kurz zu sagen, mit Operatoren kannst du Daten verarbeiten, verändern oder auf ihrer Basis Entscheidungen treffen.

Welche Operatoren gibt es?

Um dieser Frage mit dem richtigen „Feeling“ nachzugehen, empfehle ich für die akustische Untermalung dieses Beitrags den Song Smooth Operator der wundervollen Sade Adu.

Es gibt eine Reihe von Kategorien, in die Operatoren unterteilt werden. Letztes Mal haben wir neben Ausdrücken und Anweisungen auch den Zuweisungsoperator = kennengelernt. Und diesmal schauen wir uns weitere an:

  • Arithmetische Operatoren
  • Relationale Operatoren
  • Logische Operatoren
  • Bit Operatoren
  • Zusammengesetzte Operatoren

Arithmetische Operationen

Als Teilgebiet der Mathematik beschäftigt sich die Arithmetik mit den Grundrechenarten. Zu diesen gehört die

  • Addition, also das Zusammenzählen von Werten
  • Subtraktion, Werte von anderen abziehen
  • Multiplikation, das Vielfache eines Werts nehmen
  • Division, einen Teil eines Werts nehmen

Natürlich gehören zu Arithmetik auch Rechengesetze. „Punkt vor Strich“ und wie sie alle heißen. Vielleicht erinnerst du dich noch dunkel an den Schulunterricht.

In der folgenden Tabelle hast du alle arithmetischen Operatoren auf einen Blick.

Operator Operation
+ Addition
- Subtraktion
* Multiplikation
/ Division
% Modulo Division

Viel Neues gibt es zu den arithmetischen Operationen im Vergleich zur Mathematik nicht zu sagen. Sie funktionieren analog dazu.

#include <iostream>

int main() {
    int operand1 = 5;
    int operand2 = 2;

    std::cout << operand1 << " + " << operand2 << " = " << operand1 + operand2 << std::endl; // add
    std::cout << operand1 << " - " << operand2 << " = " << operand1 - operand2 << std::endl; // subtract
    std::cout << operand1 << " * " << operand2 << " = " << operand1 * operand2 << std::endl; // multiply
    std::cout << operand1 << " / " << operand2 << " = " << operand1 / operand2 << std::endl; // divide, division of integer causes lost of remainder
    std::cout << operand1 << " % " << operand2 << " = " << operand1 % operand2 << std::endl; // modulo

    return 0;
}

// listing1: arithmetic operators

Achtung, bei der Division des Ganzzahlen Datentyps Integer musst du aufpassen. Integer haben keine Nachkommastellen. Teilst du also eine Ganzzahl durch eine andere Ganzzahl, geht die Division nicht immer glatt auf. Es gibt einen nicht teilbaren Rest.

Dieser Rest wird dir nicht angezeigt und auch nicht weiter vom Programm berücksichtigt. Deshalb kann es hier leicht zu Datenverlust kommen.

Doch neben den vier Grundrechenarten kennt die Informationstechnik für Integer noch den Modulo % Operator. Seine Funktionsweise wird dir sicherlich bekannt vorkommen. Im Grunde macht er die gleiche Operation wie der Divisions-Operator /, nur mit dem Unterschied, dass du mit dem Modulo Operator den ganzzahligen Rest als Ergebnis erhältst.

Inkrement und Dekrement

Du kommst bestimmt irgendwann in eine Situation, in der du etwas mitzählen möchtest. Wie viele Runden wurden gedreht? Wie oft ist ein bestimmtes Wort gefallen? Wie viele Nachrichten sind eingegangen? Tritt das Event ein, wird eine Zählervariable hochgezählt.

Anstatt auf die Variable ständig den Wert 1 zu addieren, kannst du einfach den Inkrement Operator ++ verwenden. Setzt du den Operator als Prefix vor den Bezeichner deiner Variable, wird ihr Wert um 1 erhöht.

Operator Operation
++ Inkrement
−− Dekrement

Für den umgekehrten Fall, dass du von einem Wert herunterzählen möchtest, gibt es den Dekrement Operator --. In listing2 siehst du ein Beispiel, wie du die beiden Operatoren verwendest.

#include <iostream>

int main() {
    int operand1 = 5;
    int operand2 = 2;

    std::cout << "x = " << operand1 << " y = " << operand2 << std::endl;
    std::cout << "++x = " << ++operand1 << " --y = " << --operand2 << std::endl;

    return 0;
}

// listing2: increment and decrement

Die beiden Operatoren stellen dir zwar keine grundlegend neue Funktionalität zur Verfügung, doch du kannst mit ihnen die Lesbarkeit deines Codes erhöhen und sparst dir auch etwas Getippe.

Sieht auf den ersten Blick nach keiner großen Sache aus. Spätestens wenn wir uns mit Schleifen beschäftigen, wirst du die Daseinsberechtigung von ++ und -- bemerken.

Bedingungen und Vegleichsoperatoren

Der nächste Satz an Operatoren hilft dir, Operanden miteinander zu vergleichen und Bedingungen zu formulieren. Häufig wirst du deinen Code so gestalten, dass sich sein Verhalten bei einem bestimmten Wert ändert oder anpassen soll.

Operator Operation
== gleich
!= nicht gleich
> größer als
< kleiner als
>= größer gleich
<= kleiner gleich

Ein unliebsames Beispiel ist die Radarkontrolle des Verkehrs. Fährt ein Fahrzeug schneller als > die erlaubte Geschwindigkeit, löst die Kamera aus, der Fahrer wird geblitzt und bekommt einige Zeit später Post.

Ein relationaler Operator vergleicht also den l-value Operanden mit dem r-value Operanden. Er gibt dir an, ob die Beziehung, für die er steht, erfüllt wird oder nicht.

Sind zwei Operanden mit dem Gleichheits-Operator == verbunden und haben tatsächlich den gleichen Wert, dann ergibt der Ausdruck den boolschen Wert true. Stimmt die überprüfte Beziehung des Operators nicht, hat der Ausdruck den Boolean false.

Probier listing3 in deiner Entwicklungsumgebung ruhig einmal aus. Dann siehst du wie relationale Operatoren sich verhalten. Lass dich nicht verunsichern. Der Boolean true wird dir in der Konsole als 1 und der Boolean false als 0 dargestellt. Aus der Sicht des Compilers sind diese Werte gleichbedeutend. Du kannst sie beliebig verwenden oder auch miteinander vergleichen true == 1.

#include <iostream>

int main() {
    int operand1 = 5;
    int operand2 = 2;

    std::cout << operand1 << " is equal to " << operand2 << " : " << (operand1 == operand2) << std::endl;
    std::cout << operand1 << " is not equal to " << operand2 << " : " << (operand1 != operand2) << std::endl;
    std::cout << operand1 << " is greater than " << operand2 << " : " << (operand1 > operand2) << std::endl;
    std::cout << operand1 << " is less than " << operand2 << " : " << (operand1 < operand2) << std::endl;
    std::cout << operand1 << " is greater than or equal to " << operand2 << " : " << (operand1 >= operand2) << std::endl;
    std::cout << operand1 << " is less than or equal to " << operand2 << " : " << (operand1 <= operand2) << std::endl << std::endl;

    std::cout << " true == 1 -> " << (true == 1) << std::endl;
    std::cout << " false == 0 -> " << (false == 0) << std::endl;

    return 0;
}

// listing3: comparison and relational operators

Bei einem Detail musst du sehr gut aufpassen! Verwechsle den Zuweisungsoperator = nicht mit dem Gleichheitsoperator ==. Im besten Fall meldet sich dann der Compiler mit einer Fehlermeldung. Andernfalls crasht dein Programm und du kannst auf Fehlersuche gehen.

Wir werden relationale Operatoren noch häufig nutzen, da wir mit ihnen Entscheidungen während der Laufzeit treffen lassen und so verschiedene Wege des Programmablaufs einschlagen können.

Logische Operatoren

Schon mal was von Boolscher Algebra gehört? Klingt nach Mathe, ist es auch! George Bool hat als erster eine „Algebra der Logik“ entwickelt, die heute noch die Grundlage für den Entwurf digitaler Elektronik bildet.

Dort gibt es nur die beiden Zustände wahr und falsch. In einem Schaltkreis stehen sie für Strom fließt oder Strom fließt nicht.

Die Boolsche Algebra kennt also nur die Menge (0,1). Für diese sind die drei Operationen Konjunktion, Disjuktion und Negation definiert. Moderne Programmiersprachen wie C++ bieten Operatoren für diese logischen Operationen.

Operator Operation
&& Konjuktion, logisches und
|| Disjunktion, logisches oder
! Negation, logisches nicht

Die Konjunktion wird auch Und-Verknüpfung genannt. Sie ergbit genau dann 1, wenn beide Operanden den Wert 1 haben. Jeder andere Fall ist 0.

Für die Disjunktion wird auch der Begriff ODER-Verknüpfung benutzt. Das darfst du aber nicht als „entweder-oder“ verstehen, sondern als einschließendes Oder. Ist der erste oder der zweite Operand 1, so ergbit die Operation 1. Nur wenn beide Operanden 0 sind, ist auch das Ergebnis der ODER-Verknüpfung 0.

Die dritte Operation ist die Negation. Sie kehrt den Zustand um. Mit dem NICHT-Operator wird eine 1 zur 0 und eine 0 zur 1.

Ich hoffe, die Wahrheitstabelle in listing4 hilft dir, logische Operatoren besser zu verstehen. Mit mehr Mathematik und Theorie zur boolschen Algebra möchte ich dich an dieser Stelle nicht *Wähle ein für dich passendes Verb: begeistern, belästigen, quälen.

#include <iostream>

int main() {
    bool operand1 = true;
    bool operand2 = false;

    std::cout << "|  Table of truth  |" << std::endl;
    std::cout << "| ---------------- |" << std::endl;
    std::cout << "| a | b | and | or |" << std::endl;
    std::cout << "| ---------------- |" << std::endl;
    std::cout << "| " << operand1 << " | "<< operand1 << " |  "<< (operand1 && operand1) << "  |  "<< (operand1 || operand1) << " |"<< std::endl;
    std::cout << "| " << operand1 << " | "<< operand2 << " |  "<< (operand1 && operand2) << "  |  "<< (operand1 || operand2) << " |"<< std::endl;
    std::cout << "| " << operand2 << " | "<< operand1 << " |  "<< (operand2 && operand1) << "  |  "<< (operand2 || operand1) << " |"<< std::endl;
    std::cout << "| " << operand2 << " | "<< operand2 << " |  "<< (operand2 && operand2) << "  |  "<< (operand2 || operand2) << " |"<< std::endl;
    std::cout << std::endl;
    std::cout << "| a = " << operand1 << " | not a = " << !operand1 << " |"<< std::endl;
    std::cout << "| b = " << operand2 << " | not b = " << !operand2 << " |"<< std::endl;

    return 0;
}

// listing4: logical operators

Bit Operationen

Der nächste Satz an Operatoren sieht im ersten Moment den logischen Operatoren sehr ähnlich. Sie tragen zum Teil auch gleiche Namen. Doch die Operationen unterscheiden sich fundamental.

Während die logischen Operatoren die Werte von Variablen betrachten und mit boolschen Wahrheitswerte antworten, arbeiten Bit Operatoren tatsächlich auf Bitebene. Sie manipulieren also nicht einfach nur den gesamten Wert einer Variablen, sondern verändern einzelne Bits.

Die bitweise Darstellung von Daten entspricht der internen low-level Repräsentation des Prozessors. Die heutige Digitaltechnik baut auf dem Binärsystem auf. Dort wird jeder beliebige Wert aus einer unterschiedlichen Folge von 0 und 1 gebildet. Eine Stelle in dieser Nummernfolge ist ein Bit.

In der folgenden Tabelle findest du die Bit-Operatoren von C++.

Operator Operation
~ Bitweise Komplementär
& Bitweise UND
| bitweise ODER
^ Bitweise Exklusives ODER
<< Bitschift links
>> Bitschift rechts

Der Komplementär Operator ~ dreht jedes einzelne Bit um. Er funktioniert wie eine Negation auf Bitebene.

Der Bitweise UND Operator & stellt eine UND-Verknüpfung zwischen den entsprechenden Bits zweier Operanden her. Auf die gleiche Weise stellt der Bitweise ODER Operator | eine ODER-Verknüpfung her.

Neu ist allerdings der Bitweise Exklusive ODER Operator. Dieser führt eine XOR-Verknüpfung auf alle Bits durch. Anders als bei der ODER-Verknüpfung darf bei dieser Operation einer der beiden verglichenen Bits 1 sein, um truezu ergeben. Jede andere Kombination ergibt 0.

Damit du die Funktionsweise der Operatoren verstehst und besser die Veränderungen siehst, wird eine binäre Darstellung der Werte in listing5 gewählt. Dazu benötigen wir aus der C++ Standard Library die std::bitset Funktion. Diese fügen wir mit der Präprozessor Anweisung #include <bitset> ein.

Die Länge der Bitfolge ist von der Größe des Datentyps abhängig. Aus diesem Grund nehmen wir den mit 16 Bit kürzesten Ganzahlentyp short. In den Spitzenklammern der std::bitset<size_t N> Template Funktion kannst du die Länge der Bitfolge festlegen. Wir beschränken uns in diesem Beispiel auf die ersten 8, damit die Folge nicht zu lang wird.

Listing5 zeigt dir die dezimale und binäre Darstellung der Werte vor und nach der entsprechenden Operationen.

#include <iostream>
#include <bitset>

int main() {
    short operand1 = 7;
    short operand2 = 4;

    std::cout << "a decimal: "<< operand1 << " binary: " << std::bitset<8>(operand1) << std::endl;
    std::cout << "b decimal: "<< operand2 << " binary: " << std::bitset<8>(operand2) << std::endl << std::endl;

    unsigned short operand3 = ~operand1;
    short operand4 = ~operand2;

    std::cout << "~a decimal: "<< (operand3) << " binary: " << std::bitset<8>(operand3) << std::endl;
    std::cout << "~b decimal: "<< (operand4) << " binary: " << std::bitset<8>(operand4) << std::endl << std::endl;

    std::cout << "a & b decimal: "<< (operand1 & operand2) << " binary: " << std::bitset<8>(operand1 & operand2) << std::endl;
    std::cout << "a | b decimal: "<< (operand1 | operand2) << " binary: " << std::bitset<8>(operand1 | operand2) << std::endl;
    std::cout << "a ^ b decimal: "<< (operand1 ^ operand2) << " binary: " << std::bitset<8>(operand1 ^ operand2) << std::endl << std::endl;

    return 0;
}

// listing5: bitwise operators

Für den Komplementär-Operator ~ gibt es eine Besonderheit. Hier ist es ganz wichtig, ob das erste Bit als Vorzeichen interpretiert wird oder nicht. Damit du beide Fälle siehst wird eine zusätzliche Variable mit dem unsigned short initialisiert.

Der Bitshift-Operator verschiebt die Bits um die gewünschte Anzahl an Stellen y. Entweder nach links oder nach rechts. Hast du bemerkt, dass das Verschieben einer Binärfolge um y Stellen einer Multiplikation oder Division mit 2^y entspricht?

Das wäre ziemlich gut, denn wer sieht allgemeine Zusammenhänge auf den ersten Blick. Aber ein konkreter Fall kann dir auffallen. Biespielsweise hat der Bitshift nach links um eine Stelle die gleiche Auswirkung auf den Zahlenwert wie eine Multiplikation mit 2.

#include <iostream>
#include <bitset>

int main() {
    short operand1 = 7;
    short operand2 = 4;

    std::cout << "a decimal: " << operand1 << " binary: " << std::bitset<8>(operand1) << std::endl;
    // operand1<<2
    std::cout << "a<<2 decimal: "<< (operand1<<2) << " binary: " << std::bitset<8>(operand1<<2) << std::endl;
    // operand1>>1
    std::cout << "a>>1 decimal: "<< (operand1>>1) << " binary: " << std::bitset<8>(operand1>>1) << std::endl << std::endl;

    std::cout << "b decimal: " << operand2 << " binary: " << std::bitset<8>(operand2) << std::endl;
    // operand2<<2
    std::cout << "b<<2 decimal: "<< (operand2<<1) << " binary: " << std::bitset<8>(operand2<<1) << std::endl;
    // operand2>>1
    std::cout << "b>>1 decimal: "<< (operand2>>1) << " binary: " << std::bitset<8>(operand2>>1) << std::endl;

    return 0;
}

// listing6: bitshift

Das bleibt hängen

Es gibt ganz schön viele Operatoren. Die Arithmetischen Operatoren lassen dich mathematische Operationen der Arithmetik, wie Addition, Subtraktion, Multiplikation oder Division auf deine Variablen anwenden. Für Variablen des Datentyps Integer gibt dir dieser Satz nach den Modulo Operator. Damit lässt sich der Rest einer Gazzahldivision ermitteln. Zudem bieten dir der Inkrement- und der Dekrement-Operator eine komfotablere Möglichkeit, hoch- oder runterzuzählen.

Mit Relationalen-Operatoren kannst du Variablen miteinander vergleichen und Bedingungen formulieren.

Die Logischen Operatoren arbeiten mit dem Datentyp Boolean und bringen dir die Operationen Konjunktion, Disjunktion und Negation aus der boolschen Algebra.

Bit Operatoren verändern nicht die Werte selbst, sondern schauen bis auf die Bitebene der Daten. Dort wenden sie ihre Operationen direkt auf die einzelnen Bits an. Bits haben nur die zwei Werte 0 und 1. Dieses binäre System bildet die Grundlage für die heutige Digitaltechnik. So kannst du mit einem Bitshift das gleiche Ergebnis erhalten, wie mit einer Multiplikation oder Division.

Eigentlich habe ich dir zu Beginn noch weitere Operatoren versprochen. Doch bis hierher haben wir so viel Neues kennengelernt, dass eine Pause zum Sacken angebracht ist.

Nächstes Mal gibt es die Zusammenhängenden Operatoren und schließen das Thema Operatoren mit ergänzenden Informationen ab.

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