Abgeleitete Klassen, Vererbung von Klassenmerkmalen und Klassenhierarchien

Wir hatten in der Vorlesung 7 in das Konzept der objektorientierten Programmierung eingeführt und gesehen, dass man mittels des Konzeptes einer C++ Klasse selbstdefinierte Objekte im Programm realisieren kann. Eine Klasse stellt eine formale Beschreibung dar (ein Bauplan), wie das Objekt (bzw. die in Programmcode formulierte Idee ) beschaffen ist, und definiert, welche Merkmale (Instanzvariablen bzw. Daten-Member der Klasse) und Verhaltensweisen (Methoden der Klasse bzw. Member-Funktionen) das zu beschreibende Objekt hat. Bei der Konstruktion einer Klassenstruktur eines Programmes kann es nun geschehen, dass man das Programm in mehrere Teilideen/Teilkonzepte unterteilen kann und die so entworfenen Klassen stehen dann häufig in Beziehung zueinander. So können z.B. einige Klassen des Programms auch die Daten-Member und Verhaltensweisen von anderen Klassen verwenden (Vererbung von Merkmalen) und es stellt sich nun die Frage, wie man diese Beziehungen in der Programmiersprache C++ direkt ausdrücken kann. Das Konzept der abgeleiteten Klasse und die damit verbundenen C++ Sprachmechanismen dienen dazu, hierarchische Beziehungen, d.h. Gemeinsamkeiten zwischen den Klassen, auszudrücken.

  • Implementierungsvererbung:
    Indem eine abgeleitete Klasse die Instrumente der Basisklasse erbt, spart sich der Programmierer Schreibaufwand und die übergeordnete Struktur des Programms wird übersichtlicher.
  • Schnittstellenvererbung:
    In vielen größeren Programmen ist es nötig eine Art von übergeordnete Schnittstelle in einer Basisklasse bereitzustellen, die es erlaubt verschiedene abgeleitete Klassen über sie zu verwenden.

In diesem Unterpunkt werden wir in den Themenberech der abgeleiteten Klassen und Vererbung einführen, den man grob in die neben abgebildeten zwei Varianten einordnen kann. Bei der Implementierungsvererbung spart sich der Programmierer oft viel Schreibarbeit und oft wird durch die Vererbungsstruktur seiner Klassen das Programmkonzept übersichtlicher und ist leichter zu verstehen. Bei der Schnittstellenvererbung spricht man auch von Polymorphie zur Laufzeit, bzw. von dynamischer Polymorphie. Die Basisklasse stellt hierbei einen Schnittstelle dar, die ähnlich einer switch-Anweisung, aus unterschiedlichen Programmsträngen auswählt und die einzelnen, möglichen case-Marken sind durch die jeweiligen abgeleiteten Klassen im Programm implementiert. Eine solche polymorphe Struktur kann man, alternativ auch elegant mittels des Template-Konzeptes formulieren, wobei man in einem solchen Fall von Polymorphie zur Übersetzungszeit, bzw. von statischer Polymorphie (näheres siehe nächste Vorlesung).

Abgeleitete Klassen und die Vererbung von Klassenmerkmalen

Es wird nun die formale Struktur einer abgeleiteten Klasse (auch Subklasse) vorgestellt, die von einer Basisklasse (auch Oberklasse oder Superklasse) abgeleitet wird und mittels des Sprachkonzeptes der Vererbung Merkmale übernimmt.

class Base { 'Anweisungsblock: Instanzvariablen (Daten-Member), Konstruktoren, Member-Funktionen, Destruktor' };

class Sub : public Base{ 'Anweisungsblock: Instanzvariablen (Daten-Member), Konstruktoren, Member-Funktionen, Destruktor' };

Die abgeleitete Klasse mit dem Namen 'Sub' erbt die Klassenmerkmale der Basisklasse 'Base' und dies wird mit dem Doppelpunkt gekennzeichnet. Die zusätzliche Bezeichnung 'public' ( class Sub : public Base {...} ) kennzeichnet, dass alle die in der Basisklasse als öffentliche Daten-Member und Member-Funktionen deklarierten Größen auch für die abgeleitete Klasse als öffentlich gelten sollen. Würde man hingegen lediglich 'class Sub : Base {...}' schreiben, so wären alle vererbten Merkmale von Base privat und die abgeleitete Klasse könnte auf diese direkt nicht zugreifen.
In dem folgenden Programmbeispiel wird von einer Basisklasse 'Base' die Subklasse 'Sub' abgeleitet und so alle in ihr definierten Merkmale vererbt.

Base_SubClass_0.cpp
/* Beispiel einer einfachen abgeleiteten Klasse
*/
#include <iostream>             // Ein- und Ausgabebibliothek
using namespace std;            // Benutze den Namensraum std

//Definition der Basisklasse 'Base'
class Base{
    // Private Instanzvariablen (Daten-Member) der Klasse
    unsigned int n;
    double x;
    
    // Oeffentlicher Bereich der Klasse
    public:
        // Konstruktor mit zwei Argumenten
        Base(unsigned int set_n, double set_x) : n{set_n}, x{set_x} {
            printf("Konstruktor der Klasse Base \n");
        }
        
        // Member-Funktionen der Klasse
        // als const deklariert, da sie die privaten Instanzvariablen nicht veraendern
        unsigned int get_Nummer() const {return n;}
        double get_Ort() const {return x;}
};

//Definition der abgeleiteten Klasse 'Sub'
class Sub : public Base{
    // Private Instanzvariablen (Daten-Member) der Klasse
    double m = 1;
    
    // Oeffentlicher Bereich der Klasse
    public:
        // Konstruktor mit drei Argumenten
        Sub(unsigned int set_n, double set_x, double set_m) : Base(set_n, set_x), m{set_m} {
            printf("Konstruktor der Klasse Sub \n");
        }
        
        // Member-Funktionen der Klasse
        // als const deklariert, da sie die privaten Instanzvariablen nicht veraendern
        double get_m() const {return m;}
};

int main(){                         // Hauptfunktion
    Base Ding_0 = Base(0, 2.5);     // Konstruktor bilded eine Instanz der Klasse Base
    Sub Ding_1 = Sub(1, 3.5, 10.2); // Konstruktor bilded eine Instanz der abgeleiteten Klasse Sub

    // Terminalausgabe 
    printf("Das Ding %2d befindet sich am Ort x=%5.2f \n", Ding_0.get_Nummer(), Ding_0.get_Ort());
    printf("Das Ding %2d befindet sich am Ort x=%5.2f und hat die Masse m=%5.2f \n", Ding_1.get_Nummer(), Ding_1.get_Ort(), Ding_1.get_m());
}

