Grundlagen für das Projekt (Teil 2)

Website: Lerne ELEKTROTECHNIK und PROGRAMMIEREN
Kurs: Kurs zum Buch "Lerne Programmieren mit Projekten: C++"
Buch: Grundlagen für das Projekt (Teil 2)
Gedruckt von: Gast
Datum: Friday, 19. April 2024, 11:38

Beschreibung



1. Worum geht es?

In diesem Abschnitt lernst du, was es mit Arrays (dt. Felder) in C++ auf sich hat und wie sie dir dabei helfen können, Daten aller Art effizienter zu speichern und zu  verwalten. Außerdem sehen wir uns mehrdimensionale Felder an, mit denen zu 2D- und 3D-Datenstrukturen im Speicher ablegen kannst. 

Zum Schluss lernst du noch den Datentyp vector kennen, mit dem sich analog zu Arrays mehrere Daten des gleichen Typs speichern und verwalten lassen - allerdings mit der Möglichkeit, zur Laufzeit die Größe zu ändern, Daten zu sortieren und vieles mehr.

2. ARRAYS: Gleiche Daten zusammenfassen

Worum geht es? 

In diesem Abschnitt erkläre ich dir, was es mit Arrays (dt. "Felder") in C++ auf sich hat und wie sie dir dabei helfen können, Daten aller Art effizienter zu speichern und zu verwalten. Dazu sehen wir uns zuerst eine Definition des Begriffs "Array" an und werden dann in einigen Beispielen Arrays erzeugen und auf die darin gespeicherten Elemente zugreifen.

Machen wir zuerst ein Beispiel: Im nachstehenden Beispiel kannst du sehen, wie fünf Variablen vom Typ Integer erzeugt werden, die fast den gleichen Namen haben und die dazu dienen sollen, die Noten von Kursteilnehmern zu speichern. 

// Ähnnliche Variablen definieren
int notenKursA_Stud1;
int notenKursA_Stud2;
int notenKursA_Stud3;
int notenKursA_Stud4;
int notenKursA_Stud5;

Bei einem kleinen Kurs mag das noch funktionieren, aber wenn die Teilnehmerzahl zu groß wird, wäre es sehr umständlich, einzelne Variablen anzulegen.

Mit einem Array können wir diese Aufgabe viel einfacher lösen, denn wir müssen dazu nur einmal einen Datentyp und einen Bezeichner (d.h. einen Namen) festlegen und in eckigen Klammern die Anzahl der Variablen des gleichen Typs angeben, die im Speicher hintereinander angelegt werden sollen. 

Was kannst du danach?

  • Mehrere Variablen des gleichen Typs in einem Array gruppieren und initialisieren.


#include <iostream>
using namespace std;

int main()
{
    // Schreibarbeit sparen mit Arrays
    int notenKursA[5];

    // Arrays initialisieren
    int notenKursB[5]{1, 2, 3, 4, 5};

    return 0;
}
Hinweis: Dieser Code erzeugt keine Ausgabe. Du kannst aber trotzdem damit experimentieren und auf Fehlermeldungen achten.

2.1. Schreib- und Lesezugriff

Worum geht es? 

In diesem Abschnitt sieht du an einem Beispiel, wie der Schreib- und Lesezugriff bei Arrays funktionieren. Das Schlüsselelement dabei sind die eckigen Klammern, über die der Index angegeben wird. Aber Achtung: Das erste Element hat in C++ immer den Index 0.  

Was kannst du danach?

  • Daten aus Arrays lesen und in Arrays hinein schreiben.


#include <iostream>
using namespace std;

int main()
{
    int notenKursB[5]{1, 2, 3, 4, 5};

    // Schreib- und Lesezugriff
    notenKursB[0] = 2; // 1. Element = Index 0
    cout << notenKursB[4] << endl; // 5. Element
    //cout << notenKursB[5] << endl; // Achtung!

    return 0;
}
Experimentiere ein wenig mit den Indizes des Arrays und versuche, die Elemente darin gezielt auszugeben.

2.2. Zugriff mit Schleifen

Worum geht es? 

