Hallo und herzlich willkommen. In diesem Blogbeitrag schauen wir uns…

Halt Stop! Noch ein weiterer Datentyp und ich schalte ab! #gähn Die letzten Beiträge drehten sich alle nur um die Typisierung von Variablen und Konstanten in C++. Ausgiebig haben wir uns mit primitiven Datentypen, benutzerdefinierten Datentypen, Arrays eines Datentyps und ersten Datentypklassen aus der Standard Library beschäftigt. Langsam müssten es doch genug sein.“

So oder so ähnlich befürchte ich deine Gedanken. Und tatsächlich, es reicht vorerst. Das ist eine sehr gute Auswahl, mit der wir in der Lage sind, viele Problemstellungen zu lösen. Schließlich ist der Einsatz von Variablen und Konstanten nur sinnvoll, wenn der Rechner damit Berechnungen anstellen kann. Und genau deshalb schauen wir uns diesmal an, wie wir mit ihnen für diesen Zweck arbeiten können.

Die Kunst des Ausdrucks

Eine Berechnung oder mathematische Operation wird im Fachjargon Ausdruck (engl. Expression) genannt. Er besteht aus Operanden und Operatoren. Operanden sind Werte, auf die eine Operation angewendet wird. Die Operatoren sind Symbole für die Operationen, die ausgeführt werden sollen. Ein Ausdruck ist schließlich eine Verknüpfung von einem oder mehreren Operaden mit Operatoren.

Nachdem der Ausdruck ausgewertet wurde, tritt das Resultat an seine Stelle. Damit ist der einfachste Fall des Ausdrucks eine einzelne Variable, Konstante oder ein Literal.

Zur Veranschaulichung findest du in listing1 ein paar Beispiele.

    a = 7

    7 - 4

    b = 12 + a

// listing1: expressions

b = 12 + a ist ein zusammengesetzter Ausdruck. Das Literal 12 und die Variable a sind Operanden. Der Operator + verbindet die beiden Integer Datentypen zu einem resultierenden Wert, der wiederum der Variablen b zugewiesen wird. Der Wert des gesamten Ausdrucks entspricht folglich dem Wert von b.

Klingt verwirrend, muss es aber nicht sein.

Operatoren müssen zu den jeweiligen Operanden passen. Erinnerst du dich noch an unsere Definition eines Datentyps? Ein Datentyp besteht aus einer Menge von Werten und Operationen, die auf diese Werte angewendet werden können. Es macht einen riesigen Unterschied, ob ich die Wurzel aus d ziehe, wenn d vom Datentyp Character oder eine Hexadezimalzahl ist.

Was wenn…?

Mit einem Bedingten Ausdruck kannst du eine Fallunterscheidung vornehmen. Nehmen wir an, du möchtest die größere von zwei Variablen zur weiteren Verwendung ermitteln. Dazu musst du nur die Bedingung als Ausdruck formulieren und über den bedingten Operator ? mit den beiden Fällen wahr und falsch verbinden.

    c = (a > b) ? a : b

// listing2: conditional expression

In listing2 wird der Variablen c der größere der beiden Werte a und b zugewiesen. Wenn die Frage “Ist a größer als b” mit wahr beantwortet wird, dann bekommt c den Wert von a zugewiesen. Ist a kleiner als b ist die Antwort auf die Frage falsch und b wird zugewiesen.

Hier der syntaktische Aufbau eines bedingten Ausdrucks:

Wert = Abgefragte Bedingung ? Rückgabewert für WAHR : Rückgabewert für FALSCH

Anweisung / Statement

Richtest du einen Ausdruck als Handlungsaufforderung an die Recheneinheit, gibst du eine Anweisung (engl. Statement). Im Grunde genommen ist jedes Computerprogramm eine Abfolge von Anweisungen. Du gibst vor, was in welcher Reihenfolge abgearbeitet werden soll. Dazu formulierst du einen für die CPU verständlichen Ausdruck und verpackst diesen in eine Anweisung.

Was bedeutet das praktisch?

In listing3 siehst du exemplarische Anweisungen. Sie kommen dir bestimmt aus vorherigen Beiträgen bekannt vor.

#include <iostream>

int main()
{
    int a;                          // declaration statement

    a = 13 + 7;                     // expression statement

    std::cout << a << std::endl;    // expression statement

    return 0;
}

// listing3: statement

Alle Anweisungen in C++ enden mit einem Semikolon ;. Damit ist die Grenze zwischen zwei aufeinanderfolgenden Anweisungen definiert. Hier kannst du eine Analogie zur geschriebenen Sprache bilden. Das Semikolon ist vergleichbar mit dem Punkt . am Ende eines Satzes.

Du könnstest deinen Quellecode in einem Fließtext mit Blocksatz schreiben. Macht aber niemand. Zur besseren Lesbarkeit des Quellcodes für den Programmierer hat sich etabliert, die Anweisungen untereinander zu schreiben. Eine Zeile pro Anweisung.

