4 Grundlagen

4.1 C++ Datentypen

Datentypen sind ein grundlegendes Konzept in der Programmierung und bestimmen die Art von Daten, die eine Variable speichern kann. Sie beeinflussen auch die Operationen, die auf den Daten ausgeführt werden können, und die Menge des benötigten Speichers. C++ bietet eine Vielzahl von eingebauten Datentypen, die in diesem Artikel vorgestellt werden.

4.1.1 Grundlegende Datentypen

C++ bietet eine Reihe von grundlegenden Datentypen, um numerische Werte, Zeichen und Boolesche Werte zu speichern:

  1. Ganzzahlen-Typen:
    int alter = 30;
    unsigned int positiveZahl = 100;
  2. Fließkomma-Typen:
    double pi = 3.14159;
  3. Zeichentypen:
    char buchstabe = 'A';
  4. Boolescher Typ:
    bool istWahr = true;

4.1.2 Der sizeof Operator

Um herauszufinden, wie viel Speicher (in Bytes) ein bestimmter Datentyp benötigt, kann der sizeof Operator verwendet werden:

std::cout << "Ein int benötigt: " << sizeof(int) << " Bytes" << std::endl;

4.1.3 Typumwandlung

Manchmal muss man den Typ eines Wertes in einen anderen konvertieren. Dies kann entweder implizit (automatisch) oder explizit (manuell) geschehen.

4.1.4 TLDR;

Datentypen sind in C++ unerlässlich, um sicherzustellen, dass Daten korrekt gespeichert und verarbeitet werden. Durch ein gutes Verständnis der verschiedenen Typen und ihrer Eigenschaften können Programmierer effizienten und fehlerfreien Code schreiben. Es ist auch wichtig, sich der impliziten und expliziten Typumwandlung bewusst zu sein, um unerwartete Ergebnisse oder Fehler zu vermeiden.

4.2 Type auto

Das Schlüsselwort auto in C++ wird zur automatischen Typinferenz verwendet. Dies bedeutet, dass der Compiler selbst bestimmt, welchen Datentyp eine Variable hat, basierend auf dem Wert, der ihr während der Initialisierung zugewiesen wird. Mit der Einführung von C++11 wurde die Verwendung von auto ausgeweitet und vereinfacht, und es wurde zu einem nützlichen Werkzeug für Entwickler, insbesondere in Situationen mit komplizierten Datentypen oder Templates.

4.2.1 Anwendungsbereiche von auto

  1. Einfache Typinferenz

    Anstatt den exakten Typ einer Variable explizit anzugeben, kann auto verwendet werden, und der Compiler wird den Typ basierend auf dem Initialisierungswert bestimmen:

    auto i = 5;      // i ist vom Typ int
    auto s = "hello";// s ist vom Typ const char*
    auto d = 3.14;   // d ist vom Typ double
  2. Für Iterator-Typen

    Statt den kompletten Iterator-Typ für STL-Container anzugeben, kann auto verwendet werden:

    std::vector<int> v = {1, 2, 3, 4, 5};
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << std::endl;
    }
  3. Range-based for Loops

    In Kombination mit range-based for loops in C++11 ist auto besonders nützlich:

    for (auto &value : v) {
        std::cout << value << std::endl;
    }
  4. Für komplizierte Datentypen

    In Fällen, in denen der Datentyp eines Ausdrucks besonders kompliziert ist, kann auto den Code lesbarer machen:

    std::map<int, std::string> map = {{1, "one"}, {2, "two"}};
    auto pair = *map.begin(); // pair ist vom Typ std::pair<const int, std::string>
  5. Mit Lambdas

    auto kann auch verwendet werden, um einen Lambda-Ausdruck zu speichern:

    auto lambda = [](int x, int y) { return x + y; };
    std::cout << lambda(3, 4); // Ausgabe: 7

4.2.2 Vorsicht bei auto

Obwohl auto in vielen Situationen nützlich ist, sollte man vorsichtig sein und sicherstellen, dass der inferierte Typ tatsächlich der gewünschte Typ ist. Insbesondere bei arithmetischen Ausdrücken kann auto manchmal zu unerwarteten Typen führen.

4.2.3 TLDR;

Das Schlüsselwort auto in C++ bietet eine bequeme Möglichkeit zur automatischen Typinferenz und kann den Code erheblich verkürzen und lesbarer machen. Es ist jedoch wichtig, es mit Bedacht zu verwenden und sicherzustellen, dass der Compiler den richtigen Typ ableitet.

4.3 Variablen

Variablen sind ein grundlegendes Konzept in fast jeder Programmiersprache, und C++ ist natürlich keine Ausnahme. Sie dienen als Platzhalter für Datenwerte, die während der Laufzeit eines Programms geändert werden können. In diesem Artikel werden wir die Grundlagen von Variablen in C++ betrachten.

4.3.1 Was ist eine Variable?

In C++ ist eine Variable im Grunde ein Name, der einem Speicherort zugewiesen wird. Dieser Speicherort kann Daten enthalten und diese Daten können im Laufe der Programmausführung geändert werden.

4.3.2 Deklaration und Initialisierung

Bevor eine Variable verwendet werden kann, muss sie deklariert werden. Bei der Deklaration wird der Typ der Variable angegeben:

int zahl;
double pi;

Eine Variable kann bei der Deklaration auch initialisiert werden:

int jahre = 5;
char zeichen = 'A';

4.3.3 Datentypen

In C++ muss jede Variable einen Datentyp haben. Der Datentyp bestimmt die Art von Daten, die die Variable halten kann, und den Speicher, der für diese Variable reserviert wird. Einige der grundlegenden Datentypen sind:

4.3.4 Variable Scope (Geltungsbereich)

Der Geltungsbereich einer Variable bezieht sich darauf, wo sie im Code sichtbar und zugänglich ist:

  1. Lokale Variablen: Diese sind innerhalb von Funktionen oder Blöcken definiert und können nur dort verwendet werden, wo sie deklariert wurden.
  2. Globale Variablen: Sie werden außerhalb von Funktionen deklariert und können im gesamten Code verwendet werden.
  3. Klassenvariablen: Diese sind innerhalb von Klassen definiert und ihre Verfügbarkeit hängt von ihrem Zugriffsmodifizierer (private, protected, public) ab.

4.3.5 TLDR;