Die Superklasse Base hat dabei die Struktur der in der 7. Vorlesung vorgestellten Klasse 'Ding' und besitzt zwei private Datenmember (n und x), einen öffentlichen Konstruktor und zwei öffentliche const Member-Funktionen (get_Nummer() und get_Ort()). Die von ihr abgeleitete Klasse 'Sub' erbt nun die öffentlichen Merkmale ihrer Oberklasse und kann diese so verwenden, als wären sie direkt in der Subklasse definiert. In der Subklasse wird zusätzlich eine weitere Eigenschaft, die Masse 'm' des 'Dings' als privater Datenmember definiert, auf den man von Außen mittels der öffentlichen const Member-Funktion 'get_m()' zugreifen kann. Der Konstruktor der Klasse 'Sub' hat somit ein Argument mehr als der Konstruktor der Klasse 'Base' und beim Aufruf des Konstruktors dieser abgeleiteten Klasse wird die Masse des Dings initialisiert und zusätzlich ein Objekt der Klasse 'Base' erzeugt.
Im Hauptprogramm wird dann eine Instanz (ein Objekt) der Klasse 'Base' (Base Ding_0 = Base(0, 2.5);) und ein Objekt der Klasse 'Sub' (Sub Ding_1 = Sub(1, 3.5, 10.2);) erstellt und die Eigenschaften dieser Objekte mittels der öffentlichen const Member-Funktionen im Terminal ausgegeben.

Der Umweg des Zugriffs über die öffentlichen const Member-Funktionen ist hierbei aufgrund des 'Geheimnisprinzip' der Klasse (auch 'Information Hiding', siehe Vorlesung 7) nötig, da die Kapselung der als privat deklarierten Instanzvariablen dies erfordert. Man hätte hingegen auch die gesamten Merkmale der Klassen als öffentlich deklarieren können, sodass ein Zugriff über öffentliche const Member-Funktionen nicht mehr nötig wäre. Dies ist in dem folgenden Quelltext exemplarisch gezeigt:

Base_SubClass_1.cpp
/* Beispiel einer einfachen abgeleiteten Klasse (alle Merkmale öffentlich)
*/
#include <iostream>             // Ein- und Ausgabebibliothek
using namespace std;            // Benutze den Namensraum std

//Definition der Basisklasse 'Base'
class Base{
    // Oeffentlicher Bereich der Klasse
    public:
        // Instanzvariablen (Daten-Member)
        unsigned int n;
        double x;
        
        // Konstruktor mit zwei Argumenten
        Base(unsigned int set_n, double set_x) : n{set_n}, x{set_x} {
            printf("Konstruktor der Klasse Base \n");
        }
};

//Definition der abgeleiteten Klasse 'Sub'
class Sub : public Base{
    // Oeffentlicher Bereich der Klasse
    public:
        // Instanzvariablen (Daten-Member) der Klasse
        double m = 1;
        
        // Konstruktor mit drei Argumenten
        Sub(unsigned int set_n, double set_x, double set_m) : Base(set_n, set_x), m{set_m} {
            printf("Konstruktor der Klasse Sub \n");
        }
};

int main(){                         // Hauptfunktion
    Base Ding_0 = Base(0, 2.5);     // Konstruktor bilded eine Instanz der Klasse Base
    Sub Ding_1 = Sub(1, 3.5, 10.2); // Konstruktor bilded eine Instanz der abgeleiteten Klasse Sub

    // Terminalausgabe 
    printf("Das Ding %2d befindet sich am Ort x=%5.2f \n", Ding_0.n, Ding_0.x);
    printf("Das Ding %2d befindet sich am Ort x=%5.2f und hat die Masse m=%5.2f \n", Ding_1.n, Ding_1.x, Ding_1.m);
}

Der Zugriff auf die öffentlichen Daten-Member erfolgt nun einfach mit dem Punkt-Operator (z.B. 'Ding_1.x'), wobei man nun deutlich erkennt, dass die Daten-Member der Klasse 'Base' direkt an die Klasse 'Sub' vererbt wurden und der Zugriff so erfolgt, als wäre diese direkt in der Klasse 'Sub' definiert.

Beispiel: Eine abgeleitete Klasse der Basisklasse 'Ding'

Wir wollen im Folgenden eine Subklasse von der folgenden (leicht veränderten) Klasse 'Ding' betrachten:

Vererbung_0.cpp
/* Beispiel einer einfachen Klasse
 * Zwei private Vektoren (Ort,Geschwindigkeit) als Instanzvariablen, 
 * Konstruktor fuer das Initialisieren
 * Drei oeffentliche const Member-Funktionen
*/
#include <iostream>             // Ein- und Ausgabebibliothek
#include <vector>               // Vector-Container der Standardbibliothek
using namespace std;            // Benutze den Namensraum std

//Definition der Klasse 'Ding'
class Ding{
    // Private Instanzvariablen (Daten-Member) der Klasse
    double t = 0.0;                       // Aktueller Zeitpunkt
    vector<double> r = { 0.0, 0.0, 0.0 }; // Ortsvektor zum Ort des Dings ++
    vector<double> v = { 0.0, 0.0, 0.0 }; // Geschwindigkeit des Dings
    
    // Oeffentliche Konstruktoren und Member-Funktionen der Klasse
    public:
        // Konstruktor mit drei Argumenten zum Initialisieren
        Ding(double t_, vector<double> r_, vector<double> v_) : t{t_}, r{r_}, v{v_} { }
        
        // Member-Funktionen der Klasse
        // Drei als const deklariert, da sie die privaten Instanzvariablen nicht veraendern
        double get_zeit() const {return t;}
        vector<double> get_r() const {return r;}
        vector<double> get_v() const {return v;}
};    

int main(){                                                       // Hauptfunktion
    Ding Ding_A = Ding(0.0, {1.0, 2.0, 3.0 }, {10.0, 0.0, 0.0 }); // Konstruktor bilded eine Instanz der Klasse Ding

    // Terminalausgabe von Ort und Geschwindigkeit
    printf("Das Ding A befindet sich am Ort (%5.2f,%5.2f,%5.2f )", Ding_A.get_r()[0], Ding_A.get_r()[1], Ding_A.get_r()[2]);
    printf(" und hat die Geschwindigkeit (%5.2f,%5.2f,%5.2f ) \n", Ding_A.get_v()[0], Ding_A.get_v()[1], Ding_A.get_v()[2]);
}

        

Die Klasse 'Ding' soll nun als eine Schnittstelle fungieren und die Spezialisierung der konkreten Dinge soll innerhalb einer abgeleiteten Klasse definiert werden. Die Klasse 'Ding' stellt dabei für den Benutzer die Orte und Geschwindigkeiten der Dinge in kartesischen Koordinaten bereit. Man kann jetzt unterschiedliche Subklassen für die einzelnen unterschiedlichen Dinge erstellen. Wir möchten z.B. eine von Ding abgeleitete Klasse 'Pendel' erstellen, wobei wir bei der Instanzbildung der Klasse 'Pendel' automatisch ein Objekt der Klasse 'Ding' erstellen möchten. Das folgende C++ Programm zeigt eine mögliche Implementierung der abgeleiteten Klasse 'Pendel'.

