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 jetzt 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 Themenbereich 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 zusätzlich 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 spricht.

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 = 0, double set_x = 0) : 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;

    // Oeffentlicher Bereich der Klasse
    public:
        // Konstruktor mit drei Argumenten
        Sub(unsigned int set_n = 0, double set_x = 0, double set_m = 1) : 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 (siehe Abbildung unten links).

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 = 0, double set_x = 0) : 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;

        // Konstruktor mit drei Argumenten
        Sub(unsigned int set_n = 0, double set_x = 0, double set_m = 1) : 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.

Anwendungsbeispiel: Eine abgeleitete Klasse 'Pendel'

Bei der Konstruktion einer Verflechtung von mehreren Klassen erschafft der Programmierer mittels der unterschiedlichen Varianten des Vererbungsmechanismus (Schnittstellenvererbung und Implementierungsvererbung) ein eigenes Konstrukt, welches dann der Anwender nutzen kann. Wir stellen uns im Folgenden die Aufgabe mittels einer Klassen-Verflechtung ein Konstrukt zu Erschaffen, welches man benutzen kann, um unterschiedlichste Formen von Pendelbewegungen mittels des Computers zu berechnen. Mit diesem Programm soll, unter anderem, die Bewegung des physikalischen und mathematischen Pendels simuliert werden können. Beim Entwurf versuchen wir so allgemein wie möglich zu sein und verwenden deshalb die im vorigen Unterpunkt konstruierte Klasse 'dsolve' zur Berechnung der Pendelbewegung, obwohl diese ja konstruiert wurde um damit allgemeine Differentialgleichungssysteme lösen zu können.

Wir möchten 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 \theta(t)}{dt^2} = - \frac{g}{l} \cdot \hbox{sin}\left( \theta(t) \right) - \frac{\beta}{m} \cdot \frac{d \theta(t)}{dt} \quad , \end{equation} \] wobei $g$ die Erdbeschleunigung, $m$ und $l$ die Masse und Länge des Pendels, $\beta$ der Reibungsparameter (Stokesscher Ansatz) und $\theta(t)$ die zeitliche Entwicklung des Pendelwinkels beschreibt.

Wir schreiben nun die obere Bewegungsgleichung zweiter Ordnung in ein System von zwei miteinander gekoppelten Differentialgleichungen erster Ordnung um. Wir machen dafür die vorgegebene Variablenumbenennung ($u_1(t)=\theta(t)$ , $u_2(t)=\dot{\theta}(t) = \frac{d \theta(t)}{dt}$) und definieren das System von DGLs wie folgt: $$ \begin{eqnarray} \dot{u}_1(t) &=& \frac{d u_1}{dt} = \frac{d \theta(t)}{dt} = u_2(t) \\ \dot{u}_2(t) &=& \frac{d u_2}{dt} = \frac{d \dot{\theta}}{dt} = \frac{d^2 \theta(t)}{dt^2} = - \frac{\beta}{m} \cdot u_2(t) - \frac{g}{l} \cdot \hbox{sin}\left( u_1(t) \right) := f(t,u_1,u_2) \end{eqnarray} $$

Die numerische Lösung dieses Problems werden wir mittels der im vorigen Unterpunkt 'Projekt: Die schwingende Kette' konstruierten Klasse 'dsolve' machen (siehe C++ Programm SchwingendeKette.cpp). Das folgende C++ Programm berechnet die numerische Lösung des oben abgebildeten Systems von Differentialgleichungen und simuliert die Bewegung des physikalischen Pendels.

Pendel_0.cpp
/* Das physikalische Pendel mit Reibung (Stokesscher Ansatz)
 * Berechnung der Lösung einer Differentialgleichung zweiter Ordnung
 * bzw. eines Systems von zwei Differentialgleichungen erster Ordnung
 * mittels der Runge-Kutta Ordnung vier Methode
 * Verfahren zur Loesung der DGL ist in eine Klasse ausgelagert
 * Zeitentwicklung fuer unterschiedliche t-Werte in [a,b]
 * Konstruktor: dsolve(Anfangszeit a, Endzeit b, Anzahl der Punkte N, Anfangswerte alpha, DGLs als Funktion)
 */
