Grundlagen der Programmiersprache C++

Im Folgenden wird ein Überblick über die Programmiersprache C++ gegeben.

Lexikalische Konventionen

Einer der ersten Verarbeitungsschritte, die der Compiler durchführt ist die Zerlegung der Eingabe in sogenannte Token. Während die Eingabe aus einzelnen Buchstaben besteht, sind Token das Äquivalent zu Wörtern. In C++ gibt es die folgenden Token:

Leerzeichen, Tabulatoren, Zeilenenden und Kommentare werden ignoriert, sie trennen aber Token voneinander.

Kommentare

Ein Kommentar beginnt mit den Zeichen /* und endet mit */ und dient zum Erläutern und Beschreiben des Codes. Kommentare werden vom Compiler ignoriert.

    /*
    Diese Schreibweise für einen Kommentar ermöglicht es den Text
    über mehrere Zeilen zu verteilen.
    */

Solche Kommentare können nicht verschachtelt werden. Darüber hinaus lässt sich der Rest einer Zeile durch // als Kommentar kennzeichnen.

    // auskommentierte Zeile
    int i;  // Zähler

Bezeichner

Ein Bezeichner ist eine beliebig lange Folge von Buchstaben und Ziffern, wobei das erste Zeichen allerdings ein Buchstabe sein muss. Zu den Buchstaben wird auch das Underscore-Zeichen "_" gerechnet. Bei Bezeichnern wird zwischen Groß- und Kleinschreibung unterschieden.

Schlüsselwörter

In C++ sind u.a. folgende Schlüsselwörter reserviert und nicht anderweitig benutzbar:

    alignof  const_cast   goto      public           typedef
    alignas  constexpr    friend    protected        try
    and      continue     if        register         typeid
    and_eq   decltype     inline    reinterpret_cast typename
    asm      default      int       return           union
    auto     delete       long      short            unsigned
    bitand   do           mutable   signed           using
    bitor    double       namespace sizeof           virtual
    bool     dynamic_cast new       static           void
    break    else         noexcept  static_assert    volatile
    case     enum         not       static_cast      wchar_t
    catch    explicit     not_eq    struct           while
    char     export       nullptr   switch           xor
    char16_t extern       operator  template         xor_eq
    char32_t false        or        this
    class    final        or_eq     thread_local
    compl    float        override  throw
    const    for          private   true

Bei Schlüsselwörtern wird wie bei allen Bezeichnern Groß- und Kleinschreibung unterschieden. Bei American Standard Code for Information Interchange (ASCII)-Texten werden folgende Zeichen zur Interpunktion verwendet:

    ! % ^ & * ( ) - + = { } | ~ [ ] \ ; ' : " < > ? , . /

Außerdem folgende Zeichenkombinationen als Operatoren:

    -> ++ -- .* ->* << >> <= >= == != &&
    || *= /= %= += -= <<= >>= %= ^= &= |= ::

Literale

Integer-Konstanten

Bei Integer (ganzzahligen)-Konstanten gibt es folgende Erscheinungsformen:

Der Typ einer Konstanten (siehe auch weiter hinten) richtet sich nach ihrem Wert und den entsprechenden Wertebereichen der Datentypen in der Implementation. Durch Angabe von L oder U hinter der Konstanten kann das aber auch explizit angegeben werden, z.B. 1999U (unsigned) oder 0xabcdL (long).

Zeichenkonstanten

Ein Zeichen (character), eingeschlossen in einfache Hochkommata ('x') ist eine Zeichenkonstante, nämlich der numerische Wert des Zeichens in der internen Codierung des Rechners (meistens ASCII). Beispiel: '0' hat den numerischen Wert 48 bei ASCII-Codierung. Folgende Sonderzeichen werden in Character-Konstanten verstanden:

\0 0-Byte (Stringende)

\n Zeilenvorschub (Newline)

\t Horizontaler Tabulator (Tab)

\r Zeilenende (Carriage Return)

\’ Hochkomma (Single Quote)

\" Anführungszeichen (Double Quote)

\nnn Oktaler Zeichencode

\xhhh Hexadezimaler Zeichencode

Fließkomma-Konstanten

Eine Zahl mit Vorkommastellen, dem Punkt ".", Nachkommastellen und optional der Exponentenangabe mit e und Exponent ist eine Fließkommakonstante. Beispiel: 12.35 oder 1.602e-19 (1.602 * 10−19).

Der Typ einer solchen Konstanten ist double, durch Angabe von F (float) oder L (long double) kann der entsprechende Typ explizit angegeben werden, z.B. 0.25F.

Bei Gleitkommazahlen bzw. entsprechenden Ausdrücken muss entweder die Exponentenschreibweise verwendet werden oder es muss ein Punkt enthalten sein, sonst werden Sie als Integer-Konstante interpretiert und es kann bei Ausdrücken zu unerwünschtem Verhalten kommen. So ergibt z.B. der Ausdruck 1/2 als Ergebnis 0, da dies eine Integer-Division ist. Soll eine Fließkommadivision durchgeführt werden, so muss mindestens einer der Operanden den entsprechenden Typ haben, im Beispiel führt 1/2.0 oder 1.0/2.0 zum gewünschten Ergebnis 0.5.

C-String-Konstanten

Eine Sequenz von Zeichen, eingeschlossen in Anführungszeichen, ist eine C-String-Konstante, z.B. "abc". Der Typ einer C-String-Konstanten ist ein Array von char. Eine C-String-Konstante kann daher nicht verändert werden.