Vererbung_1.cpp
/* Beispiel einer von der Basisklasse 'Ding' abgeleitete Klasse 'Pendel'
 * Zwei zusaetzliche private Instanzvariablen (l und beta) und die Winkelspezifischen Anfangswerte (phi und v_phi) 
 * wurden der abgeleiteten Pendel-Klasse hinzugefuegt
 * Der Konstruktor der Klasse Pendel erzeugt ein Objekt Ding und initialisiert seine eigenen Daten-Member
*/
#include <iostream>             // Ein- und Ausgabebibliothek
#include <cmath>                // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
#include <vector>               // Vector-Container der Standardbibliothek
using namespace std;            // Benutze den Namensraum std

//Definition der Klasse 'Ding'
class Ding{
    // Private Instanzvariablen (Daten-Member) der Klasse
    double t = 0.0;                       // Aktueller Zeitpunkt
    vector<double> r = { 0.0, 0.0, 0.0 }; // Ortsvektor zum Ort des Dings ++
    vector<double> v = { 0.0, 0.0, 0.0 }; // Geschwindigkeit des Dings
    
    // Oeffentlicher Bereich
    public:
        // Konstruktor mit drei Argumenten zum Initialisieren
        Ding(double t_, vector<double> r_, vector<double> v_) : t{t_}, r{r_}, v{v_} { }
        
        vector<double> get_r() const { return r; } // Definition der konstanten Member-Funktion get_r, Rueckgabewert vector des Ortes 
        vector<double> get_v() const { return v; } // Definition der konstanten Member-Funktion get_v, Rueckgabewert vector der Geschwindigkeit
        double get_zeit() const { return t; }      // Definition der konstanten Member-Funktion get_zeit(), Rueckgabewert des aktuellen Zeitpunktes
};

//Definition der Klasse 'Pendel' (hier nur eindimensionales (x,y), beschrieben durch Winkel phi, Aufhaengepunkt bei (0,0,0))
class Pendel : public Ding {
    // Private Instanzvariablen (Daten-Member) der Klasse
    double l = 1.0;           // Laenge des Pendels
    double beta = 0.0;        // Reibungskoeffizient
    double phi = 0.0;         // Anfangswinkel des Pendels
    double v_phi = 0.0;       // Anfangswinkelgeschwindigkeit des Pendels

    public:
        // Konstruktor erzeugt ein Objekt Ding und initialisiert die Laenge l des Pendels und den Reibungskoeffizienten beta
        Pendel(double t_, double phi_, double v_phi_, double l_, double beta_) : 
        Ding( t_,  { l_ * sin(phi), - l_ * cos(phi), 0 },  { l_ * cos(phi) * v_phi_, l_ * sin(phi) * v_phi_, 0 }), phi{phi_}, v_phi{v_phi_} , l{l_}, beta{beta_} { }
};

int main(){                                                       // Hauptfunktion
    Pendel Pendel_A = Pendel(0.0, 0.0, 10.0, 1.0, 0.0);           // Konstruktor bilded eine Instanz der Klasse Pendel

    // Terminalausgabe von Ort und Geschwindigkeit
    printf("Das Pendel A befindet sich am Ort (%5.2f,%5.2f,%5.2f )", Pendel_A.get_r()[0], Pendel_A.get_r()[1], Pendel_A.get_r()[2]);
    printf(" und hat die Geschwindigkeit (%5.2f,%5.2f,%5.2f ) \n", Pendel_A.get_v()[0], Pendel_A.get_v()[1], Pendel_A.get_v()[2]);
}

        

Im Folgenden vereinfachen wir die Schnittstellen-Klasse 'Ding' und deklarieren die Variablen der Orte und Geschwindigkeiten als öffentliche Daten-Member. Dies hat den Vorteil, das der Benutzer vom main()-Programm aus direkt mittels des Punktoperators auf die Daten zugreifen kann (z.B. mittels 'Pendel_A.t' für die Zeit t, oder 'Pendel_A.r[0]' für die x-Koordinate des Ortes des Pendels). Zusätzlich wird die Klasse 'Ding' übersichtlicher, da die drei Member-Funktionen get_zeit(), get_r() und get_v() nicht mehr benötigt werden.

Wir möchten nun die Bewegung des Pendels für einen gewissen Zeitraum simulativ berechnen. Betrachten wir z.B. das einfache physikalische Pendel aus dem Themenbereich der Mechanik (siehe z.B. W.Greiner, Klassische Mechanik I). Die zugrundeliegende Differentialgleichung (DGL) des Problems lautet \[ \begin{equation} \frac{d^2 \phi(t)}{dt^2} = -\frac{g}{l} \cdot \hbox{sin}\left( \phi(t) \right) - \beta \cdot \frac{d \phi(t)}{dt} \quad , \end{equation} \] wobei $g$ die Erdbeschleunigung, $l$ die Länge des Pendels, $\beta$ der Reibungsparameter (Stokesscher Ansatz) und $\phi(t)$ die zeitliche Entwicklung des Pendelwinkels beschreibt. Wir schreiben diese DGL (2.Ordnung) in ein System bestehend aus zwei DGLs erster Ordnung um, und benutzen zum numerischen Lösen des Systems, die in der Musterlösung des Übungsblattes Nr. 9 vorgestellte Funktion des Runge-Kutta Ordung vier Verfahrens. Dieses Verfahren implementieren wir in einer öffentlichen Member-Funktion 'void Gehe_Zeitschritt(double dt, int N){...}'. In dieser Funktion wird mittels 'N' Zeitgitterpunkten die Bewegung des Pendels in einem gewissen Zeitintervall 'dt' berechnet. Der berechneten Endwerte werden dann als neu berechnete Werte in den Lösungsvektoren der Klasse Pendel eingetragen. Die Position des aktuell einzutragenden Wertes im Vektor wird dabei mittels einer neuen Variable gesteuert, der 'index' Variable. Diese Index-Variable wird nach Vollendung eines jeden Zeitschrittes um Eins erhöht. Am Ende der Funktion werden die aktuellen Orts- und Geschwindigkeitswerte (im kartesischem Koordinatensystem) der Daten-Member der Klasse 'Ding' aktualisiert.

Vererbung_3.cpp
/* Beispiel einer von der Basisklasse 'Ding' abgeleitete Klasse 'Pendel'
 * Zwei zusaetzliche private Instanzvariablen (l und beta) und die Winkelspezifischen Anfangswerte (phi und v_phi) 
 * wurden der abgeleiteten Pendel-Klasse hinzugefuegt
 * Der Konstruktor der Klasse Pendel erzeugt ein Objekt Ding und initialisiert seine eigenen Daten-Member
*/
#include <iostream>             // Ein- und Ausgabebibliothek
#include <cmath>                // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
#include <vector>               // Vector-Container der Standardbibliothek
using namespace std;            // Benutze den Namensraum std