#include <iostream>            // Standard Input- und Output Bibliothek in C, z.B. printf(...)
#include <cmath>               // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
#include <vector>              // Vector-Container der Standardbibliothek
#include <functional>          // Funktionen in der Argumentenliste von Funktionen
using namespace std;           // Benutze den Namensraum std

class dsolve{                           //Definition der Klasse 'dsolve'
    double a = 0;                       // Untergrenze des Zeit-Intervalls [a,b] in dem die Loesung berechnet werden soll
    double b = 1;                       // Obergrenze des Intervalls [a,b]
    int N = 10;                         // Anzahl der Punkte in die das t-Intervall aufgeteilt wird
    vector<double> alpha = {1,0};       // Zwei Anfangswerte (Anfangswinkel und Winkelgeschwindigkeit bei t=a)

    vector<vector<double>> y_RungeK_4;  // Deklaration eine double Vektor-Matrix zum speichern der Loesungen
    vector<double> Zeit;                // Deklaration eines double Vektors zum speichern der Zeit-Werte

    public:
        // Konstruktor mit fünf Argumenten (Initialisierung der Parameter, Uebergabe der DGL als Funktion, Berechnung der Loesung der DGL)
        dsolve(double a_, double b_, int N_, vector<double> alpha_, function< vector<double>(double, vector<double>)> f) : a(a_),b(b_),N(N_),alpha(alpha_) {
            double h = (b - a)/N;                   // Abstand dt zwischen den aequidistanten Punkten des t-Intervalls (h=dt)
            int n = alpha_.size();                  // Anzahl der Anfangswerte bzw. DGL-Gleichungen
            vector<double> k1(n),k2(n),k3(n),k4(n); // Deklaration der vier Runge-Kutta Parameter fuer jede Loesung
            vector<double> y(n);                    // Temporaerer Hilfsvektor
            Zeit.push_back(a_);                     // Zum Zeit-Vektor die Anfangszeit eintragen
            y_RungeK_4.push_back(alpha_);           // Zum Loesungs-Vektor die Anfangswerte eintragen

            for(int i=0; i < N; ++i){                                             // for-Schleife ueber die einzelnen Punkte des t-Intervalls
                for(int j=0; j < n; ++j){k1[j] = h*f(Zeit[i],y_RungeK_4[i])[j];}  // for-Schleife ueber die n Runge-Kutta k1-Parameter
                for(int j=0; j < n; ++j){y[j] = y_RungeK_4[i][j] + k1[j]/2;}      // Temporaerer Hilfsvektor
                for(int j=0; j < n; ++j){k2[j] = h*f(Zeit[i]+h/2,y)[j];}          // for-Schleife ueber die n Runge-Kutta k2-Parameter
                for(int j=0; j < n; ++j){y[j] = y_RungeK_4[i][j] + k2[j]/2;}      // Temporaerer Hilfsvektor
                for(int j=0; j < n; ++j){k3[j] = h*f(Zeit[i]+h/2,y)[j];}          // for-Schleife ueber die n Runge-Kutta k3-Parameter
                for(int j=0; j < n; ++j){y[j] = y_RungeK_4[i][j] + k3[j];}        // Temporaerer Hilfsvektor
                for(int j=0; j < n; ++j){k4[j] = h*f(Zeit[i]+h,y)[j];}            // for-Schleife ueber die n Runge-Kutta k4-Parameter
                for(int j=0; j < n; ++j){y[j] = y_RungeK_4[i][j] + (k1[j] + 2*k2[j] + 2*k3[j] + k4[j])/6;} // Temporaerer Hilfsvektor

                y_RungeK_4.push_back(y);                                          // Zum Loesungs-Vektor den neuen Wert eintragen
                Zeit.push_back(a + (i+1)*h);                                      // Zum Zeit-Vektor die neue Zeit eintragen
            }                                                                     // Ende for-Schleife ueber die einzelnen Punkte des t-Intervalls
        };                                                                        // Ende des Konstruktors

        const vector<vector<double>>& get_y() const { return y_RungeK_4; }        // Definition der konstanten Member-Funktion get_y(), Rueckgabewert Vektor-Matrix der Loesung der DGL
        const vector<double>& get_zeit() const { return Zeit; }                   // Definition der konstanten Member-Funktion get_zeit(), Rueckgabewert vector der zeit-Punkte
};                                                                                // Ende der Klasse 'dsolve'