Bei C-String-Konstanten können die gleichen Sonderzeichen mit \ eingegeben werden wie bei Character-Konstanten, z.B. "`Test.\n"'. Ein C-String wird vom Compiler durch ein abschließendes 0-Byte gekennzeichnet, "abc" wird im Speicher also als 'a' 'b' 'c' '\0' abgebildet.

Datentypen

Datentypen haben einen hohen Stellenwert in C++. Sie werden dazu benutzt, Problemstellungen genau abzubilden. Zunächst werden die fundamentalen Datentypen beschrieben, von denen alle anderen Datentypen abgeleitet sind.

Fundamentale Datentypen

Zeichen (character)

  char            // Zeichen (als Zahl 1Byte mit oder ohne Vorzeichen, je nach Compiler) 
  signed char     // Zeichen (als Zahl 1Byte mit Vorzeichen: -128 ... +127)
  unsigned char   // Zeichen (als Zahl 1Byte ohne Vorzeichen: 0 ... 255)

Ob der char-Typ vorzeichenbehaftet ist oder nicht, ist nicht standardisiert. In sehr vielen Implementationen ist er aber signed.

Integer

  short, short int   // mit Vorzeichen (2Byte: -32768 ... +32767)
  int                // mit Vorzeichen (meist 4Byte: -21474883648 ... +21474883647)
  long, long int     // mit Vorzeichen (4Byte: -21474883648 ... +21474883647)

Alle diese Typen gibt es auch in einer unsigned Variante, z.B. unsigned long.

Fließkomma

  float         // Einfach genaue Fließkommazahl
  double        // Doppelt genaue Fließkommazahl
  long double   // Extra genaue Fließkommazahl

In den allermeisten 32-Bit-Implementierungen, gelten die in angegebenen Werte. Bei 64-Bit-Implementierungen sind die long-Datentypen oft 8 Bit breit mit entsprechend größerem Wertebereich. Um unabhängig von einer speziellen Implementierung zu sein, sollte man sich bei beim Programmieren nicht auf die angegebenen Werte verlassen. Wenn es für die Implementierung wichtig ist, die Grenzen zu beachten, sollten die Konstanten aus dem Header <numeric_limits> verwendet werden, die in jeder Situation die korrekten Grenzen zurückgeben.

Typ Größe (in Byte) Wertebereich
Char/Integer mit Vorzeichen
char 1 -128...127
short 2 -32768...32767
int 4 -2147483648...2147483647
long 4 -2147483648...2147483647
Char/Integer ohne Vorzeichen
unsigned char 1 0...255
unsigned short 2 0...65535
unsigned int 4 0...4294967295
unsigned long 4 0...4294967295
Fließkomma
float 4 ≈ 1.4*10-45...3.4*10+38
double 8 ≈ 4.94*10 -324...1.8*10+308
Boolean
bool 1 true, false

Umwandlungen zwischen den Standard-Datentypen werden normalerweise implizit durchgeführt. Darüber hinaus können Umwandlungen erzwungen werden (siehe auch Typumwandlung):

    int x = 5;
    double y = double(x);  // double y = x auch möglich

Void

Der Datentyp void ist ein fundamentaler Datentyp, von dem keine Objekte, Arrays aus Objekten oder Referenzen erstellt werden können. void wird benutzt, um anzugeben, dass eine Funktion keinen Rückgabewert hat.

    void f()
    {
        // kein return
        // Funktion liefert keinen Wert.
    }

Boolean

Logische Werte - sogenannte Wahrheitswerte - werden mit dem Typ bool dargestellt, der die Werte true und false annehmen kann.

    bool bIsPos;         // Variable mit Vorbesetzung false
    bool bIsVar = true;  // Variable mit Vorbesetzung true
    bool bIsEmpty();     // Funktion mit boolschem Rückgabewert

    bIsPos = bIsEmpty(); // Zuweisung des Funktionswertes
    bIsPos = 5 < 7;      // Zuweisung eines logischen Ausdrucks (hier true)

auto

Bei Deklaration von Variablen mit gleichzeitiger Initialisierung kann man den Datentyp auch anhand des Typs der Initialisierungsvariablen festlegen. Anstelle des Datentyps schreibt man dann auto.

    auto bIsVar = true;  // Variable vom Typ bool
    auto i = 5;          // Variable vom Typ int
    auto d = 2.0;        // Variable vom Typ double

Dieses Verhalten hat einige Vorteile:

Diese Festlegung des Datentyps mit auto sollte man dennoch mit Sorgfalt verwenden. Sinnvolle Anwendungsmöglichkeiten sind lokale Variablen, Schleifenvariablen oder Zwischenwerte für Containerelemente.

Zusammengesetzte Datentypen

C++ bietet folgende zusammengesetzte Datentypen, die aus den elementaren Typen gebildet werden können:

Referenzen

Eine Referenz ist ein weiterer Name für ein bestehendes Objekt. Das heißt, bei der Erstellung einer Referenz wird das Objekt nicht kopiert und Änderungen an der Referenz ändern auch das ursprüngliche Objekt.

Für einen Datentyp Type ist Type& eine Referenz auf ein Objekt dieses Typs. Zu beachten ist, dass Referenzen immer initialisiert werden müssen:

    int a;
    // int& b;  // Fehler: keine Initialisierung
    int& c = a; // Gültig.
    c = 7;      // Auch a hat jetzt den Wert 7

Auf welches Objekt eine Referenz zeigt, kann nicht geändert werden. Referenzen spielen besonders bei Funktionsargumenten und Rückgabewerten eine wesentliche Rolle.

Pointer (Zeiger)

Ein Pointer ist ein Datentyp, der die Adresse eines Objekts beinhaltet. Deklariert wird ein Pointer folgendermaßen:

        Type* name;