Dem Compiler ist egal, ob die Anweisungen alle in einer oder über mehrere Zeilen verteilt stehen. Er ignoriert Leerzeichen, Tabulaturen oder carriage returns (z.B. std::endl). Das gilt aber nicht für strings. Dort zählt jedes Zeichen. Alles, was zwischen den " " Anführungszeichen steht, gehört zum string.

Da gibt es sogar Konflikte, wenn der string nicht innerhalb einer Zeile beendet oder mit einem Backslash \ getrennt wird.

#include <iostream>

int main()
{
    std::cout << "Hello
    World" << std::endl;     // error

    std::cout << "Hello \
    World" << std::endl;     // valid

    std::cout << "Hello "
    "World" << std::endl;    // valid

    return 0;
}

// listing4: strings over two lines

Bei sehr langen strings kann es für dich nützlich sein, diese auf mehrere Zeilen aufzuteilen und so für eine bessere Lesbarkeit zu sorgen.

Arten von Anweisungen

Um seinen Ruf als sehr komplexe Programmiersprache zu behaupten, unterscheidet C++ verschiedene Arten von Anweisungen. Da wären zum Beispiel:

  • Deklarationsanweisung / declaration statement
  • Ausdrucksanweisung    / expresssion statement
  • Schleifenanweisung    / loop statement
  • Auswahlanweisung      / selection statement
  • Verbundanweisung      / compound statement  (Block)

Deklarationsanweisung

Mit der Deklarationsanweisung führst du Namen in dein Programm ein und machst sie dem Compiler bekannt. Die verschiedenen Entitäten, wie beispielsweise Funktionen, Templates oder Variablen, werden alle auf ihre eigene Weise deklariert.

Bisher haben wir häufig Variablen deklariert. Nach dieser Anweisung sind das Objekt, sein Name und der Datentyp im Programm bekannt und erst dann kannst du es verwenden.

int main()
{
    int anInt; // declaration statement

    return 0;
}

// listing5: declaration statement

Ausdrucksanweisung

Eben haben wir uns angeschaut, was ein Ausdruck ist. Möchtest du nun den Rechner anweisen, einen Ausdruck auszuführen, dann gibst du ihm eine Ausdrucksanweisung. Das klingt jetzt komplizierter als es eigentlich ist. Du musst deinem Ausruck einfach ein Semikolon ; folgen lassen. Schon hast du eine Ausdrucksanweisung.

Nach der Auswertung steht ein Ausdruck für einen bestimmten Wert. Die Ausdrucksanweisung hingegen ist normalerweise mit einer Handlung verbunden.

Um das etwas klarer zu machen. Nimm den Ausdruck anInt = 3. Nach seiner Auswertung steht der Ausdruck für den Wert 3. Erst die Anweisung anInt = 3; lässt den Wert des Integer Literals 3 in den Speicher der Variablen anInt schreiben. Der Zuweisungsoperator = weißt der Variablen den Wert zu.

Auch die Ausgabe auf den Bildschirm mit std::cout ist ein Ausdruck. Der Wert ist in diesem Fall der Stream. Hängst du ein Simikolon dahinter, so erteilst du die Anweisung, den Wert auf dem Bildschirm auszugeben. Mit dem Einfüge-Operator << kannst du den Stream vor der Ausgabe um Werte ergänzen.

#include <iostream>

int main()
{
    int anInt;                      // declaration statement

    anInt = 3;                      // expression statement

    std::cout << a << std::endl;    // expression statement

    return 0;                       // expression statement
}

// listing6: expression statement

Der Zuweisungs-Operator =

Eingangs haben wir über die Bestandteile eines Ausdrucks gesprochen: Operand und Operator. Operanden können Werte aus verschiedensten Datentypen sein. Der Operator muss zu dem Operatorensatz des Datentyps passen.

Ein Operator, der sehr häufig verkommt und von uns schon auf intuitive Weise verwendet wurde, ist der Zuweisungsoperator =. Die Zuweisung ist eine besondere Ausrucksanweisung. Mit diesem Operator ersetzt du den Wert des Operanden zu seiner Linken mit dem Wert des rechten Operanden.

int main()
{
    int anInt;

    anInt = 3;  // assignment

    return 0;
}

// listing7: assignment

Eine Zuweisung besteht also aus den drei Teilen linker Operand, Operator und rechter Operand. Die Operanden links des Operators tragen die nicht sehr kreative Bezeichnung l-value. Die Operanden auf der rechten Seite heißen dementsprechend r-value.

l-values verweisen meist auf einen Ort im Speicher. Die Variable anInt in listing7 bezeichnet einen Speicherplatz und ist ein l-value. Der r-value steht andererseits für den Inhalt an einem Speicherort.

Die Zuweisung aus listing7 lässt sich demnach lesen: Der Wert des Literals wird an die Speicheradresse hinter dem Namen anInt kopiert. anInt und 3 haben anschließend denselben Wert.

Du kannst alle l-values auch als r-values verwenden. Umgekehrt funktioniert das allerdings nicht immer. Du kannst den l-value anInt aus unserem Beispiel als r-value nutzen und ihren Wert einer anderen Variablen zuweisen. Doch dem Literal 3 nicht. Ein Literal ist ähnlich wie eine Konstante während der Programmlaufzeit nicht veränderbar. Demnach kannst du den Wert von anInt nicht an diese Adresse kopieren.