Eine der großen Stärken von Arrays besteht darin, dass wir sehr unkompliziert auf alle Elemente nacheinander zugreifen können. Dazu eignet sich sehr gut die for-Schleife, die wir in einem der letzten Module schon kennen gelernt haben. In diesem Beispiel kannst du sehen, wie das funktioniert.  

Was kannst du danach?

  • Sequentieller Zugriff auf Arrays mit einer Schleife


#include <iostream>
using namespace std;

int main()
{
    int notenKursA[5];

    // Schreibzugriff mit Schleifen
    for(int i=0; i<5; i++)
    {
        int note = rand() % 6 + 1; // Psst...
        notenKursA[i] = note;
    }
    
    // Lesezugriff
    for(int i=0; i<5; i++)
    {
        cout << notenKursA[i] << endl;
    }
    
    return 0;
}

2.3. Arrays mit Variablen verwalten

Worum geht es? 

Ein Nachteil von Arrays besteht darin, dass die Datenstruktur nicht weiß, wie viele Elemente in ihr gespeichert sind. Das heißt, dass du die Verantwortung dafür hast, z.B. bei Schleifen über alle Elemente die richtige Größe anzugeben. 

Stell dir vor, du änderst nachträglich die Größe des Arrays. Das bedeutet, dass du nun deinen kompletten Code prüfen und anpassen musst. Und dabei können sehr leicht Fehler passieren.

Eine Lösung für dieses Problem besteht darin, die Array-Größe über eine Konstante zu verwalten. In diesem Beispiel kannst du sehen, wie das funktioniert.

Was kannst du danach?

  • Die Größe von Arrays mit Variablen definieren


#include <iostream>
using namespace std;

int main()
{
    // Arrays mit Variablen verwalten
    const int num_students = 10; // Größe festlegen
    int notenKursC[num_students];
    for(int i=0; i<num_students; i++)
    {
        notenKursC[i] = rand() % 6 + 1;
    }    
    
    // Lesezugriff
    for(int i=0; i<num_students; i++)
    {
        cout << notenKursC[i] << endl;
    }
    
    return 0;
}
Verändere die Größe des Arrays mit Hilfe der Variablen num_students und sieh dir die Ausgabe an.

3. Mehrdimensionale Arrays

Worum geht es? 

Wie du im letzten Abschnitt gesehen hast, ist ein Array eine Datenstruktur, die es dir ermöglicht, über einen gemeinsamen Namen und einen Index auf die einzelnen Elemente darin zuzugreifen. 

In diesem Abschnitt erweitern wir dieses Konzept um eine weitere Dimension, indem wir noch einen weiteren Index einführen und damit ein zweidimensionales Array erzeugen.

In diesem Beispiel siehst du, wie das funktioniert: Hier wird zuerst ein zweidimensionales Array definiert und dann der Inhalt einer Textdatei zeichenweise darin gespeichert.

Was kannst du danach?

  • Daten aus einer Datei einlesen und in einem 2D-Array elementweise ablegen.


#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    // 2D-Array erzeugen
    const int num_rows = 3;
    const int num_cols = 7;
    char bunny_2d[num_rows][num_cols]{};

    // Datei-Inhalt lesen
    ifstream lesen{"bunny.txt"};
    int row{0};
    while (lesen)
    {
        string text;
        getline(lesen, text);
        for (int col = 0; col < text.length(); col++)
        {
            bunny_2d[row][col] = text[col];
            cout << bunny_2d[row][col];
        }
        cout << endl; // Zeilenende erreicht
        row++;
    }
    
    return 0;
}

Schau dir den Schreib- und Lesezugriff auf das 2D-Array an und versuche z.B. auch, den Zeilen- und Spaltenindex zu vertauschen.

3.1. 2D-Array verändern und mit Doppelschleife ausgeben

Worum geht es? 

Nachdem die Daten aus der eingelesenen Textdatei nun im Speicher vorliegen, können wir sie über einen einfachen Schreibzugriff über das 2D-Array verändern. Im nachstehende Code kannst du sehen, wie die Augen des 2D-Hasen aus dem letzten Beispiel durch einen direkten Schreibzugriff verändert werden. 

 

Was kannst du danach?

  • Elemente eines 2D-Arrays gezielt verändern.


#include <iostream>
#include <fstream>
using namespace std;