// Definition der Bewegungsgleichung des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
vector<double> dgls(double t, vector<double> u_vec) {
    double m = 1.0;                                              // Masse des Pendels
    double l = 0.5;                                              // Länge des Pendels
    double beta = 0.0;                                           // Reibungskoeffizient

    vector<double> du_dt(u_vec.size());                          // Die zwei DGLs erster Ordnung werden als Standard-Vektor definiert
    du_dt[0] = u_vec[1];                                         // Zwei DGLs erster Ordnung
    du_dt[1] = - 9.81 / l * sin(u_vec[0]) - beta / m * u_vec[1]; // DGl des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
    return du_dt;                                                // Rueckgabewert der DGLs als Standard-Vektor
}                                                                // Ende: Definition der Bewegungsgleichung

// Hauptfunktion
int main(){
    vector<double> u_init {0.0,8.2};     // Vektor der zwei Anfangswerte (Anfangswinkel und Winkelgeschwindigkeit bei t=a)
    dsolve Loes1 {0,5,1000,u_init,dgls}; // Benutzt Konstruktor mit a=0, b=5, N=1000

    // Terminalausgabe
    printf("# 0: Index i \n# 1: t-Wert \n# 2: Winkel-Koordinate theta \n# 3: Winkel-Geschwindigkeit dtheta/dt \n");
    for(size_t i=0; i  < Loes1.get_zeit().size(); ++i){
        printf("%3ld %19.15f %19.15f %19.15f \n", i,Loes1.get_zeit()[i],Loes1.get_y()[i][0],Loes1.get_y()[i][0]);
    }
}  // Ende der Hauptfunktion

Die Klasse 'dsolve' besitzt die Fähigkeit, ihre Kernaufgabe (die Lösung einer Bewegungsgleichung) auf verschiedene ganz unterschiedliche Objekte anwenden zu können, als wären sie gleich. Sie stellt somit eine Art von Schnittstelle dar und entkoppeln die eigentliche Darstellung der konkreten Umsetzung der Idee von der Klasse selbst. Dies ist ein gutes Beispiel von Polymorphismus und wird in C++ durch Vererbung und virtuelle Funktionen erreicht. Im Folgenden möchten wir das Konzept der abgeleiteten Klasse auf das obere Programm anwenden. Wir werden dabei die Klasse 'dsolve' als eine abstrakte Basisklasse definieren, die eine reine virtuelle Funktion 'dgls' enthält. Die Klasse 'dsolve' dient dabei als eine allgemeine Schnittstelle für das Runge-Kutta-Verfahren und speichert unter anderem die Lösung der Bewegungsgleichung. Dabei kann die Klasse 'dsolve' jedoch nicht selbst instanziiert werden, da sie von sich aus kein eigenes Objekt bilden kann, ohne eine Bewegungsgleichung des Systems zu spezifizieren. Die Bewegungsgleichung wird dann erst in einer abgeleiteten Sub-Klasse (hier die Klasse 'Pendel') definiert und damit eine entsprechende Instanz gebildet. Das folgende C++ Programm verfolgt die angegebene Klassenstruktur und berechnet wieder die numerische Lösung der Bewegung des physikalischen Pendels.

Pendel_1.cpp
/* Das physikalische Pendel mit Reibung (Stokesscher Ansatz)
 * Berechnung der Lösung einer Differentialgleichung zweiter Ordnung
 * bzw. eines Systems von zwei Differentialgleichungen erster Ordnung
 * mittels der Runge-Kutta Ordnung vier Methode
 * Verfahren zur Loesung der DGL ist in einer abstrakten Basisklasse ausgelagert
 * Berechnung der Lösung innerhalb einer Methode (Klassenfunktion) Runge-Kutta-Verfahren
 * Überschreibung der virtuellen Funktion der Bewegungsgleichungen in der abgeleiteten Klasse (Subklasse) Pendel
 * Zeitentwicklung der fuer unterschiedliche t-Werte in [a,b]
 * Konstruktor: Pendel(Masse des Pendels, Länge des Pendels l, Reibung beta, Anfangszeit a, Endzeit b, Anzahl der Punkte N, Anfangswerte alpha)
 * Ausgabe zum Plotten mittels Python Jupyter Notebook Pendel.py: "./a.out > Pendel.dat
 */