Variablen sind unerlässliche Bestandteile in der Programmierung mit C++. Sie ermöglichen es, Daten zu speichern und zu manipulieren, was für die Funktionalität der meisten Programme von entscheidender Bedeutung ist. Ein gründliches Verständnis darüber, wie und wann man Variablen in C++ verwendet, ist für angehende Programmierer von großer Bedeutung.

4.4 Arrays

Ein Array ist eine Sammlung von Elementen desselben Datentyps, die unter einem gemeinsamen Namen gespeichert werden. Jedes Element eines Arrays kann durch einen Index (meist eine Ganzzahl) eindeutig identifiziert werden.

4.4.1 Deklaration und Initialisierung von Arrays

Ein Array wird in C++ wie folgt deklariert:

Datentyp ArrayName[Größe];

Beispiel:

int zahlen[5]; // Ein Array von fünf Ganzzahlen

Ein Array kann auch bei der Deklaration initialisiert werden:

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

Oder lassen Sie den Compiler die Größe des Arrays aus der Initialisiererliste ableiten:

int zahlen[] = {1, 2, 3, 4, 5}; // Größe wird automatisch auf 5 gesetzt

4.4.2 Zugriff auf Array-Elemente

Der Zugriff auf ein Element des Arrays erfolgt über den Index:

zahlen[0] = 10; // Das erste Element des Arrays hat den Index 0
int x = zahlen[1]; // x erhält den Wert 2

4.4.3 Größe eines Arrays

Die Größe eines Arrays kann mithilfe des sizeof-Operators bestimmt werden:

int groesse = sizeof(zahlen) / sizeof(zahlen[0]);

4.4.4 Multidimensionale Arrays

Es ist auch möglich, mehrdimensionale Arrays (z.B. Matrizen) zu deklarieren:

int matrix[3][4]; // Ein 3x4-Array (3 Zeilen und 4 Spalten)

Initialisierung eines zweidimensionalen Arrays:

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

4.4.5 Wichtige Punkte zu beachten

  1. Arrays haben eine feste Größe. Diese Größe muss bei der Deklaration bekannt sein und kann später nicht geändert werden.
  2. Der Zugriff auf ein Array-Element außerhalb seiner Grenzen führt zu undefiniertem Verhalten.
  3. In C++ gibt es keine eingebauten Funktionen, um die Größe eines Arrays zu ermitteln (wie z.B. length in anderen Sprachen). Deshalb verwenden C++-Entwickler oft Container wie std::vector, die dynamisch sind und viele hilfreiche Methoden bieten.

4.4.6 TLDR;

Arrays sind eine der grundlegendsten und ältesten Datenstrukturen in C++. Obwohl sie in ihrer Funktionalität begrenzt sind, insbesondere im Vergleich zu modernen Containerklassen wie std::vector, bieten sie dennoch einen schnellen und direkten Zugriff auf eine Sammlung von Elementen desselben Typs. Bei der Arbeit mit Arrays sollte man jedoch immer vorsichtig sein und sicherstellen, dass man nicht außerhalb ihrer Grenzen zugreift.

4.5 const und constexpr in C++

In C++ sind const und constexpr zwei Schlüsselwörter, die verwendet werden, um Konstanz und Ausdrücke zur Kompilierzeit auszudrücken. Obwohl sie in einigen Kontexten ähnlich erscheinen mögen, haben sie unterschiedliche Verwendungen und Bedeutungen.

4.5.1 const

Das Schlüsselwort const wird in C++ verwendet, um einen Wert oder ein Objekt als konstant zu deklarieren. Das bedeutet, dass sein Wert nach der Initialisierung nicht geändert werden kann.

  1. Konstante Variablen

    const int x = 10; // x kann nicht geändert werden
  2. Konstante Zeiger

  3. Konstante Funktionen

    Ein Memberfunktion kann mit const deklariert werden, was bedeutet, dass die Funktion den Zustand des Objekts nicht verändern wird.

    class MyClass {
    public:
        int getValue() const {
            return value;
        }
    private:
        int value;
    };

4.5.2 constexpr

Das Schlüsselwort constexpr wurde in C++11 eingeführt und steht für “constant expression”. Es wird verwendet, um anzugeben, dass ein Wert, eine Funktion oder eine Variable zur Kompilierzeit ausgewertet wird.

  1. constexpr Variablen

    Das bedeutet, dass der Wert dieser Variablen bereits zur Kompilierzeit festgelegt ist.

    constexpr int y = x * 2;  // y ist zur Kompilierzeit bekannt, vorausgesetzt x ist auch constexpr oder eine konstante Literale
  2. constexpr Funktionen

    Eine constexpr Funktion wird zur Kompilierzeit ausgewertet, wenn sie mit konstanten Ausdrücken aufgerufen wird. Sie kann auch zur Laufzeit ausgewertet werden, aber in diesem Fall wird sie wie eine normale Funktion behandelt.

    constexpr int square(int n) {
        return n * n;
    }
    
    constexpr int z = square(5); // z wird zur Kompilierzeit ausgewertet

4.5.3 Hauptunterschiede

4.5.4 TLDR;

Sowohl const als auch constexpr sind nützliche Werkzeuge in C++, um die Intention des Entwicklers klar auszudrücken und mögliche Fehler zu verhindern. Während const in erster Linie die Unveränderlichkeit betont, legt constexpr den Schwerpunkt auf die Kompilierzeit-Auswertung.

4.6 Strings in C++

In C++ gibt es zwei Hauptwege, um Zeichenketten oder Strings zu repräsentieren: C-Style-Zeichenketten und die std::string-Klasse. Während C-Style-Zeichenketten direkt aus der C-Sprache übernommen wurden, ist std::string eine moderne und flexiblere Möglichkeit, mit Zeichenketten in C++ zu arbeiten.

4.6.1 C-Style-Zeichenketten

C-Style-Zeichenketten sind Arrays von char-Elementen, die mit einem Null-Zeichen ('\0') enden.

Deklaration und Initialisierung:

char cstr[] = "Hallo";

Ein Problem mit C-Style-Zeichenketten ist, dass sie oft manuellen Speicher- und Zeichenkettenmanagement erfordern. Zum Beispiel:

char buffer[50];
strcpy(buffer, "Hallo, ");
strcat(buffer, "Welt!");

4.6.2 std::string