int main()
{
    // 2D-Array erzeugen
    const int num_rows = 3;
    const int num_cols = 7;    
    char bunny_2d[num_rows][num_cols]
    {
        {' ','(','\\','_','/',')'},
        {' ','(','.','_','.',')'},
        {'c','(','"',')','(','"',')'}
    };

    // 2D-Array verändern
    bunny_2d[1][2] = '>';
    bunny_2d[1][4] = '<';

    // Array im Terminal ausgeben
    for (int row{0}; row < num_rows; row++)
    {
        for (int col{0}; col < num_cols; col++)
        {
            cout << bunny_2d[row][col];
        }
        cout << endl; // Zeilenende erreicht
    }
    
    return 0;
}
Versuche, noch andere Zeichen im 2D-Array gezielt zu verändern oder dem Hasen ein 'x' als Symbol für die Augen zu verpassen.

4. VECTOR: Flexible Arrays erzeugen

Worum geht es? 

Im letzten Modul hast du Arrays als eine einfache Möglichkeit kennen gelernt, Daten des gleichen Typs unter einem gemeinsamen Namen zu speichern und einzelne Elemente über einen Index anzusprechen. Dieses eigentlich sehr praktische Konstrukt hat aber leider einige sehr schwerwiegende Nachteile: 

  • Die Größe von Arrays muss zur Compilezeit (d.h. beim Programmieren) festgelegt werden.
  • Zur Laufzeit (d.h. bei der Ausführung) können Arrays weder vergrößert noch verkleinert werden.
  • Ein Array weiß nicht, wie viele Elemente in ihm gespeichert sind.
  • Arrays verfügen über keine Mechanismen zur Fehlerprüfung (z.B. bei einem ungültigen Index). Dafür bist du verantwortlich.
  • Das Einfügen von Elementen z.B. in die Mitte des Arrays ist mit größerem Progammieraufwand verbunden, den du leisten musst.

Da sich diese Liste noch weiter fortsetzen ließe, wird es Zeit, dass du eine Alternative zu den klassischen Arrays in C++ kennen lernst: Die Klasse vector.

Mit dieser in 2003 eingeführten Datenstruktur ist es möglich, Elemente eines beliebigen Typs in einer dynamisch erweiterbaren Liste zu speichern. Die Länge eines Vektors kann zur Laufzeit angepasst werden und es ist zu jeder Zeit bekannt, wie viele Elemente darin gespeichert sind. Außerdem lassen sich mit Vektoren einige sehr nützliche Zusatzfunktionen nutzen, wie z.B. das Sortieren von Elementen nach ihrer Größe.

Einen Vektor zu definieren ist zwar sehr einfach, allerdings musst du dich dabei an eine neue Schreibweise gewöhnen. In diesem ersten Beispiel ist gezeigt, wie das funktioniert.

Was kannst du danach?

  • Vektoren auf verschiedene Arten definieren und mit Elementen initialisieren.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{
    // Definition von Vektoren
    std::vector<char> v0; // #include <vector>
    vector<int> v1{1, 2, 3, 4, 5};
    vector<string> v2{"Hallo", "Welt"};
    vector v3{3.14, 2.45}; // ohne Typ seit C++17
    // vector v4{"Hallo", 3}; // Fehler: versch. Typen
    
    return 0;
}

Dieser Code erzeugt zwar keine Ausgaben, aber du hast hier die Möglichkeit, mit verschiedenen Arten zur Definition von Vektoren zu experimentieren. Achte dabei besonders auf Fehlermeldungen.

4.1. Anzahl der Elemente ermitteln

Worum geht es? 

Nachdem du nun weißt, wie Vektoren definiert werden, lernst du jetzt, wie man die Anzahl der in einem Vektor gespeicherten Elemente ermitteln kann. In diesem Beispiel lernst du, wie das funktioniert. 

Was kannst du danach?

  • Anzahl der Elemente in einem Vektor ermitteln


#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{
    // Anzahl der Elemente ermitteln
    vector<int> v4{1, 2, 3};
    vector<char> v5{'a', 'b', 'c', 'd'};
    cout << "#Elemente in v4 = " << v4.size() << endl;
    cout << "#Elemente in v5 = " << v5.size() << endl;
    
    return 0;
}
Variiere die Anzahl der Elemente und schau dir die Ausgaben an. 

