C++ Container und die vector Klasse der Standardbibliothek

Einführung

In der vorigen Vorlesung hatten wir in das C++ Klassenkonzept eingeführt, mit welchem der Programmierer einen eigenen benutzerdefinierten Typ erstellen kann. Bei vielen Programmier-Teilaufgaben innerhalb eines Programms ist es jedoch oft nicht nötig seine eigene Programmierstruktur zu entwerfen, da es in der C++ Standardbibliothek (STL: Standard Template Library) schon viele zusammengesetzte Strukturen gibt, die man einfach übernehmen kann. So ist es z.B. beim Rechnen mit komplexwertigen Zahlen nicht nötig seine eigene Klasse der komplexwertigen Zahlenmenge zu entwerfen, sondern man kann einfach mittels '#include <complex>' die Klasse der komplexwertigen Zahlen aus der C++ Standardbibliothek verwenden (die Verwendung der Klasse 'complex' wird in der nächsten Vorlesung behandelt).

Die C++ Standardbibliothek verfügt über eine Vielzahl nützlicher Programmierkonstrukte und ein oft verwendetes Klassenkonzept sind die sogenannten C++ Container. Ein Container ist ein Objekt, das eine Sammlung von Elementen aufnimmt. Die verfügbaren STL-Container gliedern sich in sequentielle Container (wie z.B. '<vector>' und '<list>') und ungeordnete/geordnete assoziative Container (wie z.B. '<map>' und '<unordered_map>'). Wir werden nicht näher auf die feinen Unterschiede dieser unterschiedlichen Containervarianten eingehen und uns im folgenden mit dem wohl wichtigsten Container <vector> näher befassen.

#include <iostream> // Ein- und Ausgabebibliothek

// Beispiel fuer die Struktur der vector-Klasse der Standardbibliothek
class Vektor {
    // Private Instanzvariablen (Daten-Member)
private:
    double* elem; // Zeiger auf das eindimensionale Array von 'anz' double Elementen
    int anz;      // Anzahl der Elemente des vector-Objektes
    
// Oeffentliche Bereiche der Klasse 
public:
    // Ueberladene Konstruktoren der Klasse Vektor
    // Konstruktor mit einem Argument
    Vektor(int set_anz) : elem { new double[set_anz] }, anz{set_anz} {
        printf("Konstruktor(anz) erzeugt einen Vektor-Container mit %i Elementen \n", set_anz);
        for(int i = 0; i!=set_anz; ++i){
            elem[i] = 0;
        }
    }
    //...
    // Destruktor der Klasse Vektor
    ~Vektor() { 
        printf("Destruktor loescht den Vektor-Container \n");
        delete[] elem; 
    }
    
    // Oeffentliche Member-Funktionen der Klasse
    //...
}; 

int main(){               // Hauptfunktion
    Vektor w = Vektor(3); // Deklaration eines Objektes der Vektor-Klasse mit drei Elementen
}

        

Die Klassenstruktur des standard Containers <vector>

Der STL-Container <vector> der C++ Standardbibliothek stellt einen sequenziellen Typ von Objekten dar und ist somit eine Sequenz von Elementen eines bestimmten Typs. Bei der Definition eines vector-Objektes werden die einzelnen Elemente des Vektors, im Hauptspeicher aufeinanderfolgend abgelegt. Man kann sich die Struktur eines vector-Objektes als ein eindimensionales Array vorstellen, bei welchem man zusätzlich noch die Anzahl der Elemente im Programmverlauf verändern kann. Außerdem stellt die vector-Klasse mehrere Memberfunktionen bereit, die einem bei der Konstruktion des Vektors helfen. Die Vektorklasse ist als eine Template-Klasse formuliert, was bedeutet, dass der Typ T der Objekte veränderbar ist, die einzelnen Objekte jedoch von gleichem Typ sein müssen. Man erzeugt ein vector-Objekt, indem man einen der vector-Konstruktoren im Hauptprogramm aufruft (z.B. ' vector<T> v;').