//Definition der Klasse 'Ding'
class Ding{
    public:
        // Oeffentliche Instanzvariablen (Daten-Member) der Klasse
        double t = 0.0;                       // Aktueller Zeitpunkt
        vector<double> r = { 0.0, 0.0, 0.0 }; // Ortsvektor zum Ort des Dings ++
        vector<double> v = { 0.0, 0.0, 0.0 }; // Geschwindigkeit des Dings
        
        // Konstruktor mit drei Argumenten zum Initialisieren
        Ding(double t_, vector<double> r_, vector<double> v_) : t{t_}, r{r_}, v{v_} { }
};

//Definition der Klasse 'Pendel' (hier nur eindimensionales (x,y), beschrieben durch Winkel phi, Aufhaengepunkt bei (0,0,0))
class Pendel : public Ding {
    // Private Instanzvariablen (Daten-Member) der Klasse
    double l = 1.0;       // Laenge des Pendels
    double beta = 0.0;    // Reibungsparameter
    
    vector<double> phi;   // Deklaration eines double Vektors zum Speichern der Loesung fuer u_1
    vector<double> v_phi; // Deklaration eines double Vektors zum Speichern der Loesung fuer u_2
    vector<double> Zeit;  // Deklaration eines double Vektors zum Speichern der Zeit-Werte
    unsigned N_sim = 2500;            // Anzahl der Gitter-Zeitpunkte der Simulation (Dimension der folgenden Loesungsvektoren)
    unsigned index = 0;
    
    double k1_1,k2_1,k3_1,k4_1; // Deklaration der vier Runge-Kutta Parameter fuer u_1
    double k1_2,k2_2,k3_2,k4_2; // Deklaration der vier Runge-Kutta Parameter fuer u_2

    public:
        // Konstruktor erzeugt ein Objekt Ding und initialisiert die Laenge l des Pendels und den Reibungskoeffizienten beta
        Pendel(double t_, double phi_, double v_phi_, double l_, double beta_, unsigned N_sim_) : 
        Ding( t_,  { l_ * sin(phi_), - l_ * cos(phi_), 0 },  { l_ * cos(phi_) * v_phi_, l_ * sin(phi_) * v_phi_, 0 }), l{l_}, beta{beta_}, N_sim{N_sim_}{ 
            Zeit.resize( N_sim + 2 );        // Die Anzahl der Eintraege im Vektor wird auf N+2 erhoeht
            phi.resize( N_sim + 2 );         // Die Anzahl der Eintraege im Vektor wird auf N+2 erhoeht
            v_phi.resize( N_sim + 2 );       // Die Anzahl der Eintraege im Vektor wird auf N+2 erhoeht
            
            Zeit[0] = t_;                    // Zum Zeit-Vektor die Endzeit eintragen
            phi[0] = phi_;                   // Zum y-Vektor den Anfangswert alpha_1=y(a) eintragen
            v_phi[0] = v_phi_;               // Zum y'-Vektor den Anfangswert alpha_2=y'(a) eintragen
        }
        
        double f_1(double t, double u_1, double u_2){      // Deklaration und Definition der Funktion f_1(t,u_1,u_2) 
            double wert;
            wert = u_2;                                    // Eigentliche Definition der Funktion
            return wert;                                   // Rueckgabewert der Funktion f_1
        }                                                  // Ende der Funktion f_1
        
        double f_2(double t, double u_1, double u_2){      // Deklaration und Definition der Funktion f_2(t,u_1,u_2)
            double wert;
            wert = - 9.81 / l * sin(u_1) - beta * u_2;     // DGl des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
            return wert;                                   // Rueckgabewert der Funktion f_2
        }                                                  // Ende der Funktion f_2
        
        void Gehe_Zeitschritt(double dt, int N){           // Einen Zeitschritt um dt weiter mittels Runge-Kutta  
            double t_;
            double a = Zeit[index];    // Letzter berechneter Zeitpunkt
            double h = dt / N;         // Abstand dt zwischen den aequidistanten Punkten des t-Intervalls (h=dt)  
            double u_1 = phi[index];   // Definition der lokalen Variable u_1=y und Initialisierung mit dem letzten berechneten Wert 
            double u_2 = v_phi[index]; // Definition der lokalen Variable u_2=y' und Initialisierung mit dem letzten berechneten Wert
            
            for(int i=0; i <= N; ++i){                         // for-Schleife ueber die einzelnen Punkte des t-Intervalls
                t_ = a + i*h;                                  // Zeit-Parameter wird um h erhoeht

                k1_1 = h*f_1(t_,u_1,u_2);                      // Runge-Kutta Parameter k1 fuer u_1
                k1_2 = h*f_2(t_,u_1,u_2);                      // Runge-Kutta Parameter k1 fuer u_2
                k2_1 = h*f_1(t_+h/2,u_1+k1_1/2,u_2+k1_2/2);    // Runge-Kutta Parameter k2 fuer u_1
                k2_2 = h*f_2(t_+h/2,u_1+k1_1/2,u_2+k1_2/2);    // Runge-Kutta Parameter k2 fuer u_2
                k3_1 = h*f_1(t_+h/2,u_1+k2_1/2,u_2+k2_2/2);    // Runge-Kutta Parameter k3 fuer u_1
                k3_2 = h*f_2(t_+h/2,u_1+k2_1/2,u_2+k2_2/2);    // Runge-Kutta Parameter k3 fuer u_2
                k4_1 = h*f_1(t_+h,u_1+k3_1,u_2+k3_2);          // Runge-Kutta Parameter k4 fuer u_1
                k4_2 = h*f_2(t_+h,u_1+k3_1,u_2+k3_2);          // Runge-Kutta Parameter k4 fuer u_2 
                u_1 = u_1 + (k1_1 + 2*k2_1 + 2*k3_1 + k4_1)/6;
                u_2 = u_2 + (k1_2 + 2*k2_2 + 2*k3_2 + k4_2)/6;
            }                                                  // Ende for-Schleife ueber die einzelnen Punkte des t-Intervalls   
            
            Zeit[index+1] = t_;                                // Zum Zeit-Vektor den neuen Wert eintragen
            phi[index+1] = u_1;                                // Zum y-Vektor den neuen Wert eintragen
            v_phi[index+1] = u_2;                              // Zum y'-Vektor den neuen Wert eintragen
            index++;                                           // index um eins erhoehen
            
            t = t_;                                            // Neuen Zeit-Wert des Pendels in der Klasse Ding aktualisieren
            r = { l * sin(u_1), - l * cos(u_1), 0 };           // Neuen Ort Wert des Pendels in der Klasse Ding aktualisieren
            v = { l * cos(u_1) * u_2, l * sin(u_1) * u_2, 0 }; // Neuen Geschwindigkeitswert des Pendels in der Klasse Ding aktualisieren
        }                                                      // Ende der Gehe_Zeitschritt Funktion
};                                                             // Ende der Klasse 'Pendel'