Die std::string-Klasse ist Teil der C++-Standardbibliothek und bietet eine bequemere und sicherere Schnittstelle zum Arbeiten mit Zeichenketten.

Deklaration und Initialisierung:

#include <string>

std::string s = "Hallo";

Einige Operationen mit std::string:

std::string s1 = "Hallo";
std::string s2 = "Welt";
std::string s3 = s1 + ", " + s2 + "!";  // Konkatenation
std::string s = "Hallo";
s.size();  // Gibt die Länge des Strings zurück
std::string s = "Hallo";
s[0] = 'h';  // Ändert das erste Zeichen zu 'h'

4.6.3 Umwandlung zwischen std::string und C-Style-Zeichenketten

Manchmal ist es notwendig, zwischen std::string und C-Style-Zeichenketten zu konvertieren, insbesondere wenn man mit APIs arbeitet, die C-Style-Zeichenketten erfordern.

std::string str = "Hallo";
const char* cstr = str.c_str();  // Konvertiert std::string zu const char*

4.6.4 Vorteile von std::string gegenüber C-Style-Zeichenketten

4.6.5 TLDR;

Während C-Style-Zeichenketten immer noch in einigen älteren Codebasen und in bestimmten APIs verwendet werden, ist std::string in den meisten modernen C++-Anwendungen die bevorzugte Wahl. Es bietet einen erheblichen Vorteil in Bezug auf Sicherheit, Effizienz und Benutzerfreundlichkeit.

4.7 Casts

Casting ist ein Mechanismus, mit dem Entwickler den Typ eines Wertes explizit ändern können. Während C-Style-Casts aufgrund ihrer Einfachheit und Breite oft verwendet werden, hat C++ vier spezifische Cast-Operatoren eingeführt, um mehr Klarheit und Sicherheit zu bieten. Hier ist ein Überblick über beide Ansätze:

4.7.1 1. C-Style Casts

C-Style-Casts haben die Form (Typ) ausdruck oder Typ(ausdruck).

Beispiel:

double d = 3.14;
int i = (int)d; // C-Style-Cast

Diese Casts sind jedoch nicht typsicher und können in vielerlei Hinsicht interpretiert werden (als const_cast, static_cast, dynamic_cast oder reinterpret_cast), je nachdem, was im gegebenen Kontext zulässig ist. Dies macht sie potenziell gefährlich und schwer zu lesen.

4.7.2 2. C++ Casts

C++ bietet vier spezifische Casting-Operatoren, die jeweils für bestimmte Zwecke entwickelt wurden:

4.7.2.1 a) static_cast

Verwendet für allgemeine Typumwandlungen, die zur Kompilierzeit überprüft werden.

double d = 3.14;
int i = static_cast<int>(d); // Konvertiert `double` zu `int`

4.7.2.2 b) dynamic_cast

Nur für Zeiger oder Referenzen von Polymorphietypen. Wird verwendet, um sicherzustellen, dass die Umwandlung zwischen den Zeigertypen eines Vererbungshierarchie gültig ist.

Base* bPtr = new Derived();
Derived* dPtr = dynamic_cast<Derived*>(bPtr); // Gültig, wenn `Derived` von `Base` erbt und `Base` mindestens eine virtuelle Methode hat

Wenn der Cast fehlschlägt (d.h. das Objekt ist nicht vom Typ Derived), gibt dynamic_cast einen Nullzeiger zurück.

4.7.2.3 c) const_cast

Verwendet, um das const-Attribut eines Objekts hinzuzufügen oder zu entfernen.

const int a = 10;
int* p = const_cast<int*>(&a); // Entfernt `const`

Dies sollte mit Vorsicht verwendet werden, da das Ändern eines zuvor als const deklarierten Wertes undefiniertes Verhalten verursachen kann.

4.7.2.4 d) reinterpret_cast

Für low-level Casts, die einen beliebigen Zeiger in einen anderen Zeigertyp umwandeln. Diese Art von Cast sollte nur verwendet werden, wenn man genau weiß, was man tut, da sie sehr unsicher sein kann.

int i = 42;
int* p = &i;
char* ch = reinterpret_cast<char*>(p); // Interpretiert den `int`-Zeiger als `char`-Zeiger

4.7.3 TLDR;

Obwohl C-Style-Casts einfacher zu schreiben sind und in vielen Legacy-Codes vorhanden sind, wird dringend empfohlen, in modernem C++-Code die spezifischen C++-Casting-Operatoren zu verwenden. Sie bieten nicht nur mehr Sicherheit und Genauigkeit, sondern machen den Code auch ausdrucksstärker und einfacher zu verstehen.

4.8 Funktionen

Funktionen sind eine der grundlegenden Bausteine in der Programmierung. Sie ermöglichen es uns, Codeblöcke zu definieren, die eine spezifische Aufgabe erfüllen und bei Bedarf aufgerufen werden können. Dieser Artikel gibt einen Überblick über Funktionen in C++, ihre Deklaration, Definition und Verwendung, ergänzt durch praktische Codebeispiele.

4.8.1 Grundlagen der Funktionen

Eine Funktion in C++ besteht aus einem Rückgabetyp, einem Funktionsnamen, Parametern in Klammern und einem Funktionskörper.

Syntax:

rückgabetyp funktionsname(parameter1, parameter2, ...) {
    // Funktionskörper
    // ...
    return wert; // Nur notwendig, wenn ein Rückgabetyp außer void angegeben ist.
}

4.8.2 Funktionsbeispiel

Hier ist ein einfaches Beispiel für eine Funktion, die zwei Zahlen addiert:

int addiere(int a, int b) {
    return a + b;
}

int main() {
    int summe = addiere(5, 3);
    std::cout << "Das Ergebnis von 5 + 3 ist: " << summe << std::endl; // Ausgabe: 8
    return 0;
}

4.8.3 Rückgabetyp void

Wenn eine Funktion keinen Wert zurückgeben soll, wird der Rückgabetyp void verwendet:

void begruesse() {
    std::cout << "Hallo, Welt!" << std::endl;
}

int main() {
    begruesse(); // Ausgabe: Hallo, Welt!
    return 0;
}

4.8.4 Parameter und Argumente

// 'x' und 'y' sind Parameter
double multipliziere(double x, double y) {
    return x * y;
}

int main() {
    // 2.5 und 3.0 sind Argumente
    double produkt = multipliziere(2.5, 3.0);
    std::cout << produkt << std::endl; // Ausgabe: 7.5
    return 0;
}