Type* ist der Datentyp "Pointer auf Type". name enthält die Adresse eines Objektes vom Typ Type.

Es gibt zwei prinzipielle Operationen mit Pointern:

Beispiel:

    char c1 = 'A';
    char* p = &c1;  // p enthält jetzt die Adresse von c1
    char c2 = *p;   // c2 enthält jetzt ebenfalls 'A'
    p = &c2;  // p zeigt jetzt auf c2

Um darzustellen, dass ein Pointer (momentan) auf kein Objekt zeigt, kann ihm nullptr zugewiesen werden. Das ist für jeden Pointer möglich.

    char* p;
    if (error)
       p = nullptr;  // Null-Pointer
    else
       p = some_function();

Die Konstante NULL, wie aus C bekannt, sollte für diesen Zweck nicht benutzt werden. Das Dereferenzieren von einem nullptr ist ein Programmierfehler mit undefinierten Auswirkungen.

Es gibt zwei wichtige Unterschiede zwischen Pointern und Referenzen:

In aktuellen Versionen von C++ gibt es Smartpointer, die einige Vorteile gegenüber einfachen Pointer haben (siehe Smartpointer).

Felder

Felder sind ein- oder mehrdimensionale geordnete Ansammlungen eines Datentyps, z.B. ein Feld aus Integern.

Mit der folgenden Deklaration wird eine Feld aus fünf Fließkommazahlen erstellt:

    double arr[5];

Über den Index-Operator wird auf einzelne Elemente zugegriffen:

    double a[10];  // Ein Array mit 10 doubles, indiziert von 0..9
    int k[5];      // Ein Array mit 5 ints, indiziert von 0..4
    a[0] = 0.0;    // Zuweisung an ein Element
    a[9] = 0.9;
    k[3] = 125;
    a[3] = k[3];

Mehrdimensionale Felder sind Felder, die wieder Felder als Elemente enthalten.

    // Feld mit 10 Elementen vom Typ "Array mit 20 Element vom Typ char"
    // äquivalent: Matrix mit 10 x 20 char-Elementen
    char matrix[10][20];
    matrix[0][0]  = 'a';  // äußeres Feld zuerst indiziert
    matrix[0][10] = 'b';
    matrix[9][0]  = 'c';

Die Indizes eines mit n dimensionierten Feldes laufen immer von 0 bis n - 1. Die Indizierung wird nicht überprüft. Zugriff auf ein Feld außerhalb seiner Grenzen ist ein Programmierfehler mit undefinierten Auswirkungen.

C++ stellt in der Standardbibliothek Container zur Verfügung, bei denen eine Prüfung der Grenzen erfolgt.

Der Zugriff auf Felder kann auch über Pointer erfolgen. Dabei ist a[i] äquivalent zu *(a+i).

    int* p;     // Pointer auf int
    int a[10];  // Array aus 10 ints
    int i;
    p = &(a[3]);  // p zeigt jetzt auf das Element a[3]
    i = *p;       // *p ist der Inhalt von a[3]
    p++;          // p zeigt jetzt auf a[4]
    *p = i;       // a[4] erhält jetzt über p den Wert von i

Dies wird insbesondere bei der Übergabe von Feldern in Funktionen benutzt.

Speicherverwaltung

Im Gegensatz zu anderen Sprachen liegt die Speicherverwaltung bei C++ in der Verantwortung des Programmierers. Es gibt einige Anforderungen an die Speicherverwaltung:

Gewöhnlich haben Objekte eine Lebensdauer, die durch ihren Gültigkeitsbereich bestimmt wird. Manchmal ist es jedoch sinnvoll, Objekte zu erzeugen, deren Lebensdauer vom aktuellen Gültigkeitsbereich unabhängig ist bzw. deren Art und Anzahl erst zur Ausführung des Programms bekannt ist. Dafür gibt es dynamische Speicherverwaltung.

Klassische dynamische Speicherverwaltung mit new/delete

Klassisch gibt es dafür die Operatoren new und delete. Objekte, die durch new angelegt wurden, befinden sich im Freispeicher. Der Programmierer ist selbst für die Speicherverwaltung verantwortlich, d.h. Objekte, die mit new explizit angelegt werden, müssen auch wieder explizit mit delete zerstört werden.

Beispiel für das Erzeugen/Zerstören eines Objekts:

int* p = nullptr;
delete p;     // Zerstören mit nullptr erlaubt, aber ohne Wirkung;
p = new int(5);  // Neuen Integerwert erstellen
delete p;     // Zerstören des Integerwertes (Wert des Zeigers unverändert)
p = nullptr;  // Zeiger wieder auf nullptr setzen

Beispiel für das Erzeugen/Zerstören eines Feldes von Objekten:

char* p = new char [10]; // Feld mit 10 char-Werten
p[3] = 'c';              // Zugriff auf das 4.Zeichen   
delete [] p;             // Löschen des Speichers für die Integerwerte
p = nullptr;             // Zeiger wieder auf nullptr setzen

Der Unterschied zwischen dem Anlegen/Zerstören von Objekten und Feldern ist unbedingt zu beachten. Wird ein delete auf ein Feld angewendet oder ein delete[] auf ein Objekt, bedeutet dies undefiniertes Verhalten, was meist zu einer Speicherverletzung oder einer anderen unerwünschten Programmfunktion führt.

Der Aufruf von delete auf einen nullptr ist unproblematisch. Daher ist es sinnvoll, Zeiger immer mit nullptr zu initialisieren. Dynamische Objekte sollten möglichst immer "parallel" erzeugt und zerstört werden: Entweder Neuerstellen/Zerstören innerhalb einer Funktion oder bei Klassen Zerstören von Objekten, die im Konstruktor erstellt wurden, im Destruktor.