int main(){                       // Hauptfunktion
    double a = 0.0;               // Untergrenze des Zeit-Intervalls
    double b = 0.95;              // Untergrenze des Zeit-Intervalls
    int N_RK = 100;               // Anzahl der Gitter-Zeitpunkte des Runge-Kutta Ordnung vier Verfahrens
    int N_sim = 1000;             // Anzahl der Gitter-Zeitpunkte der Simulation (Anzahl der ausgegebenen Punkte)
    double dt = (b - a)/N_sim;    // Abstand dt zwischen den aequidistanten Punkten des Sim-Intervalls (h=dt)   
    double t;                     // Aktueller Zeitwert
    
    Pendel Pendel_A = Pendel(a, 0.0, 40.0, 0.5, 0.0, N_sim); // Konstruktor bilded eine Instanz der Klasse Pendel und erzeugt implizit ein Objekt Ding
    
    printf("# 0: Index i \n# 1: t-Wert \n"); // Beschreibung der ausgegebenen Groessen
    printf("# 2: x(t) \n# 3: y(t) \n");
    printf("# 4: v_x(t) \n# 5: v_y(t) \n");
    printf("%5d %19.15f %19.15f %19.15f %19.15f %19.15f \n",0, Pendel_A.t, Pendel_A.r[0], Pendel_A.r[1], Pendel_A.v[0], Pendel_A.v[1]);
    
    // Terminalausgabe von Ort und Geschwindigkeit des Pendels 
    for(int i=1; i  <= N_sim; ++i){                          // for-Schleife zur Terminalausgabe der Loesung (Zeitgitterpunkte)
        Pendel_A.Gehe_Zeitschritt(dt, N_RK);                 // Aufruf der Methode Gehe_Zeitschritt(dt, N) der Klasse Pendel
        printf("%5d %19.15f %19.15f %19.15f %19.15f %19.15f \n",i, Pendel_A.t, Pendel_A.r[0], Pendel_A.r[1], Pendel_A.v[0], Pendel_A.v[1]); 
    }                                                        // Ende for-Schleife (Zeitgitterpunkte)
}                                                            // Ende main()-Funktion

Die DGL bestimmenden Funktionen sind als Member-Funktionen der Klasse Pendel hinzugefügt, wobei es hier vielleicht sinnvoll gewesen wäre sie in dem privaten Bereich der Klasse zu deklarieren. Im Hauptprogramm wird nun ein Objekt (Name: 'Pendel_A') der abgeleiteten Klasse 'Pendel' erzeugt, indem der Klassen-Konstruktor aufgerufen wird ( Pendel Pendel_A = Pendel(a, 0.0, 40.0, 0.5, 0.0, N_sim); ), wobei die sechs Komponenten der Argumentenliste die Anfangs-/Rahmenbedingungen des Pendels festlegen (hier $\phi(0)=0$, $\dot{\phi}(0)=40$ und $l=0.5$). Die Simulation der Bewegung des Pendels und die Terminalausgabe der berechneten Werte wird innerhalb der for-Schleife 'for(int i=1; i <= N_sim; ++i){ ... }' gemacht. Es werden 'N_sim = 1000' Zeitgitterpunkte ausgegeben, wobei innerhalb eines jeden Zeitschrittes eine Runge-Kutta Ordnung vier Methode mit 'N_RK = 100' Gitterpunkten verwendet wird. Die Visualisierung berechneten Daten der Pendelbewegung (mittels eines Python-Skriptes) wird am Ende dieses Unterpunktes besprochen.

Das bis jetzt besprochene Beispiel einer abgeleiteten Klasse ist hauptsächlich eine Art der Schnittstellen-Vererbung. Im Folgenden werden wir eine weitere Variante der Vererbung kennenlernen, die sogenannte 'gemeinsame Implementierungs' Variante. Zusätzlich werden wir sehen, wie man Unterklassen von Unterklassen (Sub-Subklassen) erstellt.

Wir möchten nun auch die lineare Näherung des Pendels (der harmonische Oszillator mit Reibung ) simulieren. Da wir viele der schon in der Pendelklasse definierten Konstrukte wieder benötigen, ist es sinnvoll, die neue Klasse des mathematischen Pendels als abgeleitete Klasse der Klasse 'Pendel' zu definieren (class Pendel_math : public Pendel { ... }). Der Übersichtlichkeit halber trennen wir den Quelltext des Hauptprogramms (Pendel.cpp) vom den Quelltexten der Klassen (Pendel.hpp). Die Header-Datei besitzt das folgende Aussehen:

Pendel.hpp
/* Beispiel einer von der Basisklasse 'Ding' abgeleitete Klasse 'Pendel' und einer davon abgeleiteten Sub-Subklasse Pendel_math
 * Zwei zusaetzliche private Instanzvariablen (l und beta) und die Winkelspezifischen Anfangswerte (phi und v_phi) 
 * wurden der abgeleiteten Pendel-Klasse hinzugefuegt
 * Der Konstruktor der Klasse Pendel erzeugt ein Objekt Ding und initialisiert seine eigenen Daten-Member
 * Der Konstruktor der, von der Klasse Pendel abgeleiteten Subklasse Pendel_math, erzeugt ein Objekt Pendel (und implizit somit auch ein Objekt Ding`) 
*/
#include <iostream>             // Ein- und Ausgabebibliothek
#include <cmath>                // Bibliothek fuer mathematisches (e-Funktion, Betrag, ...)
#include <vector>               // Vector-Container der Standardbibliothek
using namespace std;            // Benutze den Namensraum std

// Definition der Klasse 'Ding'
class Ding{
    public:
        // Oeffentliche Instanzvariablen (Daten-Member) der Klasse
        double t = 0.0;                       // Aktueller Zeitpunkt
        vector<double> r = { 0.0, 0.0, 0.0 }; // Ortsvektor zum Ort des Dings ++
        vector<double> v = { 0.0, 0.0, 0.0 }; // Geschwindigkeit des Dings
        
        // Konstruktor mit drei Argumenten zum Initialisieren
        Ding(double t_, vector<double> r_, vector<double> v_) : t{t_}, r{r_}, v{v_} { }
};

// Definition der Klasse 'Pendel' (hier nur eindimensionales (x,y), beschrieben durch Winkel phi, Aufhaengepunkt bei (0,0,0))
class Pendel : public Ding {
    // Private Instanzvariablen (Daten-Member) der Klasse
    vector<double> phi;    // Deklaration eines double Vektors zum Speichern der Loesung fuer u_1
    vector<double> v_phi;  // Deklaration eines double Vektors zum Speichern der Loesung fuer u_2
    vector<double> Zeit;   // Deklaration eines double Vektors zum Speichern der Zeit-Werte
    unsigned N_sim = 2500; // Anzahl der Gitter-Zeitpunkte der Simulation (Dimension der folgenden Loesungsvektoren)
    unsigned index = 0;    // Anzahl der schon berechneten Loesungswerte
    
    double k1_1,k2_1,k3_1,k4_1; // Deklaration der vier Runge-Kutta Parameter fuer u_1
    double k1_2,k2_2,k3_2,k4_2; // Deklaration der vier Runge-Kutta Parameter fuer u_2