4.8.5 Standardwerte für Parameter

In C++ können Sie Standardwerte für Funktionenparameter definieren:

void zeigeNachricht(std::string nachricht = "Keine Nachricht") {
    std::cout << nachricht << std::endl;
}

int main() {
    zeigeNachricht(); // Ausgabe: Keine Nachricht
    zeigeNachricht("Hallo!"); // Ausgabe: Hallo!
    return 0;
}

4.8.6 Überladen von Funktionen

In C++ ist es möglich, mehrere Funktionen mit demselben Namen zu definieren, solange sie unterschiedliche Parameter haben. Dies nennt man Funktionsüberladung.

int addiere(int a, int b) {
    return a + b;
}

double addiere(double a, double b) {
    return a + b;
}

int main() {
    std::cout << addiere(3, 4) << std::endl;       // Ausgabe: 7
    std::cout << addiere(2.5, 1.5) << std::endl;  // Ausgabe: 4.0
    return 0;
}

4.8.7 TLDR;

Funktionen sind ein zentrales Konzept in der C++-Programmierung und in der Programmierung im Allgemeinen. Sie ermöglichen es, wiederholten Code in separate Einheiten auszulagern, die wiederverwendet werden können. Dies fördert eine klare Struktur, Modularität und Wartbarkeit des Codes. Es ist wichtig, sich mit der Syntax und den Best Practices für Funktionen in C++ vertraut zu machen, um effizient und effektiv zu programmieren.

4.9 Call by Value vs. Call by Reference in C++

In C++ können Funktionen ihre Argumente entweder nach Wert oder nach Referenz übergeben. Dieser Unterschied ist für das Verständnis des Funktionsverhaltens und der möglichen Nebenwirkungen entscheidend.

4.9.1 Call by Value

Bei der Übergabe nach Wert wird eine Kopie des Arguments an die Funktion übergeben. Änderungen an diesem kopierten Wert innerhalb der Funktion haben keine Auswirkungen auf das ursprüngliche Argument außerhalb der Funktion.

Beispiel:

void funktionByValue(int x) {
    x = 10;
}

int main() {
    int a = 5;
    funktionByValue(a);
    std::cout << a;  // Gibt 5 aus, da 'a' nicht geändert wurde
    return 0;
}

4.9.2 Call by Reference

Bei der Übergabe nach Referenz wird ein Alias oder eine Referenz auf das ursprüngliche Argument an die Funktion übergeben, nicht eine Kopie. Änderungen am Alias innerhalb der Funktion wirken sich daher auf das ursprüngliche Argument aus.

Beispiel:

void funktionByReference(int &x) {
    x = 10;
}

int main() {
    int a = 5;
    funktionByReference(a);
    std::cout << a;  // Gibt 10 aus, da 'a' durch die Funktion geändert wurde
    return 0;
}

4.9.3 Pointer als Alternative

Neben direkten Referenzen kann C++ auch mit Pointern arbeiten, die Adressen von Variablen speichern. Übergeben Sie die Adresse einer Variablen an eine Funktion, können Sie über den Pointer die Originalvariable modifizieren.

void funktionMitPointer(int *x) {
    *x = 10;
}

int main() {
    int a = 5;
    funktionMitPointer(&a);
    std::cout << a;  // Gibt 10 aus
    return 0;
}

4.9.4 Wann sollte was verwendet werden?

4.9.5 TLDR;

Das Verständnis der Unterschiede zwischen Call by Value und Call by Reference ist entscheidend, um vorherzusehen, wie Funktionen sich verhalten und um unbeabsichtigte Seiteneffekte zu vermeiden. In C++ bieten beide Ansätze ihre eigenen Vorteile und Anwendungsfälle.

4.10 Arithmetik

Arithmetik bezieht sich auf die grundlegenden mathematischen Operationen, die in der Programmierung häufig eingesetzt werden, wie Addition, Subtraktion, Multiplikation und Division. Dieser Artikel bietet einen Überblick über die arithmetischen Operationen und ihre Anwendung in der Programmierung.

4.10.1 Grundlegende arithmetische Operationen

  1. Addition (+): Summiert zwei Zahlen.

    int a = 5;
    int b = 3;
    int summe = a + b; // Ergebnis ist 8
  2. Subtraktion (-): Zieht eine Zahl von einer anderen ab.

    int differenz = a - b; // Ergebnis ist 2
  3. **Multiplikation (*)**: Multipliziert zwei Zahlen.

    int produkt = a * b; // Ergebnis ist 15
  4. Division (/): Teilt eine Zahl durch eine andere. Beachten Sie, dass bei der Division von Ganzzahlen das Ergebnis abgerundet wird.

    int quotient = a / b; // Ergebnis ist 1 bei Ganzzahldivision
  5. Modulus (%): Gibt den Rest einer Division zurück. Häufig verwendet, um zu überprüfen, ob eine Zahl durch eine andere Zahl teilbar ist.

    int rest = a % b; // Ergebnis ist 2

4.10.2 Inkrement und Dekrement

In vielen Programmiersprachen gibt es spezielle Operatoren für das Inkrementieren (Erhöhen um 1) und Dekrementieren (Verringern um 1) von Variablen:

4.10.3 Arithmetische Zuweisungen

Arithmetische Zuweisungen kombinieren eine arithmetische Operation mit einer Zuweisung. Sie sind in vielen Programmiersprachen üblich und erleichtern die Schreibweise:

4.10.4 Fließkommadivision vs. Ganzzahldivision

In einigen Programmiersprachen, einschließlich C++ und Java, unterscheidet sich die Division, je nachdem, ob die beteiligten Zahlen Ganzzahlen oder Fließkommazahlen sind:

int c = 5 / 2;      // Ergebnis ist 2, da dies eine Ganzzahldivision ist
double d = 5.0 / 2; // Ergebnis ist 2.5, da dies eine Fließkommadivision ist

4.10.5 TLDR;

Arithmetik ist eine grundlegende Komponente in der Programmierung. Die Fähigkeit, mathematische Operationen auf Daten anzuwenden, ist entscheidend für die Datenverarbeitung und -analyse. Ein solides Verständnis der arithmetischen Operationen und ihrer Nuancen in der gewählten Programmiersprache ist für jeden Programmierer unerlässlich.