Der grundlegende Aufbau der STL-vector-Klasse ähnelt der Vektor Klasse, die im nebenstehenden C++ Programm beispielhaft erstellt wurde. Der Konstruktor von Vektor alloziert mithilfe des Operators 'new double[set_anz]' Speicher im dynamischen Speicherbereich des Computers (Freispeicher, Heap). Der Destruktor '~Vektor() { ... }' gibt dann den benutzten Speicher wieder frei. Beim Aufruf des Konstruktors werden die privaten Instanzvariablen ( 'double* elem;' und 'int anz;') initialisiert und ein 'printf(...)' Befehl gibt im Terminal aus, dass der Konstruktor aufgerufen wurde. Im Hauptprogramm wird lediglich ein Objekt der vector-Klasse mit drei Elementen deklariert. Führt man das Programm aus, so erhält man die nebenstehende Terminalausgabe.

Die Klasse <vector> am Beispiel eines Integer Vektors

In diesem Unterpunkt werden wir lernen, wie man einen vector-Container erstellt und ihn mit Elementen füllt. Dies werden wir am Beispiel eines Vektors, bestehend aus Integer-Zahlen machen. Im folgenden C++ Programm wird ein Integer vector-Objekt mittels der Anweisung 'vector<int> w = {1,4,6,8,9,5,3};' deklariert und gleichzeitig mit sieben Integer Zahlen initialisiert.

Vector_1.cpp
#include <iostream>                           // Ein- und Ausgabebibliothek
#include <vector>                             // Sequenzieller Container vector<Type> der Standardbibliothek
using namespace std;                          // Benutze den Namensraum std

int main(){                                   // Hauptfunktion
    vector<int> w = {1,4,6,8,9,5,3};          // Deklaration und Initialisierung eines Integer-vector-Containers mit sieben Einträgen
    vector <int>::iterator Iter;              // nur fuer Schleifen mit ::iterator noetig
    
    w.push_back(10);                          // Einfuegen eines neuen Elementes am Ende des Vektors
    printf("w = (");
    for (auto& n : w){                        // Bereichsbasierte for-Schleife zum Ausgeben der einzelnen Elemente des Vektors
        printf("%3i ", n);
    }
    printf(") \n \n");
    
    w.insert(w.begin()+3,99);                            // Einfuegen eines neuen Elementes an der vierten Position des Vektors
    
    printf("w = (");
    for ( Iter = w.begin() ; Iter != w.end() ; Iter++ ){ // for-Schleife zum Ausgeben der einzelnen Elemente des Vektors (mittels ::iterator)
        printf("%3i ", *Iter);
    }
    printf(") \n \n");
    
    w[5] = 77;                                           // Neue Wert-Zuweisung fuer das 6. Elementes des Vektors
    
    printf("w = (");
    for(int i=0; i<w.size(); ++i){                       // Normale for-Schleife zum Ausgeben der einzelnen Elemente des Vektors
        printf("%3i ", w[i]);
    }
    printf(") \n");
}
Anweisung Bedeutung
v.push_back(val); Fügt die Daten aus val an das Ende des Vektors v an.
v.pop_back(); Entfernt das letzte Element des Vektors v.
v.insert(pos,val); Fügt die Daten aus val an die Position pos des Vektors v ein.
v.size(); Gibt die Anzahl aller Elemente im Vektors v zurück.
v.resize(n); Setzt die Anzahl der Elemente im Vektors auf n.
v.clear();. Entfernt alle Elemente des Vektors v.
v.front(); Gibt die Referenz auf das erste Element von v zurück.
v.back(); Gibt die Referenz auf das letzte Element von v zurück.
v.capacity(); Die Anzahl der Elemente die in v gespeichert werden können.
v.at(n); Repräsentiert das n. Element des Vektors v (prüft zuvor, ob n im erlaubten Bereich liegt).
v[n]; Repräsentiert das n. Element des Vektors v (prüft nicht, ob n im erlaubten Bereich liegt).

Es werden danach verschiedene Operationen (Methoden, Member-Funktionen der Klasse vector) angewandt, um den definierten Vektor zu verändern. Der Vektor wird dann mittels drei unterschiedlicher Schleifenvarianten im Terminal ausgegeben, wobei eine dieser Varianten den speziellen vector-'::iterator' benutzt, der im Programm am Anfang schon deklariert wurde. Die nebenstehende Abbildung zeigt die gesamte Terminalausgabe des Programms. Nach der Erzeugung des Vektors $w$ wird diesem ein zusätzliches Element (die Integer Zahl '10') hinzugefügt. Dieses Einfügen am Ende des Vektors wird mittels der Anweisung 'w.push_back(10);' gemacht. Der Vektor wird dann mittels einer bereichsbasierten for-Schleife im Terminal ausgegeben. Es wird dann, mittels der Anweisung 'w.insert(w.begin()+3,99);', nach dem dritten Element die Zahl '99' dem Vektor hinzugefügt. Der Vektor wird dann mittels einer for-Schleife im Terminal ausgegeben, die den Iterator vector-'::iterator' benutzt. Danach wird der Wert des sechten Elementes des Vektors auf '77' abgeändert (w[5] = 77;) und der Vektor mittels einer 'normalen' for-Schleife ausgegeben.

