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 }
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.
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.
#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.
#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"); }
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.
/* 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.
# 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.
/* 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).