4.11 Logische und bitweise Operationen

In der Informatik und Programmierung sind logische und bitweise Operationen essenzielle Werkzeuge, um Entscheidungen zu treffen und Daten auf niedriger Ebene zu manipulieren.

4.11.1 Logische Operationen

Logische Operationen werden häufig in bedingten Anweisungen verwendet, um Entscheidungen zu treffen. Die gängigsten logischen Operatoren sind:

  1. AND (&&): Gibt true zurück, wenn beide Operanden wahr sind.

    bool res = (a == 5) && (b == 3);  // res ist true, wenn a 5 ist und b 3 ist
  2. OR (||): Gibt true zurück, wenn mindestens einer der Operanden wahr ist.

    bool res = (a == 5) || (b == 4);  // res ist true, wenn a 5 ist oder b 4 ist
  3. NOT (!): Kehrt den Wahrheitswert des Operanden um.

    bool res = !(a == 5);  // res ist false, wenn a 5 ist

4.11.2 Bitweise Operationen

Bitweise Operationen arbeiten auf einzelnen Bits von Ganzzahlen. Sie sind nützlich für Aufgaben wie das Setzen oder Löschen spezifischer Bits und die Manipulation von Daten auf binärer Ebene.

  1. Bitweises AND (&): Jedes Bit im Ergebnis ist 1, wenn das entsprechende Bit in beiden Operanden 1 ist.

    int res = 5 & 3;  // 5 ist 0101 und 3 ist 0011; das Ergebnis (1) ist 0001
  2. Bitweises OR (|): Jedes Bit im Ergebnis ist 1, wenn mindestens ein entsprechendes Bit in einem der Operanden 1 ist.

    int res = 5 | 3;  // Das Ergebnis (7) ist 0111
  3. Bitweises XOR (^): Jedes Bit im Ergebnis ist 1, wenn genau ein entsprechendes Bit in einem der Operanden 1 ist.

    int res = 5 ^ 3;  // Das Ergebnis (6) ist 0110
  4. Bitweises NOT (~): Kehrt jeden Bit des Operanden um.

    int res = ~5;  // Das Ergebnis wird -6 sein (abhängig von der Zweierkomplementdarstellung)
  5. Linksverschiebung (<<): Verschiebt die Bits eines Operanden nach links.

    int res = 5 << 1;  // Das Ergebnis (10) ist 1010
  6. Rechtsverschiebung (>>): Verschiebt die Bits eines Operanden nach rechts.

    int res = 5 >> 1;  // Das Ergebnis (2) ist 0010

4.11.3 Anwendungsbereiche

4.11.4 TLDR;

Sowohl logische als auch bitweise Operationen sind wichtige Werkzeuge im Arsenal eines jeden Programmierers. Sie bieten die Mittel, um sowohl hochrangige Entscheidungen zu treffen als auch Daten auf der tiefsten Ebene zu manipulieren. Ein Verständnis ihrer Arbeitsweise und Anwendung ist entscheidend für viele Programmieraufgaben.

4.12 Kontrollstrukturen

Kontrollstrukturen ermöglichen es, den Fluss eines Programms zu steuern. In C++ gibt es eine Vielzahl von solchen Strukturen, darunter bedingte Anweisungen, Schleifen und Auswahlstrukturen.

4.12.1 Bedingte Anweisungen

4.12.1.1 if-Anweisung:

Diese Anweisung überprüft, ob eine bestimmte Bedingung erfüllt ist, und führt dann den entsprechenden Codeblock aus.

if (Bedingung) {
    // Code, der ausgeführt wird, wenn die Bedingung wahr ist
}

4.12.1.2 if-else-Anweisung:

Ermöglicht das Hinzufügen eines alternativen Codeblocks, der ausgeführt wird, wenn die Bedingung nicht erfüllt ist.

if (Bedingung) {
    // Code für wahren Zustand
} else {
    // Code für falschen Zustand
}

4.12.1.3 if-else if-else-Kette:

Wird verwendet, um mehrere Bedingungen nacheinander zu überprüfen.

if (Bedingung1) {
    // Code für Bedingung1
} else if (Bedingung2) {
    // Code für Bedingung2
} else {
    // Code, wenn keine der vorherigen Bedingungen erfüllt ist
}

4.12.2 Schleifen

4.12.2.1 while-Schleife:

Führt einen Codeblock aus, solange eine Bedingung erfüllt ist.

while (Bedingung) {
    // Codeblock
}

4.12.2.2 do-while-Schleife:

Ähnlich wie die while-Schleife, führt aber den Codeblock mindestens einmal aus, bevor die Bedingung überprüft wird.

do {
    // Codeblock
} while (Bedingung);

4.12.2.3 for-Schleife:

Klassische Schleife mit Initialisierung, Bedingung und Inkrement/Update.

for (Initialisierung; Bedingung; Update) {
    // Codeblock
}

4.12.2.4 Range-based for-Schleife (C++11):

Ermöglicht das einfache Durchlaufen von Elementen in Containern.

for (auto element : container) {
    // Codeblock
}

4.12.3 Auswahlstrukturen

4.12.3.1 switch-Anweisung:

Wird verwendet, um basierend auf dem Wert einer Variable oder eines Ausdrucks einen von mehreren Codeblöcken auszuführen.

switch (variable) {
    case Wert1:
        // Code für Wert1
        break;
    case Wert2:
        // Code für Wert2
        break;
    default:
        // Code, wenn kein case zutrifft
}

4.12.4 Ternärer Operator

Der ternäre Operator ? : ist eine Kurzform der if-else-Anweisung.

Variable = (Bedingung) ? WertWennWahr : WertWennFalsch;

4.12.5 break und continue

4.12.6 break

Die break-Anweisung wird verwendet, um die Ausführung der innersten Schleife oder switch-Anweisung, in der sie sich befindet, sofort zu beenden.

4.12.7 continue

Die continue-Anweisung wird in Schleifen verwendet und bewirkt, dass der aktuelle Durchlauf der Schleife sofort beendet und der nächste Durchlauf (falls vorhanden) gestartet wird.

for (int i = 0; i < 10; ++i) {
    if (i % 2 == 0) {
        continue; // Überspringt den restlichen Code in der Schleife, wenn i gerade ist
    }
    std::cout << i << std::endl;
}