#include <iostream>            // Standard Input- und Output Bibliothek in C, z.B. printf(...)
#include <cmath>               // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
#include <vector>              // Vector-Container der Standardbibliothek
using namespace std;

// Abstrakte Basisklasse 'dsolve'
class dsolve {
public:
    // Daten-Member der Klasse
    double a;                        // Untergrenze des Zeit-Intervalls
    double b;                        // Obergrenze des Intervalls
    int N;                           // Anzahl der Punkte
    vector<double> alpha;            // Anfangswerte

    vector<vector<double>> ym;       // Lösungsmatrix
    vector<double> t;                // Zeit-Vektor

    // Virtuelle Funktion dgls, wird an anderer Stelle definiert
    virtual vector<double> dgls(double t, vector<double> u_vec) = 0;

    // Konstruktor mit vier Argumenten (Initialisierung der Daten-Member)
    dsolve(double a_, double b_, int N_, vector<double> alpha_) :a(a_), b(b_), N(N_), alpha(alpha_) {
        t.push_back(a_);                // Zum Zeit-Vektor die Anfangszeit eintragen
        ym.push_back(alpha_);           // Zur Loesungs-Matrix die Anfangswerte eintragen
    }

    // Methode der Runge-Kutta-Methode
    void solve() {
        double h = (b - a)/N;                   // Abstand dt zwischen den aequidistanten Punkten des t-Intervalls (h=dt)
        int n = alpha.size();                   // Anzahl der Anfangswerte bzw. DGL-Gleichungen (hier n=2)
        vector<double> k1(n),k2(n),k3(n),k4(n); // Deklaration der vier Runge-Kutta Parameter fuer jede Loesung
        vector<double> y(n);                    // Temporaerer Hilfsvektor

        for (int i = 0; i < N; ++i) {                                         // for-Schleife ueber die einzelnen Punkte des t-Intervalls
            for (int j = 0; j < n; ++j) k1[j] = h * dgls(t[i], ym[i])[j];     // for-Schleife ueber die n Runge-Kutta k1-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k1[j] / 2;          // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k2[j] = h * dgls(t[i] + h / 2, y)[j]; // for-Schleife ueber die n Runge-Kutta k2-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k2[j] / 2;          // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k3[j] = h * dgls(t[i] + h / 2, y)[j]; // for-Schleife ueber die n Runge-Kutta k3-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k3[j];              // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k4[j] = h * dgls(t[i] + h, y)[j];     // for-Schleife ueber die n Runge-Kutta k4-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j]) / 6; // Temporaerer Hilfsvektor

            ym.push_back(y);              // Zum Loesungs-Vektor den neuen Wert eintragen
            t.push_back(a + (i + 1) * h); // Zum Zeit-Vektor die neue Zeit eintragen
        }                                 // Ende for-Schleife ueber die einzelnen Punkte des t-Intervalls
    }                                     // Ende des Methode 'solve()'
};  // Ende der Klasse 'dsolve'

// Unterklasse 'Pendel'
class Pendel : public dsolve {
public:
    double m;    // Masse des Pendels
    double l;    // Länge des Pendels
    double beta; // Reibungskoeffizient

    // Überschreibung der virtuellen Funktion dgls
    vector<double> dgls(double t, vector<double> u_vec) override {
        vector<double> du_dt(u_vec.size());                          // Die zwei DGLs erster Ordnung werden als Standard-Vektor definiert
        du_dt[0] = u_vec[1];                                         // Zwei DGLs erster Ordnung
        du_dt[1] = - 9.81 / l * sin(u_vec[0]) - beta / m * u_vec[1]; // DGl des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
        return du_dt;                                                // Rueckgabewert der DGLs als Standard-Vektor
    }                                                                // Ende: Definition der Bewegungsgleichung

    // Konstruktor mit sieben Argumenten (Initialisierung der Instanzvariablen (m,l,beta), vier Argumente für dsolve)
    Pendel(double m_ = 1.0, double l_ = 1.0, double beta_ = 0.0, double a_ = 0, double b_ = 1, int N_ = 10, vector<double> alpha_ = {0.1, 0}) :
        dsolve(a_, b_, N_, alpha_), m(m_), l(l_), beta(beta_) {}