Die Speicherverwaltung wird komplexer, sobald Referenzen und Pointer verwendet werden. Im folgenden Beispiel gibt die Funktion badUse eine ungültige Referenz zurück. Das Objekt auf dem Stack von badUse wurde zerstört, aber die Referenz existiert noch. Zugriffe auf die Referenz führen dann zu Speicherverletzungen (so genannte dangling pointer). Daher ist darauf zu achten, dass Objekte länger als ihre Referenzen leben.

string& badUse() {
    string s = "schlecht";
    return s;
}
void f() {
    // Speicherverletzung: referenziertes Objekt wurde schon zerstört
    //string& s = badUse();
    //s.size();
}

Um den String sicher zurückzugeben, kann er als Wert zurückgegeben werden. Dabei wird aber eine Kopie erstellt und nicht mit dem ursprünglichen Objekt weitergearbeitet.

string BadUse() {
    string s = "schlecht";
    return s;
}
void f() {
    string s_stack = badUse();
    string& s_ref = s_stack;  // OK. Objekt lebt lang genug.
    s_ref.size();
}

Strukturen

Strukturen (struct) sind zusammengesetzte Datentypen aus verschiedenartigen Elementen. Der Gebrauch von Strukturen soll an folgenden Beispielen erläutert werden:

    // Beispiel für eine Strukturdeklaration:
    struct BirthDate
    {
        string name;
        int year, month, day;
    };

    // Definition eines Objektes:
    BirthDate mm;
    mm.name  = "Max Mueller";
    mm.year  = 1999;
    mm.month = 2;
    mm.day   = 29;

Strukturen sind in C++ tatsächlich Klassen mit dem einzigen Unterschied, dass der Zugriff auf die Elemente standardmäßig nicht eingeschränkt (siehe auch Beschreibung Klassen) ist. Mit dem Punktoperator kann auf die einzelnen Elemente der Struktur zugegriffen werden. Bei Pointern kann die Dereferenzierung und der Punktoperator zum Pfeiloperator zusammengezogen werden:

    BirthDate mm;
    BirthDate *pmm = &mm;

    mm.name;        // Diese Zugriffe sind gleichwertig
    (*pmm).name;
    pmm->name;

Konstanten und Aufzählungen

Benutzerdefinierte Konstanten sind Werte, die einmal gesetzt und dann nicht mehr geändert werden. Konstanten führen zu besser wartbarem Code als direkt im Code eingesetzte Literale. Das Schlüsselwort const kann der Deklaration eines Objekts zugefügt werden, wobei dadurch der Typ des Elements nicht verändert wird.

Da eine Konstante nicht verändert werden darf, muss sie bei ihrer Definition initialisiert werden. Folgendes Beispiel verdeutlicht die Verwendung von benutzerdefinierten Konstanten:

Beispiel:

    const double pi = 3.14159265;
    const int array[] = { 0, 1, 2 ,3 };
    // const int i;  // Fehler, keine Initialisierung
    // pi = 25.9;    // Fehler, da Zuweisung

Bei Zeigern sind zwei Objekte beteiligt, der Zeiger und das Element auf den der Zeiger zeigt. Wenn der Zeiger const ist, steht das Schlüsselwort rechts vom *. Wenn der Element const ist, steht das Schlüsselwort links vom *.

Beispiel:

    char* c0;  // Zeiger auf char

    char* const c1;  // Konstanter Zeiger auf char

    char const* c2;  // Zeiger auf konstanten char
    const char* c3;  // konstanter Zeiger auf (nicht konstanten) char

    const char* const c1;  // Konstanter Zeiger auf konstanten char

Aufzählungstyp

Mit enum wird ein neuer Datentyp definiert, der nur die angegebenen Werte annehmen kann:

    enum class Color
    {
        red,
        yellow,
        blue
    };

Intern ist der Datentyp als Integer repräsentiert. Bei Bedarf kann ein bestimmter Datentyp für die Repräsentation ausgewählt werden:

    enum class Color
        : byte
    {
        red,
        yellow,
        blue
    };

Wenn man keine Werte angibt, beginnt die Aufzählung bei 0 und wird jeweils um 1 erhöht. Optional können die Werte definiert werden, wobei fehlende Werte den nächsthöheren Wert erhalten (z.B. hier C02 = 2):

    enum class CentCoin
    {
        C01 = 1,
        C02,
        C05 = 5,
        C10 = 10,
        C20 = 20,
        C50 = 50
    };

Variablen werden wie folgt angelegt und zugewiesen:

    Color favColor = Color::red;
    CentCoin aCoin;
    aCoin = CentCoin::C50;

Um mit diesen Variablen Berechnungen durchzuführen, müssen sie erst zu einem Integer-Datentyp konvertiert werden (siehe auch Typumwandlung ):

    Color favColor = Color::red;
    int colorCode = (int)(Color::red);

    CentCoin e = CentCoin::C05;
    e = (CentCoin)((int)e + 5); // e = CentCoin::C10;

Die Vorteile gegenüber Integer-Konstanten sind:

            enum class Enum0
            {
                Value = 0,
            };
            enum class Enum1
            {
                Value = 0,
            };
            Enum0 e0 = Enum0::Value;
            Enum0 e1 = Enum1::Value;

Präprozessorkonstanten

Schließlich können auch wie in C Konstanten mit dem Präprozessor definiert werden:

    #define PI 3.14159265
    #define false 0
    #define true 1

Die Verwendung von const ist jedoch vorzuziehen.

Grundlegende Konzepte

C++ definiert einige Konzepte die hier kurz beschrieben werden.

Deklaration und Definition