4.2. Auf Vektor-Elemente zugreifen

Worum geht es? 

Wenn du auf Elemente in einem Vektor zugreifen möchtest, dann hast du dafür mehrere Möglichkeiten. In diesem Beispiel lernst du drei Varianten hierfür kennen.

Was kannst du danach?

  • Auf die Elemente eines Vektors auf unterschiedliche Arten zugreifen.


#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{
    // Auf Elemente von Vektoren zugreifen
    vector<char> v6{'a', 'b', 'c', 'd', 'e'};
    for (int i{0}; i < v6.size(); i++) // klassisch
    {
        cout << v6[i] << endl;    // ohne und ...
        //cout << v6.at(i) << endl; // ... mit Prüfung
    }

    for (char element : v6) // bereichsbezogen
    {
        cout << element << endl;
    }    
    return 0;
}

Variiere die Anzahl der Elemente und schau dir die Ausgaben an.

4.3. Elemente an Vektoren anhängen

Worum geht es? 

Bisher hast du gelernt, wie man Vektoren definiert, mit Werten initialisiert und wie man auf die einzelnen Elemente zugreifen kann. Jetzt lernst du, wie du neue Elemente zu einem Vektor hinzufügen kannst - egal, ob ans Ende oder mittendrin. 

Was kannst du danach?

  • Elemente an einen Vektor anhängen


#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{
    // Element an Vektor anhängen
    vector<string> v7{"Hallo "};
    v7.push_back("Welt");
    for (string element : v7)
        cout << element;
    cout << endl;    
        
    // Element an Position einfügen
    v7.insert(v7.begin()+1, "du ");
    v7.insert(v7.begin()+2, "schöne ");
    v7.insert(v7.end(), "!");
    for (string element : v7)
        cout << element;    
        
    return 0;
}

Füge weitere Elemente zum Vektor hinzu und ersetze zur Übung eine der bereichsbezogenen Schleifen durch eine konventionelle for-Schleife.

4.4. Elemente an Position löschen

Worum geht es? 

Eine weitere nützliche Funktion von vector ist das Löschen von Elementen an einer beliebigen Position. Dies funktioniert ähnlich wie das Einfügen über die Angabe der Element-Position mit Hilfe von begin() oder end()

Was kannst du danach?

  • Elemente eines Vektors löschen


#include <iostream>
#include <string>
#include <vector>
using namespace std;

int main()
{
    // Element an Position löschen
    vector<string> v7{"Hallo ", "du ", "schöne ", "Welt", "!"};
    v7.erase(v7.begin()+1);
    v7.erase(v7.end()-3);
    // v7.erase(v7.end()); // Fehler!
    v7.pop_back();
    for (string element : v7)
        cout << element; 
    
    return 0;
}

Lösche unterschiedliche Elemente des Vektors und verletze auch bewusst dessen Grenzen. Dabei kann, je nach Compiler, entweder eine Fehlermeldung auftreten, oder auch nicht.

4.5. Einen Vektor sortieren

Worum geht es? 

Ein weiterer Vorteil der Klasse vector gegenüber einem klassischen Array besteht darin, dass eine ganze Reihe von Zusatzfunktionen zur Verfügung steht, die du in deinen Programmen nutzen kannst. Du kannst zum Beispiel wie hier gezeigt die Elemente deines Vektors der Größe nach sortieren. 

Was kannst du danach?

  • Die Elementen eines Vektors der Größe nach sortieren.


#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    // Vektoren definieren
    vector<int> v8{3,7,8,2,4,4,10};
    vector<int> v9{'b','a','d','e','c'};

		// Vektoren sortieren    
    sort(v8.begin(), v8.end()); // vorwärts
    sort(v9.rbegin(), v9.rend()); // rückwärts

    // Inhalte ausgeben
    for (int element : v8)
        cout << element << endl;
    for (char element : v9)
        cout << element << endl;
    
    return 0;
}

Definiere einen zusätzlichen Vektor mit einem neuen Datentyp deiner Wahl und sortiere in ebenfalls.