    // Methode der Ausgabe der kartesische Koordinaten des Pendels am Stützpunkt i
    vector<double> r(size_t i) {
        vector<double> w = {0.0,0.0};
        if (i < t.size()) { w = {l*sin(ym[i][0]), -l*cos(ym[i][0])};}
        return w;
    }
    // Methode der Ausgabe der Geschwindigkeit des Pendels in kartesische Koordinaten am Stützpunkt i
    vector<double> v(size_t i) {
        vector<double> w = {0.0,0.0};
        if (i < t.size()) { w = {l*cos(ym[i][0])*ym[i][1], l*sin(ym[i][0])*ym[i][1]};}
        return w;
    }
}; // Ende der Unterklasse 'Pendel'

// Hauptprogramm
int main() {
    double m = 1.0;                   // Masse des Pendels
    double l = 0.5;                   // Länge des Pendels
    double beta = 0;                  // Reibungskoeffizient
    double a = 0;                     // Untergrenze des Zeit-Intervalls
    double b = 5;                     // Obergrenze des Intervalls
    int N = 1000;                     // Anzahl der Punkte
    vector<double> alpha = {0.0,8.2}; // Anfangswerte

    Pendel P_A = Pendel(m,l,beta,a,b,N,alpha); // Konstruktor bilded eine Instanz der Klasse Pendel
    P_A.solve();                               // Methode der Runge-Kutta-Methode ausführen

    // Terminalausgabe
    printf("# 0: Index i \n# 1: t-Wert \n# 2: x-Koordinate \n# 3: y-Koordinate \n# 4: Geschwindigkeit dx/dt \n# 5: Geschwindigkeit dy/dt \n");
    for(size_t i=0; i  < P_A.t.size(); ++i){
        printf("%3ld %19.15f %19.15f %19.15f %19.15f %19.15f \n",i,P_A.t[i],P_A.r(i)[0],P_A.r(i)[1],P_A.v(i)[0],P_A.v(i)[1]);
    }
} // Ende Hauptprogramm

Die Klasse 'dsolve' agiert hierbei als eine abstrakte Schnittstellen-Basisklasse, welche die Eigenschaft besitzt, eine Bewegungsgleichung lösen zu können, jedoch für eine Objekt-Erzeugung noch die Festlegung der Differentialgleichung des betrachteten Systems benötigt. Allgemein bezeichnet man eine Klasse mit mindestens einer reinen virtuellen Funktion als abstrakt ('rein' bedeutet hier, dass die virtuelle Funktion nur deklariert wird und die eigentliche Definition außerhalb der Klasse geschieht). Die Klasse 'dsolve' ist somit aufgrund der rein virtuellen Funktion 'virtual vector<double> dgls(double t, vector<double> u_vec) = 0;' eine abstrakte Klasse und kann nicht instanziiert werden. Der Konstruktor in 'dsolve' initialisiert dabei nur die Daten-Member und die eigentliche Runge-Kutta-Methode der Lösung der Bewegungsgleichung ist innerhalb einer Methode 'solve()' definiert. Bei abstrakten Klassen wird generell ein virtueller Destruktor empfohlen, um auch bei komplizierteren Subklassen ein ordnungsgemäßes Aufräumen sicherzustellen (z.B. mittels virtual ~dsolve() = default;) - in unserem Fall ist dies jedoch nicht zwingend erforderlich.