In diesem Beispiel werden nur ungerade Zahlen zwischen 0 und 9 ausgegeben, da der Code die Ausgabe überspringt und zum nächsten Durchlauf der Schleife zurückkehrt, wenn i eine gerade Zahl ist.

4.12.8 TLDR;

Kontrollstrukturen sind fundamental in jeder Programmiersprache und ermöglichen es, komplexe Programme und Algorithmen zu erstellen. In C++ gibt es eine Vielzahl von solchen Strukturen, die dem Entwickler die Flexibilität bieten, den Codefluss genau so zu gestalten, wie er benötigt wird.

4.13 Initialisierung in C++: Zuweisung vs. Listeninitialisierung

Die Initialisierung von Variablen und Objekten ist ein grundlegendes Konzept in C++, das die Zuweisung eines Anfangswerts an eine Variable beinhaltet. Es gibt mehrere Möglichkeiten, eine Variable in C++ zu initialisieren, und jede hat ihre eigenen Vorteile und Nuancen.

4.13.1 Zuweisungsinitialisierung

Bei der Zuweisungsinitialisierung wird der Zuweisungsoperator = verwendet:

int a = 5;
std::string s = "Hello, World!";

4.13.2 Listeninitialisierung

Mit C++11 wurde die Listeninitialisierung eingeführt, die auch als uniforme Initialisierung bezeichnet wird. Sie verwendet geschweifte Klammern {}:

int a{5};
std::string s{"Hello, World!"};

4.13.2.1 Vorteile der Listeninitialisierung:

4.13.3 Initialisierung ohne Zuweisungsoperator

Es ist auch möglich, Variablen ohne den Zuweisungsoperator zu initialisieren:

int a(5);
std::string s("Hello, World!");

Dies ist die traditionelle Methode, Konstruktoren in C++ aufzurufen, und sie funktioniert sowohl für eingebaute Typen als auch für benutzerdefinierte Klassen.

4.13.4 Listeninitialisierung ohne und mit Zuweisungsoperator

Sie können die Listeninitialisierung mit oder ohne den Zuweisungsoperator verwenden:

int a{5};         // Ohne Zuweisungsoperator
int b = {5};      // Mit Zuweisungsoperator

Beide Ansätze sind gültig und führen zum gleichen Ergebnis. Die Wahl zwischen ihnen hängt oft von persönlichen Vorlieben oder dem Codestil eines Projekts ab.

4.13.5 TLDR

C++ bietet mehrere Möglichkeiten, Variablen und Objekte zu initialisieren. Während die traditionelle Zuweisungsinitialisierung und die Konstruktoraufrufsyntax immer noch weit verbreitet sind, bietet die Listeninitialisierung einen einheitlichen Ansatz, der viele der Tücken früherer Initialisierungsmethoden vermeidet. Es ist wichtig, mit allen Methoden vertraut zu sein und die am besten geeignete Methode für die jeweilige Situation auszuwählen.

4.14 Namespaces

In der Programmierung dienen Namespaces dazu, den Code zu organisieren und Namenskonflikte zu vermeiden. Dies ist besonders nützlich in großen Projekten oder wenn man mit mehreren Bibliotheken arbeitet. C++ bietet mit dem namespace-Konstrukt eine elegante Möglichkeit, solche logischen Codebereiche zu erstellen.

4.14.1 Was sind Namespaces?

Ein Namespace in C++ ist ein deklarativer Bereich, der dazu dient, Identifikatoren (Klassen, Funktionen, Variablen usw.) zu gruppieren. So können beispielsweise zwei Bibliotheken die gleiche Klassen- oder Funktionsnamen verwenden, ohne sich gegenseitig zu stören.

4.14.2 Grundlegende Syntax

namespace MeinNamespace {
    int meineVariable = 5;
    void meineFunktion() {
        // Funktion implementieren
    }
}

4.14.3 Zugriff auf Elemente in einem Namespace

Um auf Elemente in einem Namespace zuzugreifen, verwenden Sie den Namespace-Namen gefolgt von :::

#include <iostream>

namespace Beispiel {
    int zahl = 42;
}

int main() {
    std::cout << Beispiel::zahl << std::endl; // Ausgabe: 42
    return 0;
}

4.14.4 Das using-Schlüsselwort

Anstatt ständig den vollständigen Namespace-Pfad zu schreiben, können Sie das using-Schlüsselwort verwenden:

using Beispiel::zahl;

int main() {
    std::cout << zahl << std::endl; // Ausgabe: 42
    return 0;
}

4.14.5 using namespace

Sie können auch alle Inhalte eines Namespaces auf einmal mit using namespace importieren:

using namespace Beispiel;

int main() {
    std::cout << zahl << std::endl; // Ausgabe: 42
    return 0;
}

4.14.6 Vorsicht beim Verwenden von using namespace

Obwohl using namespace bequem ist, kann es zu Problemen führen, insbesondere wenn zwei Namespaces denselben Namen für verschiedene Dinge verwenden. Dies kann zu unerwarteten Namenskonflikten und schwer zu findenden Fehlern führen. Ein häufiger Anfängerfehler in C++ ist, using namespace std; am Anfang jeder Datei hinzuzufügen. Dies kann zu Namenskollisionen führen, besonders wenn man mit mehreren Bibliotheken arbeitet.

4.14.7 Verschachtelte Namespaces

Namespaces können auch verschachtelt werden:

namespace Äußerer {
    int x = 10;

    namespace Innerer {
        int x = 20;
    }
}

int main() {
    std::cout << Äußerer::x << std::endl;           // Ausgabe: 10
    std::cout << Äußerer::Innerer::x << std::endl;  // Ausgabe: 20
    return 0;
}

4.14.8 Anonyme Namespaces

Anonyme Namespaces sind spezielle Namespaces, die keinen Namen haben. Sie sind nur in der Datei sichtbar, in der sie definiert sind:

namespace {
    int interneVariable = 42;
}

int main() {
    std::cout << interneVariable << std::endl; // Ausgabe: 42
    return 0;
}

4.14.9 TLDR;

Namespaces sind ein mächtiges Werkzeug in C++, das es ermöglicht, Code sauber zu organisieren und Namenskonflikte zu vermeiden. Es ist jedoch wichtig, sorgfältig darüber nachzudenken, wie und wann using namespace eingesetzt wird, um unerwartete Probleme zu vermeiden. Indem man die besten Praktiken verfolgt, kann man den Vorteil von Namespaces nutzen und gleichzeitig den Code lesbar und wartbar halten.