Initialisierung oder Zuweisung

Zuweisung, Deklaration, Initialisierung… langsam wird es unübersichtlich. Die Begriffe lassen sich leicht verwechseln. Doch für eine gute Kommunikation mit anderen Entwicklern ist ein Konsenz in der Bedeutung der Begriffe unabdingbar.

Also, wo liegt der Unterschied zwischen einer Initialisierung und einer Zuweisung?

Das Bekanntmachen eines Objekts mit der Deklaration ist eine Voraussetzung, um das Objekt verwenden zu können. Damit du es schließlich benutzen kannst, musst du ihm vorher einen Wert geben. Mit der Initialisierung weist du einem Objekt beim Anlegen einen Anfangswert zu.

Um eine Initialisierung auszudrücken, bietet dir C++ mehrere Notationen. Mit dem Zuweisungsoperator = kannst du wie in der Programmiersprache C variablen initialisieren. Die universellere Form ist aber mit einer durch geschweifte Klammern {} begrenzten Initialisierungsliste.

Wenn du dir nicht sicher bist, warum du welche Form zur Initialisierung nehmen solltest, dann entscheide dich für die Notation mit den geschweiften Klammern {}. Diese bringt den Vorteil, den Informationsverlust von ungewollten Typen Konvertierung zu verhindern. Aus Kompatibilitätsgründen zu C ist die Notation mit dem Zuweisungsoperator gültiges C++. Das bringt leider implizite Konvertierung mit sich und damit die Gefahr, Informationen zu verlieren.

Du kannst Fehler während der Laufzeit verhindern, indem du keine Variable uninitialisiert lässt. Der Compiler beschwert selten über nicht initialisierte Variablen, denn bei der Deklaration wird nur der Speicherplatz umschlossen. Die Daten darin besteen ohne Initialisierung aus irgendwelchen Werten. Konstanten müssen sogar initialisiert werden, denn dort hast du während der Laufzeit keine Chance mehr, einen Wert zuzuweisen.

Also überleg dir vor dem Anlegen einer neuen Variablen ihren Wert.

#include <vector>

int main()
{
    int i1;                             // declaration
    i1 = 7;                             // assigning

    double d1 = 1.5;                    // c-stlye initializing
    double d2 {1.5};                    // initializing with initializer list

    std::vector<int> v1 {1,2,3,4,5,6};  // initializing with initializer list

    int i2 = 7.8;                       // i2 implicitly converts to 7
    int i3 {7.8};                       // error

    return 0;
}

// Listing8: initialising and assigning

Jetzt haben wir so viel über Initialisierung gesprochen. Worin liegt denn der Unterschied zu einer Zuweisung? Betrachtest du die c-style Initialisierung mit = sind Abweichungen kaum zu erkennen. Die grundsätzliche Regel lautet: Eine Zuweisung bezieht ich immer auf ein bereits existierendes Objekt.

Für listing8 bedeutet das, dass nach der Deklaration von int i1; der Variablen der Wert zugewiesen wird i1 = 7;. Anders sieht es bei der Variablen d1 aus. Die double Variable wird mit einem Wert initialisiert, double d1 = 1.5.

Da fehlt noch was

Nachdem wir uns die Deklarationsanweisung und die Ausdrucksanweisung genauer betrachtet haben, fehlen uns noch ein paar bereits Genannte.

Die Schleifenanweisung erkennst du an den Schlüsselwörtern for, while und do .. while. Auswahlanweisungen beginnen mit den Schlüsselwörtern if und switch. Und eine Verbundanweisung, auch Block genannt, enthält zwischen seinen geschweiften Klammern {} eine Folge von Anweisungen. Die Folge kann leer sein, weitere Verbundsanweisungen beherbergen. Der Block stellt die Zusammengehörigkeit dar und lässt so den Ablauf eines Programms steuern.

Mit diesen Anweisungen und dem Programmfluß beschäftigen wir uns ein anderes Mal. Dafür brauchen wir neben dem Zuweisungsoperator = zunächst noch weitere Operatoren.

Das bleibt hängen

Da haben wir wieder einmal einiges gelernt. Wir wissen nun, dass in einem Ausdruck Operaden mit Operatoren verbunden werden und so einen neuen Wert darstellen. Möchtest du eine solche Operation vom Rechner ausführen lassen, dann musst du ihm den Ausruck als Anweisung übergeben. Eine Anweisung in C++ endet immer mit einem Semikolon ;.

Anweisungen lassen sich auf mehrere Arten unterteilen. Angeschaut haben wir uns die Deklarations- und Ausdrucksanweisung sowie die Zuweisung. Die Zuweisung besteht aus l-value, Zuweisungsoperator = und r-value.

Zudem können wir nun die Zuweisung von der Initialisierung unterscheiden.

Mit den anderen Anweisungsarten lässt sich der Programmfluß steuern. Doch wir können damit noch nicht loslegen. Uns fehlen die nötigen Operatoren dafür. Um diese kümmern wir uns beim nächsten Mal.

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