Die nebenstehende Tabelle listet einige der verfügbaren Vektoroperatoren auf. Die Methode 'v.resize(n);' wird in dem unteren Quelltext des C++ Programms Vector_2.cpp benutzt. In dem Programm wurde zunächst ein Integer-Vektor $w$ mit fünf Elementen deklariert (vector<int> w(5);). Die fünf Elemente des Vektors erhalten dann, innerhalb einer for-Schleife, ihre entsprechenden Werte und der Vektor wird im Terminal ausgegeben. Dann wird der Vektor mittels 'w.resize(7);' auf eine Kapazität von sieben erhöht und die Wertzuweisungen an die neuen Elemente gemacht. Die Terminalausgabe des Programms (siehe rechte kleine Abbildung) zeigt die Erhöhung der Kapazität des Vektors.

Vector_2.cpp
#include <iostream>                    // Ein- und Ausgabebibliothek
#include <vector>                      // Sequenzieller Container vector<Type> der Standardbibliothek
using namespace std;                   // Benutze den Namensraum std

int main(){                            // Hauptfunktion
    vector<int> w(5);                  // Deklaration eines Integer-vector-Containers mit fuenf Einträgen
    
    for(int i = 0; i < w.size(); ++i){ // Normale for-Schleife zum Ausgeben der einzelnen Elemente des Vektors
        w[i] = i*i;                    // Wert-Zuweisung an das t-te Elementes des Vektors
    }
    
    printf("w = (");
    for (auto& n : w){                 // Bereichsbasierte for-Schleife zum Ausgeben der einzelnen Elemente des Vektors
        printf("%3i ", n);
    }
    printf(") \n");
    
    w.resize(7);                       // Die Anzahl der Eintraege im Vektor w wird auf 7 erhoeht
    w[5] = 5*5;                        // Wert-Zuweisung an das 5-te Elementes des Vektors
    w[6] = 6*6;                        // Wert-Zuweisung an das 6-te Elementes des Vektors
    
    printf("w = (");
    for (auto& n : w){                 // Bereichsbasierte for-Schleife zum Ausgeben der einzelnen Elemente des Vektors
        printf("%3i ", n);
    }
    printf(") \n");
}



Die Klasse <vector> als ein Container von Objekten

Wie wir im vorigen Unterpunkt gesehen haben, kann man die Klasse <vector> benutzen, um eindimensionale Arrays in Form von Standardvektoren darzustellen. Die Verwendung von einem vector-Objekt anstelle von einem integrierten eindimensionalen Array hat den Vorteil, dass man die Kapazität des Vektors im Laufe des Programms einfach verändern kann. In diesem Unterpunkt wollen wir einen weiteren Anwendungsfall der Klasse <vector> betrachten, bei dem die Elemente des Containers selbst Objekte von Klassen sind. Betrachten wir z.B. die in der vorigen Vorlesung entworfene Klasse 'Ding' und stellen uns vor, dass wir eine bestimmte Anzahl von Dingen in eine Kiste einsperren möchten. Wir möchten somit einen Container, bestehend aus Objekten der Klasse 'Ding' erstellen.

Wir möchten im Folgenden eine gewisse Anzahl von Teilchen (z.B. unsigned int Anz_Teilchen = 10;) in eine zweidimensionale Kiste einsperren (Abmessung der Kiste z.B. double kiste_x = 40; und double kiste_y = 40; ). Die 10 Teilchen sollen im Hauptprogramm als Instanzen der Klasse Ding erzeugt werden, wobei die Klasse Ding eine, ein wenig allgemeinere Struktur hat, als die in der vorigen Vorlesung vorgestellte Ding-Klasse. Die Klasse Ding sollte neben den Ortskoordinaten (double x = 0, y = 0, z = 0;) auch die Teilchengeschwindigkeiten als private Daten-Member enthalten (z.B. zunächst initialisiert auf Null: double v_x = 0, v_y = 0, v_z = 0;). Mittels des Konstruktors kann der Benutzer dann die Anfangsorte und Anfangsgeschwindigkeiten der einzelnen Teilchen festlegen.