4.15 Unions

Eine Union in C++ ist ein Datentyp, der es ermöglicht, mehrere verschiedene Typen von Daten im selben Speicherbereich zu speichern. Dies wird durch das Überlagern der Daten in einem gemeinsamen Speicherbereich erreicht, so dass sich alle Mitglieder der Union denselben Speicher teilen. Das bedeutet, dass eine Union immer nur so groß ist wie ihr größtes Mitglied (plus mögliche Ausrichtungspolsterung).

4.15.1 Grundlegende Syntax

Die Deklaration einer Union ähnelt der einer Struktur:

union MeinTyp {
    int integer;
    double dezimal;
    char zeichen;
};

Man kann nun einen Wert zu integer oder dezimal oder zeichen zuweisen, aber nicht gleichzeitig. Denn wenn man einen Wert zu einem Mitglied zuweist, wird der vorherige Wert des anderen Mitglieds überschrieben.

4.15.2 Verwendung

Hier ist ein einfaches Beispiel zur Verwendung einer Union:

union Zahl {
    int i;
    double d;
};

Zahl meineZahl;
meineZahl.i = 42; // Setzt den int-Wert
// meineZahl.d enthält jetzt keine gültigen Daten mehr
meineZahl.d = 3.14; // Setzt den double-Wert und überschreibt den int-Wert

4.15.3 Typische Verwendung von Unions

Ein häufiger Anwendungsfall für Unions ist die Implementierung von Tagged Unions oder Varianten. Hierbei wird eine Struktur zusammen mit einer Union verwendet, um sowohl den tatsächlich gespeicherten Datentyp (in Form eines Tags oder eines Enum) als auch den Datenwert selbst zu speichern.

enum Typ { INTEGER, DEZIMAL, ZEICHEN };

struct TaggedZahl {
    Typ typ;
    union {
        int i;
        double d;
        char c;
    } wert;
};

4.15.4 Vorsichtsmaßnahmen

4.15.5 Moderne Alternativen

Mit den neueren C++-Versionen gibt es sicherere Alternativen zu Unions, wie z.B. std::variant (ab C++17). std::variant ermöglicht es, einen von mehreren Typen sicher zu speichern und bietet Mechanismen, um auf den aktuell gespeicherten Typ sicher zuzugreifen.

4.15.6 TLDR;

Während Unions ein nützliches Werkzeug für spezifische Anforderungen sein können, insbesondere in Systemprogrammierung oder eingebetteten Anwendungen, gibt es in vielen Fällen sicherere und modernere Alternativen im C++-Standard. Es ist wichtig, die Funktionsweise und die möglichen Fallstricke von Unions zu verstehen, bevor man sie verwendet.

4.16 Zeiger und Referenzen

Zeiger und Referenzen sind beide Mittel in C++, um indirekt auf Daten zuzugreifen. Obwohl sie ähnliche Funktionen haben, unterscheiden sie sich in ihrer Syntax und ihrer Art und Weise, wie sie verwendet werden.

4.16.1 Zeiger

Ein Zeiger ist eine Variable, die die Adresse einer anderen Variable speichert.

Deklaration und Initialisierung

int x = 10;
int* ptr = &x; // Zeiger ptr zeigt auf die Adresse von x

Zugriff auf den Wert über den Zeiger

*ptr = 20; // Ändert den Wert von x auf 20 über den Zeiger

Arithmetik mit Zeigern

Man kann Zeigern (abhängig vom Typ) Inkrementieren und Dekrementieren, was oft bei Arrays genutzt wird.

int arr[3] = {10, 20, 30};
int* arrPtr = arr; // Zeigt auf das erste Element
++arrPtr; // Zeigt nun auf das zweite Element

Nullzeiger

Ein Zeiger kann auf nullptr (in C++11 und darüber) oder auf NULL (in älteren C++ Versionen) gesetzt werden, was bedeutet, dass er auf nichts zeigt.

int* nullPtr = nullptr;

4.16.2 Referenzen

Eine Referenz ist ein Alias für eine andere Variable. Im Gegensatz zu einem Zeiger benötigt eine Referenz keine Dereferenzierung und kann nicht neu zugewiesen werden, nachdem sie einmal initialisiert wurde.

Deklaration und Initialisierung

int y = 50;
int& ref = y; // ref ist ein Alias für y

Zugriff auf den Wert über die Referenz

ref = 60; // Ändert den Wert von y auf 60

Referenzen sind besonders nützlich bei Funktionen und Operatoren, wenn man mit der Originalvariable und nicht mit ihrer Kopie arbeiten möchte.

4.16.3 Hauptunterschiede

  1. Initialisierung: Ein Zeiger kann uninitialisiert bleiben, während eine Referenz immer zur Initialisierungszeit initialisiert werden muss.

  2. Zuweisbarkeit: Nach der Initialisierung kann ein Zeiger neu zugewiesen werden, um auf verschiedene Variablen zu zeigen. Eine Referenz hingegen kann nach ihrer Initialisierung nicht geändert werden.

  3. Nullwert: Ein Zeiger kann einen Nullwert (nullptr oder NULL) haben, was bedeutet, dass er auf nichts zeigt. Referenzen können nicht “null” sein.

  4. Syntax: Zeiger verwenden * für die Dereferenzierung und & für die Adressaufnahme. Referenzen verwenden & für die Deklaration und benötigen keine spezielle Syntax für den Zugriff.

4.16.4 TLDR;

Sowohl Zeiger als auch Referenzen sind mächtige Werkzeuge in C++ und haben ihre eigenen Anwendungsfälle. Während Zeiger mehr Flexibilität und Komplexität bieten, bieten Referenzen eine einfachere und sicherere Möglichkeit, indirekt auf Daten zuzugreifen. Es ist wichtig, den richtigen Typ für den richtigen Anwendungsfall auszuwählen. In vielen modernen C++-Programmen wird bevorzugt mit Referenzen gearbeitet, wo immer es möglich ist, um die Sicherheit und Lesbarkeit des Codes zu erhöhen.

4.17 Smart Pointer in C++

Smart Pointer sind Klassenobjekte, die wie herkömmliche Zeiger in C++ agieren, aber mit dem zusätzlichen Vorteil, dass sie den Speicher automatisch verwalten. Dadurch wird die Wahrscheinlichkeit von Speicherlecks und anderen speicherbezogenen Problemen verringert.