    public:
        // Oeffentliche Instanzvariablen (Daten-Member) der Klasse
        double l = 1.0;       // Laenge des Pendels
        double beta = 0.0;    // Reibungsparameter
        
        // Konstruktor erzeugt ein Objekt Ding und initialisiert die Laenge l des Pendels und den Reibungskoeffizienten beta
        Pendel(double t_, double phi_, double v_phi_, double l_, double beta_, unsigned N_sim_) : 
        Ding( t_,  { l_ * sin(phi_), - l_ * cos(phi_), 0 },  { l_ * cos(phi_) * v_phi_, l_ * sin(phi_) * v_phi_, 0 }), l{l_}, beta{beta_}, N_sim{N_sim_}{ 
            Zeit.resize( N_sim + 2 );        // Die Anzahl der Eintraege im Vektor Zeit wird auf N+2 erhoeht
            phi.resize( N_sim + 2 );         // Die Anzahl der Eintraege im Vektor phi wird auf N+2 erhoeht
            v_phi.resize( N_sim + 2 );       // Die Anzahl der Eintraege im Vektor v_phi wird auf N+2 erhoeht
            
            Zeit[0] = t_;                    // Zum Zeit-Vektor die Endzeit eintragen
            phi[0] = phi_;                   // Zum y-Vektor den Anfangswert alpha_1=phi(a) eintragen
            v_phi[0] = v_phi_;               // Zum y'-Vektor den Anfangswert alpha_2=v_phi(a) eintragen
        }
        
        double f_1(double t, double u_1, double u_2){      // Deklaration und Definition der Funktion f_1(t,u_1,u_2) 
            double wert;
            wert = u_2;                                    // Eigentliche Definition der Funktion
            return wert;                                   // Rueckgabewert der Funktion f_1
        }                                                  // Ende der Funktion f_1
        
        virtual double f_2(double t, double u_1, double u_2){ // Deklaration und Definition der Funktion f_2(t,u_1,u_2)
            double wert;                                      // virtuell, da diese Funktion in weiteren abgeleiteten Klassen ueberschieben werden wird
            wert = - 9.81 / l * sin(u_1) - beta * u_2;        // DGl des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
            return wert;                                      // Rueckgabewert der Funktion f_2
        }                                                     // Ende der Funktion f_2
        
        void Gehe_Zeitschritt(double dt, int N){              // Einen Zeitschritt um dt weiter mittels Runge-Kutta  
            double t_;
            double a = Zeit[index];    // Letzter berechneter Zeitpunkt
            double h = dt / N;         // Abstand dt zwischen den aequidistanten Punkten des t-Intervalls (h=dt)  
            double u_1 = phi[index];   // Definition der lokalen Variable u_1=y und Initialisierung mit dem letzten berechneten Wert 
            double u_2 = v_phi[index]; // Definition der lokalen Variable u_2=y' und Initialisierung mit dem letzten berechneten Wert
            
            for(int i=0; i <= N; ++i){                         // for-Schleife ueber die einzelnen Punkte des t-Intervalls
                t_ = a + i*h;                                  // Zeit-Parameter wird um h erhoeht

                k1_1 = h*f_1(t_,u_1,u_2);                      // Runge-Kutta Parameter k1 fuer u_1
                k1_2 = h*f_2(t_,u_1,u_2);                      // Runge-Kutta Parameter k1 fuer u_2
                k2_1 = h*f_1(t_+h/2,u_1+k1_1/2,u_2+k1_2/2);    // Runge-Kutta Parameter k2 fuer u_1
                k2_2 = h*f_2(t_+h/2,u_1+k1_1/2,u_2+k1_2/2);    // Runge-Kutta Parameter k2 fuer u_2
                k3_1 = h*f_1(t_+h/2,u_1+k2_1/2,u_2+k2_2/2);    // Runge-Kutta Parameter k3 fuer u_1
                k3_2 = h*f_2(t_+h/2,u_1+k2_1/2,u_2+k2_2/2);    // Runge-Kutta Parameter k3 fuer u_2
                k4_1 = h*f_1(t_+h,u_1+k3_1,u_2+k3_2);          // Runge-Kutta Parameter k4 fuer u_1
                k4_2 = h*f_2(t_+h,u_1+k3_1,u_2+k3_2);          // Runge-Kutta Parameter k4 fuer u_2 
                u_1 = u_1 + (k1_1 + 2*k2_1 + 2*k3_1 + k4_1)/6;
                u_2 = u_2 + (k1_2 + 2*k2_2 + 2*k3_2 + k4_2)/6;
            }                                                  // Ende for-Schleife ueber die einzelnen Punkte des t-Intervalls   
            
            Zeit[index+1] = t_;                                // Zum Zeit-Vektor den neuen Wert eintragen
            phi[index+1] = u_1;                                // Zum y-Vektor den neuen Wert eintragen
            v_phi[index+1] = u_2;                              // Zum y'-Vektor den neuen Wert eintragen
            index++;                                           // index um eins erhoehen
            
            t = t_;                                            // Neuen Zeit-Wert des Pendels in der Klasse Ding aktualisieren
            r = { l * sin(u_1), - l * cos(u_1), 0 };           // Neuen Ort Wert des Pendels in der Klasse Ding aktualisieren
            v = { l * cos(u_1) * u_2, l * sin(u_1) * u_2, 0 }; // Neuen Geschwindigkeitswert des Pendels in der Klasse Ding aktualisieren
        }                                                      // Ende der Gehe_Zeitschritt Funktion
};                                                             // Ende der Klasse 'Pendel'

//Definition der Klasse 'Pendel_math' (mathematische Pendel, harmonischer Oszillator) als abgeleitete Klasse der Klasse Pendel
class Pendel_math : public Pendel {
    public:
        // Konstruktor erzeugt ein Objekt 'Pendel' und implizit auch ein Objekt Ding`
        Pendel_math(double t_, double phi_, double v_phi_, double l_, double beta_, unsigned N_sim_) : Pendel(t_, phi_, v_phi_, l_, beta_, N_sim_){}
    
            double f_2(double t, double u_1, double u_2) override { // Ueberschreiben der virtuellen Funktion aus der Klasse 'Pendel'
                double wert;
                wert = - 9.81 / l * u_1 - beta * u_2;               // DGl des harmonischen Oszillators (omega_0**2 = g/l) mit Reibung (Stokesscher Ansatz)
                return wert;                                        // Rueckgabewert der Funktion f_2
            }                                                       // Ende der Funktion f_2                                                
};                                                                  // Ende der Klasse 'Pendel_math'