Nun wird zusätzlich noch das zeitliche Verhalten der Dinge als eine inline Funktion definiert. Die Teilchen sollen hierbei zunächst nicht miteinander wechselwirken. Die Begrenzungen der Kiste bilden jedoch nicht überwindbare Barrieren für die klassischen Teilchen und es soll eine vollständige Reflexion an den Kistenbegrenzungen stattfinden. Wir implementieren dieses zeitliche Verhalten der Teilchen als eine inline-Methode der Klasse 'Ding' und benennen sie 'Gehe_Zeitschritt': inline void Ding::Gehe_Zeitschritt(double dt, double max_x, double max_y, double max_z){ ... } .
Die Funktion beschreibt die Veränderung der Ortskoordinaten $(x,y,z)$ bei einem Zeitschritt $dt$, wobei die speziellen Randbedingungen an die Bewegung der Teilchen (Reflexion an den Rändern der Kiste) mittels der Argumente double max_x, double max_y, double max_z spezifiziert werden.

Im Hauptprogramm sollen dann ein vector-Container mittels 'vector<Ding> Kiste_Teilchen;' deklariert werden und dieser wird dann mittels 'Kiste_Teilchen.push_back( Ding {...} );' aufgefüllt. Nach diesem Initialisierungsprozess wird die zeitliche Entwicklung der Teilchen für z.B. 100 Zeitschritte (double Anz_tSchritte = 100; mit double dt = 0.05;) im Terminal ausgegeben. Das folgende C++ Programm stellt eine Realisierung des vector-Containers bestehend aus 10 Dingen mit zeitlicher Entwicklung der Ortrskoordinaten dar.

Vector_Dinge.cpp
/* Beispiel fuer einen Vector-Container bestehend aus mehreren Elementen des Typs Ding
 * Ding ist eine Klasse bestehend aus
 * Sieben privaten Instanzvariablen (Ort und Geschwindigkeiten des Dings in 3D)
 * Fuenf ueberladenen Konstruktoren
 * Sieben oeffentlichen const Member-Funktionen
 * und einer oeffentlichen Member-Funktionen 'Gehe_Zeitschritt', 
 * die eine zeitliche Entwicklung im Programm implementiert
 * Ausgabe zum Plotten (Python) mittels: "./a.out > Vector_Dinge.dat" 
 * python3 PythonPlot_Vector_Dinge.py
*/
#include <iostream>                   // Ein- und Ausgabebibliothek
#include <vector>                     // Sequenzieller Container vector<Type> der Standardbibliothek
using namespace std;                  // Benutze den Namensraum std

//Definition der Klasse 'Ding'
class Ding{
    // Private Instanzvariablen (Daten-Member) der Klasse
    unsigned int n;                   // Nummer des Dinges
    double x = 0, y = 0, z = 0;       // Ort des Dinges
    double v_x = 0, v_y = 0, v_z = 0; // Geschwindigkeit des Dinges
    
    // Oeffentliche Konstruktoren und Member-Funktionen der Klasse
    public:
        // Fuenf ueberladene Konstruktoren der Klasse
        // Konstruktor mit sieben Argumenten
        Ding(unsigned int set_n, double set_x, double set_y, double set_z, double set_v_x, double set_v_y, double set_v_z) : n{set_n}, x{set_x}, y{set_y}, z{set_z}, v_x{set_v_x}, v_y{set_v_y}, v_z{set_v_z} {
            printf("# Konstruktor(n,x,y,z,,vx,vy,vz) erzeugt das Ding %i \n",n);
        }
        // Konstruktor mit vier Argumenten
        Ding(unsigned int set_n, double set_x, double set_y, double set_z) : n{set_n}, x{set_x}, y{set_y}, z{set_z} {
            printf("# Konstruktor(n,x,y,z) erzeugt ein das Ding %i \n",n);
        }
        // Konstruktor mit drei Argumenten
        Ding(unsigned int set_n, double set_x, double set_y) : n{set_n}, x{set_x}, y{set_y} {
            printf("# Konstruktor(n,x,y) erzeugt das Ding %i \n",n);
        }
        // Konstruktor mit zwei Argumenten
        Ding(unsigned int set_n, double set_x) : n{set_n}, x{set_x} {
            printf("# Konstruktor(n,x) erzeugt das Ding %i \n",n);
        }
        // Konstruktor ohne Argument (Standard-Konstruktor)
        Ding() : n{0} {
            printf("# Konstruktor() erzeugt das Ding %i \n", n);
        }
        