Von der abstrakten Basisklasse 'dsolve' wurde dann die Unterklasse 'Pendel' abgeleitet. Diese Subklasse 'Pendel' überschreibt die rein virtuelle Funktion 'dgls' und definiert somit die Bewegungsgleichung des physikalischen Pendels. Mittels des Konstruktors der Pendel-Klasse ist es nun möglich, eine Instanz eines Pendel-Objektes zu erstellen. Der Pendel-Konstruktor ruft dabei den Konstruktor der abstrakten Basisklasse auf und initialisiert seine eigenen Instanzvariablen (Masse $m$ und Länge $l$ des Pendels und der Reibungskoeffizient $\beta$). Zusätzlich werden in der Pendel-Klasse noch zwei Methoden definiert, die den Ort und die Geschwindigkeit des Pendels an einer gewünschten zeitlichen Stützstelle in kartesischen Koordinaten ausgeben. Im Hauptprogramm wird dann eine Instanz der Pendelklasse erstellt und die vererbte Funktion 'solve()' aufgerufen. Am Ende werden die berechneten Lösungswerte der Pendelbewegung im Terminal ausgegeben. 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 (den harmonischen 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 eine abgeleitete Klasse der Klasse 'Pendel' zu definieren (class Pendel_math : public Pendel { ... }). Der Übersichtlichkeit halber trennen wir den Quelltext des Hauptprogramms (Pendel.cpp) von dem Quelltext der Klassen (Pendel.hpp). Die Header-Datei, welche die gesamte Klassenhierarchie zusammenfasst, besitzt das folgende Aussehen:

Pendel.hpp
/* Header-Datei
 * Das physikalische und mathematische Pendel mit Reibung (Stokesscher Ansatz)
 * Berechnung der Lösung einer Differentialgleichung zweiter Ordnung
 * bzw. eines Systems von zwei Differentialgleichungen erster Ordnung
 * mittels der Runge-Kutta Ordnung vier Methode
 * Verfahren zur Loesung der DGL ist in einer abstrakten Basisklasse ausgelagert
 * Berechnung der Lösung innerhalb einer Methode (Klassenfunktion) Runge-Kutta-Verfahren
 * Überschreibung der virtuellen Funktion der Bewegungsgleichungen in der abgeleiteten Klasse (Subklasse) Pendel
 * Zeitentwicklung der fuer unterschiedliche t-Werte in [a,b]
 * Konstruktor: Pendel(Masse des Pendels, Länge des Pendels l, Reibung beta, Anfangszeit a, Endzeit b, Anzahl der Punkte N, Anfangswerte alpha)
 * bzw. Konstruktor: Pendel_math(...)
 */
#include <cmath>               // Bibliothek für mathematisches (e-Funktion, Betrag, ...)
#include <vector>              // Vector-Container der Standardbibliothek
using namespace std;           // Benutze den Namensraum std

// Abstrakte Basisklasse 'dsolve'
class dsolve {
public:
    // Daten-Member der Klasse
    double a;                        // Untergrenze des Zeit-Intervalls
    double b;                        // Obergrenze des Intervalls
    int N;                           // Anzahl der Punkte
    vector<double> alpha;            // Anfangswerte

    vector<vector<double>> ym;       // Lösungsmatrix
    vector<double> t;                // Zeit-Vektor

    // Virtuelle Funktion dgls, wird an anderer Stelle definiert
    virtual vector<double> dgls(double t, vector<double> u_vec) = 0;

    // Konstruktor mit vier Argumenten (Initialisierung der Daten-Member)
    dsolve(double a_, double b_, int N_, vector<double> alpha_) :a(a_), b(b_), N(N_), alpha(alpha_) {
        t.push_back(a_);                // Zum Zeit-Vektor die Anfangszeit eintragen
        ym.push_back(alpha_);           // Zur Loesungs-Matrix die Anfangswerte eintragen
    }

    // Methode der Runge-Kutta-Methode
    void solve() {
        double h = (b - a)/N;                   // Abstand dt zwischen den aequidistanten Punkten des t-Intervalls (h=dt)
        int n = alpha.size();                   // Anzahl der Anfangswerte bzw. DGL-Gleichungen (hier n=2)
        vector<double> k1(n),k2(n),k3(n),k4(n); // Deklaration der vier Runge-Kutta Parameter fuer jede Loesung
        vector<double> y(n);                    // Temporaerer Hilfsvektor

        for (int i = 0; i < N; ++i) {                                         // for-Schleife ueber die einzelnen Punkte des t-Intervalls
            for (int j = 0; j < n; ++j) k1[j] = h * dgls(t[i], ym[i])[j];     // for-Schleife ueber die n Runge-Kutta k1-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k1[j] / 2;          // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k2[j] = h * dgls(t[i] + h / 2, y)[j]; // for-Schleife ueber die n Runge-Kutta k2-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k2[j] / 2;          // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k3[j] = h * dgls(t[i] + h / 2, y)[j]; // for-Schleife ueber die n Runge-Kutta k3-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + k3[j];              // Temporaerer Hilfsvektor
            for (int j = 0; j < n; ++j) k4[j] = h * dgls(t[i] + h, y)[j];     // for-Schleife ueber die n Runge-Kutta k4-Parameter
            for (int j = 0; j < n; ++j) y[j] = ym[i][j] + (k1[j] + 2 * k2[j] + 2 * k3[j] + k4[j]) / 6; // Temporaerer Hilfsvektor

            ym.push_back(y);              // Zum Loesungs-Vektor den neuen Wert eintragen
            t.push_back(a + (i + 1) * h); // Zum Zeit-Vektor die neue Zeit eintragen
        }                                 // Ende for-Schleife ueber die einzelnen Punkte des t-Intervalls
    }                                     // Ende des Methode 'solve()'
};  // Ende der Klasse 'dsolve'

// Sub-Klasse 'Pendel'
class Pendel : public dsolve {
public:
    double m;    // Masse des Pendels
    double l;    // Länge des Pendels
    double beta; // Reibungskoeffizient

    // Überschreibung der virtuellen Funktion dgls
    vector<double> dgls(double t, vector<double> u_vec) override {
        vector<double> du_dt(u_vec.size());                          // Die zwei DGLs erster Ordnung werden als Standard-Vektor definiert
        du_dt[0] = u_vec[1];                                         // Zwei DGLs erster Ordnung
        du_dt[1] = - 9.81 / l * sin(u_vec[0]) - beta / m * u_vec[1]; // DGl des physikalischen Pendels mit Reibung (Stokesscher Ansatz)
        return du_dt;                                                // Rueckgabewert der DGLs als Standard-Vektor
    }                                                                // Ende: Definition der Bewegungsgleichung

    // Konstruktor mit sieben Argumenten (Initialisierung der Instanzvariablen (m,l,beta), vier Argumente für dsolve)
    Pendel(double m_ = 1.0, double l_ = 1.0, double beta_ = 0.0, double a_ = 0, double b_ = 1, int N_ = 10, vector<double> alpha_ = {0.1, 0}) :
        dsolve(a_, b_, N_, alpha_), m(m_), l(l_), beta(beta_) {}

    // Methode der Ausgabe der kartesische Koordinaten des Pendels am Stützpunkt i
    vector<double> r(size_t i) {
        vector<double> w = {0.0,0.0};
        if (i < t.size()) { w = {l*sin(ym[i][0]), -l*cos(ym[i][0])};}
        return w;
    }
    // Methode der Ausgabe der Geschwindigkeit des Pendels in kartesische Koordinaten am Stützpunkt i
    vector<double> v(size_t i) {
        vector<double> w = {0.0,0.0};
        if (i < t.size()) { w = {l*cos(ym[i][0])*ym[i][1], l*sin(ym[i][0])*ym[i][1]};}
        return w;
    }
}; // Ende der Unterklasse 'Pendel'

//Definition der Sub-Sub-Klasse 'Pendel_math' (mathematische Pendel, harmonischer Oszillator) als abgeleitete Klasse der Unterklasse 'Pendel'
class Pendel_math : public Pendel {
    public:
        // Konstruktor mit sieben Argumenten (Initialisierung der Instanzvariablen (m,l,beta), vier Argumente für dsolve)
        Pendel_math(double m_ = 1.0, double l_ = 1.0, double beta_ = 0.0, double a_ = 0, double b_ = 1, int N_ = 10, vector<double> alpha_ = {0.1, 0}) :
            Pendel(m_, l_, beta_, a_, b_, N_, alpha_){}

        // Überschreibung der virtuellen Funktion dgls
        vector<double> dgls(double t, vector<double> u_vec) override {
            vector<double> du_dt(u_vec.size());                        // Die zwei DGLs erster Ordnung werden als Standard-Vektor definiert
            du_dt[0] = u_vec[1];                                       // Zwei DGLs erster Ordnung
            du_dt[1] = - 9.81 / l * u_vec[0] - beta / m * u_vec[1];    // DGl des mathematischen Pendels mit Reibung (Stokesscher Ansatz)
            return du_dt;                                              // Rueckgabewert der DGLs als Standard-Vektor
        }                                                              // Ende: Definition der Bewegungsgleichung
}; // Ende der Klasse 'Pendel_math'

Diese Header-Datei bindet man im folgenden Hauptprogramm Pendel.cpp 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 virtuelle Funktion 'dgls' (nun DGL des harmonischen Oszillators mit Reibung). 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. Da die Klasse 'Pendel' selbst eine Unterklasse der abstrakten Klasse 'dsolve' ist, bezeichnet man die Klasse 'Pendel_math' als eine Sub-Subklasse.

Im Hauptprogramm Pendel.cpp (siehe folgender C++ Quelltext) wird nun eine Instanz der Klasse 'Pendel' ( Pendel P_A = Pendel(m,l,beta,a,b,N,alpha); ) und eine Instanz der Klasse 'Pendel_math' ( Pendel_math P_B = Pendel_math(m,l,beta,a,b,N,alpha); ) erzeugt, wobei die folgenden, gleichen Parameter an die beiden Konstruktoren der Klasse übergeben wurden: $m=1$ [Kg], $l=0.5$ [meter], $\beta = 0$ [Kg/s], $\theta(0)=0$ [rad], $\dot{\theta}(0)=8.2$ [rad/s]. Die Bewegung der beiden Pendel wird von $a=0$ [s] bis $b=5$ [s] simuliert, wobei $N=1000$ Zeit-Gitterpunkte bei der Runge-Kutta Methode verwendet werden.

Pendel.cpp
/* Das physikalische und mathematische Pendel mit Reibung (Stokesscher Ansatz)
 * Berechnung der Lösung einer Differentialgleichung zweiter Ordnung
 * bzw. eines Systems von zwei Differentialgleichungen erster Ordnung
 * mittels der Runge-Kutta Ordnung vier Methode
 * Verfahren zur Loesung der DGL ist in einer abstrakten Basisklasse ausgelagert
 * Berechnung der Lösung innerhalb einer Methode (Klassenfunktion) Runge-Kutta-Verfahren
 * Überschreibung der virtuellen Funktion der Bewegungsgleichungen in der abgeleiteten Klasse (Subklasse) Pendel
 * Zeitentwicklung der fuer unterschiedliche t-Werte in [a,b]
 * Konstruktor: Pendel(Masse des Pendels, Länge des Pendels l, Reibung beta, Anfangszeit a, Endzeit b, Anzahl der Punkte N, Anfangswerte alpha)
 * bzw. Konstruktor: Pendel_math(...)
 * Ausgabe zum Plotten mittels Python Jupyter Notebook Pendel.py: "./a.out > Pendel.dat
 */
#include "Pendel.hpp" // Header-Datei des physikalischen und mathematischen Pendels mit Reibung (Stokesscher Ansatz)
#include <iostream>   // Standard Input- und Output Bibliothek in C, z.B. printf(...)
using namespace std;

// Hauptprogramm
int main() {
    double m = 1.0;                   // Masse des Pendels
    double l = 0.5;                   // Länge des Pendels
    double beta = 0;                  // Reibungskoeffizient
    double a = 0;                     // Untergrenze des Zeit-Intervalls
    double b = 5;                     // Obergrenze des Intervalls
    int N = 1000;                     // Anzahl der Punkte
    vector<double> alpha = {0.0,8.2}; // Anfangswerte

    Pendel P_A = Pendel(m,l,beta,a,b,N,alpha);           // Konstruktor bilded eine Instanz der Klasse Pendel
    P_A.solve();                                         // Methode der Runge-Kutta-Methode ausführen

    Pendel_math P_B = Pendel_math(m,l,beta,a,b,N,alpha); // Konstruktor bilded eine Instanz der Klasse Pendel_math
    P_B.solve();                                         // Methode der Runge-Kutta-Methode ausführen

    // Terminalausgabe, Beschreibung der ausgegebenen Groessen
    printf("# 0: Index i \n# 1: t-Wert \n");
    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");
    // Terminalausgabe, ausgegeben der Werte
    for(size_t i=0; i  < P_A.t.size(); ++i){
        printf("%3ld %19.15f ",i,P_A.t[i]);
        printf("%19.15f %19.15f %19.15f %19.15f ",P_A.r(i)[0],P_A.r(i)[1],P_A.v(i)[0],P_A.v(i)[1]);
        printf("%19.15f %19.15f %19.15f %19.15f ",P_B.r(i)[0],P_B.r(i)[1],P_B.v(i)[0],P_B.v(i)[1]);
        printf("\n");
    }
} // Ende Hauptprogramm

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, Dokumentation 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 eine rein abstrakte Klasse dadurch definiert, dass sie ausschließlich virtuelle Daten-Member und Funktionen definiert, die dann in den jeweiligen Unterklassen überschrieben werden. Die in den Pendelklassen verwendete abstrakte Klasse 'dsolve' stellt somit keine rein abstrakte Klasse dar.

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.