Diese Header-Datei bindet man im folgenden Hauptprogramm mittels '#include "Pendel.hpp"'ein. Die zusätzlich eingefügte Klasse 'Pendel_math' wird als Subklasse der Klasse Pendel definiert und die Klasse erbt somit ihre Daten-Member und Member-Funktionen und man spart sich somit viel Schreibarbeit durch diese 'gemeinsame Implementierung'. Der Konstruktor der Klasse 'Pendel_math' bildet eine Instanz der Klasse 'Pendel' und überschreibt die $f_2$-Funktion (DGL des harmonischen Oszillators mit Reibung), die schon in der Klasse 'Pendel als 'virtual' definiert wurde. Die Klasse 'Pendel_math' ist ein typisches Beispiel einer Implementierungsvererbung, wobei die abgeleitete Klasse die Instrumente der ihrer Oberklasse 'Pendel' erbt. Der Programmierer spart sich durch eine solche Implementierung Schreibaufwand und die übergeordnete Struktur des Programms wird übersichtlicher.

Im Hauptprogramm Pendel.cpp (siehe folgender C++ Quelltext) wird nun eine Instanz der Klasse 'Pendel' ( Pendel Pendel_A = Pendel(a, 0.0, 8.2, 0.5, 0.0, N_sim); ) und eine Instanz der Klasse 'Pendel_math' ( Pendel_math Pendel_B = Pendel_math(a, 0.0, 8.2, 0.5, 0.0, N_sim); ) erzeugt, wobei die folgenden, gleichen Parameter an die beiden Konstruktoren der Klasse übergeben wurden: $\phi(0)=0$ [rad], $\dot{\phi}(0)=8.2$ [rad/s], $l=0.5$ [m] und $\beta = 0$. Die Bewegung der beiden Pendel soll von $a=0$ [s] bis $b=5$ [s] simuliert werden, wobei $N_{sim} = 1000$ Lösungswerte bei der Berechnung gespeichert werden. Die Berechnung von einem Zeitpunkt zum Nächsten wurde dabei mit der Runge-Kutta Methode und $N_{RK} = 500$ Zeit-Gitterpunkten durchgeführt.

Pendel.cpp
/* Beispiel einer, von der Basisklasse 'Ding' abgeleitete Klasse 'Pendel'
 * Zwei zusaetzliche private Instanzvariablen (l und beta) und die winkelspezifischen Anfangswerte (phi und v_phi) 
 * wurden der abgeleiteten Pendel-Klasse hinzugefuegt
 * Der Konstruktor der Klasse Pendel erzeugt ein Objekt Ding und initialisiert seine eigenen Daten-Member
 * Von der Klasse Pendel wurde eine weitere Subklasse 'Pendel_math' abgeleitet, welche die lineare Näherung der Pendel-DGL benutzt (gedaempfter harmonischer Ozillator)
*/
#include "Pendel.hpp"          // Pendel und Ding Klassen
#include <iostream>            // Ein- und Ausgabebibliothek

int main(){                    // Hauptfunktion
    double a = 0.0;            // Untergrenze des Zeit-Intervalls
    double b = 5.0;           // Untergrenze des Zeit-Intervalls
    int N_RK = 500;            // Anzahl der Gitter-Zeitpunkte des Runge-Kutta Ordnung vier Verfahrens
    int N_sim = 1000;          // Anzahl der Gitter-Zeitpunkte der Simulation (Anzahl der ausgegebenen Punkte)
    double dt = (b - a)/N_sim; // Abstand dt zwischen den aequidistanten Punkten des Sim-Intervalls (h=dt)   
    
    Pendel Pendel_A = Pendel(a, 0.0, 8.2, 0.5, 0.0, N_sim);           // Konstruktor bilded eine Instanz der Subklasse Pendel und erzeugt implizit ein Objekt Ding
    Pendel_math Pendel_B = Pendel_math(a, 0.0, 8.2, 0.5, 0.0, N_sim); // Konstruktor bilded eine Instanz der Sub-Subklasse Pendel_math 
    
    printf("# 0: Index i \n# 1: t-Wert \n");                           // Beschreibung der ausgegebenen Groessen
    printf("# 2: x(t) , physikalisches Pendel \n# 3: y(t) , physikalisches Pendel \n");
    printf("# 4: v_x(t) , physikalisches Pendel \n# 5: v_y(t) , physikalisches Pendel \n");
    printf("# 6: x(t) , mathematisches Pendel \n# 7: y(t) , mathematisches Pendel \n");
    printf("# 8: v_x(t) , mathematisches Pendel \n# 9: v_y(t) , mathematisches Pendel \n");
    printf("%5d %19.15f %19.15f %19.15f %19.15f %19.15f ",0, Pendel_A.t, Pendel_A.r[0], Pendel_A.r[1], Pendel_A.v[0], Pendel_A.v[1]);
    printf("%19.15f %19.15f %19.15f %19.15f \n",Pendel_B.r[0], Pendel_B.r[1], Pendel_B.v[0], Pendel_B.v[1]);
    
    // Terminalausgabe von Ort und Geschwindigkeit des Pendels 
    for(int i=1; i  <= N_sim; ++i){                          // for-Schleife zur Terminalausgabe der Loesung (Zeitgitterpunkte)
        Pendel_A.Gehe_Zeitschritt(dt, N_RK);                 // Aufruf der Methode Gehe_Zeitschritt(dt, N) der Klasse Pendel
        Pendel_B.Gehe_Zeitschritt(dt, N_RK);                 // Aufruf der Methode Gehe_Zeitschritt(dt, N) der Klasse Pendel
        printf("%5d %19.15f %19.15f %19.15f %19.15f %19.15f ",i, Pendel_A.t, Pendel_A.r[0], Pendel_A.r[1], Pendel_A.v[0], Pendel_A.v[1]); 
        printf("%19.15f %19.15f %19.15f %19.15f \n",Pendel_B.r[0], Pendel_B.r[1], Pendel_B.v[0], Pendel_B.v[1]); 
    }                                                        // Ende for-Schleife (Zeitgitterpunkte)
}                                                            // Ende main()-Funktion

Beim Ausführen des Programmes werden die berechneten Daten des physikalischen und mathematischen Pendels im Terminal ausgegeben:

Mittels des folgenden Pythonskriptes kann man sich dann die Bewegungen der beiden Pendel visualisieren. Hierfür speichern Sie bitte wieder die Terminalausgabe mit './a.out > Pendel.dat' in die Datei 'Pendel.dat'.

Pendel.py
# Python Programm zum Visualisieren der Daten des Pendel.cpp Programms, 
# Mathematisches vs. physikalisches Pendel ohne Reibung
# 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 200 -i './Pendel_%04d.jpg' -s 800x800  Pendel.mp4 
# (die Framerate ergibt sich durch N_sim/b = 1000/5)

import matplotlib
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/ )

import matplotlib.gridspec as gridspec
params = {
    'figure.figsize'    : [8,8],
#    'text.usetex'       : True,
    'axes.titlesize' : 18,
    'axes.labelsize' : 15,  
    'xtick.labelsize' : 15 ,
    'ytick.labelsize' : 15 
}
matplotlib.rcParams.update(params) 

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

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()
    plt.title(r'Mathematisches Pendel ohne Reibung') # Titel der Abbildung