Eine Deklaration macht einen Namen dem Programm bekannt. Eine Deklaration ist gleichzeitig auch eine Definition, es sei denn,

Beispiele für Definitionen:

    int a;
    int f(int x) { return x + a; }
    struct S { int a; int b;};
    enum class { up, down };

Beispiele für reine Deklarationen:

    extern int a;
    int f(int);
    struct S;

Gültigkeitsbereich

Ein Gültigkeitsbereich definiert, in welchen Teilen eines Quelltextes ein Name sichtbar, d.h. verwendbar ist. In C++ gibt es folgende Arten von Gültigkeitsbereichen:

Lokal: Ein Name, der lokal in einem Block oder einer Funktion deklariert wird, kann nur dort und nicht außerhalb benutzt werden. Parameter einer Funktion werden so behandelt, als wären sie im äußeren Block der Funktion deklariert.

Global: Ein Name, deklariert außerhalb aller Blöcke und Klassen, kann überall nach seiner Deklaration benutzt werden. Diese Namen werden global genannt.

Klasse: Der Name eines Klassenmitglieds ist lokal in dieser Klasse und kann nur von einer Memberfunktion der Klasse, über den Punkt-Operator, den Pfeil-Operator, den Bereichsauflösungs-Operator (::) oder von einer abgeleiteten Klasse benutzt werden.

Durch Deklaration eines gleichen Namens in einem eingeschlossenen Block oder einer Klasse wird der "äußere" Name "versteckt". Über den Scope-Operator "::" kann man allerdings darauf auch zugreifen.

    int x;  // Global

    void some_function()
    {
        int x;    // Versteckt globale Variable
        x = 1;    // Zuweisung an lokale Variable
        ::x = 1;  // Zuw. an globale Variable über Scope-Operator
    }

Lebensdauer und Speicherklassen

Objekte haben eine Lebensdauer, die direkt nach der Erstellung beginnt und am Ende des Blocks der Erstellung bzw. direkt vor dem Destruktor endet. Sie dürfen nur innerhalb ihrer Lebensdauer verwendet werden.

Es gibt drei fundamentale Speicherklassen in C++:

Automatischer Speicher: Hier werden lokale Variablen und Funktionsargumente abgelegt. Sie werden automatisch bei der Definition angelegt und am Ende des Gültigkeitsbereichs wieder gelöscht. Zur Laufzeit werden sie auf einer Datenstruktur abgelegt, die als Stapelspeicher bzw. Stack bezeichnet wird.

            struct S {};

            void func(S s0)  // Lebensdauer von s0 beginnt
            {
                // Lebensdauer von s1 beginnt
                S s1 = s0;
                // Lebensdauer von s1 endet
            }
            // Lebensdauer von s0 endet

Statischer Speicher: Globale Variablen leben von Programmbeginn bis Programmende. Sie werden in der Reihenfolge ihrer Definition erstellt.

            int s0 = 10;      // Lebensdauer von s0 beginnt
            int main()
            {
                int s1 = s0 + 5;  // Lebensdauer von s1 beginnt
                return 0;       // Lebensdauer von s1 endet
            }
            // Lebensdauer von s0 endet

Im Beispiel wird erst die Variable s0 erstellt, dann s1. Die Variable s1 lebt während der main-Funktion, s0 auch während des Restes des Programms. s1 wird beim Verlassen der Funktion, s0 erst am Ende des Programms zerstört, d.h. erst s1 und dann s0. Auch mit static gekennzeichnete Variablen in Funktionen und Klassen haben diese Lebensdauer.

Freispeicher/dynamischer Speicher: Diese Speicherklasse wird benutzt, wenn Speicherplatz während der Programmausführung angefordert wird. Diese Art Speicher wird auch dynamischer Speicher genannt. Die Benutzung dieser Speicherklasse wird unter SmartPointer beschrieben.

Ausdrücke / Operatoren

C++ beinhaltet eine Vielzahl von Operatoren, die in Ausdrücken Werte verändern können. Die folgende Tabelle enthält eine Zusammenfassung aller Operatoren in C++. Für jeden Operator ist ein gebräuchlicher Name und ein Beispiel seiner Benutzung angegeben. Die hier vorgestellten Bedeutungen treffen für fundamentale Typen zu. Zusätzlich kann man Bedeutungen für Operatoren definieren, die auf benutzerdefinierte Typen angewendet werden (siehe Operatoren überladen).