        // Member-Funktionen der Klasse
        void Gehe_Zeitschritt(double dt, double max_x, double max_y, double max_z); // Deklaration einer Member-Funktion (Definition findet ausserhalb der Klasse statt)
        
        // als const deklariert, da sie die privaten Instanzvariablen nicht veraendern:
        unsigned int get_Nummer() const {return n;}
        double get_Ort_x() const {return x;}
        double get_Ort_y() const {return y;}
        double get_Ort_z() const {return z;}
        
        double get_Geschw_x() const {return v_x;}
        double get_Geschw_y() const {return v_y;}
        double get_Geschw_z() const {return v_z;}
};

/* Definition der Funktion Gehe_Zeitschritt(...) als inline-Methode der Klasse Ding
 * Die Funktion beschreibt die Veränderung der Ortskoordinaten (x,y,z) bei einem Zeitschritt dt
 * Es sind zusaetzlich spezielle Randbedingungen an die Bewegung formuliert, so dass sich die 
 * Dinge nur in einem Bereich von [0,max_x], [0,max_y] und [0,max_z] bewegen koennen */
inline void Ding::Gehe_Zeitschritt(double dt, double max_x, double max_y, double max_z){ 
    x = x + v_x * dt;
    y = y + v_y * dt;
    z = z + v_z * dt;
    if (x >= max_x || x <= 0){v_x = - v_x;}
    if (y >= max_y || y <= 0){v_y = - v_y;}
    if (z >= max_z || z <= 0){v_z = - v_z;}
}   

int main(){                         // Hauptfunktion
    double kiste_x = 40;            // Laenge der Kiste
    double kiste_y = 40;            // Breite der Kiste
    unsigned int Anz_Teilchen = 10; // Definition der Anzahl der zu erzeugenden Dinge
    
    double t;                       // Deklaration des Zeitparameters
    double dt = 0.05;               // Definition der Laenge des Zeitschrittes
    double Anz_tSchritte = 100;     // Anzahl der Zeitschritte
    
    vector<Ding> Kiste_Teilchen;    // Deklaration eines vector-Containers
    
    for (unsigned int n = 0; n < Anz_Teilchen; ++n){ // for-Schleife zum Auffuellen des Containers mit Elementen vom Typ 'Ding' 
        Kiste_Teilchen.push_back( Ding {n, 1, (n+1)*kiste_x/Anz_Teilchen, 0, (n+1.0), 0, 0} ); // Initialisierung: Nur in x-y-Ebene, Geschwindigkeit nur in x-Richtung
    }
    
    printf("# 0: Index i \n# 1: Zeit t \n# 2: Nummer des Teilchens 1 \n");          // Beschreibung der ausgegebenen Groessen
    printf("# 3: x-Position des Teilchens 1 \n# 4: y-Position des Teilchens 1 \n"); // Beschreibung der ausgegebenen Groessen
    printf("# 5: Nummer des Teilchens 2 \n# 6: .... bis Anzahl der Teichen \n");    // Beschreibung der ausgegebenen Groessen
    
    for (int i = 0; i < Anz_tSchritte; ++i){              // for-Schleife fuer die zeitliche Entwicklung der Dinge in der Kiste
        t = i * dt;                                       // Zeit geht in dt-Schritten voran
        printf("%3i %10.6f ", i, t);                      // Ausgabe Index i und Zeit t
        for (auto& n : Kiste_Teilchen){                   // Bereichsbasierte for-Schleife zum Ausgeben der x-y-Orte der Dinge
            printf("%3i %10.6f %10.6f ", n.get_Nummer(), n.get_Ort_x(), n.get_Ort_y()); // Ausgabe Teilchenorte
            n.Gehe_Zeitschritt(dt,kiste_x, kiste_y, 0.0); // Aufruf der inline Funktion Gehe_Zeitschritt(...)
        }
        printf("\n");
    }
}

        

Die nebenstehende Animation visualisiert die produzierten Daten des C++ Programmes und wurde mittels eines Jupyter Notebooks erstellt (näheres siehe weiter unten). Nachdem der vector-Container mit dem Namen 'Kiste_Teilchen' mittels 'vector<Ding> Kiste_Teilchen;' deklariert ist, wird er in der folgenden for-Schleife mit 10 Ding-Objekten aufgefüllt.

    for (unsigned int n = 0; n < Anz_Teilchen; ++n){ 
        Kiste_Teilchen.push_back( Ding {n, 1, (n+1)*kiste_x/Anz_Teilchen, 0, (n+1.0), 0, 0} ); 
    }