#    plt.title(r'Physikalisches Pendel ohne Reibung') # Titel der Abbildung
    plt.xlabel('x')                                  # Beschriftung x-Achse
    plt.ylabel('y')                                  # Beschriftung y-Achse
    plt.xlim(-0.6, 0.6)                              # Limitierung der x-Achse
    plt.ylim(-0.6, 0.6)                              # Limitierung der y-Achse
    
    plt.scatter( data[it,6], data[it,7], s=50, marker='o', c="red")      # Zeichnen des Pendel-Koerpers
    plt.plot( [0 , data[it,6]] , [0 , data[it,7]] ,c="blue",linewidth=1) # Zeichnen der Pendel-Stange
#    plt.scatter( data[it,2], data[it,3], s=50, marker='o', c="red")      # Zeichnen des Pendel-Koerpers
#    plt.plot( [0 , data[it,2]] , [0 , data[it,3]] ,c="blue",linewidth=1) # Zeichnen der Pendel-Stange

    # Bild-Ausgabe mit Speicherung eines individuellen Iteration-Namens
    pic_name = "./Bilder/" + "Pendel_" + "{:0>4d}".format(it) + ".jpg"
    plt.savefig(pic_name, dpi=200,bbox_inches="tight",pad_inches=0.05,format="jpg")
    plt.close()

Das Pythonskript Pendel.py erstellt 1001 separate Bilder der zeitlichen Entwicklung eines der Pendel und speichert diese in einem Unterordner 'Bilder', den Sie vor dem Ausführen des Python Programmes erstellt haben müssen. Die produzierten Bilder kann man sich dann in einer Animation als Film zusammenfügen. Hierfür wurde das Programm FFmpeg (siehe auch FFmpeg, Documentation und FFmpeg, Ubuntu Wiki) verwendet. Nachdem man sich das Programm installiert hat, öffnet man sich ein Terminal in dem Verzeichnis 'Bilder' und erzeugt mittels des folgenden Befehls das Video 'Pendel.mp4': 'ffmpeg -framerate 200 -i './Pendel_%04d.jpg' -s 800x800 Pendel.mp4'. Die Framerate bezeichnet hierbei die Geschwindigkeit der Abfolge der einzelnen Bilder in Bilder/Sekunde. Der gewählte Wert von '-framerate 200' ergibt sich dadurch, dass wir 1001 Bilder in einem Zeitintervall von $(b-a)=5$ [s] produziert haben, und man somit eine Bildgeschwindigkeit von $1001/5 \approx 200$ [Bilder/s] hat. Die unten abgebildeten Animationen visualisieren die Bewegung des physikalischen und mathematischen Pendels.

Führt man die Simulation mit Stokesscher Reibung durch (hier speziell $\beta = 0.8$), so erhält man die folgenden gedämpften Bewegungen der beiden Pendel:

Abschließend sollte erwähnt werden, dass die konstruierte Verflechtung der Klassen 'Ding', 'Pendel' und 'Pendel_math' als ein Beispiel für abgeleitete Klassen anzusehen ist, um die unterschiedlichen Varianten des Vererbungsmechanismus (Schnittstellenvererbung und Implementierungsvererbung) zu verdeutlichen. Die Sinnhaftigkeit des Beispiels, und im Speziellen die Notwendigkeit und Implementierung einer Schnittstellen-Klasse 'Ding' ist sicherlich fraglich und kann auch einfach weggelassen werden. Möchte man dennoch eine solche eine solche Schnittstellenbasisklasse definieren, so ist es wohl sinnvoll diese als rein abstrakte Superklasse zu definieren. Eine rein abstrakte Klasse ist dadurch definiert, dass sie ausschließlich virtuelle Daten-Member und Funktionen definiert, die dann in den jeweiligen Unterklassen überschrieben werden. Eine solche abstrakte Schnittstellen-Superklasse könnte z.B. das folgende Aussehen besitzen:

Ding_Abstrakt.hpp
#include <cmath>            // Bibliothek fuer mathematisches (e-Funktion, Betrag, ...)
#include <vector>           // Vector-Container der Standardbibliothek
#include <array>            // Array-Container der Standardbibliothek

// Definition der abstrakten Klasse 'Ding'
class Ding
{
public:
    // Definition der rein virtuellen Member Funktionen
    virtual std::array<double,3> get_r() const = 0; // Ortsvektor zum Ort des Dings (Standardarray fester Groesse)
    virtual std::array<double,3> get_v() const = 0; // Geschwindigkeit des Dings (Standardarray fester Groesse)
    virtual double get_t() const = 0;               // Aktueller Zeitpunkt
};

// Definition der abgeleiteten Klasse 'Pendel' 
class Pendel : public Ding
{
    double l = 1.0;          // Laenge des Pendels
    unsigned index = 0;      // Anzahl der schon berechneten Loesungswerte
    std::vector<double> phi; // Deklaration eines double Vektors zum Speichern der Loesung fuer u_1
    // ....
    
public:
    // Konstruktor ....
    
    // Ueberschreiben der virtuellen Funktion get_r() aus der Klasse 'Ding'
    std::array<double,3> get_r() const override
    {
        return { l * sin(phi[index]), -l*cos(phi[index]), 0 };
    }
    
    // Ueberschreiben der virtuellen Funktion get_v() aus der Klasse 'Ding'
    // ...
};

Es wurde hierbei auch das noch nicht besprochene array-Konstrukt der Standardbibliothek verwendet (std::array<double,3>). Dieser neue Datentyp stellt wie der Standardvektor auch eine Sequenz von Elementen dar, jedoch ist seine Größe fest und kann mittels 'size()' angegeben werden. Im Gegensatz zu integrierten Arrays (siehe Vorlesung 5) kann man aber ein solches Array in einfacher Weise, direkt als ein Rückgabewert einer Funktion verwenden.

Mehrfachvererbung und Klassenhierarchien

In dem vorigen Unterpunkt wurde in das C++ Konstrukt der abgeleiteten Klasse eingeführt. Besitzt eine abgeleitete Klasse gleichzeitig mehrere Oberklassen, so spricht man von einer Mehrfachvererbung. Die C++ Struktur einer solchen Mehrfachvererbung besitzt formal das folgende Aussehen:

class Base_1 { 'Anweisungsblock: Instanzvariablen (Daten-Member), Konstruktoren, Member-Funktionen, Destruktor' };

class Base_2 { 'Anweisungsblock: Instanzvariablen (Daten-Member), Konstruktoren, Member-Funktionen, Destruktor' };

class Sub : public Base_1, public Base_2{ 'Anweisungsblock: Instanzvariablen (Daten-Member), Konstruktoren, Member-Funktionen, Destruktor' };

Die abgeleitete Klasse mit dem Namen 'Sub' erbt hierbei die Klassenmerkmale von der Basisklassen 'Base_1' und 'Base_1'. In umfangreichen C++ Programmen baut sich somit eine Klassenhierarchie bestehend aus Basis-, Sub- und Sub-Subklassen auf.