Bereichsauflösung Klassenname::Element
Bereichsauflösung Namensbereichs−Name::Element
global ::Name
global ::qualifizierter-Name
Elementselektion Objekt.Element
Elementselektion Zeiger->Element
Indizierung Zeiger\[Ausdruck\]
Funktionsaufruf Ausdruck(Ausdrucksliste)
Werterzeugung Typ(Ausdrucksliste)
Postinkrement Lvalue++
Postdekrement Lvalue--
Typidentifikation typeid(Typ)
Laufzeit-Typinformation typeid(Ausdruck)
zur Laufzeit geprüfte Konvertierung dynamic_cast<Typ>(Ausdruck)
zur Übersetzungszeit geprüfte Konvertierung static_cast<Typ>(Ausdruck)
ungeprüfte Konvertierung reinterpret_cast<Typ>(Ausdruck)
const-Konvertierung const_cast<Typ>(Ausdruck)
Objektgröße sizeof Objekt
Typgröße sizeof(Typ)
Präinkrement ++Lvalue
Prädekrement --Lvalue
Komplement ~Ausdruck
Nicht !Ausdruck
einstelliges Minus -Ausdruck
einstelliges Plus +Ausdruck
Adresse \&Lvalue
Dereferenzierung *Ausdruck
Erzeugung (Belegung) new Typ
Erzeugung (Belegung und Initialisierung) new Typ(Ausdrucksliste)
Erzeugung (Plazierung) new(Ausdrucksliste) Typ
Erzeugung(Plazierung und Initialisierung) new(Ausdrucksl.) Typ(Ausdrucksl.)
Zerstörung (Freigabe) delete Zeiger
Feldzerstörung delete [] Zeiger
Cast (Typkonvertierung) (Typ) Ausdruck
Elementselektion Objekt.*Zeiger−auf−Element
Elementselektion Objekt->*Zeiger−auf−Element
Multiplikation Ausdruck * Ausdruck
Division Ausdruck / Ausdruck
Modulo (Rest) Ausdruck % Ausdruck
Addition Ausdruck + Ausdruck
Subtraktion Ausdruck - Ausdruck
Linksschieben Ausdruck << Ausdruck
Rechtsschieben Ausdruck >> Ausdruck
Kleiner als Ausdruck < Ausdruck
Kleiner gleich Ausdruck <= Ausdruck
Größer als Ausdruck > Ausdruck
Größer gleich Ausdruck >= Ausdruck
Gleich Ausdruck == Ausdruck
Ungleich Ausdruck != Ausdruck
Bitweises Und Ausdruck & Ausdruck
Bitweises Exklusiv-Oder Ausdruck ^ Ausdruck
Bitweises Oder Ausdruck \
Logisches Und Ausdruck && Ausdruck
Logisches Oder Ausdruck \
Bedingte Zuweisung Ausdruck ? Ausdruck : Ausdruck
Einfache Zuweisung Lvalue = Ausdruck
Multiplikation und Zuweisung Lvalue *= Ausdruck
Division und Zuweisung Lvalue /= Ausdruck
Modulo und Zuweisung Lvalue %= Ausdruck
Addition und Zuweisung Lvalue += Ausdruck
Subtraktion und Zuweisung Lvalue -= Ausdruck
Linksschieben und Zuweisung Lvalue <<= Ausdruck
Rechtsschieben und Zuweisung Lvalue >>= Ausdruck
Und und Zuweisung Lvalue &= Ausdruck
Oder und Zuweisung Lvalue \
Exklusiv-Oder und Zuweisung Lvalue ^= Ausdruck
Ausnahme werfen throw Ausdruck
Komma (Sequenzoperator) Ausdruck, Ausdruck

Hinweise

Anweisungen

if-else-Anweisung

Bedingte Anweisung:

    if ( expression ) statement_1

bzw. mit else-Teil:

    if ( expression )
        statement_1
    else
        statement_2

bzw. als Statement mit ?-Operator:

    ( expression ) ? statement_1 : statement_2;

Bei dieser Anweisung wird zunächst die Bedingung (expression) ausgewertet. Ist das Ergebnis true, so wird statement_1 ausgeführt, ansonsten wird in den beiden letzten Varianten statement_2 ausgeführt.

Beispiel:

    int a = 1;
    int b = 2;
    int z = 0;
    if (a < b) {    // Bedingung in runden Klammern
        z = b;      // Geschweifte Klammern bei nur einer
    }               // Anweisung eigentlich nicht notwendig
    else
        z = a;      // else-Teil kann auch entfallen

    if (a < b)
        z = 1;  // a < b => z = 1
    else if (a == b)  // Alternative Bedingung
        z = 0;  // a = b => z = 0
    else
        z = -1;  // a > b => z = -1

    z = (a>b)?a:b; // z = max(a,b)

switch-Anweisung

Bedingte Anweisung mit Ausführung abhängig vom Wert eines Ausdrucks.

    switch ( expression )
    {
        case constant_expression_1:
            statements
        case constant_expression_2:
            statements
        case default:
            statements
    }

Für expression sind nur int- oder char-Ausdrücke zulässig. Normalerweise besteht eine switch-Anweisung aus einem Block, mehreren Labels und Anweisungen. Wichtig ist, dass es sich um einen konstanten Ausdruck (constant_expression) hinter dem Schlüsselwort case handelt (keine Verwendung von Variablen, Funktionsaufrufen etc.). Es können beliebig viele case verwendet werden. Der optionale case default wird aufgerufen, wenn keiner der anderen Fälle zutrifft. Für benutzerdefinierte Datentypen ist switch-case nicht benutzbar. Dann wird eine Kette von if-else-Anweisungen benutzt.

Beispiel:

    char c = 'u';

    switch (c)
    {
        // x wird durch * ersetzt
        case 'x':
            c = '*';
            break;
        // Vokale werden durch ? ersetzt
        case 'a':
        case 'e':
        case 'i':
        case 'o':
        case 'u':
            c = '?';
            break;
        default:  // Alle anderen werden durch Leerzeichen ersetzt
            c = ' ';
            break;
    }

Bei case handelt es sich um ein Label, von dem aus der Programmablauf fortgesetzt wird. Insbesondere werden auch die Anweisungen der nachfolgenden case-Labels ausgeführt, solange bis die Ausführung der switch-Struktur durch ein break-Statement abgebrochen wird (fall-through). Im Regelfall wird daher jeder Block hinter einem case-Label durch ein break-Statement abgeschlossen.

while-Anweisung

Bei dieser Schleifenstruktur wird zunächst der Ausdruck bewertet. Ist dieser wahr, so wird die abhängige Anweisung ausgeführt. Nach der Ausführung wird der Ausdruck erneut bewertet. Dieser Vorgang wird so lange durchgeführt, bis die Bewertung des Ausdrucks falsch ist. Dann wird mit der Anweisung, die hinter statement steht fortgefahren.

    while ( expression ) statement