Die zum Anfang initialisierten Orte und Geschwindigkeiten der Teilchen werden mittels des Konstruktors festgelegt: 'Ding { Argumentenliste des aufgerufenen Konstruktors }'. Mittels der vom Benutzer festgelegten Argumentenliste wird einer der überladenen Konstruktoren der Klasse Ding aufgerufen. Hier wurde der Konstruktor mit sieben Argumenten gewählt ( Ding(Nummer,x,y,z,vx,vy,vz) ), bei dem die Teilchennummer und der Orte und die Geschwindigkeiten des Teilchens individuell initialisiert wird. Die gewählte Anfangskonfiguration der Teilchen entspricht einer Teilchenbewegung in x-Richtung, wobei alle Teilchen bei x=1 und z=0 starten und ihre y-Postion äquidistant variiert. Die Teilchen mit einer hohen Teilchennummer n bewegen sich schneller als die Teilchen mit niedriger Nummer.

Die zeitliche Entwicklung des Systems wird durch die for-Schleife
'for (int i = 0; i < Anz_tSchritte; ++i){ ... }'
eingeleitet. In dieser Schleife wird die Zeitvariable t in äquidistanten Zeitschritten dt iterativ erhöht (t = i * dt;). Zu jedem Zeitschritt wird jedes Teilchen einzeln zeitlich entwickelt, indem die Member-Funktion 'Gehe_Zeitschritt' aufgerufen wird:


    n.Gehe_Zeitschritt(dt,kiste_x, kiste_y, 0.0);

Zusätzlich werden für jedes Teilchen, seine Nummer und seine aktuelle x- und y-Position ausgegeben. Leitet man die Terminalausgabe in die externe Datei 'Vector_Dinge.dat' um und benutzt das unten abgebildete Python Skript PythonPlot_Vector_Dinge.py, so kann man sich die Bewegung der Teilchen als eine Abfolge von Bildern visualisieren und diese Bilder bilden die Grundlage des oben abgebildeten Videos. Beachten Sie, dass man bevor man das Python Programm startet, einen Unterordner 'Bilder' erstellt haben muss, indem die einzelnen Bilder gespeichert werden.

PythonPlot_Vector_Dinge.py
# Python Programm zum Plotten der Daten des Vector-Containers mit 10 Dingen (Vector_Dinge.cpp)
# Es werden hier mehrere Bilder der zeitlichen Entwicklung des Systems in einem Ordner 'Bilder' gespeichert
# !!!! Sie muessen vor der Ausfuehrung des Programms den Ordner Bilder erstellen !!!!
# Die einzelnen Bilder kann mann dann mittels des folgenden Terminalbefehls zu einem Video binden:
# ffmpeg -framerate 5 -i './Vector_Dinge_%03d.png' -c:v libx264 Vector_Dinge.mp4

import matplotlib.pyplot as plt             # Python Bibliothek zum Plotten (siehe https://matplotlib.org/ )
import numpy as np                          # Python Bibliothek fuer Mathematisches (siehe https://numpy.org/ )

data = np.genfromtxt("./Vector_Dinge.dat")  # Einlesen der berechneten Daten von Vector_Dinge.cpp

plt.title(r'Container mit Teilchen')        # Titel der Abbildung
plt.ylabel('y')                             # Beschriftung y-Achse
plt.xlabel('x')                             # Beschriftung x-Achse

r = 200                                     # Radius eines Dings
plot_min=0                                  # Festlegung der x-Untergrenze (Abmessung Kiste)
plot_max=40                                 # Festlegung der x-Obergrenze (Abmessung Kiste)
anz_teilchen = 10                           # Definition der Anzahl der Dinge

cmap = plt.cm.Blues                                  # Definition der Farbschattierung der Dinge
line_colors = cmap (np.linspace(0.2,1,anz_teilchen)) # Definition der Farbschattierung der Dinge