Es gibt drei Haupttypen von Smart Pointern in der C++ Standardbibliothek:

4.17.1 1. std::unique_ptr

Ein std::unique_ptr ist ein Smart Pointer, der den Besitz eines dynamisch allozierten Objekts exklusiv besitzt. Es darf nur eine Instanz eines unique_ptr geben, der auf ein bestimmtes Objekt zeigt.

Eigenschaften:

Beispiel:

#include <memory>

std::unique_ptr<int> ptr1 = std::make_unique<int>(5); // C++14 und darüber
std::unique_ptr<int> ptr2 = std::move(ptr1); // Besitztransfer

4.17.2 2. std::shared_ptr

Ein std::shared_ptr ist ein Smart Pointer, der den Besitz eines Objekts mit anderen shared_ptr Instanzen teilen kann. Ein internes Zählwerk (Referenzzähler) hält fest, wie viele shared_ptr Instanzen dasselbe Objekt besitzen. Wenn der letzte shared_ptr, der auf das Objekt zeigt, zerstört wird oder aus dem Geltungsbereich kommt, wird das Objekt freigegeben.

Eigenschaften:

Beispiel:

#include <memory>

std::shared_ptr<int> ptr1 = std::make_shared<int>(5);
std::shared_ptr<int> ptr2 = ptr1; // Beide zeigen jetzt auf die Zahl 5 und teilen den Besitz

4.17.3 3. std::weak_ptr

Ein std::weak_ptr ist eine Art Begleiter für shared_ptr. Es “zeigt” auf dasselbe Objekt, das von einem oder mehreren shared_ptr besessen wird, erhöht aber den Referenzzähler nicht. Es wird oft verwendet, um zyklische Referenzen zu vermeiden.

Eigenschaften:

Beispiel:

#include <memory>

std::shared_ptr<int> shared = std::make_shared<int>(5);
std::weak_ptr<int> weak = shared;

4.17.4 TLDR;

Smart Pointer in C++ bieten eine automatische Speicherverwaltung, die viele der Probleme herkömmlicher Zeiger vermeidet. Sie sind ein essenzielles Werkzeug in der modernen C++-Entwicklung und sollten immer dann verwendet werden, wenn dynamisch zugewiesener Speicher benötigt wird.

4.18 Move-Semantik in C++

Die Move-Semantik, eingeführt in C++11, erlaubt es, Ressourcen von einem Objekt zu einem anderen zu “verschieben”, anstatt sie zu kopieren, was oft schneller und effizienter ist.

R-Wert-Referenzen: Diese sind der Schlüssel zur Implementierung der Move-Semantik. Eine R-Wert-Referenz wird mit && deklariert und kann an einen R-Wert (einen temporären Wert oder solche, die mit std::move() modifiziert werden) gebunden werden.

int&& rvalueRef = std::move(someInt);

Move-Konstruktor und Move-Zuweisungsoperator: Diese erlauben es, Ressourcen von einem temporären Objekt zu einem neuen zu verschieben.

class MyClass {
    int* data;

public:
    // Move-Konstruktor
    MyClass(MyClass&& other) noexcept : data(other.data) {
        other.data = nullptr; // Setze die Datenquelle des anderen Objekts zurück
    }

    // Move-Zuweisungsoperator
    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

4.19 Perfect Forwarding in C++

Perfect Forwarding bezieht sich auf die Möglichkeit, den Typ und den “L-Wert/R-Wert-Status” eines Arguments an eine andere Funktion weiterzuleiten, wobei der genaue Typ und das Status unverändert bleiben.

Dies wird oft mit Vorlagensyntax und std::forward verwendet:

template <typename T>
void wrapperFunction(T&& arg) {
    someOtherFunction(std::forward<T>(arg));
}

Mit Perfect Forwarding kann wrapperFunction das gegebene Argument als entweder L-Wert oder R-Wert an someOtherFunction weitergeben.

4.20 Warum sind sie wichtig?

Zusammenfassend bietet C++ durch die Move-Semantik und Perfect Forwarding eine enorme Steigerung der Effizienz und Flexibilität, insbesondere bei ressourcenintensiven oder generischen Programmieraufgaben. Sie sind unerlässliche Werkzeuge in der modernen C++-Entwicklung.

4.21 Aufzählungen (Enumerations)

In C++ sind Aufzählungen (enumerations) ein Weg, um Namen für eine Menge von verwandten Konstanten zu definieren. Es gibt zwei Hauptarten von Aufzählungen in C++: die traditionelle enum und die stärker typisierte, sicherere enum class (auch als “scoped enumerations” bekannt), die mit C++11 eingeführt wurde.

4.21.1 1. Traditionelle enum

Eine traditionelle enum-Deklaration definiert einen neuen Typ, der eine Menge von benannten Ganzzahlkonstanten repräsentiert.

enum Farbe {
    ROT,   // 0
    GRUEN, // 1
    BLAU   // 2
};

Es ist auch möglich, den Wert jeder Konstanten explizit anzugeben:

enum Signal {
    GRUEN = 1,
    GELB = 2,
    ROT = 3
};

Einige Einschränkungen und Eigenschaften von traditionellen enums:

4.21.2 2. enum class

Mit C++11 wurde die enum class-Deklaration eingeführt, um mehr Typsicherheit und einen besseren Umfang zu bieten.

enum class Frucht {
    Apfel,
    Birne,
    Banane
};

Mit enum class:

enum class Byte : unsigned char {
    Wert1,
    Wert2,
    // ...
};

4.21.3 Zugriff und Verwendung

Für enum:

Farbe c = ROT;
int i = GRUEN; // möglich, aber nicht immer wünschenswert

Für enum class:

Frucht f = Frucht::Apfel;
// int j = Frucht::Birne; // Fehler: Keine implizite Konvertierung zu int
int j = static_cast<int>(Frucht::Birne); // explizite Konvertierung nötig

4.21.4 TLDR;

Während traditionelle enums in älterem Code nützlich und verbreitet sind, bietet enum class in vielen Situationen erhebliche Vorteile in Bezug auf Typsicherheit und Klarheit. Wenn Sie in der Lage sind, wird empfohlen, enum class zu verwenden, um potenzielle Fehler und Verwirrungen zu vermeiden.