Beispiel:

    int b=5;
    while (b-- > 0)
    {
    ...
    }

for-Anweisung

Die for-Schleife ist wie die while-Schleife kopfgesteuert, d.h. die Bedingung (hier: expression_2) wird vor dem ersten Ausführen der bedingten Anweisung geprüft. Zusätzlich enthält die for-Schleife einen Initialisierungsausdruck (expression_1), der vor der Schleife einmalig vor Beginn ausgeführt wird. Weiterhin einen Iterations-Ausdruck (expression_3), der zusätzlich am Ende jeder Ausführung der bedingten Anweisung durchgeführt wird.

    for ( expression_1; expression_2; expression_3 ) statement

Eine for-Schleife entspricht also im Prinzip einer while-Schleife:

    // Diese for-Schleife ist identisch ...
    for (a; b; c)
        d;
    // ... mit einer solchen while-Schleife
    a;
    while (b)
    {
        d;
        c;
    }

Beispiel für die Anwendung der for-Schleife:

    int k = 5;
    int fak = 1;
    for (i = k; i > 1 ; --i)
        fak *= i;  // fak = Fakultät von k

for-Schleife auf Range-Basis

Soll über einen Datentyp mit mehreren gleichen Elementen z.B. Felder iteriert werden, können range-basierte for-Schleifen genutzt werden. Die Syntax lautet:

    for ( range_declaration : range_datatype ) statement

Beispiel mit einem Feld:

    int v[] = {0, 1, 2, 3, 4, 5};

    for (auto &i : v)  // Schleife über alle Elemente in v, das Schleifenelement wird als Referenz über i angesprochen
        cout << i << ' ';

    // Ausgabe: 0 1 2 3 4 5

Bei auto werden die Elemente kopiert, bei auto& referenziert.

do-while-Anweisung

Im Gegensatz zu den bisher genannten Schleifenkonstruktionen ist die do-while-Schleife fußgesteuert. Die bedingte Anweisung wird mindestens einmal beim Start der Schleife ausgeführt. Nach dieser Ausführung wird der Ausdruck expression ausgewertet. Ist das Ergebnis wahr, so wird die bedingte Anweisung erneut durchgeführt.

    do statement while ( expression );

Man beachte das Semikolon am Ende der Konstruktion.

Sprunganweisungen

Wie bereits erwähnt, existieren in C++ mehrere Sprunganweisungen (engl: jump-statements), die vor allem bei den Schleifen und der switch-Anweisung zum Einsatz kommen. Durch den Einsatz der Sprunganweisungen wechselt der Programmablauf unbedingt.

extern-Anweisung

Die extern-Anweisung wird verwendet um eine bereits global definierte Variable in einer anderen Datei bekannt zu machen. Dies ist dann nur eine weitere Deklaration und nicht eine neue Definition dieser Variablen.

Datei1.cpp:
       int x = 1;
Datei2.cpp:
       extern int x;
       int y = ++x;
       // x == 2
       // y == 2

Funktionen

Eine Funktion ist eine Zusammenfassung von Anweisungen zu einer Einheit. Sie erhält Objekte eines bestimmten Typs als Argumente und liefert wiederum als Ergebnis ein Objekt eines bestimmten Typs.

Eine Funktionsdefinition in C++ spezifiziert den Namen der Funktion, den Typ des zurückgelieferten Objektes und die Typen und Namen der Argumente. Das Zurückliefern eines Wertes aus einer Funktion geschieht mit Hilfe der return-Anweisung. Die Funktion main() hat eine spezielle Bedeutung, sie wird beim Start des Programms aufgerufen.

    long fak(int k)  // Returnwert long
                     // Ein Argument int k
    {
        // ...
        return erg;  // Ergebnis
    }

    int main()  // Hauptprogramm
    {
        long x;
        x = fak(5);  // Aufruf der Funktion
        return 0;    // Ende des Programms
    }

Funktionsparameter werden in C++ normalerweise als Wert übergeben (call by value). Soll eine Referenz übergeben werden (call by reference), so muss dazu der Übergabe-Parameter als Referenz deklariert werden. Wenn ein Pointer übergeben wird, wird zwar der Pointer kopiert, aber das Objekt nicht, sodass sich auch hier call by reference ergibt.

Bei call by value wird eine Kopie des Werts an die Funktion übergeben. Dies hat zur Folge, dass Änderungen an den Parameter-Variablen nach dem Rücksprung aus der Funktion verloren gehen.

    void test(int x, int y)
    {
        x = 10;
        y = 20;  // x ist hier 10, y = 20
    }

    int main()  // Hauptprogramm
    {
        int wert1 = 15;
        int wert2 = 25;
        test(wert1, wert2);  // Aufruf der Funktion
                             // wert1, wert2 sind jetzt immer noch 15, 25
        return 0;
    }

Möchte man Variablen per call by reference übergeben, so ist dies über Pointer möglich, aber das ist relativ umständlich:

    void some_function(int* p) {
        *p = 99;
    }

    int main()
    {
        int a;
        some_function(&a);
        // a hat jetzt den in some_function()
        // zugewiesenen Wert
    }

In C++ kann dies einfacher über Referenzen realisiert werden. Das Beispiel mit der Funktion ließe sich mit Referenzen folgendermaßen umschreiben:

    void some_function(int& i) {
        i = 99;
    }

    int main()
    {
        int a;
        some_function(a);
        // a hat jetzt den in some_function()
        // zugewiesenen Wert
    }

Für Funktionsparameter sollten die folgenden Konventionen verwendet werden:

Ausgehende Parameter: Rückgabewerte sollten immer Werte oder Referenzen sein: X f() oder X& f(). Bei Referenzen ist darauf zu achten, dass dann die Referenz auch außerhalb der Funktion gültig sein muss.