for it in range(len(data[:,0])):                     # for-Schleife fuer die zeitliche Entwicklung der Dinge in der Kiste
    print(it)                                        # Terminalausgabe der Erstellung des i-ten Bildes
    plt.cla()
    for i in range(anz_teilchen):                    # for-Schleife ueber die Teilchen in der Kiste
        plt.scatter(data[it,3*i+3],data[it,3*i+4], marker='o', color=line_colors[i], s=r) # Kennzeichnung der Position des Dinges durch einen blauen Kreis
        plt.text(data[it,3*i+3],data[it,3*i+4], str(int(data[it,3*i+2])), fontsize=10, verticalalignment='center', horizontalalignment='center', color="red") # Ding Nr.
    plt.xlim(-1,45)                                 # Plot-Limit x-Achse
    plt.ylim(-1,45)                                 # Plot-Limit y-Achse
    
    # Bild-Ausgabe mit Speicherung eines individuellen Iteration-Namens
    pic_name = "./Bilder/" + "Vector_Dinge_" + "{:0>3d}".format(it) + ".png" 
    plt.savefig(pic_name, dpi=200,bbox_inches="tight",pad_inches=0.05,format="png")
    plt.close()

Wir möchten nun kleine Abänderungen in den Anfangsbedingungen der Teilchen machen. Die Größe der Kiste soll jetzt den Abmessungen des Leitbildes der Vorlesung illustration_DeborahMoldawski.jpg entsprechen und es sollen 30 Teilchen in diese Kiste eingesperrt sein. Die Teilchen sollen am Anfang auf der linken Seite auf mittlerer Höhe starten und sich dann in unterschiedlicher Weise nach rechts bewegen. Das folgende C++ Programm stellt eine solche Teilchenentwicklung dar.

Vector_Dingea.cpp
/* Beispiel fuer einen Vector-Container bestehend aus mehreren Elementen des Typs Ding
 * Ding ist eine Klasse  bestehend aus
 * Sieben privaten Instanzvariablen (Ort und Geschwindigkeiten des Dings in 3D)
 * Fuenf ueberladenen Konstruktoren
 * Sieben oeffentlichen const Member-Funktionen
 * und einer oeffentlichen Member-Funktionen 'Gehe_Zeitschritt', 
 * die eine zeitliche Entwicklung im Programm implementiert
 * Initialisierung der Dinge und Abmessung der Kiste wurden an die 
 * Einbindung eines Hintergrundbildes angepasst
 * Ausgabe zum Plotten (Python) mittels: "./a.out > Vector_Dingea.dat"
 * python3 PythonPlot_Vector_Dingea.py
*/
#include <iostream>                   // Ein- und Ausgabebibliothek
#include <vector>                     // Sequenzieller Container vector<Type> der Standardbibliothek
#include <cmath>                      // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
using namespace std;                  // Benutze den Namensraum std


//Definition der Klasse 'Ding'
class Ding{
    // Private Instanzvariablen (Daten-Member) der Klasse
    unsigned int n;                   // Nummer n des Objektes
    double x = 0, y = 0, z = 0;       // Definition der Orte 
    double v_x = 0, v_y = 0, v_z = 0; // Definition der Geschwindigkeiten
    
    // Oeffentliche Konstruktoren und Member-Funktionen der Klasse
    public:
        // Fuenf ueberladene Konstruktoren der Klasse
        // Konstruktor mit sieben Argumenten
        Ding(unsigned int set_n, double set_x, double set_y, double set_z, double set_v_x, double set_v_y, double set_v_z) : n{set_n}, x{set_x}, y{set_y}, z{set_z}, v_x{set_v_x}, v_y{set_v_y}, v_z{set_v_z} {
            printf("# Konstruktor(n,x,y,z,,vx,vy,vz) erzeugt das Ding %i \n",n);
        }
        // Konstruktor mit vier Argumenten
        Ding(unsigned int set_n, double set_x, double set_y, double set_z) : n{set_n}, x{set_x}, y{set_y}, z{set_z} {
            printf("# Konstruktor(n,x,y,z) erzeugt ein das Ding %i \n",n);
        }
        // Konstruktor mit drei Argumenten
        Ding(unsigned int set_n, double set_x, double set_y) : n{set_n}, x{set_x}, y{set_y} {
            printf("# Konstruktor(n,x,y) erzeugt das Ding %i \n",n);
        }
        // Konstruktor mit zwei Argumenten
        Ding(unsigned int set_n, double set_x) : n{set_n}, x{set_x} {
            printf("# Konstruktor(n,x) erzeugt das Ding %i \n",n);
        }
        // Konstruktor ohne Argument (Standard-Konstruktor)
        Ding() : n{0} {
            printf("# Konstruktor() erzeugt das Ding %i \n", n);
        }
        