Ein- und Ausgehende Parameter: Parameter, die geändert werden, sollten Referenzen sein: f(X&)

Eingehende Parameter: Parameter elementarer Datentypen (z.B. int): f(X) Parameter aus zusammengesetzten Datentypen oder Klassen (z.B. vector): f(const X&)

In C++ ist es außerdem möglich, mehrere Funktionen mit gleichlautendem Funktionsnamen zu versehen. Dieses Überladen von Funktionsnamen unterscheidet der Compiler beim Aufruf und der Definition der Funktionen durch Anzahl, Typ und Reihenfolge ihrer Parameter.

    double pos(double x) {        // Funktion für "double"-Argumente
        return (x < 0) ? -x : x;  // liefert Absolutbetrag
    }

    int pos(int x) {             // Funktion für "int"-Argumente
        return (x < 0) ? 0 : x;  // liefert für negative Werte 0
    }

    int main()
    {
        pos(-1);    // pos(int)-Funktion wird aufgerufen
                    // (liefert 0)
        pos(-1.0);  // pos(double)-Funktion wird aufgerufen
                    // (liefert 1.0)
    }

Bei der Deklaration von Funktionen kann eine weitere Flexibilität durch Defaultparameter erreicht werden. Dabei wird einem Funktionsparameter standardmäßig ein Wert zugeordnet. Nur wenn die Funktion einen abweichenden Wert erhalten soll, muss dieser explizit angegeben werden.

    void test(int x, int y=1);  // Deklaration: Standardwert y=1

    int main()
    {
        test(5);    // x=5; y=1
        test(7,4);  // x=7; y=4
    }

    void test(int x, int y)  // Definition der Funktion
    { ... }

Die Definition der Defaultparameter erfolgt bei der Deklaration. Bei der Definition wird der Standardwert nicht mehr angegeben. Beachten Sie ggf. auftretende Mehrdeutigkeiten beim Überladen von Funktionen mit Defaultparametern. Defaultparameter können nur für hinten stehende Parameter benutzt werden.

Beispiele für unterschiedliche Funktionen bzgl. des Überladens und mit Defaultparametern:

    double pos(double x, int y);

    double pos(double x, double y);  // y unterschiedlicher Typ
    double pos(double x);            // unterschiedliche Anzahl
    double pos(int x, double y);     // unterschiedliche Reihenfolge

    double pos(double y, int x);  // gleiche Funktion (Name der Argumente spielt keine Rolle) !!!
    int pos(double y, int x);     // falsche Überladung (Typ des Rückgabeparameters spielt keine Rolle) !!!
    double pos(double x, int y=1);  // gleiche Funktion (auch zu double pos(double x));

Präprozessor

Bei C++ gibt es einen Präprozessor, der vor der Compilierung Ersetzungen durchführt. Zeilen, die im Sourcecode mit # eingeleitet werden, heißen Steuerzeilen.

include

Der Präprozessor wird hauptsächlich dazu verwendet, Deklarationen aus anderen Headerdateien verfügbar zu machen. Eine Steuerzeile der Form #include <Dateiname> oder #include "Dateiname" wird vom Präprozessor durch die spezifizierte Datei ersetzt.

Dabei ist die Form mit "" relativ zur aktuellen Datei und sollte für eigenen Quellcode benutzt werden. Ordner werden mit einem Schrägstrich (/) abgetrennt, nicht wie bei Windows mit einem umgekehrten Schrägstrich (\). Das übergeordnete Verzeichnis kann mit .. ausgewählt werden. Dateien, die in der anderen Form (<>) angegeben sind, werden in den voreingestellten Include-Verzeichnissen gesucht und dienen zum Einbinden von Systembibliotheken.

define

Mit der Direktive #define können Konstanten definiert werden. Es gilt folgende Syntax:

    #define KONSTANTE WERT

Der Präprozessor ersetzt vor der Compilierung jedes Auftreten von KONSTANTE im Sourcecode durch den WERT. Dadurch können diese Konstanten auch da benutzt werden, wo die Werte zur Compilierzeit feststehen müssen. Beispiel:

    #define ARRAY_SIZE 100
    int array[ARRAY_SIZE];

In C++ ist es allerdings besser, das const-Schlüsselwort zu verwenden, bei dem die weitere Informationen z.B. über den Datentyp nicht verloren gehen:

    const int array_size = 100;
    int array[array_size];

Zur besseren Unterscheidung von Variablen und Funktionen sollten für Präprozessorkonstanten nur Großbuchstaben und Unterstriche verwendet werden.

Include guards

Mit den bedingten Direktiven #ifdef und #ifndef lässt sich prüfen, ob ein Makro gegenwärtig definiert ist oder nicht. Das ist hilfreich, um Mehrfacheinbindungen von Headerdateien zu verhindern:

    #ifndef HEADER_H
    #define HEADER_H

    class Klasse
    {
    };

    #endif

Auch wenn der Header mehrmals eingebunden wird, wird nur eine Kopie des Inhalts eingefügt:

    #include "header.h"
    #include "header.h"

Die Definition der Konstanten verhindert, dass die Headerdatei mehrmals eingefügt wird und dann mehrere Definitionen der Klasse sichtbar sind, was verboten ist.

Bei vielen Compilern kann dies einfacher durch die Direktive #pragma once erreicht werden:

    #pragma once

    class Klasse
    {
    };

Eine weitere Anwendung des Präprozessor sind funktionsartige Makros. Weil dabei keine Typ-Informationen vorhanden sind, sollten sie vermieden werden und werden hier daher nicht weiter betrachtet.