        // Member-Funktionen der Klasse
        void Gehe_Zeitschritt(double dt, double max_x, double max_y, double max_z); // Deklaration einer Member-Funktion (Definition findet ausserhalb der Klasse statt)
        
        // als const deklariert, da sie die privaten Instanzvariablen nicht veraendern:
        unsigned int get_Nummer() const {return n;}
        double get_Ort_x() const {return x;}
        double get_Ort_y() const {return y;}
        double get_Ort_z() const {return z;}
        
        double get_Geschw_x() const {return v_x;}
        double get_Geschw_y() const {return v_y;}
        double get_Geschw_z() const {return v_z;}
};

/* Definition der Funktion Gehe_Zeitschritt(...) als inline-Methode der Klasse Ding
 * Die Funktion beschreibt die Veränderung der Ortskoordinaten (x,y,z) bei einem Zeitschritt dt
 * Es sind zusaetzlich spezielle Randbedingungen an die Bewegung formuliert, so dass sich die 
 * Dinge nur in einem Bereich von [50,max_x], [50,max_y] und [50,max_z] bewegen koennen */
inline void Ding::Gehe_Zeitschritt(double dt, double max_x, double max_y, double max_z){
    x = x + v_x * dt;
    y = y + v_y * dt;
    z = z + v_z * dt;
    if (x >= max_x || x <= 90){v_x = - v_x;}
    if (y >= max_y || y <= 90){v_y = - v_y;}
    if (z >= max_z || z <= 90){v_z = - v_z;}
}   

int main(){                         // Hauptfunktion
    double kiste_x = 5334 - 200;    // Laenge der Kiste (ein wenig kuerzer als die Abmessung des Bildes)
    double kiste_y = 4000 - 200;    // Breite der Kiste (ein wenig kuerzer als die Abmessung des Bildes)
    unsigned int Anz_Teilchen = 30; // Definition der Anzahl der zu erzeugenden Dinge
    
    double t;                       // Deklaration des Zeitparameters
    double dt = 0.01;               // Definition der Laenge des Zeitschrittes
    double Anz_tSchritte = 200;     // Anzahl der Zeitschritte
    
    vector<Ding> Kiste_Teilchen;    // Deklaration eines vector-Containers
    
    for (unsigned int n = 0; n < Anz_Teilchen; ++n){ // for-Schleife zum Auffuellen des Containers mit Elementen vom Typ 'Ding' 
        Kiste_Teilchen.push_back( Ding {n, 100, kiste_y/2, 0, kiste_x/2 + kiste_x*pow(sin(n),2), kiste_y*sin(2*M_PI*n/Anz_Teilchen)/2 , 0} );
    }
    
    printf("# 0: Index i \n# 1: Zeit t \n# 2: Nummer des Teilchens 1 \n");          // Beschreibung der ausgegebenen Groessen
    printf("# 3: x-Position des Teilchens 1 \n# 4: y-Position des Teilchens 1 \n"); // Beschreibung der ausgegebenen Groessen
    printf("# 5: Nummer des Teilchens 2 \n# 6: .... bis Anzahl der Teichen \n");    // Beschreibung der ausgegebenen Groessen
    
    for (int i = 0; i < Anz_tSchritte; ++i){              // for-Schleife fuer die zeitliche Entwicklung der Dinge in der Kiste
        t = i * dt;                                       // Zeit geht in dt-Schritten voran
        printf("%3i %10.6f ", i, t);                      // Ausgabe Index i und Zeit t
        for (auto& n : Kiste_Teilchen){                   // Bereichsbasierte for-Schleife zum Ausgeben der x-y-Orte der Dinge
            printf("%3i %10.6f %10.6f ", n.get_Nummer(), n.get_Ort_x(), n.get_Ort_y()); // Ausgabe Teilchenorte
            n.Gehe_Zeitschritt(dt,kiste_x, kiste_y, 0.0); // Aufruf der inline Funktion Gehe_Zeitschritt(...)
        }
        printf("\n");
    }
}

Man kann sich die Entwicklung nun wieder grafisch mittels des Python Skriptes PythonPlot_Vector_Dingea.py veranschaulichen. Das unten abgebildete Video wurde mittels des Jupyter Notebooks Vector_Dinge.ipynb erstellt (siehe Vector_Dinge.html für den HTML-Export des Notebooks).