Beruflich Dokumente
Kultur Dokumente
Klassische
Mechanik mit C++
Basics und Anwendungen
Klassische Mechanik mit C++
Elias Posoukidis
Klassische
Mechanik mit C++
Basics und Anwendungen
Elias Posoukidis
Essen, Deutschland
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detail-
lierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar.
der Bibliothek vereinfacht wird. Nicht die ganze Funktionalität der gsl wird abgedeckt,
sondern nur die Teile, die für das Lösen der Aufgaben des Buches notwendig sind. Der
Aufbau der Klassen orientiert sich an Beispielen, die auf der Webseite der GNU Scientific
Library zu finden sind. Am Anfang von jedem Kapitel wird gezeigt, wie der Übergang von
einem solchen Beispiel zu einer C++-Klasse durchgeführt wird. Am Ende sind Beispiele
aus der Physik zu finden. Der dritte und letzte Teil baut auf das Wissen der beiden ersten
Teile auf und zeigt, wie Computermodelle auf Basis von Physikaufgaben konstruiert
werden, was im zweiten Band mit weiteren Beispielen fortgesetzt wird.
Die Entscheidung modernes C++ einzusetzen folgt nicht dem derzeitigen Trend, Pro-
gramme mit Skript-Sprachen zu erstellen. C++ wurde und wird immer noch als schwer
zu erlernen betrachtet. Die Sprache hat sich in den letzten Jahren jedoch gewandelt.
Ideen aus anderen Programmiersprachen sind in die neuen Versionen eingeflossen und
das Programmieren mit C++ ist einfacher geworden. Die Länge der Quelltexte ist deut-
lich kürzer geworden und unterscheidet sich nicht wesentlich vom Umfang der Quelltex-
te anderer Sprachen, wenn diese nicht nur zur Demonstration eines Konzepts gedacht
sind. Besonderer Wert wurde darauf gelegt, dass die Programme in diesem Buch nicht
monolithisch aufgebaut sind, sondern aus wiederverwendbaren Komponenten bestehen.
Gleichzeitig wurde auf tiefe Vererbungshierarchien verzichtet, um das Verständnis der
Quelltexte zu vereinfachen. Die Quelltexte sind ausführlich kommentiert und werden in
kleinen Abschnitten im Text eingebunden. Sie sollen genau wie mathematische Formeln
als Fortsetzung des Textes gesehen werden. Die Kommentare in den Quelltexten ergänzen
den laufenden Text.
Das Buch richtet sich an Studierende der Physik und verwandter naturwissenschaftli-
cher Fächer, die bereits einen Grundkurs in klassischer Mechanik absolviert haben und
mit der Differenzial- und Integralrechnung vertraut sind. Der Leser sollte über Grund-
kenntnisse in C++ verfügen oder bereit sein, sich diese anzueignen.
Für die Übersetzung der Quelltexte ist ein C++-Compiler erforderlich, der den Stan-
dard C++17 unterstützt. Als IDE wurde der Qt Creator und als Compiler clang
in der Version 6.0 unter Linux Mint 19 eingesetzt. Alle Quelltexte stehen unter
www.springer.com/978-3-662-60904-0 zum Download bereit. Dieser Text wurde mit LATEX
und dem TeXstudio-Editor erstellt. Obwohl dieses Buch mit höchster Sorgfalt geschrie-
ben wurde, kann es trotzdem Fehler enthalten. Verbesserungsvorschläge, Bemerkungen
und Hinweise sind stets willkommen.
Dem Springer Verlag und insbesondere Frau Dr. Lisa Edelhäuser und Frau Anja
Dochnal möchte ich für die freundliche Unterstützung danken. Mein besonderer Dank
gilt meiner Familie, die während der Entstehung dieses Buches große Geduld bewiesen
hat.
I C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1 Eine Tour durch C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Standardein- und -ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Fehlerbehandlung durch Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 STL-Container und Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 Teilaufgaben mit Funktionen lösen . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.6 λ-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.7 Rechnen mit komplexen Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.8 Benutzerdefinierte Literale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.9 Statische Variablen und Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.10 Präprozessor-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.11 Schreiben und Lesen von Daten in Dateien . . . . . . . . . . . . . . . . . . 47
1.12 Aufzählungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
1.13 Zeichenketten in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
1.14 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2 Klassen in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.2 Einfache Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.3 Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.4 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
2.5 Programmieren mit Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . 111
2.6 Schnittstellen für Klassen mithilfe von Templates definieren . . . . 119
2.7 Eigene Werkzeuge bauen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
2.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
3 Tabellen und Views . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.2 Eine Klasse für Zahlentabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.3 Eine Klasse für Sichten auf Tabellen . . . . . . . . . . . . . . . . . . . . . . . . 152
3.4 Erstellen von Views auf Zahlentabellen . . . . . . . . . . . . . . . . . . . . . . 157
3.5 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
3.6 Beispiele aus der Physik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.7 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
viii Inhaltsverzeichnis
C++
1 Eine Tour durch C++
Übersicht
1.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 Standardein- und -ausgabe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Fehlerbehandlung durch Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.4 STL-Container und Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.5 Teilaufgaben mit Funktionen lösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.6 λ-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.7 Rechnen mit komplexen Zahlen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.8 Benutzerdefinierte Literale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.9 Statische Variablen und Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.10 Präprozessor-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.11 Schreiben und Lesen von Daten in Dateien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
1.12 Aufzählungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
1.13 Zeichenketten in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
1.14 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
1.1 Einleitung
Eine so mächtige Programmiersprache wie C++ in wenigen Seiten vorzustellen, ist unmög-
lich. Das Angebot an Funktionen und Programmiermethoden ist so groß, dass zwangs-
läufig eine Auswahl stattfinden muss. Doch welche Teile der Sprache sind wichtig und
welche nicht? Eine eindeutige Antwort auf diese Frage gibt es nicht, denn die Sprache
wird von einem Systemprogrammierer anders als von einem Naturwissenschaftler und
nochmal anders von einem Spielentwickler genutzt. Es macht deswegen Sinn, sich auf
Themen zu konzentrieren, die für eigene Projekte nützlich sind, was Gegenstand die-
ses Kapitels ist. Ziel des Buchs ist, mithilfe von C++ eine Sammlung von Komponenten
zu programmieren, welche die Erstellung von physikalischen Modellen vereinfacht. Dies
ist dann erreicht, wenn z.B. die Lösung eines linearen Gleichungssystems mit 3-4 Pro-
grammzeilen implementiert ist. Idealerweise entsteht am Ende keine lose Sammlung von
Komponenten, sondern nur solche die Wiederholungen von Quelltexten minimieren. Wir
wollen dabei nicht das Rad neu erfinden, sondern nur die eigene Kreativität in den Auf-
bau von Programmen einfließen lassen. (Seit C++17 können Funktionen, Klassen usw.
direkt in Header-Dateien geschrieben werden, ohne die One Definition Rule zu verletzen,
wovon wir Gebrauch machen werden. Der Begriff Programm wird oft als Synonym für
eine Funktion gebraucht, wenn diese durch Kompilierung eine ausführbare Datei erzeu-
gen kann. Die Verwendung von englischen Begriffen in der Computerliteratur hat sich
so durchgesetzt, das es sich teilweise nicht vermeiden lässt, bestimmte Wörter im Text
einfließen zu lassen, wie z.B. Compiler an Stelle von Übersetzer oder View für Sicht
usw.)
1.2.1 Beispielanwendungen
Wir wollen in diesem Abschnitt mit einem Programm beginnen, welches statt eines Hello
World Zahlen und ihre Quadrate in die Standardausgabe schreibt. Wir durchlaufen in
einer Schleife die Werte von -2 bis 2 und geben diese und ihre Quadrate in jeweils einer
neuen Zeile aus. Die Breite jeder Ausgabe wird über std::setw festgelegt.
/**
* @brief ex1 Zahlen und ihre Quadrate werden in die Standardausgabe
* geschrieben
*/
inline void ex1 () {
for ( double x = -2; x <= 2; x++) {
// setze mit std :: setw die Breite der Ausgabe
std :: cout << std :: setw (2) << x //
<< std :: setw (5) << std :: pow(x, 2) //
<< std :: endl;
}
}
-2 4
-1 1
0 0
1 1
2 4
Listing 1.2
Mit einem interaktiven Programm soll die Hypotenuse eines rechtwinkligen Dreiecks,
dessen Katheten der Benutzer eingibt, berechnet werden.
Theorie Wir werden zur Berechnung der Hypotenuse den Satz vom Pythagoras c =
√
x2 + y 2 anwenden.
Quelltext und Daten Zur Ein- und Ausgabe der Daten werden wir die iostream-
Bibliothek einsetzen. Die Deklaration der zwei double-Variablen hat zur Folge, dass
std::cin die Eingabe von Zahlen erwartet. Nach der Eingabe wird die Hypotenuse be-
rechnet und ausgegeben.
/**
* @brief ex2 Berechnung der Hypotenuse eines rechtwinkligen Dreiecks
* Version 1.0
*/
inline void ex2 () {
double x, y;
// erste Kathete
std :: cout << " input x=?\t";
std :: cin >> x;
// zweite Kathete
std :: cout << " input y=?\t";
std :: cin >> y;
// Berechnung und Ausgabe der Hypotenuse
std :: cout << " output :" << sqrt(std :: pow(x, 2) + std :: pow(y, 2));
}
input x=? 3
input y=? 4
output :5
Listing 1.4
Wir stellen jedoch fest, dass die Implementierung einige Schwachstellen aufweist. Eine
wäre, dass das Programm keine Möglichkeit hat, auf falsche Eingaben seitens des Be-
6 1 Eine Tour durch C++
nutzers zu reagieren. Wird statt einer Zahl eine Zeichenkette eingegeben, werden die
nachfolgenden Eingaben nicht ausgeführt und ihre Werte auf 0 gesetzt. Das Programm
würde mit der Anzeige eines falschen Ergebnisses beendet werden.
input x=? 4
input y=? y
output :4
Listing 1.5
Der Benutzer könnte für x oder y einen negativen Wert eingeben. Das Programm würde
die Rechnung zu Ende führen. Die Ergebnisse wären mathematisch korrekt, obwohl die
Eingaben des Benutzers keinen Sinn machen.
input x=? -3
input y=? 4
output :5
Listing 1.6
Die Software muss in der Lage sein, Benutzereingaben bezüglich ihrer Gültigkeit zu
testen. Wir werden in den kommenden Abschnitten das Programm überarbeiten und
diese Schwachstellen beheben.
Wir fahren mit einem Beispiel aus der Physik fort und betrachten ein Teilchen, das sich
geradlinig mit konstanter Geschwindigkeit v = 10 m/s bewegt. Zum Zeitpunkt t0 = 0
befindet es sich am Ort x0 = 0. Es soll der Ort als Funktion der Zeit für 0 ≤ t ≤ 5 s, in
Schritten ∆t = 1 s auf dem Bildschirm ausgegeben werden.
Theorie Die Formel für den Ort als Funktion der Zeit lautet
wobei x0 = 0 und v = 10 m/s ist und der Einheitsvektor ⃗ex in Richtung der Bewegung
des Teilchens zeigt.
Quelltext und Daten Das Programm besteht aus einer Schleife, die den Ort x für die
Zeiten t durch Anwendung der Formel (Gl. 1.1) berechnet und auf dem Bildschirm mittels
std::cout ausgibt.
/**
* @brief ex3 Geradlinige Bewegung mit konstanter Geschwindigkeit
*/
inline void ex3 () {
// Ort und Geschwindigkeit zum Zeitpunkt t=0
const double x0 = 0, v0 = 10;
1.3 Fehlerbehandlung durch Exceptions 7
Nach Ausführung des Programms erhalten wir pro Reihe ein Wertepaar (t, x) mit den
dazugehörigen Einheiten.
t=0 s x=0 m
t=1 s x=10 m
t=2 s x=20 m
t=3 s x=30 m
t=4 s x=40 m
t=5 s x=50 m
Listing 1.8
/**
* @brief f00 try -catch - Beispiel
* @param x
*/
inline void f00(int x) {
// ....
if (x == 0) {
// Ausnahme vom Typ int wird ausgeworfen
throw 0;
}
// ....
}
Durch diese Strategie wird eine Trennung von Fehlererkennung und Fehlerbehandlung
erreicht. Zusätzlich kann ein Programm durch die Möglichkeit, Objekte vom unter-
schiedlichem Typ auszuwerfen, gezielt auf Fehler reagieren. Dies wird durch eine Rei-
he von vordefinierten Ausnahmeobjekten, die alle von einer Basisklasse std::exception
abgeleitet werden, unterstützt. Wird einer Funktion ein falsches Eingabeargument
übergeben oder erfolgt bei einem interaktiven Programm eine falsche Eingabe sei-
tens des Benutzers, kann ein Programm mit dem Auswerfen einer Instanz der Klasse
std::invalid_argument reagieren. Ein Objekt vom Typ std::domain_error kann ausge-
worfen werden, wenn sich Daten außerhalb eines Definitionsbereichs befinden. Schließ-
lich erwähnen wir std::runtime_error. Diese Ausnahme kann ausgeworfen werden, wenn
während der Ausführung eines Programms unerwartet Fehler auftreten, die nicht einer
spezifischen Gruppe zugeordnet werden können.
1.3 Fehlerbehandlung durch Exceptions 9
1.3.1 Beispielanwendungen
Das Beispielprogramm (Listing 1.3) soll weiterentwickelt werden, sodass es auf fehlerhafte
Benutzereingaben reagieren kann. Das Objekt std::cin erwartet in diesem Programm
immer die Eingabe von Zahlen. Dies haben wir durch die Deklaration der Variablen x
und y so festgelegt. In der neuen Version des Programms (Listing 1.10) testen wir mit
der Elementfunktion cin::fail(), ob die eingegebenen Werte tatsächlich vom erwarteten
Typ sind. Wenn nicht, werfen wir eine Ausnahme vom Typ std::invalid_argument aus.
Damit hätten wir den Fall der Eingabe eines Werts vom falschen Typ berücksichtigt.
Wir testen zusätzlich, ob die Eingabe auch logisch einen Sinn macht, indem wir negative
Zahlen und 0 ebenfalls als Fehler betrachten und darauf mit dem Auswerfen einer weiteren
Ausnahme vom Typ std::domain_error reagieren. Die Art des Fehlers wird anhand des
Ausnahmetyps identifiziert und es wird mit einer entsprechenden Fehlermeldung reagiert.
/**
* @brief ex1 Berechnung der Hypotenuse eines rechtwinkligen Dreiecks
* Version 2
*/
inline void ex1 () {
try {
double x = 0, y = 0;
Es folgt die Reaktion des Programms, wenn statt einer positiven Zahl für die Länge der
zweiten Kathete eine negative Zahl eingegeben wird (Listing 1.11).
x=? 1
y=? -3
input parameter must be >0:y
Listing 1.11
Wird statt einer Zahl ein Buchstabe eingegeben, wird das Programm, mit der Anzeige
einer Fehlermeldung, beendet (Listing 1.12).
x=? 4
y=? a
invalid argument for:y
Listing 1.12
Das Programm ist besser geworden, denn es reagiert auf nicht vorgesehene Situationen
in einer geordneten Weise. Wir können uns aber mit dem Ergebnis nicht zufrieden geben,
denn identische Teile des Quelltexts wie z.B. die Eingabeüberprüfung erscheinen zweimal
innerhalb der Funktion. Zusätzlich fällt auf, dass das Programm nach Auftreten eines
Fehlers beendet wird. Der Benutzer muss es in diesem Fall neu starten, um ein Ergebnis
zu berechnen. In den nächsten Abschnitten werden wir diese Schwachstellen beheben.
std::vector v;
...
std::max_element(v.begin(),v.end())
Abb. 1.1: Die stl besteht aus Algorithmen und Datenstrukturen, die über Iteratoren
miteinander kommunizieren.
Iteratoren sind Objekte, die auf Elemente eines Containers zeigen und per Anweisung
sich von einem Element zu einem anderen innerhalb desselben Containers bewegen kön-
nen. Die Algorithmen der stl machen von ihnen massiv Gebrauch. Iteratoren machen
es möglich, entweder über alle oder über eine Teilmenge der Elemente eines Containers
einen Algorithmus anzuwenden. Der Bereich innerhalb dessen iteriert werden soll, wird
durch zwei Iteratoren festgelegt. Der erste Iterator zeigt auf das erste Element und der
zweite auf eine Position nach dem letzten Element des Bereichs. Nehmen wir als Beispiel
einen sequenziellen Container (Abb. 1.2). Der Anfang und das Ende des Containers wer-
den durch zwei feste, an die Instanz gebundene Iteratoren begin() und end() angegeben.
Der erste der beiden Iteratoren zeigt auf das erste Element des Containers, der zweite
aber auf eine Position nach dem letzten Element und somit auf eine ungültige Position.
Ein dritter Iterator durchläuft alle Elemente des Containers (Abb. 1.2).
Abb. 1.2: Ein Iterator (itr) durchläuft alle Elemente. Das Element, auf das der Iterator
zeigt, wird einer Funktion fn übergeben.
Alternativ kann durch zwei andere, nicht mit dem Container mitgelieferten Iteratoren,
ein Bereich festgelegt werden (Abb. 1.3). In beiden Fällen endet die Iteration eine Position
vor dem zweiten Iterator.
12 1 Eine Tour durch C++
1.4.1 Beispiele
Es soll eine Funktion geschrieben werden, die eine Zahlenfolge von x = 0, . . . 1 mit
∆x = 0.23 in einem Container speichert und anschließend die Werte auf dem Bildschirm
ausgibt.
Kurzbeschreibung Da wir nicht vorher die Anzahl der zu speichernden Elemente be-
rechnen wollen, wählen wir als Container einen std::vector, denn dieser stellt automa-
tisch den notwendigen Speicher zur Verfügung.
Quelltext und Daten Mit einer Schleife und der Elementfunktion push_back werden
die Zahlen immer am Ende des Containers hinzugefügt. Wir geben die gespeicherten
Elemente zweimal auf den Bildschirm aus: Einmal mithilfe von Iteratoren und einer for-
Schleife und dann mit einer Variante einer for-Schleife (range-based for-loop), die mit
einer kompakten Syntax dasselbe tut.
/**
* @brief ex0 Speicherung von Daten in einem Container
*/
inline void ex0 () {
// erzeuge leeren Vektor
std :: vector <double > v;
// fülle Vektor
for ( double x = 0; x < 1; x += 0.23) {
v. push_back (x);
}
Listing 1.14
Das Füllen und Kopieren von std::vector-Containern sowie die Anwendung einer Reihe
von Algorithmen soll mit diesem Programm demonstriert werden. Die Iteratoren durch-
laufen immer alle Elemente des Containers. Zur Ausgabe auf dem Bildschirm wird ein
std::ostream_iterator eingesetzt. Dieser benutzt den <<-Operator, um die Elemente der
Container an einem Ausgabestrom weiterzuleiten.
/**
* @brief ex1 Anwendung von Algorithmen auf einen Container
*/
inline void ex1 () {
// Speicher wird reserviert
std :: vector <double > v1 (9);
// kopiere Vektor
auto v2 = v1;
// Skalarprodukt v1 * v2
auto val1 = std :: inner_product (v1. begin (), v1.end (), v2.begin (), 0);
std :: cout << val1 << std :: endl;
}
-4,-3,-2,-1,0,1,2,3,4,
-4,-3,-2,-1,0,1,2,3,4,
4,3,2,1,0,-1,-2,-3,-4,
0
-60
Listing 1.16
1.5.1 Beispielanwendungen
Wir entwickeln das Programm aus Abschnitt 1.3.1 (Listing 1.10) weiter mit dem Ziel,
es modularer zu gestalten. Teile des Quelltextes werden in eine Funktion (Listing 1.17)
ausgelagert, die ab jetzt für die Benutzereingaben zuständig sein wird.
/**
* @brief read_input lese eine double -Zahl von der Standardeingabe
* @param c Name der aktuellen Variablen
* @return die eingelesene Variable
*/
inline double read_input ( const char *c) {
double x;
std :: cout << c << "=?\t";
std :: cin >> x;
1.5 Teilaufgaben mit Funktionen lösen 15
Bei jeder Eingabe seitens des Benutzers wird getestet, ob diese eine positive Zahl war.
Die neue Funktion wird, wie im nächsten Quelltextabschnitt zu sehen ist, innerhalb
eines try-Blocks aufgerufen, der zusammen mit den catch-Blöcken in eine while-Schleife
eingebettet ist. Im Fall einer fehlerhaften Eingabe wird eine Ausnahme ausgeworfen,
von einem catch-Block aufgefangen und eine Fehlermeldung ausgegeben. Der Benutzer
wird jedes Mal aufgefordert, durch die Eingabe eines Befehls das Programm entweder zu
beenden oder eine neue Berechnung zu starten.
/**
* @brief ex1 Berechnung der Hypotenuse eines rechtwinkligen Dreiecks ,
* Version 3.0 ( Benutzerschnittstelle )
*/
inline void ex1 () {
constexpr auto maxstreamsize =
std :: numeric_limits <std :: streamsize >:: max ();
char cmd = 'y';
// Programm beenden ?
do {
16 1 Eine Tour durch C++
std :: cout << "new claculation ? y[yes] / n[no]" << std :: endl;
std :: cin >> cmd;
} while (cmd != 'y' && cmd != 'n');
}
}
Wir wollen anhand eines Beispiels aus der Physik die Aufteilung zwischen Programm-
logik und Darstellung der Daten demonstrieren. Dies ist sinnvoll, denn die Berechnung
von Daten und ihre Präsentation, sind zwei voneinander unabhängige Aufgaben. Die
Ergebnisse könnten in einem Terminal oder in einer grafischen Benutzeroberfläche ange-
zeigt werden. In beiden Fällen würde aber die Berechnung der Ergebnisse durch dasselbe
Unterprogramm erfolgen.
Ein Auto fährt mit einer konstanten Geschwindigkeit v0 = 50 km/h auf gerader Straße.
Der Fahrer erkennt ein Hindernis, welches sich in einem Abstand von 30 m befindet.
Die Reaktionszeit tR des Fahrers beträgt ungefähr 0.7 s und das Auto kann mit einer
Bremsverzögerung bis 6 m/s2 bremsen.
x1
x0
a
v0 v
d
O A B
Abb. 1.4: Ein Auto bremst, um einen Zusammenprall mit einem Hindernis, dass sich in
einem Abstand d befindet, zu verhindern. OA: Das Auto fährt mit konstanter Geschwin-
digkeit. AB: Das Auto bremst mit konstanter Beschleunigung ⃗a.
Theorie Wir beginnen mit der Herleitung der Formeln für die notwendige Beschleuni-
gung, damit das Auto gerade noch vor dem Hindernis zum Stillstand kommt. Das Auto
fährt vom Zeitpunkt, in dem der Autofahrer das Hindernis sieht, bis er die Gefahr er-
kennt und bremst, mit konstanter Geschwindigkeit. Danach bremst er kontinuierlich mit
konstanter Beschleunigung (Abb. 1.4). Für den ersten Abschnitt der Bewegung gilt
x 0 = v 0 tR (1.2)
1.5 Teilaufgaben mit Funktionen lösen 17
Kurzbeschreibung Die Berechnung der Beschleunigung und die Darstellung der Er-
gebnisse werden in zwei Funktionen aufgeteilt. Das Programm soll die kleinstmögliche
Beschleunigung berechnen und für den Fall, dass es nicht mehr möglich ist dem Hin-
dernis auszuweichen, über eine Fehlermeldung in Form einer Ausnahme dies dem Fahrer
melden.
Quelltext Innerhalb einer Funktion implementieren wir (Gl. 1.2) und (Gl. 1.4) und
berechnen a für gegebene d, v0 und tR . Sollte die Distanz bis zum Hindernis kleiner als
|⃗x0 | (Abb. 1.4) oder aber eine Beschleunigung a erforderlich sein, die nicht vom Auto
erreicht werden kann, werden Ausnahmen ausgeworfen.
/**
* @brief brake_assist Berechnung der Beschleunigung damit das Auto
* noch vor dem Hindernis zum Stehen kommt
* @param v0 Geschwindigkeit (der Fahrer sieht das Hindernis )
* @param d Abstand des Autos zum Hindernis
* @param tR Reaktionszeit des Fahrers
* @return Beschleunigung
*/
inline double brake_assist ( double v0 , double d, double tR) {
double x0 = v0 * tR;
if (x0 > d) {
throw std :: range_error (std :: to_string (0));
}
/**
* @brief break_assist_ui Benutzerschnittstelle
* @param v0 Geschwindigkeit (der Fahrer sieht das Hindernis )
* @param d Abstand des Autos zum Hindernis
* @param rtime Reaktionszeit des Fahrers
*/
void break_assist_ui ( double v0 , double d, double rtime) {
std :: cout << "d=" << d << " m,";
std :: cout << "v0=" << v0 << " m/s,";
std :: cout << "tR=" << rtime << " m/s" << std :: endl;
std :: cout << std :: setprecision (2);
try {
const double a = brake_assist (v0 , d, rtime);
double x0 = v0 * rtime ;
double x = x0 + 0.5 * std :: pow(v0 , 2) / a;
std :: cout << "need a=" << a << " m/s^2" << std :: endl;
std :: cout << "use a=" << std :: ceil(a) << " m/s^2" << std :: endl;
std :: cout << " distance =" << x << std :: endl;
} catch (std :: range_error err) {
std :: cout << "need : " << err.what () << " m/s^2" << std :: endl;
std :: cout << "car will crash " << std :: endl;
}
}
Auf Basis der in der Aufgabe vorgegebenen Zahlenwerte erstellen wir ein Szenario und
ergänzen dies durch ein zusätzliches für eine zweifache Anfangsgeschwindigkeit des Autos.
/**
* @brief ex2 Auto bremst , um einem Hindernis ausweichen
*/
void ex2 () {
const double d = 30.0;
const double rtime = 0.7;
const double v0 = 14; //m/s;
break_assist_ui (v0 , d, rtime );
// zweiter Versuch mit Geschwindigkeit 2* v0
std :: cout << " -------------------" << std :: endl;
break_assist_ui (2 * v0 , d, rtime );
}
Daten Folgende Ausgabe, zeigt, dass das Auto es im ersten Fall schaffen wird, recht-
zeitig vor dem Hindernis zum Stehen zu kommen, während dies beim zweiten Mal nicht
mehr möglich ist.
distance =30
-------------------
d=30 m,v0 =28 m/s,tR =0.7 m/s
need : 37.692308 m/s^2
car will crash
Listing 1.22
1.6 λ-Funktionen
Eine λ-Funktion ist ein Objekt, das sich genau wie eine Funktion verhält und direkt an
der Stelle im Quelltext platziert werden kann, wo es benötigt wird. Genauer handelt
es sich um Instanzen von Klassen, die vom Compiler generiert werden und über einen
überladenen Klammeroperator verfügen. λ-Funktionen haben eine kompakte Syntax und
können in Variablen gespeichert oder direkt einer anderen Funktion übergeben werden
(Abb. 1.5). Über die Erfassungsliste können der λ-Funktion Variablen entweder als Kopie
Parameterliste
Funktionskörper
Erfassungsliste
double b = 9;
...
auto fn = [b]( double x){
Variable
return x+b;
}
Abb. 1.5: Eine λ-Funktion wird in einer Variablen gespeichert. Über die Erfassungsliste
wird der Parameter b als Kopie übergeben.
oder als Referenz übergeben werden. Diese Variablen stammen aus dem Block, in dem die
λ-Funktion definiert ist. Die Parameterliste und der Funktionskörper spielen die gleiche
Rolle wie auch in ganz normalen Funktionen.
1.6.1 Beispiele
ßer als ein vorgegebenes xmin und kleiner als ein xmax sind. λ-Funktionen, die mehrmals
angewandt werden, werden in Variablen gespeichert.
/**
* @brief ex5 lambda - Funktionen und Container
*/
inline void ex5 () {
// reserviere Speicher für 20 double Zahlen
std :: vector <double > v1 (20);
0 ,1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,
7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,
11 ,12 ,13 ,14 ,15 ,16 ,
Listing 1.24
1.6 λ-Funktionen 21
Kurzbeschreibung Wir werden das Programm mit drei unterschiedlichen Ansätzen rea-
lisieren, wobei in allen drei Varianten folgende Anforderungen umgesetzt werden sollen:
Die erste Variante wird die Anforderungen ohne Verwendung von Funktionen umsetzen,
so wie dies bei der Erstellung eines Prototyps der Fall sein würde. Die zweite Version
wird mithilfe von Funktionen eine Aufgabetrennung enthalten und die dritte neben der
Aufgabeteilung zusätzlich stl-Algorithmen einsetzen.
Quelltext und Daten Die erste Version des Programms ist mit drei Schleifen, eine für
die Wertezuweisung und zwei für die Ausgabe, erstellt.
/**
* @brief ex1 Die Bahngeschwindigkeit als Funktion
* des Abstands von der Rotationsachse , Version 1.0
*/
inline void ex1 () {
// Synonym für std :: vector
using Array = std :: vector <double >;
// Winkelgeschwindigkeit und Radius
const double omega = 0.5;
const double R = 1.0;
// Felder speichern
// Radius , Bahngeschwindigkeit , Zentripetalbeschleunigung
Array radii , velocities , accelerations ;
// Berechnung der Werte
for ( double r = 0.1; r < R; r += 0.2) {
// Radius wird hinzugefügt
radii. push_back (r);
// berechne Bahngeschwindigkeit
22 1 Eine Tour durch C++
Wir führen die Funktion aus und erhalten folgende Ausgabe auf dem Bildschirm.
Listing 1.26
Quelltext, Version 2.0 Wir formulieren das Beispielprogramm neu und zeigen, wie
dasselbe Problem mit λ-Funktionen gelöst werden kann. Nach der Definition der Felder
zur Speicherung von r, v und a, folgt die Implementierung der Formeln (Gl. 1.5) in
Form von λ-Funktionen. Eine weitere λ-Funktion regelt die Ausgabe auf dem Bildschirm.
Wir können so, ohne zusätzliche Schleifen zu programmieren, die Daten mit anderen
Trennzeichen ausgeben.
/**
* @brief ex7 Die Bahngeschwindigkeit als Funktion
* des Abstands von der Rotationsachse , Version 2.0
*/
inline void ex2 () {
using Array = std :: vector <double >;
const double omega = 0.5;
const double R = 1.0;
1.6 λ-Funktionen 23
Quelltext, Version 3.0 Es folgt eine dritte Version des Programms, diesmal unter Ver-
wendung von stl-Algorithmen. Wir generieren die Werte für die Abstände von der Dreh-
achse mittels std::generate_n und einer anonymen λ-Funktion, der wir eine Referenz auf
einen Startwert übergeben. Dieser wird um 0.2 innerhalb der λ-Funktion inkrementiert.
Die Formeln (Gl. 1.5) erscheinen innerhalb der Funktion std::transform. Zur Ausgabe
der Felder wird eine variadische Funktion eingesetzt. Sie erlaubt es mit einer beliebigen
Anzahl von Feldern zu arbeiten.
/**
* @brief ex8 Die Bahngeschwindigkeit als Funktion
* des Abstands von der Rotationsachse , Version 3.0
*/
inline void ex3 () {
using Array = std :: vector <double >;
const double omega = 0.5;
Zwei Teilchen mit den Massen m1 und m2 rotieren gleichförmig auf zwei Kreisbahnen um
einen gemeinsamen Mittelpunkt mit Radien R1 und R2 . Die Umlaufzeit von m1 beträgt
T1 = 20 s und die von m2 , T2 = 15 s.
1. Wenn zum Zeitpunkt t0 = 0 der Winkel ∆φ zwischen den zwei Radiusvektoren 0 ist,
wie viel Zeit verstreicht bis dieser Winkel 10◦ , 30◦ , 60◦ , 90◦ wird?
2. Wenn φ1 der vom Radiusvektor R1 innerhalb eines Zeitintervalls überstrichene Winkel
bezüglich seiner Anfangsposition und φ2 der entsprechende Winkel für R2 ist, erstellen
Sie Daten für φ1 und φ2 als Funktion der Zeit und ermitteln Sie durch einfache Suche
Näherungswerte für die Zeiten, zu denen sich die Winkeldifferenzen ∆φ einstellen.
Vergleichen Sie die Werte mit den exakten Resultaten.
1.6 λ-Funktionen 25
m2
ϕ2
R2 ϕ1
m1 m1
R1 R1 ∆ϕ
R2 m2
0 0
(a) (b)
Abb. 1.6: Gleichförmige Kreisbewegung von zwei Teilchen (a) Anfangsposition (b) Po-
sition zu einem späteren Zeitpunkt
Lösung Wenn ω1 die Winkelgeschwindigkeit der Masse m1 und ω2 die der Masse m2
ist, gilt
2π 2π
ω1 = , ω2 = , (1.6)
T1 T2
und damit für die Winkel als Funktionen der Zeit
φ1 = ω1 t, φ2 = ω2 t. (1.7)
φ2 − φ1
t= (1.8)
ω2 − ω1
benötigt wird (Abb. 1.6).
Kurzbeschreibung
Entwurf einer Datenstruktur durch Kombination von std::vector und std::array, die
eine Zahlentabelle mit vier Spalten und beliebig vielen Reihen darstellt
Berechnung der Winkel und Winkeldifferenzen für ein vorgegebenes Zeitintervall
Implementierung einer einfachen Suche innerhalb der Tabelle
Suche nach der gewünschten Winkeldifferenz und Ausgabe des Ergebnisses auf dem
Bildschirm
Quelltext und Daten Die Tabelle wird als ein std::vector von beliebig vielen
std::array-Instanzen definiert. Dort werden die Bewegungsdaten der beiden Massen
geschrieben. In Schritten von ∆t = 0.001 s wird die Tabelle mit Daten gefüllt.
/**
* @brief ex4 Gleichförmige Kreisbewegung von zwei Teilchen
*/
inline void ex4 () {
// Tabelle als Feld von Feldern mit vier Spalten
26 1 Eine Tour durch C++
// Umlaufzeiten
const double T1 = 20;
const double T2 = 15;
// Winkelgeschwindigkeiten
const auto omega1 = 2 * Math :: PI / T1;
const auto omega2 = 2 * Math :: PI / T2;
// Erzeuge Daten
for ( double t = 0; t < T1; t += 0.001) {
const double phi1 = omega1 * t;
const double phi2 = omega2 * t;
// Compiler ermittelt die Länge des Arrays
// und den Datentypen der Elemente
std :: array vals = { t, phi1 , phi2 , phi2 - phi1 };
table. push_back (vals);
}
Es folgt die Implementierung von (Gl. 1.8) und die Suche. Wenn ∆φD ein Eintrag in der
Tabelle und ∆φ die gesuchte Winkeldifferenz ist, betrachten wir die Suche als erfolgreich,
wenn |∆φD − ∆φ| < ϵ mit 10−6 ≤ ϵ ≤ 10−2 , da wir nicht erwarten können, dass der
gesuchte Wert mit einem Eintrag in der Tabelle exakt übereinstimmt.
Listing 1.31
Die erste Spalte enthält den Reihenindex, die zweite die Zeiten, die dritte die Winkeldif-
ferenz und die vierte Spalte die exakt berechnete Zeit (Gl. 1.8).
Wir wollen in diesem Abschnitt eine einfache lineare Interpolation implementieren. Für
einen diskreten Datensatz (x, y), der mithilfe einer bekannten Funktion erstellt wurde,
sollen Geraden berechnet werden, welche immer zwei benachbarte Punkte miteinander
verbinden. Anschließend sollen mithilfe dieser Geraden Werte für nicht tabellierte Daten
berechnet und die Ergebnisse mit den exakt berechneten Werten verglichen werden.
y
(x1 , y1 )
y
(x2 , y2 )
(x0 , y0 )
x x
0
Abb. 1.7: Für jeweils zwei Punkte wird eine Gerade berechnet. Mit diesen Geraden
werden nicht tabellierte Punkte berechnet.
28 1 Eine Tour durch C++
Theorie Gesucht wird für xi < x < xi+1 eine Approximation für den Wert y = f (x).
Wir berechnen die Gerade, welche die Punkte (xi , yi ) und (xi+1 , yi+1 ) verbindet. Be-
trachten wir die Punkte (x0 , y0 ) und (x1 , y1 ) (Abb. 1.7). Für die gesuchte Gerade
y = a1 x + a0 muss
{
y0 = a 1 x 0 + a 0 (1.9a)
y1 = a 1 x 1 + a 0 (1.9b)
gelten, wobei durch dieses Gleichungssystem a0 und a1 ermittelt werden. Wir erhalten
als Lösung:
y1 − y0
a1 = (1.10a)
x1 − x0
y1 − y0
a 0 = y1 − x1 (1.10b)
x1 − x0
Kurzbeschreibung Das Programm soll innerhalb einer Funktion umgesetzt werden, die
aus folgenden drei Teilen besteht:
Quelltext und Daten Wir beginnen mit der Implementierung des Algorithmus zur li-
nearen Interpolation, wobei die Koeffizienten a0 und a1 mit einer λ-Funktion durch
Anwendung der Formeln (Gl. 1.10a) und (Gl. 1.10b) berechnet werden.
/**
* @brief ex1 Berechnung von Daten mittels linearer Interpolation .
*/
inline void ex1 () {
// ----------------------------------------------------------
// Berechnung der Geraden a1 x + a0 , die durch die Punkte
// (x0 ,y0) und (x1 ,y1) geht
// --------------------------------------------------------
auto line = []( double x0 , double y0 , double x1 , double y1) {
double a1 = (y1 - y0) / (x1 - x0);
double a0 = y1 - a1 * x1;
return std :: make_pair (a0 , a1);
};
// ----------------------------------------------------------
// für eine Gerade y(x) = a1 * x + a0 berechne y(x)
// ----------------------------------------------------------
auto lininterp = []( double a0 , double a1 , double x) { //
return a0 + a1 * x;
};
Im zweiten Abschnitt erstellen wir einen Datensatz aus zwanzig Werten (x, cos(x)) mit
x = 0, 1, . . . 19 (Listing 1.33).
1.6 λ-Funktionen 29
// ---------------------------------------------
// Erstellung von Beispieldaten (x,y)
// -----------------------------------------------
// reserviere Speicher für 20 Elemente
std :: vector <double > xvalues (20) , yvalues (20);
// fülle x = 0.1 ,0.2 ,...
double xval = 0;
std :: generate ( xvalues . begin () , xvalues .end (), [& xval ]() { //
return xval += 0.1;
});
// fülle y = cos(x)
std :: transform ( xvalues . begin () ,
xvalues .end () , //
std :: begin ( yvalues ),
[]( double x) { return std :: cos(x); });
Es folgt die Berechnung eines Näherungswertes für cos x mit x = 1.23. Innerhalb des
erstellten Datensatzes suchen wir nach xi , xi+1 , sodass xi < x < xi+1 ist. Wir berechnen
die Gerade und den interpolierten Wert.
// -------------------------------------
// Berechnung von y(x =1.23)
// ----------------------------------------
const double x = 1.23;
// suche 1. tes x_i >= x
auto itr = std :: upper_bound ( xvalues . begin (), xvalues .end (), x);
// wenn gefunden ...
if (itr != xvalues .end ()) {
// finde Index im Feld
auto idx = static_cast <size_t >( std :: distance ( xvalues .begin (),
itr));
std :: cout << " found index : " << idx //
<< " with x=" << xvalues [idx] //
<< " and y=" << yvalues [idx] << std :: endl;
const double x0 = xvalues [idx - 1];
const double x1 = xvalues [idx ];
const double y0 = yvalues [idx - 1];
const double y1 = yvalues [idx ];
auto [a0 , a1] = line(x0 , y0 , x1 , y1);
std :: cout << "(x0 ,y0)=(" << x0 << "," << y0 << ")" << std :: endl;
std :: cout << "(x1 ,y1)=(" << x1 << "," << y1 << ")" << std :: endl;
std :: cout << "(a0 ,a1)=(" << a0 << "," << a1 << ")" << std :: endl;
std :: cout << "x=" << x << std :: endl;
std :: cout << " interpolated value:" << lininterp (a0 , a1 , x) <<
std :: endl;
std :: cout << "true value :" << std :: cos(x) << std :: endl;
}
}
Listing 1.35
1.7.1 Beispielanwendungen
Für eine gegebene komplexe Zahl z = 3+4i wird der Realteil Re {z} = 3, der Imaginärteil
( )
Im {z} = 4, der Betrag |z| = 5, die Phase φ = arctan 43 = 0.927, z 2 = 25 und das kom-
plex Konjugierte z̄ = 3 − 4i berechnet. Es folgt eine Umrechnung von Polarkoordinaten
in kartesische für z.
/**
* @brief ex1 Eigenschaften von komplexen Zahlen
*/
inline void ex1 () {
using namespace std :: literals :: complex_literals ;
std :: complex <double > z = 3. + 4.i;
std :: cout << "real part:" << std :: real(z) << std :: endl;
std :: cout << " imaginary part:" << std :: imag(z) << std :: endl;
std :: cout << " absolute value :" << std :: abs(z) << std :: endl;
std :: cout << " phase angle :" << std :: arg(z) << std :: endl;
std :: cout << "norm:" << std :: norm(z) << std :: endl;
std :: cout << " complex conjugate :" << std :: conj(z) << std :: endl;
std :: cout << " polar component :" << std :: polar (5. , 0.927295) <<
std :: endl;
}
real part :3
imaginary part :4
absolute value :5
phase angle :0.927295
norm :25
complex conjugate :(3 , -4)
polar component :(3 ,4)
Listing 1.37
/**
* @brief ex2 Rechnen mit komplexen Zahlen
*/
inline void ex2 () {
using namespace std :: literals :: complex_literals ;
std :: complex <double > z1 = 3. + 4i, z2 = 2. - 5i;
std :: cout << "z1=" << z1 << ", z2=" << z2 << std :: endl;
std :: cout << "z1+conj(z2)=" << z1 + std :: conj(z2) << std :: endl;
std :: cout << "z1 -conj(z2)=" << z1 - std :: conj(z2) << std :: endl;
Listing 1.39
32 1 Eine Tour durch C++
3.5_g 0.035 kg
return static_cast<double>(x)*1e-3;
}
Umrechnung von g in kg
Abb. 1.8: Benutzerdefiniertes Literal zur Umrechnung von g in kg
/**
* Umrechnung von Einheiten Ausgabe : SI - Einheiten
*/
namespace nmx {
// ** Massen g -> kg
constexpr double operator "" _g(long double x) {
return static_cast <double >(x) * 1e -3;
}
} // namespace nmx
1.8.2 Beispielanwendungen
1. Es soll die Dichte eines Blocks der Masse = 50 g mit Länge 5 cm, Breite 3 cm und
Höhe 2 cm berechnet werden.
2. Ein Auto fährt mit einer konstanten Geschwindigkeit v = 36 km/h. Wie lautet die
Geschwindigkeit in m/s?
34 1 Eine Tour durch C++
Quelltext und Daten Zur Umrechnung aller Einheiten, werden, die in Listing 1.40 bis
Listing 1.43 definierten Literale eingesetzt. Zusätzlich dazu benötigen wir die Formel für
die Dichte d = mV und die Frequenz f = N /∆t.
/**
* @brief ex3 Umrechnung von Einheiten mithilfe von benutzerdefinierten
* Literalen
*/
void ex3 () {
std :: cout << std :: setprecision (2) << std :: scientific ;
const auto g = 9.81;
// Aufgabe (1)
auto volume = 5. _cm * 3. _cm * 2. _cm;
auto mass = 50.0 _g;
auto density = mass / volume ;
std :: cout << "V=" << volume << std :: endl;
std :: cout << "d=" << density << std :: endl;
// Aufgabe (2)
std :: cout << "v=" << 36.0 _km / 1. _h << std :: endl;
// Aufgabe (3)
auto period = 2 * M_PI * sqrt (30.0 _cm / g);
std :: cout << " period =" << period << std :: endl;
// Aufgabe (4)
auto frequency = 9000 / 1.0 _min;
std :: cout << "f=" << frequency << std :: endl;
}
V =3.00e -05
d =1.67e+03
v =1.00e+01
period =1.10e+00
f =1.50e+02
Listing 1.45
Die Variable erhält beim ersten Funktionsaufruf, falls nichts anderes angegeben wird, den
Wert 0. Mithilfe von statischen Variablen innerhalb von Funktionen können sehr einfach
Zahlenfolgen oder Filter zur Auswahl von Elementen eines Containers nach bestimmtem
Kriterien realisiert werden.
1.9.1 Beispielanwendungen
Für ein vorgegebenes n sollen die ersten n geraden Zahlen in einem Feld gespeichert und
auf dem Bildschirm ausgeben werden.
Kurzbeschreibung Die Erstellung der Zahlenfolge wird eine Funktion, welche die Rol-
le eines Zahlengenerators spielen wird, übernehmen. Eine andere Funktion wird durch
Aufruf des Zahlengenerators ein Feld füllen. Die Zahlen werden schließlich auf dem Bild-
schirm ausgeben.
Quelltext und Daten Das Programmieren des Zahlengenerators als λ-Funktion ist ein-
fach. Eine statische Variable innerhalb der Funktion erhält den Anfangswert 0. Bei jedem
Aufruf wird der Wert um 2 erhöht. Zur Speicherung der Zahlen wird ein leeres Feld mit
n Elementen initialisiert. Einem speziellen stl-Algorithmus std::generate_n werden das
Feld und der Zahlengenerator übergeben und damit das Feld gefüllt.
/**
* @brief ex4 Speicherung und Ausgabe von n geraden Zahlen
*/
inline void ex4 () {
// lambda zur Generierung von Zahlen
auto fn = []() {
static double val = 0;
return val += 2; // erste Zahl ist die 2
};
//C-Feld erzeuge Speicher für 9 Elemente
const size_t n = 9;
int values [9];
std :: generate_n (values , n, fn);
// Ausgabe auf dem Bildschirm
for (auto x : values ) {
std :: cout << x << ",";
}
std :: cout << std :: endl;
}
} // namespace nmx :: apps :: x007
Listing 1.47
36 1 Eine Tour durch C++
In diesem Beispiel werden wir mithilfe von statischen Variablen innerhalb von Funktionen
Elemente einer Zahlenfolge herausfiltern.
Ein imaginärer Planet umkreist in einer elliptischen Bahn unsere Sonne mit dp = αde
und a > 0, wobei dp und de die Längen der großen Halbachsen des Planeten und der Erde
von der Sonne sind. Es soll ein Programm geschrieben werden, mit dem die Umlaufzeit des
Planeten in Abhängigkeit von α = 2, 3 . . . , 50 berechnet wird. Jeder zehnte berechnete
Wert soll auf dem Bildschirm ausgegeben werden.
Theorie Wir wenden das dritte Kepler’sche Gesetz für die Umlaufzeiten und die großen
Halbachsen der Umlaufbahnen der Erde und des imaginären Planeten an.
Tp2 dp3 3
2
= 3 = α 3 ⇒ Tp = α 2 Te (1.11)
Te de
Damit hätten wir die Umlaufzeit des Planeten in Abhängigkeit von der Umlaufzeit der
Erde berechnet.
3
Kurzbeschreibung Wertepaare der Form (α, α 2 ) werden mit einer Schleife erzeugt und
in einem Container gespeichert. Zur Ausgabe werden die Elemente des Containers mit
einer Schleife durchlaufen und mittels eines Filters jedes zehnte Element des Containers
auf dem Bildschirm geschrieben.
Quelltext und Daten Die berechneten Werte werden als Zahlenpaare (std::pair) in ei-
nem std::vector gespeichert. Der Container reserviert den nötigen Speicher automatisch,
was bedeutet, dass wir uns nicht vorher auf die Anzahl der Elemente festlegen müssen.
Für die Implementierung des Filters wählen wir eine λ-Funktion. Dort wird ein Zähler
mit dem Wert 1 als static initialisiert. Bei jedem Aufruf wird dieser Zähler um 1 erhöht.
Wenn der Wert 10 erreicht wird, gibt die Funktion ein true zurück und der Zähler wird
wieder auf eins gesetzt, andernfalls lautet der Rückgabewert false.
/**
* @brief ex3 Umlaufzeit eines Planeten innerhalb des Sonnensystems
*/
inline void ex3 () {
// Synonym für den Typ des Wertepaares
using Item = std :: pair <double , double >;
// Container zur Speicherung der Werte
std :: vector <Item > values ;
// der Container wird gefüllt
for ( double a = 2; a < 50; a += 1) {
values . push_back ({ a, std :: pow(a, 1.5) });
}
// Filter
auto filter = []() {
static size_t d = 1;
// wenn nicht 10 erhöhe Wert um 1
d = (d == 10) ? 1 : d + 1;
return d == 1;
};
1.9 Statische Variablen und Funktionen 37
(11 ,36.4829)
(21 ,96.2341)
(31 ,172.601)
(41 ,262.528)
Listing 1.49
Statische Variablen oder Funktionen, die innerhalb einer Klasse oder Struktur deklariert
sind, werden über diese abgerufen. Sie sind aber nicht an Instanzen der Klasse oder
Struktur gebunden, sondern werden von allen Instanzen geteilt. Da nur eine Kopie dieser
Variablen existiert und diese über den Klassennamen aufgerufen werden, eignen sie sich
sehr gut, um Konfigurationsparameter zu speichern. Ähnliches gilt für statische Funk-
tionen. Es handelt sich um globale Funktionen. Dadurch, dass sie auch über eine Klasse
aufgerufen werden, können mit ihnen Hilfsfunktionen gruppiert werden.
Programme benötigen oft Konfigurationsparameter, die bei ihrem Start geladen werden
und das Verhalten oder das Aussehen eines Programms festlegen. Die Beispielprogramme
in diesem Buch werden Daten hauptsächlich in Dateien speichern. Um den Überblick zu
behalten ist es sinnvoll, diese in einem bestimmten Verzeichnis bzw. in Unterverzeichnisse
eines Basisverzeichnisses zu speichern (Abb. 1.9). Da die Beispielprogramme auf unter-
schiedlichen Rechnern laufen sollen, ist es wichtig, dass der Name des Basisverzeichnisses
konfigurierbar ist.
38 1 Eine Tour durch C++
data_directory
home_directory
(Basisverzeichnis Daten)
data
x001 Dateien
…
current_directory plot.tex
x015
(aktuelles Verzeichnis) table.csv
…
x030
Abb. 1.9: Konfigurationsparameter zur Ausgabe von Dateien in Verzeichnissen
Wenn ein Programm in kompilierter Form vorliegt, kann dies nur über das Lesen
einer externen Textdatei geschehen, die beim Start eines Programms geladen wird. Für
die hier vorliegenden Beispielprogramme wird der Quelltext zur Verfügung gestellt. Wir
werden deshalb den Namen des Basisverzeichnisses als statische Variable innerhalb einer
Struktur speichern und diesen direkt in die Quelltextdatei schreiben.
/**
* @brief The Config struct Speicherung von Konfigurationsdaten
*/
struct Config {
private :
// in diesem Verzeichnis werden Dateien mit Daten geschrieben
inline static std :: string current_directory ;
public :
// Basisverzeichnis , alle anderen
// Verzeichnisse befinden sich in diesem Verzeichnis
inline static const std :: string home_directory =
"/home/ eliasp67 / xbook2 ";
Die Struktur erhält zusätzlich eine private Variable mit dem Namen eines aktuellen
Verzeichnisses. Dieser kann jederzeit über die statische Funktion Config::set_current_dir
festgelegt werden.
/**
* @brief set_current_dir setze Namen des aktuellen
* Datenverzeichnisses
* @param dirname Name des Ordners
*/
inline static void set_current_dir ( const std :: string & dirname ) { //
current_directory = dirname ;
}
Wird get_current_dir aufgerufen, erhalten wir nicht nur den Namen, sondern auch den
kompletten Pfad zum Verzeichnis.
/**
* @brief get_current_dir gebe aktuelles Datenverzeichnis
* @param flag wenn false ohne Pfad ( optional )
* @return der Name des aktuelles Datenverzeichnisses
*/
inline static std :: string get_current_dir (bool flag = true) { //
return flag ? ( data_directory + "/" + current_directory ) :
current_directory ;
}
/**
* @brief get_current_file gebe vollständigen Pfad zu einer Datei
* @param fname Name der Datei
* @return vollständigen Pfad zu einer Datei
*/
inline static std :: string get_current_file ( const std :: string
&fname) { //
return get_current_dir () + "/" + fname ;
}
/**
* @brief get_current_file gebe vollständigen Pfad zu einer Datei
* kann mit der Compiler - Variablen __func__ genutzt werden
* @param fname Name der Datei ohne Dateiendung
* @param ext Dateiendung
* @return vollständigen Pfad zu einer Datei
*/
inline static std :: string get_current_file ( const char *fname , const
char *ext) { //
std :: stringstream sstream ;
sstream << fname << "." << ext;
return get_current_file ( sstream .str ());
}
}; // Config
Wir möchten eine kleine Gruppe von nützlichen mathematischen Funktionen und Va-
riablen einführen, die im Rahmen dieses Buches immer wieder gebraucht werden. Dazu
gehören die Zahlen e und π sowie statische Funktionen zur Umrechnung vom Gradmaß
ins Bogenmaß und umgekehrt.
/**
* @brief The Math struct Mathematische Hilfsfunktionen
*/
struct Math {
// 3.14159265358979323846
inline static constexpr double PI = M_PI;
// 2.71828182845904523536
inline static constexpr double E = M_E;
// rad -> deg
inline static double to_radians ( double x) { return PI / 180 * x; }
// deg -> rad
inline static double to_degrees ( double x) { return 180 / PI * x; }
Es folgt eine Funktion, die testet, ob eine double-Zahl 0 ist. Eine weitere testet, ob zwei
double-Zahlen gleich sind und eine letzte liefert das Vorzeichen einer Zahl.
Die universelle Gravitationskonstante, der Radius und die Masse der Erde sowie die
Gravitationsbeschleunigung g in der Nähe der Erdoberfläche sind Konstanten, die wir
ebenfalls in Form von statischen Variablen speichern möchten. Zu diesem Zweck füh-
ren wir einen Namensraum gravitation ein. Für die Erddaten wird eine Struktur Earth
innerhalb dieses Namensraums eingeführt und für die Universalkonstante G eine mit
dem Namen Astronomy. Die Werte für G und g werden von der GNU Scientific Library
bereitgestellt.
1.9 Statische Variablen und Funktionen 41
/**
* physikalische Konstanten
*/
namespace gravitation {
// Astronomische Daten
struct Astronomy {
inline static double G{ GSL_CONST_MKSA_GRAVITATIONAL_CONSTANT };
};
// Erddaten
struct Earth {
inline static constexpr double g{ GSL_CONST_MKSA_GRAV_ACCEL };
inline static constexpr double mass{ 5.9722 e24 };
// zur Initialisierung des Erdradius wird das benutzerdefinierte
// Literal km benutzt
inline static constexpr double radius { 6378.137 _km };
};
} // namespace gravitation
/**
* @brief kinetic_energy Hilfsfunktion
* @param mass Masse
* @param velocity Geschwindigkeit
* @return kinetische Energie eines Teilchens
*/
inline static double kinetic_energy ( double mass , double velocity ) {
return 0.5 * mass * pow(velocity , 2);
}
/**
* @brief potential_energy Hilfsfunktion
* @param mass Masse
* @param height Höhe
* @return potenzielle Energie im konstanten Gravitationsfeld
*/
inline static double potential_energy ( double mass , double height ) {
return mass * gravitation :: Earth ::g * height ;
}
}; // Mechanics
} // namespace nmx
1.9.6 Beispielanwendungen
Es sollen Sinus, Kosinus und Tangens für die Winkel φ = 0° . . . 90° in Schritten von
∆φ = 30° in die Standardausgabe geschrieben werden.
Quelltext und Daten Zur Ausgabe der Ergebnisse benötigen wir die Umrech-
nung vom Bogenmaß ins Gradmaß, da die trigonometrischen Funktionen der C++-
Standardbibliothek die Angabe der Winkel in Bogenmaß erwarten. Die Funktionswerte
werden innerhalb einer Schleife berechnet, nachdem die Winkel mittels der Hilfsfunktion
Listing 1.55 ins Bogenmaß umgerechnet werden. Es wird darauf geachtet, dass statt einer
sehr großen Zahl (1.6331e+16) für den Tangens von π2 , nan ausgedruckt wird.
/**
* @brief ex1 Umrechnung Bogen - und Gradmaß
*/
inline void ex1 () {
std :: cout << std :: scientific << std :: setprecision (3);
Listing 1.61
1.9 Statische Variablen und Funktionen 43
Ein Satellit mit der Masse m = 720 kg wird in eine kreisförmige Umlaufbahn um die
Erde in einer Höhe h = αR gebracht, wobei R der Erdradius und α > 1 ist.
Setzen Sie die Gravitationsbeschleunigung auf der Erdoberfläche sowie den Erdradius
und die Erdmasse als bekannt voraus.
Mm
FZ = G , (1.12)
r2
wobei G die universelle Gravitationskonstante, M die Erdmasse, m die Satellitenmasse
und r der Kreisbahnradius gemessen vom Erdmittelpunkt ist. Die Gravitationsbeschleu-
nigung im Abstand r vom Erdmittelpunkt beträgt
GM
g= . (1.13)
r2
Wenn g0 die Gravitationsbeschleunigung auf der Erdoberfläche und g1 die, entlang der
Satellitenbahn ist, gilt:
GM
( )2
g0 =
R2 R g0
⇒ g1 = g0 = . (1.14)
GM R + αR (1 + α)2
g1 = 2
(R + h)
Kurzbeschreibung Wir werden den Radius der Kreisbahn, die Gravitationskraft, die
Gravitationsbeschleunigung und das Verhältnis gg10 in Abhängigkeit von α ausgeben. Es
werden dabei die vordefinierten Werte für die universelle Gravitationskonstante und die
Erdbeschleunigung (Listing 1.57) eingesetzt.
Quelltext und Daten Der Quelltext ist einfach zu verstehen. Zu beachten ist die
Programmierung der for-Schleife, in der die Werte für α direkt mit einer Liste
(std::initializer_list) angegeben werden.
/**
* @brief ex2 Satellit auf kreisförmiger Bahn um die Erde
*/
inline void ex2 () {
using namespace gravitation ;
const double m = 720;
std :: cout << std :: scientific << std :: setprecision (3);
// Namen der Spalten ( erste Reihe)
44 1 Eine Tour durch C++
std :: cout << " alpha " << std :: setw (12) << "r" << std :: setw (12) //
<< " force " << std :: setw (12) << "g1" << std :: setw (12) //
<< "g1/g0" << std :: endl;
// berechne Daten
for ( double alpha : { 1.55 , 1.75 , 1.81 , 1.92 }) {
const double c = 1 + alpha ;
const double r = alpha * Earth :: radius ;
const double force = Astronomy ::G * Earth :: mass * m / pow(r, 2);
const double g1 = Earth ::g / pow(c, 2);
std :: cout << alpha << std :: setw (12) << r << std :: setw (12) //
<< force << std :: setw (12) << g1 << std :: setw (12) //
<< g1 / Earth ::g << std :: endl;
}
}
Listing 1.63
1.10 Präprozessor-Anweisungen
Mit der Anweisung #define haben wir die Möglichkeit, Zeichenketten einzuführen, die
vor der Übersetzung des Programms durch den Compiler vom Präprozessor gegen eine
andere Zeichenkette ausgetauscht werden. Der Compiler sieht anschließend nur die expan-
dierten Makros. Neben einfachen Makros besteht die Möglichkeit auch Makrofunktionen
zu schreiben.
Im folgenden Programm sehen wir wie ein Programm vor der Bearbeitung durch den
Präprozessor aussieht.
/**
* Berechnung der Fläche eines Kreises
*/
# include <iostream >
int main(void){
1.10 Präprozessor-Anweisungen 45
Danach sieht das Programm so aus (Listing 1.65), wobei wir hier nur den Teil des er-
zeugten Codes dargestellt haben, der für uns interessant ist.
....
int main(void){
Die vordefinierten Makros __FILE__ und __LINE__ werden vom Präprozessor durch
den Dateinamen bzw. durch die Zeilennummer ersetzt (Listing 1.66). __func__ und
__PRETTY_FUNCTION__ sind keine Makros, sondern vordefinierte Variablen, die vom Com-
piler ersetzt werden. Beide geben den Namen der Funktion wieder, in der sie stehen.
__PRETTY_FUNCTION__ enthält den Funktionsnamen inklusive Klassenzugehörigkeit und
Namensraum. __PRETTY_FUNCTION__ gehört nicht zum Standard, wird aber von den meis-
ten Compilern unterstützt.
Wird einem Makroargument ein # vorangestellt, wandelt der Präprozessor das Argu-
ment in eine Zeichenkette um. Im folgenden Beispiel nutzen wir dies, um eine Bedingung
als Zeichenkette auszugeben und so die Arbeit des Programms zur Laufzeit zu verfolgen.
/**
* @brief ex1 Präprozessor - Anweisungen
*/
inline void ex1 () {
// wandle Argument in Zeichenkette um
# define MYSTRING (x) #x
int n = 10;
std :: cout << " check condition :" << MYSTRING (n > 10) << std :: endl;
std :: cout << "in file: " << __FILE__ << std :: endl;
std :: cout << " function name: " << __func__ << std :: endl;
std :: cout << " pretty function name: " << __PRETTY_FUNCTION__ <<
std :: endl;
std :: cout << "at line: " << __LINE__ << std :: endl;
if (n > 10) {
std :: cout << "is true" << std :: endl;
46 1 Eine Tour durch C++
} else {
std :: cout << "is false " << std :: endl;
}
}
Listing 1.67
1.10.3 Beispielanwendungen
Wir wollen in einem Beispiel mittels eines Makros den Aufruf von stl-Algorithmen für
den Fall, dass alle Elemente eines Containers durchlaufen werden müssen, vereinfachen.
Um in diesen Fall nicht immer die Anweisungen std::begin und std::end schreiben zu
müssen, führen wir ein Makro mit dem Namen ALL ein.
Quelltext und Daten In der folgenden Funktion wird eine Liste mit den Zahlen 0 bis 9
generiert. Anschließend wird nach dem ersten Element gesucht, das größer als 5 ist. Die
Zahlen ab diesem Element sowie die ganze Liste werden auf dem Bildschirm geschrieben
(Listing 1.70). Zum Schluss wird das eingeführte Makro ALL durch die Anweisung #undef
gelöscht.
int main(void)
{
// definiere Makro
# define ALL(C) std :: begin (C), std :: end(C)
// lösche Makro
#undef ALL
return 0;
}
Nachdem der Präprozessor das Makro ALL expandiert hat, sieht der für uns relevante
Quelltextabschnitt so aus:
int main(void)
{
std ::list <double > lst (10);
std :: generate (std :: begin (lst),std :: end(lst), []() {
static double x = 0;
return x++;
});
auto fitr = std :: find_if (std :: begin (lst),std :: end(lst),
[]( double x) { return x > 5; });
std :: copy(lst. begin () , fitr ,
std :: ostream_iterator <double >( std ::cout , ","));
std :: cout << std :: endl;
std :: copy(std :: begin (lst),std :: end(lst),
std :: ostream_iterator <double >( std ::cout , ","));
return 0;
}
0,1,2,3,4,5,
0,1,2,3,4,5,6,7,8,9,
Listing 1.70
sen und geschrieben werden können. Mit einem ifstream werden Daten von einer Datei
gelesen und mit einem ofstream Daten in eine Datei geschrieben.
1.11.1 Beispielanwendungen
Wir beginnen mit einem Beispielprogramm, welches die Funktionen f1 (x) = x2 , f2 (x) =
x3 , f3 (x) = x4 und f4 (x) = x5 auswertet und die Ergebnisse in eine Datei schreibt.
Quelltext und Daten Die Anwendung wird innerhalb einer Funktion realisiert. Da nicht
anders angegeben, werden die Daten durch ein Komma separiert in eine Datei geschrie-
ben werden. Mithilfe der Konfigurationsparameter (Listing 1.50) wird der Dateiname
inklusive Verzeichnis festgelegt. Der stream-Status wird getestet, um herauszufinden, ob
die Öffnung der Datei erfolgreich war. Anschließend werden mit einer Schleife die Funk-
tionswerte generiert und in die Datei geschrieben. Wir profitieren von der Tatsache, dass
nach Ablauf der Lebenszeit des ofstream-Objekts die Datei automatisch geschlossen wird.
/**
* @brief ex1 Ausgabe von Funktionswerten in eine Datei
*/
inline void ex1 () {
// Konfigurationsdaten nutzen
using namespace nmx :: settings ;
// Pfad zu einem Ausgabeverzeichnis benennen
Config :: set_current_dir ("x002");
// Dateiname zu Ausgabe der Werte
std :: string fname = Config :: get_current_file (__func__ , "dat");
try {
std :: ofstream fout{ fname };
// wurde die Datei zum Schreiben geöffnet ?
if (! fout) {
throw std :: ios_base :: failure { fname };
}
// Formatierung
fout << std :: scientific << std :: setprecision (2);
// Ausgabe
for ( double x = -2; x <= 2; x += 1) {
fout << std :: setw (9) << x << "," << std :: pow(x, 2) //
<< "," << std :: setw (9) << std :: pow(x, 3) //
<< "," << std :: setw (9) << std :: pow(x, 4) //
<< "," << std :: setw (9) << std :: pow(x, 5) << std :: endl;
}
Das soeben geschriebene Programm kann als Grundlage dienen, um beliebige Funkti-
onswerte zu berechnen und zu speichern. Wir können durch Austausch der Funktionen
innerhalb der Schleife und durch Änderung des Dateinamens das Programm zur Generie-
rung von Daten unserer Wahl nutzen. Wenn schnell ein Programm erstellt werden soll,
ist das eine akzeptable Lösung. Ist aber das Ziel, mehr als ein kleines Testprogramm
zu schreiben, sollte ein modularer Ansatz in Betracht gezogen werden. Das Öffnen ei-
ner Datei und die Generierung der Daten können als eigenständige Teile implementiert
werden.
/**
* @brief open_file öffne Ausgabestrom für eine Datei
* @param fname Dateiname
*/
auto open_file ( const std :: string & fname ) {
std :: ofstream fout{ fname };
// wurde die Datei zum Schreiben geöffnet ?
if (! fout) {
throw std :: ios_base :: failure { fname };
}
return fout;
}
In einem zweiten Schritt überlegen wir, wie Werte für eine beliebige Anzahl von Funk-
tionen gespeichert werden können. Wir setzen als erstes ein Synonym für den Typ der
Funktionen, die wir auswerten wollen. Wir sparen dadurch das Schreiben von langen
Bezeichnern.
/**
* Synonym für eine Funktion mit Eingabe double , Rückgabewert double
*/
using RealFN = std :: function < double ( double ) >;
Zur Generierung der Funktionsdaten benötigen wir neben der Liste mit den Funktio-
nen auch den Wertebereich, innerhalb dessen die Funktionen ausgewertet werden. Die
Daten sollen in eine Datei geschrieben werden, was die Übergabe eines Ausgabestroms
notwendig macht.
/**
* @brief fn_data Ausgabe von Funktionswerten in eine Datei
* @param fout Ausgabestrom
* @param fnlst Liste mit Funktionen f(x) = y
* @param params Feld mit Intervallgrenzen und Schrittweite
*/
inline void fn_data (std :: ofstream &fout ,
std :: initializer_list <RealFN > fnlst ,
std :: array <double , 3> params ) {
// structured bindings jedes Element des Feldes wird
// einer Variablen zugeordnet
auto [xmin , xmax , dx] = params ;
for ( double x = xmin; x <= xmax; x += dx) {
// für jedes x
fout << x;
for (auto &fn : fnlst ) {
// werden die dazugehörigen Funktionswerte ausgegeben
fout << "," << fn(x);
}
fout << std :: endl;
}
}
Es folgt ein Beispiel, welches mit den soeben erstellten Funktionen das Programm Listing
1.71 neu formuliert.
/**
* @brief ex2 schreibe Funktionsdaten für x^2,x^3,x^4,x^5
*/
inline void ex2 () {
using namespace nmx :: settings ;
Config :: set_current_dir ("x002");
std :: string fname = Config :: get_current_file (std :: string ( __func__ )
+ ".dat");
try {
// öffne Ausgabestrom
auto fout = open_file ( fname );
fout << std :: scientific << std :: setprecision (2);
1.12 Aufzählungen
Eine bequeme Art, eine Liste von ganzzahligen Konstanten als separaten Datentyp zu
definieren, sind Aufzählungen. Die Elemente der Liste werden im Standardfall von 0
aufwärts nummeriert. Aufzählungen können einen Namen haben, der gleichzeitig auch
der Name des Datentyps ist. Die Werte dieses Datentyps werden automatisch in int
umgewandelt. Eine automatische Konvertierung findet für die Elemente der neuen Vari-
ante enum class nicht statt. Hier ist im Standardfall eine explizite Umwandlung mittels
static_cast<int> notwendig. Im folgenden Beispiel wird mithilfe von enum und enum class
auf die Elemente eines Feldes zugegriffen.
/**
* @brief ex1 Aufzählungstypen
*/
inline void ex1 () {
enum Idx { a, b, c, d };
enum class CIdx { a, b, c, d };
umfangreich diese Klasse ist. Wir wollen uns in diesem Abschnitt auf das Suchen und
Ersetzen von Teilen einer Zeichenkette sowie die Aufteilung von Zeichenketten konzen-
trieren.
1.13.1 Beispielanwendungen
Eine vorgegebene Zeichenkette soll in Teilen aufgespalten werden und speziell an Stellen,
an denen ein vorgegebenes Trennzeichen vorkommt.
Kurzbeschreibung Wir werden die Klasse std::stringstream einsetzen, die ähnlich wie
std::cout arbeitet. Statt aber die Daten auf dem Bildschirm zu schreiben, werden diese
auf Zeichenfelder umgeleitet. Die Daten in einem std::stringstream können jederzeit als
ein std::string ausgegeben werden.
Quelltext und Daten Wir übergeben eine Zeichenkette einem std::stringstream, das
kombiniert mit dem Aufruf der Funktion std::getline diese Zeichenkette immer dort
spaltet, wo ein Trennzeichen (hier ein Komma) gefunden wird. Genauer formuliert liest
std::getline den Inhalt des std::stringstream und kopiert diesen in einem std::string,
bis ein vorgegebenes Trennzeichen vorkommt. In diesem Fall gibt std::getline false
zurück. Die Teile der Zeichenkette werden in die Variable token geschrieben und auf dem
Bildschirm ausgegeben (Listing 1.79).
/**
* @brief ex4 Zeichenketten für ein gegebenes Trennzeichen aufteilen
*/
inline void ex4 () {
std :: string txt = "eins ,zwei ,drei ,vier ,fünf ,sechs ,"
"sieben ,acht ,neun und zehn";
eins -zwei -drei -vier -fünf -sechs -sieben -acht -neun und zehn -
Listing 1.79
1.13 Zeichenketten in C++ 53
Das soeben besprochene Beispiel ist einfach zu programmieren, hat aber den Nachteil,
dass es die Angabe nur eines Trennzeichens erlaubt. Falls für eine Zeichenkette mehr als
ein Trennzeichen angegeben werden soll, müssen wir nach einer alternativen Möglichkeit
suchen, dies zu programmieren.
Quelltext Eine mögliche Lösung führt über die Aufrufe der Elementfunktionen
find_first_not_of und find_first_of von std::string.
/**
* @brief ex3 Zeichenketten aufteilen mit mehreren Trennzeichen
*/
inline void ex3 () {
// Textvorgabe
std :: string txt = "eins ,zwei ,drei ,vier ,fünf ,sechs ,"
"sieben ,acht ,neun und zehn";
// Trennzeichen : Leerzeichen und Komma
std :: string delim = " ,";
// Anfangsposition
size_t beg , pos = 0;
// finde die erste Stelle nach "pos" in der KEIN Trennzeichen
// gefunden wurde
while (( beg = txt. find_first_not_of (delim , pos)) !=
std :: string :: npos) {
// finde die erste Stelle in der EIN Trennzeichen gefunden wurde
pos = txt. find_first_of (delim , beg + 1);
// schreibe die gefunden Teil - Zeichenkette
std :: cout << beg << "\t" //
<< (pos != std :: string :: npos ? pos : 1000) //
<< "\t" << txt. substr (beg , pos - beg) << "\n";
}
}
Daten Es folgt die Ausgabe des Programms. Jede extrahierte Zeichenkette wird zusam-
men mit den Positionen in der ursprünglichen Zeichenkette angegeben. Zu beachten ist,
dass im Programm nicht mit den expliziten Längen der Zeichenketten gearbeitet wird.
Der Grund ist, dass z.B. die Zeichenkette «fünf» intern nicht die Länge 4 hat.
0 4 eins
5 9 zwei
10 14 drei
15 19 vier
20 25 fünf <---
26 31 sechs
32 38 sieben
39 43 acht
44 48 neun
49 52 und
53 1000 zehn
Listing 1.81
54 1 Eine Tour durch C++
Wir wollen mit einem Beispiel die Schritte aufzeigen, die notwendig sind, um einen Teil
eines Textes zu suchen und zu ersetzen. Dabei sollen wieder Methoden der std::string-
Klasse zum Einsatz kommen. Im Beispieltext «die Bewegung eines Objekts in einer Flüs-
sigkeit beschreiben ...» soll das Wort «Objekts» (wir nennen es w1 ) durch das Wort
«Teilchens» (w2 ) ersetzt werden.
Quelltext Wir beginnen mit der Initialisierung der Variablen für den Beispieltext sowie
für w1 und w2 .
/**
* @brief ex2 Suchen und Ersetzen von Zeichenketten innerhalb
* eines Texts
*/
inline void ex2 () {
// Textvorlage
std :: string txt = "die Bewegung eines Objekts in einer"
" Flüssigkeit beschreiben ...";
std :: cout << "Text:" << std :: endl;
std :: cout << txt << std :: endl;
// Wort im Text
const std :: string w1 = " Objekts ";
// wird ersetzt durch
const std :: string w2 = " Teilchens ";
In einem ersten Versuch suchen wir die Position des ersten Zeichens von w1 und löschen
ab dieser Position n Zeichen, wobei n die Länge von w1 ist. Anschließend fügen wir w2
an der Position des ersten Zeichens von w1 ein.
//**------------------
// Methode 1
// --------------------
std :: cout << " Methode 1:" << std :: endl;
auto txt1 = txt;
size_t pos = txt1.find(w1);
// finde Position von w1 und schneide es heraus
if (pos != std :: string :: npos) {
txt1.erase (pos , w1. length ());
std :: cout << txt1 << std :: endl;
} else {
std :: cout << "not found :" << std :: endl;
}
// füge an der Position eine neue Zeichenkette ein
if (pos != std :: string :: npos) {
txt1. insert (pos , w2);
std :: cout << txt1 << std :: endl;
} else {
std :: cout << "not found :" << std :: endl;
}
In einem zweiten Versuch ersetzen wir beide Schritte durch einen. Hier wird wieder die
Position des ersten Zeichens von w1 gesucht und anschließend mit einer Elementfunktion
von std::string, durch w2 ersetzt.
//**----------------------
// Methode 2
// ------------------------
std :: cout << " Methode 2:" << std :: endl;
auto txt2 = txt;
pos = txt2.find(w1);
// ausschneiden und ersetzen mit einem Befehl
if (pos != std :: string :: npos) {
txt2. replace (pos , w1. length () , w2);
std :: cout << txt2 << std :: endl;
} else {
std :: cout << "not found :" << std :: endl;
}
}
Wir sehen in Listing 1.85 die Ausgabe des Programms. An erster Stelle befindet sich
der ursprüngliche Text; es folgt das Ersetzen von w1 durch w2 in zwei Schritten und
schließlich das Ganze in einem Schritt.
Text:
die Bewegung eines Objekts in einer Flüssigkeit beschreiben ...
Methode 1:
die Bewegung eines in einer Flüssigkeit beschreiben ...
die Bewegung eines Teilchens in einer Flüssigkeit beschreiben ...
Methode 2:
die Bewegung eines Teilchens in einer Flüssigkeit beschreiben ...
Listing 1.85
Wir wollen in diesem Abschnitt eine Physikaufgabe, die in Form einer Textdatei vorliegt,
mit einem Programm bearbeiten. Der Inhalt der Textdatei soll keine vollständig durchge-
rechnete Aufgabe sein, sondern vielmehr eine Vorlage, in der die bekannten Größen und
Antworten nicht im Dokument stehen, sondern an deren Stelle nur Platzhalter zu finden
sind. Dem Programm sollen die bekannten Größen übergeben werden, welches dann die
Antworten berechnet und bekannte sowie berechnete Größen im Text ersetzt. Wir begin-
nen mit der Formulierung der Aufgabe, die wir ganz normal lösen werden, damit wir auf
dieser Basis mit der Erstellung des Programms und der Textvorlage fortfahren können.
56 1 Eine Tour durch C++
Eine Masse m wird in einer Höhe h von der Erdoberfläche in eine kreisförmige Umlauf-
bahn um die Erde gebracht. Wie groß ist die mechanische Energie der Masse?
Mm
U = −G (1.15)
R
gegeben, wobei M die Masse der Erde und R der Abstand der Masse vom Erdmittelpunkt
ist. Zur Berechnung der Geschwindigkeit der Masse nutzen wir die Tatsache, dass die
Gravitation die Rolle der Zentripetalkraft spielt.
√
Mm v2 M M
G 2 =m ⇒ v2 = G ⇒v= G , (1.16)
R R R R
wobei wir hier direkt die Beträge gleichgesetzt haben. Es folgt die gesamte mechanische
Energie:
Mm
E = K + V = −G (1.17)
2R
Erstellung einer Vorlage für die Aufgabe Wir speichern die Aufgabe in leicht abgeän-
derter Form als Textdatei ab, die wir als Vorlage nutzen wollen.
---------------------Aufgabe -----------------------------------------
Eine Masse m=$1 kg wird in einer Höhe h=$2 m von der
Erdoberfläche in eine kreisförmige Umlaufbahn um die Erde gebracht .
Wie groß ist die mechanische Energie der Masse ?
--------------------Antwort -----------------------------------------
Der Bahnradius beträgt R=$3 m
und die mechanische Energie ist E=$4 J
Im Vergleich zum ursprünglichen Aufgabentext hat sich Folgendes geändert: Jeder be-
kannten sowie gesuchten Größe wird ein =-Zeichen sowie ein Platzhalter hintenan gestellt.
Die Platzhalter sind nummeriert, sodass sie vom Programm eindeutig identifiziert werden
können. Alle Größen werden in SI-Einheiten angegeben.
Kurzbeschreibung Wir benötigen zur Erstellung des Programms einen Container zur
Speicherung des Texts, der aus der Datei geladen wird. Zusätzlich wird ein assoziativer
Container, der jedem Platzhalter eine Zahl zuordnet, benötigt. Der Text soll im ersten
Schritt geladen werden und Zeile für Zeile nach Platzhaltern durchsucht werden. Jedes
Mal, wenn einer gefunden wird, ersetzt das Programm diesen durch den dazugehörigen
Wert.
1.13 Zeichenketten in C++ 57
Quelltext Wir wollen lange Bezeichner vermeiden und setzen Synonyme für die Con-
tainer zur Speicherung des Texts und der Platzhalter-Werte-Paare. Für den Text wählen
wir einen std::vector der jede gelesene Zeile des Textes als Element betrachtet und am
Ende einfügt. Der andere Container wird eine std::map sein, die jedem Platzhalter eine
Zahl zuordnen wird.
Es folgt eine Funktion zum Einlesen des Texts. Nach der Öffnung des Eingabestroms
wird jede Zeile eingelesen und im Vektor gespeichert.
/**
* @brief read_text Einlesen des Texts
*/
inline auto read_text () {
const std :: string fname = dirname + "/qstn1 .txt";
// öffne Datei
std :: ifstream ifs{ fname };
if (! ifs) {
throw std :: ios_base :: failure (" error open:" + fname);
}
return lines ;
}
Die Berechnung der fehlenden Größen ist Aufgabe der nächsten Funktion. Ihr wird die
Adresse des Containers mit den bekannten Größen übergeben, die unbekannten werden
berechnet und dem Container hinzugefügt.
/**
* @brief calc Berechnung der fehlenden Größen
* @param values (std :: map) enthält die vorgegebenen und
* die zu berechnenenden Werte in der Form (Name ,Wert)
*/
inline void calc( Vartable & values ) {
using namespace gravitation ;
double mass = values .at("$1");
double height = values .at("$2");
double radius = Earth :: radius + height ;
58 1 Eine Tour durch C++
Das Suchen und Ersetzen im Text findet in einer separaten Funktion statt. Es wird über
die Zeilen des Dokuments iteriert. In jeder Zeile wird überprüft, ob sie einen Platzhalter
enthält, der dann ersetzt wird. Dafür werden die Elementfunktionen von std::string aus
der Standardbibliothek genutzt.
/**
* @brief calc_replace
* @param txt das Dokument als ein Feld von Zeichenketten
* @param values Tabelle mit ( Platzhalter , berechnete Werte )
*/
inline auto replace ( Document &txt , const Vartable & values ) {
// Iteration über alle Zeilen des Dokuments
for (auto &line : txt) {
// Iteration über alle Platzhalter
for (const auto &item : values ) {
// Platzhalter
const auto key = item. first ;
// Wert als Zeichenkette
const auto val = convert (item. second );
size_t pos;
// suche
while (( pos = line.find(key)) != std :: string :: npos) {
// ersetze
line. replace (pos , key. length (), val);
}
}
}
}
Zur Konvertierung einer Zahl in eine Zeichenkette wird folgende Funktion eingesetzt,
welche zusätzlich eine Formatierung durchführt.
/**
* @brief convert Konvertierung der Zahl in eine Zeichenkette
* @param x Zahl
* @return Zahl formatiert als Zeichenkette
*/
inline auto convert ( double x) {
std :: stringstream sstream ;
sstream << std :: scientific << std :: setprecision (2) << x;
return sstream .str ();
}
Das Dokument wird mithilfe der nächsten Funktion abgespeichert. Nach der Überprü-
fung, ob die Ausgabedatei erfolgreich geöffnet wurde, wird über die Elemente des Con-
tainers iteriert und in die Datei geschrieben.
/**
* @brief save Dokument wird gespeichert
* @param doc das Dokument
*/
inline void save_answer ( const Document &doc) {
const std :: string fname = dirname + "/ answr1 .txt";
std :: ofstream ofs{ fname };
if (! ofs) {
throw std :: ios_base :: failure (" error open:" + fname);
}
/**
* @brief ex1 Bearbeitung eines Texts und Berechnung der
* unbekannten physikalischen Größen
*/
inline void ex1 () {
auto doc = read_text ();
Vartable vtable { { "$1", 10 }, { "$2", 300. _km } };
calc( vtable );
replace (doc , vtable );
save_answer (doc);
}
---------------------Aufgabe -----------------------------------------
Eine Masse m=1.00 e+01 kg wird in einer Höhe h=3.00e+05 m von der
Erdoberfläche in eine kreisförmige Umlaufbahn um die Erde gebracht .
Wie groß ist die mechanische Energie der Masse ?
--------------------Antwort -----------------------------------------
Der Bahnradius beträgt R =6.68 e+06 m
und die mechanische Energie ist E= -2.98e+08 J
1.14 Übungsaufgaben
1. Speichern Sie die Zahlen 1, 2, ..., 100 in einem std::vector und berechnen Sie anschlie-
ßend mit dem stl-Algorithmus std::accumulate die Summe dieser Zahlen.
2. Berechnen Sie den Ausdruck eiπ + 1.
3. Das Paket pgfplots basiert auf TikZ und wird zur Erzeugung von Diagrammen direkt
in den LATEX-Quelltext integriert. Folgender Quelltext
\begin{ tikzpicture }
\begin{axis }[ xlabel = $x$, ylabel = {$f(x)$},]
\ addplot [ domain = -2:1 , samples =100 , color=black ,]{2*x^2 + x - 1};
\end{axis}
\end{ tikzpicture }
Listing 1.95
erzeugt das Diagramm für die Funktion f (x) = 2x2 + x − 1 (Abb. 1.10), wobei mit
domain = -2:1 die Grenzen entlang der x-Achse, mit color=black die Farbe der Kurve
und mit 2*x^2 + x - 1 die zu zeichnende Funktion angegeben wird. Führen Sie Platz-
halter der Form @1,@2,... für die Angabe der Grenzen, der Farbe und der Funktion
ein und speichern Sie den Quelltext (Listing 1.95) als Vorlage ab. Erzeugen Sie mit
einem Programm durch Bearbeitung der abgespeicherten Vorlage, ein Diagramm für
f (x) = x3 und x ∈ [−1, 1]. Wählen Sie für die Farbe der Kurve color=red.
4. Ein Fahrzeug startet aus der Ruhelage mit einer Beschleunigung aA = 1 m/s2 und
befindet sich 300 m hinter einem zweiten Fahrzeug B, welches sich mit konstanter
Geschwindigkeit vB = 10 m/s bewegt. Schreiben Sie ein Programm, welches die Be-
wegungsdaten t, xA (Ort des Fahrzeugs A), xB (Ort des Fahrzeugs B), vA , vB für
die zwei Fahrzeuge aufzeichnet und diese in die Standradausgabe schreibt.
4
f (x)
Übersicht
2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.2 Einfache Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.3 Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.4 Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
2.5 Programmieren mit Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
2.6 Schnittstellen für Klassen mithilfe von Templates definieren . . . . . . . . . . . . . . . 119
2.7 Eigene Werkzeuge bauen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
2.8 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
2.1 Einleitung
C++ ist keine rein objektorientierte Sprache. Sie unterstützt aber neben anderen auch
diesen Programmierstil, in dem Klassen das zentrale Element sind. Es handelt sich dabei
um benutzerdefinierte Vorlagen zur Konstruktion von Objekten, die über eigene Daten
verfügen und mittels eigener Funktionen, den Elementfunktionen, diese Daten manipu-
lieren. Instanzen einer Klasse sind Objekte, die mithilfe dieser Vorlagen erzeugt werden
und über ihre Elementfunktionen mit anderen Objekten interagieren, die nicht von dem-
selben Typ sein müssen. Es entsteht durch die Verwendung des Begriffs «Objekt» der
Eindruck, dass mit Klassen nur Gegenstände des Alltags abgebildet werden können. Dies
ist jedoch nicht richtig. Mit Klassen können genauso gut Prozesse beschrieben werden,
die durch die Interaktion von Objekten entstehen, die wiederum Instanzen von Klassen
sind. Als Beispiel könnte hier die Beschreibung der Bewegung einer Rakete dienen, in
der mit einer Klasse die Rakete selbst und mit einer anderen Klasse die Bewegungsdaten
ihrer Flugbahn verwaltet werden. Allgemein kann jedes System, welches einen inneren
Zustand besitzt und diesen durch Interaktion mit anderen Systemen verändern kann,
als Klasse dargestellt werden. Zu den wichtigsten Eigenschaften einer Klasse (neben der,
eigene Daten und Elementfunktionen zu besitzen) gehört die Möglichkeit, Eigenschaften
von anderen Klassen zu erben und die eigenen Eigenschaften an andere zu vererben.
Sowohl Daten als auch Elementfunktionen einer Klasse können Zugriffsrechte zugewiesen
werden. Mit diesen wird festgelegt, wer auf die Elemente der Klasse zugreifen darf. Durch
die Schlüsselwörter public, protected und private wird eine Klasse in Bereiche aufgeteilt.
Daten und Elementfunktionen, die sich im public-Bereich befinden, unterliegen keiner
Zugriffsbeschränkung und sind auch außerhalb der Klasse sichtbar und, wenn nicht als
konstant deklariert, auch manipulierbar. Die Summe aller public-Elementfunktionen bil-
det die Schnittstelle eines Objekts zur Außenwelt. Sind Daten und Elementfunktionen im
protected-Bereich deklariert, können diese nur von public abgeleiteten Klassen genutzt
werden. Ausschließlich innerhalb der Klasse sichtbar sind alle ihre Elemente, welche im
private-Bereich deklariert wurden (Listing 2.1).
/**
* @brief The Example class Beispielklasse
*/
class Example
{
private :
// Daten der Klasse
// Zugriff nur innerhalb der Klasse
double _a , _b;
// ......
protected :
// nur innerhalb der Klasse und
// von abgeleiteten Klassen sichtbar
void set_a( double x) { _a = x; }
public :
// ...
// Elementfunktion greift auf
// private Daten zu. Ist selbst
// außerhalb der Klasse sichtbar
double sum () const { return _a + _b; }
// ...
};
Als erstes Beispiel zur Konstruktion einer Klasse wählen wir das mathematische Pen-
del. Unter diesem Begriff finden wir in Physikbüchern nicht nur die Beschreibung des
Gegenstands, sondern auch die seiner Bewegung durch die Aufstellung und Lösung der
Bewegungsgleichungen. Wir werden uns hier auf die Abbildung der Eigenschaften des
Systems mittels einer Klasse konzentrieren und die Bewegung außer Acht lassen. Damit
hätten wir ein sehr einfaches System, dass aus einer dünnen, masselosen Stange besteht,
die am oberen Ende so befestigt ist, dass sie sich ohne Reibung drehen kann, und einer
Masse, z.B. einer Kugel, die am anderen Ende befestigt ist und hin und her schwingen
kann (Abb. 2.1). Zu den Eigenschaften des Pendels gehören die Länge der Stange l und
die Masse der Kugel
√ m. Nicht dazu gehören z.B. die Auslenkung φ und die Schwingungs-
dauer T0 = 2π gl . Während der Winkel φ klar zur Beschreibung der Bewegung des
Systems gehört, ist nicht sofort ersichtlich, warum die Schwingungsdauer keine Eigen-
schaft des Systems ist, hängt sie doch nur von l ab. Ein Argument ist, dass diese Formel
nur für kleine Ausschläge gilt und für größere der Maximalausschlag mit in die Formel
aufgenommen wird.
l l
φ m
m
(a) (b)
Abb. 2.1: Mathematisches Pendel. (a) Ruhelage (b) Ausgelenkt um einen Winkel φ
Entwurf Aus der Beschreibung des Systems folgt, dass die Klasse für das Pendel die
Länge l und die Masse m intern speichern wird. Auf diese zwei Attribute wird nur ein
lesender Zugriff erlaubt sein, da ihre Änderung die Beschreibung eines anderen Pendels
zur Folge hätte.
Quelltext Wir beginnen mit der Programmierung der Klasse und speichern die Masse
und die Länge des Pendels im private-Bereich ab.
/**
* @brief The Pendulum class Eigenschaften eines mathematischen Pendels
*/
class Pendulum
{
private :
double _length , _mass ;
public :
Private Daten können außerhalb der Klasse nur mithilfe von Elementfunktionen geän-
dert werden. Da wir dies aber nicht zulassen wollen, kann ihre Initialisierung nur über
den Konstruktor erfolgen. Innerhalb des Konstruktors wird geprüft, ob gültige Werte
übergeben wurden.
/**
* @brief Pendulum Konstruktor
* @param l Länge
* @param m Masse
*/
Pendulum ( double l, double m)
: _length { l }
, _mass { m } {
// Länge und Masse sind positive Größen
if (l <= 0 || m <= 0) {
throw std :: domain_error ( __func__ );
}
}
Die Beschreibung der Klasse ist mit den beiden folgenden Funktionen zum Lesen der
Länge und der Masse abgeschlossen.
/**
* @brief length Zugriff auf interne Variable
* @return Länge des Pendels
*/
double length () const { return _length ; }
/**
* @brief mass Zugriff auf interne Variable
* @return Masse des Pendels
*/
double mass () const { return _mass ; }
}; // Pendulum
Beispiel
Es folgt ein kleines Beispielprogramm (Listing 2.6), welches eine Instanz eines mathema-
tischen Pendels erstellt und seine Eigenschaften sowie die Schwingungsdauer für kleine
Ausschläge auf den Bildschirm ausgibt. Die Schwingungsdauer berechnen wir mit Ele-
mentfunktionen der soeben erstellten Klasse.
2.2 Einfache Klassen 65
/**
* @brief ex1 Beispiel : Eigenschaften eines mathematischen Pendels
*/
inline void ex1 () {
using namespace gravitation ;
Pendulum p{ 1, 2 };
// Schwingungsdauer für kleine Auslenkungen
const double T0 = 2 * Math :: PI * std :: sqrt(p. length () / Earth ::g);
std :: cout << p. length () << std :: endl;
std :: cout << p.mass () << std :: endl;
std :: cout << T0 << std :: endl;
}
Wir werden mit einer Klasse die physikalischen Eigenschaften einer idealen Feder be-
schreiben, welche entlang ihrer Achse gedehnt oder gestaucht wird. Zur Konstruktion
der Klasse werden Informationen zu den Eigenschaften von Federn benötigt. Als Infor-
mationsquelle könnten die Daten eines Experiments oder eine Aufgabe aus einem Phy-
sikbuch genutzt werden. In typischen Physikaufgaben wird die Bewegung einer Masse,
welche am Ende einer Feder befestigt ist, untersucht. Im Idealfall besitzt die Feder selbst
keine Masse oder diese ist im Vergleich zur befestigten Masse vernachlässigbar klein.
Die Feder übt auf die Masse eine Kraft F = −kx aus, welche proportional zur Auslen-
kung ist. Die Konstante k heißt Federkonstante und ist eine Eigenschaft der Feder. Über
die Federkonstante lässt sich die in der Feder gespeicherte elastische potenzielle Energie
U = 12 kx2 berechnen. Damit hätten wir alle Informationen, um zum Entwurf der Klasse
überzugehen.
x=0 x=0
F = −kx F = −kx
k m k
x x
(a) (b)
Abb. 2.2: (a) Die gedehnte Feder übt eine der Auslenkung proportionale Kraft auf eine
Masse m aus. (b) Die Feder wird isoliert betrachtet und dient als Vorlage für die zu
implementierende Klasse.
Entwurf Da die Feder masselos ist, bleibt die Federkonstante k als einzige konstante
charakteristische Eigenschaft übrig. Diese wird einmal bei der Erzeugung einer Instanz
gesetzt und darf über eine Methode der Klasse nur gelesen werden. Die Auslenkung ist
66 2 Klassen in C++
ebenfalls ein Zustand der Feder, die aber durch Interaktion mit einem anderen Objekt
verändert werden kann. Wir werden die momentane Auslenkung der Feder in einer Va-
riablen speichern. Ihr Wert soll aber mittels einer Elementfunktion zu verändern sein.
Die Kraft und die potenzielle Energie hängen unmittelbar mit der Auslenkung zusammen
und sind auch Eigenschaften der Feder. Wir werden die Klasse so programmieren, dass
die beiden Größen als Variablen innerhalb der Klasse angelegt und dass bei Änderung
der Auslenkung diese automatisch neu berechnet werden.
Zwei oder mehrere Federn können miteinander zu einem System kombiniert werden,
das wieder die Eigenschaften einer Feder hat. Die resultierende Federkonstante wird aus
den beiden Federkonstanten des Systems berechnet. Wir möchten die Berechnung einer
äquivalenten Feder für zwei oder mehrerer Federn im Entwurf der Klasse mit berücksich-
tigen.
Quelltext Wir speichern im private-Bereich mit vier Variablen, die im Entwurf aufge-
listeten Attribute der Klasse. Außer der Federkonstanten haben alle anderen Attribute
bei Erzeugung einer Instanz den Wert 0. Dies entspricht der Situation einer entspannten
Feder.
/**
* @brief The Spring class Klasse
* Eigenschaften einer idealen Feder
*/
class Spring
{
private :
double _k; // Federkonstante
double _elongation = 0; // Auslenkung
// Kraft , elastische potenzielle Energie
double _force = 0, _ePotential = 0;
public :
Es soll nicht möglich sein, eine Instanz ohne die Angabe einer Federkonstante zu erzeu-
gen. Deswegen erhält die Klasse einen Konstruktor mit einem Eingabeargument für die
Federkonstante. Dieses ist immer eine positive Zahl. Innerhalb des Konstruktors wird
überprüft, ob k diese Bedingung erfüllt.
/**
* @brief Spring Konstruktor
* @param k Federkonstante
*/
Spring ( double k)
: _k{ k } {
if (_k <= 0) {
throw std :: domain_error ( __func__ );
}
}
Wird die Feder gedehnt oder gestaucht, wird nicht nur der Wert der Auslenkung an-
gepasst, sondern auch die Kraft und die potenzielle Energie werden neu berechnet. Die
Richtung der Auslenkung wird über das Vorzeichen bestimmt.
/**
* @brief set_elongation setze Wert für die Auslenkung
* @param x Auslenkung
*/
void set_elongation ( double x) {
_elongation = x; // neue Auslenkung
_force = -_k * x; // aktualisiere Kraft
// aktualisiere potenzielle Energie
_ePotential = 0.5 * _k * pow( _elongation , 2);
}
Die Werte von k, x, F und U können über folgende Elementfunktionen einzeln abgefragt
werden.
/**
* @brief k lese ...
* @return Federkonstante
*/
double k() const { return _k; }
/**
* @brief elongation lese ...
* @return die Auslenkung der Feder
*/
double elongation () const { return _elongation ; }
/**
* @brief force lese ...
* @return Federkraft ( momentaner Zustand der Feder)
*/
double force () const { return _force ; }
/**
* @brief e_potential lese ...
* @return elastische potenzielle Energie
* ( momentaner Zustand der Feder)
*/
double e_potential () const { return _ePotential ; }
/**
* @brief values
* @return Feld mit Auslenkung ,Kraft , potenzielle Energie
*/
auto values () const { //
return std :: array { _elongation , _force , _ePotential };
}
Die Implementierung der Klasse wäre an dieser Stelle abgeschlossen, wenn wir uns nicht
vorgenommen hätten, aus einzelnen Federn zusammengesetzte Systeme zu untersuchen.
Wir beschränken uns hier der Einfachheit halber auf eindimensionale Systeme und ent-
nehmen die nötigen Informationen aus Abb. 2.3. Es gibt zwei Möglichkeiten zwei Federn
miteinander zu kombinieren. Die erste ist, die Federn hintereinander (in Reihe, Abb.
2.3b) und die zweite ist, sie parallel zueinander zu schalten (Abb. 2.3c).
k1 k1 k1 k2 k1 k2
k2 k3
Bevor wir mit der Implementierung der Funktionen beginnen, geben wir die Formeln
zur Berechnung von äquivalenten Federn ohne Herleitung an:
1 1 1
= + (Reihenschaltung) (2.1a)
k k1 k2
k = k 1 + k2 (Parallelschaltung) (2.1b)
Für jede der oben aufgelisteten Formeln wird eine Funktion implementiert. Dabei kann
es sich nicht um Elementfunktionen der Klasse handeln, da sie keine Eigenschaften ei-
ner Instanz verwalten oder manipulieren, sondern vielmehr neue Instanzen generieren.
Der Bezug zur Klasse ist jedoch gegeben; deswegen führen wir die folgenden statischen
Funktionen ein.
2.2 Einfache Klassen 69
/**
* @brief add_paralell Parallel - Schaltung von zwei Federn
* @param s1 erste Feder
* @param s2 zweite Feder
* @return äquivalente Feder
*/
inline static Spring add_paralell ( const Spring &s1 , const Spring
&s2) {
double k = s1.k() + s2.k();
return Spring { k }; // Kopierkonstruktor automatisch generiert
}
/**
* @brief add_series Reihen - Schaltung von zwei Federn
* @param s1 erste Feder
* @param s2 zweite Feder
* @return äquivalente Feder
*/
inline static Spring add_series ( const Spring &s1 , const Spring s2) {
double tmp = 1. / s1.k() + 1. / s2.k();
// Kopierkonstruktor automatisch generiert
return Spring { 1 / tmp };
}
}; // Spring
Obwohl wir keinen Kopierkonstruktor für die Klasse Spring implementiert haben, wird
einer innerhalb der Funktionen genutzt. Der Compiler hat in diesen Fällen einen Kopier-
konstruktor automatisch generiert.
Beispiel
Eine äquivalente Feder wird für die Kombination aus Abb. 2.3d berechnet. Es werden drei
Instanzen der Klasse Spring erzeugt. Mithilfe der eingeführten statischen Funktionen (Lis-
ting 2.15 und Listing 2.16) wird die äquivalente Feder berechnet. Für die Federkonstanten
werden die Beispielwerte k1 = 200 N/m, k2 = 100 N/m und k3 = 100 N/m eingesetzt.
Die äquivalente Feder wird um x = 0.1 m gedehnt. Berechnet wird die Kraft, welche
von der Feder auf eine befestigte Masse ausgeübt wird sowie die in der Feder gespei-
cherte elastische potenzielle Energie. Die Daten werden anschließend auf dem Bildschirm
angezeigt (Listing 2.18).
/**
* @brief ex1 Berechnung von äquivalenten Federn
*/
inline void ex1 () {
const Spring s1{ 200 }, s2{ 100 }, s3{ 100 };
Spring s = Spring :: add_paralell (s1 , s2);
70 2 Klassen in C++
Listing 2.18
Wenn eine reelle Zahl als das Verhältnis zweier ganzer Zahlen dargestellt werden kann,
so haben wir eine rationale Zahl, die durch einen Bruch m n
mit n, m ∈ Z und m ̸=
0 dargestellt werden kann. Diese Zahlen können unter anderem addiert, subtrahiert,
multipliziert und dividiert werden.
Entwurf Die Attribute der Klasse sind schnell gefunden. Es sind der Nenner und Zähler
einer rationalen Zahl.
Quelltext Beide Attribute sollen außerhalb der Klasse nicht verändert werden können.
Andernfalls hätte dies zur Folge, dass ein Objekt seine Identität verlieren würde. Deshalb
werden Nenner und Zähler im private-Bereich gespeichert.
/**
* @brief Rational Klasse für rationale Zahlen
*/
class Rational
{
private :
long long _num , _den;
public :
Der einzige Konstruktor der Klasse erwartet einen Wert für Zähler und Nenner. Werden
keine Werte angegeben, ist der Zähler 0 und der Nenner 1. Innerhalb des Konstruktors
wird geprüft, ob der Nenner einen gültigen Wert besitzt, d.h. nicht 0 ist. Im Fall eines
ungültigen Wertes wird eine Ausnahme ausgeworfen. Zusätzlich werden, falls der Nenner
negativ ist, Zähler und Nenner mit −1 multipliziert.
/**
* @brief Rational Konstruktor
* @param n Nenner
* @param d Zähler
*/
inline Rational (long long n = 0, long long d = 1)
: _num{ n }
, _den{ d } {
if (d == 0) {
throw std :: domain_error ( __func__ );
}
if (d < 0) {
_num *= -1;
_den *= -1;
}
}
Nenner und Zähler können mittels der beiden folgenden Elementfunktionen gelesen wer-
den.
/**
* @brief num lese ...
* @return Zähler
*/
inline auto num () const { return _num; }
/**
* @brief den lese ...
* @return Nenner
*/
inline auto den () const { return _den; }
Die Funktion der Standardbibliothek std::gcd berechnet den größten gemeinsamen Teiler
zweier Zahlen. Wir nutzen diese Funktion, um den Bruch zu vereinfachen.
/**
* @brief simplify vereinfache Bruch
*/
inline void simplify () {
long gcd = std :: gcd(_num , _den);
72 2 Klassen in C++
_num /= gcd;
_den /= gcd;
}
Die Klasse enthält einen Typ-Umwandlungsoperator, mit dem Instanzen in double kon-
vertiert werden. Dies ist nützlich, wenn mit gemischten Termen aus rationalen und reellen
Zahlen gerechnet wird. Durch das Schlüsselwort explicit lassen wir es nicht zu, dass eine
Konvertierung vom Compiler automatisch durchgeführt wird.
/**
* @brief operator double Darstellung als reelle Zahl
*/
explicit operator double () const { //
return static_cast <double >( _num) / static_cast <double >( _den);
}
Mit folgendem Ausdruck wird zur Übersetzungszeit getestet, ob ein Datentyp eine ganze
Zahl repräsentiert. Mehr Information zu diesen Ausdrücken sind unter dem Stichwort
type_traits zu finden.
/**
* Test: ist T eine ganze Zahl?
* std :: is_arithmetic_v true wenn z.B. int , bool , float ,...
* std :: is_integral_v true wenn z.B. int , bool , char ,...
*/
template <class T>
static constexpr bool is_int_like_v = //
(std :: is_arithmetic_v <T> && std :: is_integral_v <T> //
&& !std :: is_same_v <T, bool > && !std :: is_same_v <T, char >);
Ob ein Datentyp eine ganze oder eine rationale Zahl repräsentiert, kann zur Überset-
zungszeit mit folgendem Ausdruck ermittelt werden.
Wenn eine rationale Zahl zu einer ganzen oder rationalen Zahl addiert wird, dann soll
das Ergebnis eine rationale Zahl sein. Wird hingegen eine rationale Zahl mit einer Fließ-
kommazahl addiert, so soll das Ergebnis eine Fließkommazahl sein. Für zwei rationale
Zahlen r1 und r2 ist der Ausdruck r1 += r2 äquivalent zu r1 = r1 + r2 und der Rück-
gabewert ist eine Referenz auf r1 und damit eine Referenz auf eine rationale Zahl. Es ist
2.2 Einfache Klassen 73
deswegen nicht möglich, die Addition mit einer Fließkommazahl in die Implementierung
des Operators += mit aufzunehmen. Daraus folgt, dass der Operator nur dann aufgerufen
werden darf, wenn das Eingabeargument eine ganze oder eine rationale Zahl ist.
Durch den Einsatz von enable_if können wir dem Compiler Bedingungen angeben, für
die eine Funktion bzw. ein Operator aufgerufen werden darf. Wir werfen als erstes einen
Blick auf die Definition von enable_if, das seit C++11 zum Standard gehört und in der
Header-Datei <type_traits> zu finden ist.
Listing 2.27
Wir sehen, dass, falls die Bedingung B wahr ist, diese Struktur ein Synonym type vom
Typ T definiert, sonst aber leer ist. Betrachten wir als nächstes die Implementierung von
+=.
/**
* @brief operator +=
* @param r ganze oder rationale Zahl
* @return Referenz auf aufrufendes Objekt
*/
template <class T, std :: enable_if_t < is_rational_like_v <T>> * =
nullptr >
inline Rational & operator +=( const T &r) {
if constexpr ( is_int_like_v <T >) {
_num = _num + _den;
} else {
_num = _num * r._den + r._num * _den;
_den = _den * r._den;
}
simplify ();
return *this;
}
/**
* @brief operator -=
* @param r ganze oder rationale Zahl
* @return Referenz auf aufrufendes Objekt
*/
template <class T, std :: enable_if_t < is_rational_like_v <T>> * =
nullptr >
inline Rational &operator -=( const T &r) {
if constexpr ( is_int_like_v <T >) {
_num = _num - _den;
} else {
_num = _num * r._den - r._num * _den;
_den = _den * r._den;
}
simplify ();
return *this;
}
/**
* @brief operator *=
* @param r ganze oder rationale Zahl
* @return Referenz auf aufrufendes Objekt
*/
template <class T, std :: enable_if_t < is_rational_like_v <T>> * =
nullptr >
inline Rational & operator *=( const T &r) {
if constexpr ( is_int_like_v <T >) {
_num = _num * r;
} else {
_num *= r._num;
_den *= r._den;
}
simplify ();
return *this;
}
/**
* @brief operator /=
* @param r ganze oder rationale Zahl
* @return Referenz auf aufrufendes Objekt
*/
template <class T, std :: enable_if_t < is_rational_like_v <T>> * =
nullptr >
inline Rational & operator /=( const T &r) {
if (r.num () == 0) {
throw std :: overflow_error (" operator /=");
}
if constexpr ( is_int_like_v <T >) {
_den *= r;
} else {
_num *= r._den;
2.2 Einfache Klassen 75
_den *= r._num;
}
return *this;
}
Damit ist die Implementierung der Klasse vorerst abgeschlossen. Es bleibt aber noch
Arbeit zu tun, da die binären Operatoren +,-,*,/ programmiert werden müssen. Wir
holen dies weiter unten in diesem Kapitel nach.
In den bisher vorgestellten Beispielen wurden Klassen mit wenigen Attributen implemen-
tiert. Bei wachsender Anzahl der Eigenschaften eines Objekts muss überlegt werden, wie
diese innerhalb der Klasse so gruppiert werden können, dass der Quelltext übersichtlich
bleibt. Wir werden mit einer Klasse zur Beschreibung von rechtwinkligen Dreiecken eine
Möglichkeit zur Gruppierung von Variablen vorstellen. Wie immer beginnen wir mit der
Identifizierung der charakteristischen Eigenschaften des Objekts. Ein Dreieck (Abb. 2.4)
wird durch die Angabe seiner drei Seiten und Winkel vollständig beschrieben.
c
b
C a B
Abb. 2.4: Rechtwinkliges Dreieck
Quelltext Wir beabsichtigen auch hier die Änderung der Eigenschaften des Objekts
außerhalb der Klasse nicht zu erlauben und speichern diese im private-Bereich ab. Zur
Speicherung der Winkel und Seiten verwenden wir jeweils ein dreidimensionales Feld
und für die Fläche und den Umfang eine Variable. Damit sind die Attribute der Klasse
in logische Einheiten gruppiert und die Klasse selbst bleibt übersichtlich. Wir achten
76 2 Klassen in C++
darauf, dass der Winkel A und die Seite a usw. dieselbe Position innerhalb ihrer Felder
innehaben. Der Grund wird weiter unten klar.
/**
* @brief The RightTriangle Klasse für ein rechtwinkliges Dreieck
*/
class RightTriangle
{
private :
// speichere Seiten
std :: array <double , 3> _sides ;
// speichere Winkel
std :: array <double , 3> _angles ;
// Fläche , Umfang
double _area , _perimeter ;
public :
Dem Konstruktor werden die zwei senkrechten Seiten übergeben. Mit einer Funktion
der Standardbibliothek (std::hypot) wird die Hypotenuse berechnet. Alle Winkel des
Dreiecks werden mit Formeln aus der Trigonometrie (Gl. 2.2) berechnet und gespeichert.
Dem Konstruktor dürfen nur positive Zahlen übergeben werden. Eine entsprechende
Überprüfung findet innerhalb des Konstruktors statt.
/**
* @brief RightTriangle Konstruktor
* @param a Kathete
* @param b Gegenkathete
*/
inline RightTriangle ( double a, double b) {
if (a <= 0.0 || b <= 0.0) {
throw std :: domain_error ( __func__ );
}
const double c = std :: hypot (a, b);
_sides = { a, b, c };
_angles = { asin(a / c), asin(b / c), 0.5 * Math ::PI };
_area = 0.5 * a * b;
_perimeter = a + b + c;
}
Da auf die Seiten und Winkel außerhalb der Klasse nicht zugegriffen werden kann, kön-
nen diese Werte nur gelesen werden, wenn Schnittstellen in Form von Elementfunktio-
nen existieren. Wir könnten für jede interne Variable eine zugehörige Elementfunktion
programmieren. Dies würde aber zu einer Flut von Elementfunktionen führen. Um den
Programmieraufwand so gut es geht zu reduzieren, greifen wir per Index, repräsentiert
durch einen Aufzählungstypen auf die Winkel und Seiten des Dreiecks zu. Durch die
Positionierung der Winkel und Seiten in ihren jeweiligen Feldern kommen wir mit einer
Aufzählung aus.
2.2 Einfache Klassen 77
/**
* @brief The Idx enum Zugriff auf die Seiten und Winkel des
* Dreiecks per Index ( Aufzählungstyp )
*/
enum Idx { a, b, c };
Die zwei nächsten Funktionen geben Referenzen auf die internen Felder zurück. Es
ist somit z.B. möglich, mit angles()[Idx::a] auf den Winkel A und entsprechend mit
sides()[Idx::a] auf die Seite a lesend zuzugreifen.
/**
* @brief sides
* @return Feld mit Längen der Seiten
*/
inline const auto &sides () const { return _sides ; }
/**
* @brief angles
* @return Feld mit allen Winkeln
*/
inline const auto & angles () const { return _angles ; }
/**
* @brief side Länge einer Seite
* @param idx Index
*/
inline auto side(Idx idx) const { return _sides .at(idx); }
/**
* @brief angle lese einen Winkel
* @param idx Index
*/
inline auto angle (Idx idx) const { return _angles .at(idx); }
Die Abfrage der Fläche und des Umfangs des Dreiecks erfolgt mithilfe der nächsten beiden
Elementfunktionen (Listing 2.39, Listing 2.40).
78 2 Klassen in C++
/**
* @brief area
* @return Fläche des Dreiecks
*/
inline double area () const { return _area; }
/**
* @brief perimeter
* @return Umfang des Dreiecks
*/
inline double perimeter () const { return _perimeter ; }
Zur Ausgabe der Daten des Dreiecks wird der <<-Operator überladen. Das Lesen der
Winkel und Seiten erfolgt mittels der oben diskutierten Elementfunktionen Listing 2.37
und Listing 2.38.
/**
* @brief operator << Ausgabe der Daten eines Dreiecks
* @param os Ausgabestrom
* @param t Dreieck
* @return Referenz auf Ausgabestrom
*/
friend std :: ostream &operator <<( std :: ostream &os , const
RightTriangle &t) {
using Idx = RightTriangle :: Idx;
os << "{" << t.side(Idx ::a) << ",";
os << t.side(Idx ::b) << ",";
os << t.side(Idx ::c) << "}" << std :: endl;
os << "{" << t. angle (Idx ::a) << ",";
os << t. angle (Idx ::b) << ",";
os << t. angle (Idx ::c) << "}" << std :: endl;
os << "{" << t.area () << "," //
<< t. perimeter () << "}" << std :: endl;
return os;
}
}; // RightTriangle
Beispiel
Wir instanziieren durch die Angabe der beiden senkrecht aufeinanderstehenden Seiten
ein Dreieck und geben seine Daten aus. Im nächsten Schritt wird eine Kopie des Drei-
ecks über den Kopierkonstruktor angelegt. Obwohl für die Klasse kein Kopierkonstruktor
implementiert wurde, erzeugt der Compiler automatisch einen, mit dem die internen Va-
riablen in der Reihenfolge, in der sie angelegt wurden, kopiert werden.
2.2 Einfache Klassen 79
/**
* @brief ex1 Beispiel Ausgabe der Daten für ein rechtwinkliges
* Dreieck
*/
inline void ex1 () {
using Triangle = RightTriangle ;
Triangle tr0{ 1, 2 };
std :: cout << tr0;
// kopiere Dreieck
Triangle tr1 = tr0;
std :: cout << " -------------------------------\n";
std :: cout << tr1;
}
{1 ,2 ,2.23607}
{0.463648 ,1.10715 ,1.5708}
{1 ,5.23607}
-------------------------------
{1 ,2 ,2.23607}
{0.463648 ,1.10715 ,1.5708}
{1 ,5.23607}
Listing 2.43
Klassen eignen sich nicht nur zur Abbildung von materiellen Gegenständen, sondern
auch zur Beschreibung von Prozessen. Folgende Physikaufgabe soll als Beispiel für einen
Prozess dienen, den wir mit einer Klasse abbilden werden.
Ein Massenpunkt der Masse m bewegt sich auf gerader Strecke und beschleunigt mit a
gleichförmig. Zum Zeitpunkt t0 = 0 befindet er sich am Ort x0 und hat eine Geschwin-
digkeit v0 . Mit einem Computerprogramm sollen Bewegungsdaten für die Bewegung des
Massenpunktes berechnet werden.
Theorie Zur Erstellung des Programms werden die Formeln für die zu berechnenden
Größen benötigt. Dazu müssen folgende Fragen beantworten werden:
Welche Geschwindigkeit hat der Massenpunkt und an welchem Ort befindet er sich
zu einem späteren Zeitpunkt t1 ?
Um welche Strecke hat sich der Massenpunkt in der Zeit t1 weiterbewegt? Wie groß
war seine Durchschnittsgeschwindigkeit?
80 2 Klassen in C++
Wir legen den Einheitsvektor ⃗ex entlang der Bewegungsrichtung. Der Ort und die Ge-
schwindigkeit als Funktionen der Zeit lauten:
x(t) = x0 + v0 t + 1 at2 (2.3a)
2
v(t) = v0 + at. (2.3b)
s
v̄ = , (2.4)
t
wobei s, die von 0 bis t zurückgelegte Strecke ist.
Quelltext Die Masse und die Beschleunigung sind laut Angaben der Aufgabe für eine
gegebene Situation feste Größen und werden zusammen mit den Anfangsbedingungen,
die ebenfalls fest sind, im privaten Bereich der Klasse gespeichert. Bei den Anfangsbe-
dingungen handelt es sich um zusammenhängende Größen, die wir als Elemente eines
Feldes speichern.
/**
* @brief The Motion class
* Geradlinige Bewegung mit konstanter Beschleunigung
*/
class Motion
{
private :
// Masse , Beschleunigung
double _mass , _acceleration ;
// Anfangsbedingungen
std :: array <double , 2> _ivalues ;
public :
Da nur ein lesender Zugriff auf die privaten Variablen erlaubt sein soll, können diese nur
über den Konstruktor initialisiert werden. Durch die Festlegung des dritten Eingabear-
guments als Feld mit zwei Elementen stellen wir sicher, dass die korrekte Anzahl von
Anfangsbedingungen dem Konstruktor übergeben wird.
2.2 Einfache Klassen 81
/**
* @brief Motion Konstruktor
* @param m Masse
* @param a Beschleunigung
* @param init Anfangsbedingungen
*/
Motion ( double m, double a, std :: array <double , 2> init)
: _mass { m }
, _acceleration { a } {
_ivalues = init;
}
Es folgen die Implementierungen von Gl. 2.3a und Gl. 2.3b für Ort und Geschwindigkeit
als Funktion der Zeit.
/**
* @brief x Ortsfunktion
* @param t Zeit
* @return Ort zum Zeitpunkt t
*/
double x( double t) const {
return _ivalues [0] + _ivalues [1] * t //
+ 0.5 * + _acceleration * std :: pow(t, 2);
}
/**
* @brief v Geschwindigkeit
* @param t Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
double v( double t) const { return _ivalues [1] + _acceleration * t; }
Die zurückgelegte Strecke als Funktion der Zeit wird von dieser Funktion berechnet.
/**
* @brief distance Berechnung der zurückgelegten Strecke
* @param t Zeit
* @return Strecke
*/
double distance ( double t) const { return x(t) - _ivalues [0]; }
Die zurückgelegte Distanz benutzen wir in der nächsten Funktion, um die mittlere Ge-
schwindigkeit zu berechnen.
82 2 Klassen in C++
/**
* @brief vmean Berechnung der mittleren Geschwindigkeit
* @param t Zeit
* @return mittlere Geschwindigkeit
*/
double vmean ( double t) const { return distance (t) / t; }
Es folgen die Methoden zum Lesen von Masse, Beschleunigung und Anfangsbedingungen.
/**
* @brief mass lese ...
* @return Masse
*/
double mass () const { return _mass ; }
/**
* @brief acceleration lese ...
* @return Beschleunigung
*/
double acceleration () const { return _acceleration ; }
/**
* @brief init_values
* @return Referenz auf Feld mit Anfangsbedingungen
*/
const auto & init_values () const { return _ivalues ; }
}; // Motion
Wir nehmen die soeben bearbeitete Physikaufgabe als Ausgangsszenario für ein Pro-
gramm, welches Benutzereingaben über die Tatstatur einliest, Ergebnisse berechnet und
diese auf dem Bildschirm ausgibt. Wir haben bereits ähnliches im ersten Kapitel pro-
grammiert.
2.2 Einfache Klassen 83
Ein Massenpunkt der Masse m bewegt sich auf gerader Strecke und beschleunigt mit a
gleichförmig. Zum Zeitpunkt t0 = 0 befindet er sich am Ort x0 und hat eine Geschwin-
digkeit v0 . Es soll ein interaktives Computerprogramm erstellt werden, das a, x0 , v0 und
t1 von der Tastatur einließt und x(t1 ), v(t1 ), die Strecke s(t1 ) und die Durchschnittsge-
schwindigkeit v̄(t1 ) ausgibt.
Theorie Für diese Art von Bewegung wurde im vorherigen Beispiel ein Modell in Form
einer Klasse erstellt. Dieses werden wir zur Realisierung des Programms einsetzen.
Kurzbeschreibung Wir benötigen eine Benutzerschnittstelle, über die der Benutzer mit
der Modellklasse kommuniziert. Sie muss folgende Eigenschaften haben:
Zusätzlich soll das Programm ähnlich wie im Abschnitt 1.5.1 nicht nach einer Berechnung
von Bewegungsdaten beendet werden, sondern neue Daten berechnen, bis der Benutzer
es mit einem Befehl beendet.
Quelltext Wir beginnen mit der Programmierung der Benutzerschnittstelle und stellen
als erstes fest, dass es sich sowohl bei den Eingabe- als auch bei den Ausgabewerten
um reelle Zahlen handelt. Dies bedeutet, dass wir zu ihrer Speicherung jeweils ein Feld
anlegen können. Wir wissen auch, dass bei einer Interaktion mit einem Benutzer alle
Werte einen Namen haben müssen, damit die physikalischen Größen bei jeder Ein- und
Ausgabe identifiziert werden können. Wir speichern deswegen in jedem Feld statt einer
reinen Zahl ein Paar, bestehend aus Namen und Wert der physikalischen Größe.
/**
* @brief The UserInput class ( Benutzerschnittstelle )
* Geradlinige Bewegung mit konstanter Beschleunigung
*/
class UserInput
{
using Value = std :: pair <std :: string , double >;
private :
// Eingabe (Name ,Wert) ,...
std :: vector <Value > _in;
// Ausgabe (Name ,Wert) ,...
std :: vector <Value > _out;
protected :
Mit einer Hilfsfunktion wird jede Eingabe des Benutzers gelesen. Sie automatisiert den
Eingabeprozess, indem mit einem Funktionsaufruf der Name einer physikalischen Größe
auf dem Bildschirm geschrieben und dann ihr Wert gelesen und auf Gültigkeit überprüft
wird.
/**
* @brief UserInput :: read
* @param label Eingebeaufforderung
* @return Benutzereingabe als Zahl
*/
double read(std :: string & label ) {
double val;
std :: cout << label << "=?\t";
std :: cin >> val;
// Fehler wenn val nicht vom Typ double ist
if (std :: cin.fail ()) {
throw std :: invalid_argument (label );
}
return val;
}
public :
Es folgt der Konstruktor, der die Namen aller physikalischen Größen speichert und gleich-
zeitig ihren Wert auf 0 setzt.
/**
* @brief UserInput :: UserInput Standardkonstruktor
*/
inline UserInput () {
// Namen für Eingabevariablen werden initialisiert
for (const auto label : { "x0", "v0", "a", "t1" }) {
_in. push_back ( Value { label , 0 });
}
// Namen für Ausgabevariablen werden initialisiert
for (const auto & label : { "x", "v", "s", "vbar" }) {
_out. push_back ( Value { label , 0 });
}
}
Die nächste Elementfunktion ist für die Berechnung der Ergebnisse zuständig. Sie tut
dies mithilfe einer Instanz der Prozessklasse.
/**
* @brief calculate Ergebnisse werden berechnet
*/
inline void calculate () {
// Komponente zur Berechnung der Ergebnisse
x022 :: Motion motion { 1, _in[Idx ::a]. second , {
_in[Idx ::x0 ]. second , _in[Idx :: v0 ]. second } };
// Wertezuweisung
2.2 Einfache Klassen 85
Die Aufforderung zur Eingabe der Werte geschieht über eine weitere Elementfunktion
(Listing 2.57). Wir sehen, wie dies über eine Schleife abgewickelt wird.
/**
* @brief UserInput :: user_input Eingabeaufforderung
*/
inline void input () {
for (auto &vals : _in) {
vals. second = read(vals. first );
}
// letzte Eingabe : Zeit
if (_in.back (). second <= 0) {
throw std :: domain_error ("time must be >0");
}
}
/**
* @brief output Ausgabe der Ergebnisse
*/
inline void output () {
std :: cout << std :: fixed << std :: setprecision (2);
std :: cout << " ------------output -------" << std :: endl;
// iteriere über Ausgabewerte (Name ,Wert) ,...
for (const auto &[ label , val] : _out) {
std :: cout << label //
<< "=" << val << std :: setw (3) << "\t";
}
std :: cout << std :: endl;
}
/**
* @brief ui Benutzerschnittstelle (ein Rechengang )
*/
inline void ui () {
constexpr auto maxstreamsize =
std :: numeric_limits <std :: streamsize >:: max ();
try {
86 2 Klassen in C++
Innerhalb einer Schleife wird ein kompletter Rechengang, solange wiederholt, bis der
Benutzer diesen per Befehl beendet. Dies geschieht durch die Eingabe von y oder n.
/**
* @brief run Eingabe ,Berechnung , Ausgabe
*/
inline void run () {
char cmd = 'y'; // die Schleife soll wiederholt werden
std :: cout << " ------------input -------------" << std :: endl;
while (cmd == 'y') {
ui(); // starte Eingabe ,Berechnung , Ausgabe
do {
std :: cout << "new calculation ? y[yes] / n[no]" <<
std :: endl;
std :: cin >> cmd;
} while (cmd != 'y' && cmd != 'n');
}
}
Ein Beispielprogramm zu schreiben ist einfach, denn die ganze Arbeit wird von den
Elementfunktionen der Klasse übernommen.
/**
* @brief ex1 Beispielprogramm Geradlinige Bewegung mit konstanter
* Beschleunigung
*/
inline void ex1 () {
UserInput ui;
ui.run ();
}
Wie das Programm arbeitet, zeigt folgende Ausgabe auf dem Bildschirm:
------------input -------------
x0=? 10
v0=? 2
a=? 2
t1=? 10
------------output -------
x =130.00 v =22.00 s =120.00 vbar =12.00
new claculation ? y[yes] / n[no]
y
x0=? 2
v0=? 3
a=? 1
t1=? 1
------------output -------
x =5.50 v=4.00 s =3.50 vbar =3.50
new claculation ? y[yes] / n[no]
n
Listing 2.62
2.3 Klassentemplates
Klassen sind Vorlagen zur Erstellung von Objekten. Klassentemplates sind Vorlagen zur
Erstellung von Klassen. Beim Entwurf einer Klasse müssen die Datentypen aller Attribute
bekannt sein. Bei Klassentemplates wird einem oder mehreren Attributen kein Datentyp
zugewiesen. Stattdessen wird ein Platzhalter eingesetzt. Wird dieser festgelegt, entsteht
eine neue Klasse. Wir wollen dieses Konzept anhand eines Beispiels demonstrieren.
Die Umsetzung unseres Vorhabens setzt als erstes die Identifizierung der Eigenschaften
eines Polynoms voraus. Dies geschieht am besten über die Untersuchung der mathema-
tischen Formel. Bei Polynomen handelt es sich um Ausdrücke der Form
Quelltext Ein erster Gedanke wäre, eine Klasse für die Variante mit komplexen und
eine für die mit reellen Koeffizienten zu entwerfen. Ein Blick auf die Formel zeigt aber,
88 2 Klassen in C++
dass dies überflüssig ist. Verwenden wir statt Komplex (complex) und Reell (double) einen
Platzhalter für den Datentyp der Koeffizienten, hätten wir mit einem Klassentemplate
beide Varianten. Wir bleiben aber nicht nur bei diesem Template-Parameter, sondern
führen noch einen weiteren ein, der den Grad des Polynoms festlegt. Dieser zweite Pa-
rameter erlaubt es bereits während der Kompilierung, ein Feld passender Größe für die
Speicherung der Koeffizienten anzulegen. Dabei wird das Element mit dem Index 0 des
Feldes dem Koeffizienten c0 zugeordnet usw.
/**
* @brief The Polynom class Darstellung von Polynomen
* @param T Datentyp der Koeffizienten
* @param N Grad des Polynoms . N+1 : Anzahl der Koeffizienten
*/
template <class T, size_t N>
class Polynom
{
protected :
// speichere Koeffizienten , Anzahl Grad des Polynoms +1
std :: array <T, N + 1> _c;
public :
Der Konstruktor erwartet die Liste der Koeffizienten des Polynoms, beginnend mit dem
Element c0 . Durch die Verwendung eines std::array wird sichergestellt, dass die richtige
Anzahl der Koeffizienten übergeben wird.
/**
* @brief Polynom Konstruktor
* @param carray Feld mit Koeffizienten
*/
Polynom (std :: array <T, N + 1> clst) {
std :: copy(std :: begin (clst), std :: end(clst), std :: begin(_c));
}
Ein Zugriff auf die Koeffizienten des Polynoms erfolgt mittels der Elementfunktion (Lis-
ting 2.65). Eine Überprüfung auf Gültigkeit des Index wird ebenfalls durchgeführt.
/**
* @brief c lese Koeffizienten
* @param idx Index Position im Feld
* @return Koeffizient
*/
T c( size_t idx) const {
// Zugriff mit Index - Überprüfung
return _c.at(idx);
}
Alle Elementfunktionen zur Auswertung der Polynome sollen mithilfe von gsl -Funktionen
implementiert werden. Je nach Datentyp der Koeffizienten und des Eingabearguments
müssen folgende Fälle berücksichtigt werden:
Es ist möglich, alle Fälle mittels einer Elementfunktion vom Compiler generieren zu las-
sen. Es ist aber übersichtlicher, wenn die Implementierung in zwei Elementfunktionen
aufgeteilt wird. Wir beginnen mit der Auswertung des Polynoms, wenn das Eingabear-
gument komplex ist. Es werden zwei Versionen je nach Datentyp der Koeffizienten vom
Compiler generiert.
/**
* @brief eval Auswertung des Polynoms
* @param z komplexe Variable
* @return Wert des Polynoms an der Stelle z
*/
auto eval( gsl_complex z) const {
// komplexe Koeffizienten , komplexe Variable
if constexpr (std :: is_same_v <T, gsl_complex >) {
return gsl_complex_poly_complex_eval ( //
_c.data () ,
N + 1,
z);
} else {
// reelle Koeffizienten , komplexe Variable
return gsl_poly_complex_eval (_c.data (), N, z);
}
}
Wenn das Eingabeargument des Polynoms reell ist, generiert der Compiler ebenfalls zwei
Versionen, denn wenn die Koeffizienten des Polynoms komplex sind, muss das Eingabe-
argument in eine komplexe Zahl umgewandelt werden.
/**
* @brief eval Auswertung des Polynoms
* @param x reelle Variable
* @return Wert des Polynoms an der Stelle x
*/
auto eval( double x) const {
90 2 Klassen in C++
Polynome können auch mit der überladenen Version des Klammeroperators ausgewertet
werden. Als Template implementiert, wird intern immer die richtige Variante von eval
(Listing 2.66 und Listing 2.67) aufgerufen.
/**
* @brief operator () Auswertung des Polynoms
* @param x Variable ( komplex oder reell)
* @return Wert des Polynoms an der Stelle x
*/
template <class R>
auto operator ()(R x) const {
return eval(x);
}
Wir kümmern uns als Nächstes um die Ausgabe von Polynomen auf dem Bildschirm oder
in Dateien. Hierzu überladen wir den Operator <<, der die Daten eines Polynoms an Ob-
jekte der ostream-Klasse weiterleitet. Die Koeffizienten werden innerhalb von Klammern
und durch Komma getrennt ausgegeben.
/**
* @brief operator << Ausgabe der Koeffizienten des Polynoms
* {c0 ,c1 ,c2 ,... , cn}
* @param os Ausgabestrom
* @param p Polynom
* @return Referenz auf Ausgabestrom
*/
friend inline std :: ostream &operator <<( std :: ostream &os , const
Polynom &p) {
os << "{" << p.c(0);
for ( size_t idx = 1; idx < p._c.size (); idx ++) {
os << "," << p.c(idx);
}
return os << "}";
}
Ein Problem muss noch gelöst werden. Wenn die Koeffizienten vom Typ gsl_complex sind,
meldet der Compiler bei der Verwendung des <<-Operators einen Fehler. Wir schließen
diese Lücke und überladen den <<-Operator für die Ausgabe von Elementen vom Typ
gsl_complex.
/**
* @brief operator <<
* @param os Ausgabestrom
* @param c gsl - Darstellung von komplexen Zahlen
* @return Referenz auf Ausgabestrom
*/
inline std :: ostream &operator <<( std :: ostream &os , const gsl_complex &c)
{
return os << "(" << GSL_REAL (c) << "," << GSL_IMAG (c) << ")";
}
Wir führen eine letzte Elementfunktion ein, mit welcher der Grad des Polynoms abgefragt
werden kann.
/**
* @brief degree
* @return Grad des Polynoms
*/
inline constexpr size_t degree () const { return N; }
}; // Polynom
Die Klasse ist hiermit vollständig implementiert und kann mit folgendem Programm
getestet werden.
Beispiel Das Polynom p1 (x) = 3x2 +2x+1 wird auf dem Bildschirm ausgegeben und für
x = −0.5 ausgewertet. Dasselbe gilt auch für p2 (x) = 4x3 + 3x2 − 2x + 1. Das komplexe
Polynom p3 (x) = (2 + 2i)x + 1 + i wird ebenfalls auf dem Bildschirm ausgegeben und
für x = 1 und x = 1 + 2i ausgewertet.
/**
* @brief cppx4a Beispiel Auswertung von Polynomen
*/
inline void ex1 () {
// Polynom 2 Grades mit reellen Koeffizienten
Polynom <double , 2> p1{ { 1., 2., 3. } };
std :: cout << "p1=" << p1 << std :: endl;
std :: cout << "p1 ( -0.5)=" << p1 ( -0.5) << std :: endl;
std :: cout << " ----------------" << std :: endl;
// Polynom 3 Grades mit reellen Koeffizienten
Polynom <double , 3> p2{ { 1., -2., 3., 4. } };
std :: cout << "p2=" << p2 << std :: endl;
std :: cout << "p2 ( -0.5)=" << p2 ( -0.5) << std :: endl;
std :: cout << " ----------------" << std :: endl;
// Polynom 1 Grades mit komplexen Koeffizienten
92 2 Klassen in C++
// Ausgabe
std :: cout << "p3=" << p3 << std :: endl;
std :: cout << "p3 (1)=" << p3 (1) << std :: endl;
std :: cout << "p3 ({1 ,2})=" << p3( gsl_complex { 1., 2. }) << std :: endl;
}
p1 ={1 ,2 ,3}
p1 ( -0.5) =0.75
----------------
p2 ={1 , -2 ,3 ,4}
p2 ( -0.5) =2.25
----------------
p3 ={(1 ,1) ,(2,2)}
p3 (1) =(3 ,3)
p3 ({1 ,2}) =( -1 ,7)
Listing 2.73
Wir stellen uns eine Situation vor, in der ein Programm intensiv Gebrauch von be-
stimmten Funktionswerten machen muss. Dies könnte z.B. bei Benutzung von n! zur
Berechnung von Taylorpolynomen oder der Werte von sin und cos für eine Grafikausga-
be sein. In diesem Fall könnte, statt die Funktionswerte immer wieder neu zu berechnen,
eine Tabelle erstellt werden, die häufig benutzte Werte zwischenspeichert.
Quelltext Wir wählen zur Speicherung der (x, y)-Werte ein std::map. Das Klassentem-
plate std::function<Y(X)> steht stellvertretend für jede Funktion bzw. für jedes Funkti-
onsobjekt, dass ein Eingabeargument vom Typ X erwartet und einen Wert vom Typ Y
zurückgibt. Der dritte Template-Parameter wird standardmäßig auf false gesetzt. Seine
Bedeutung wird weiter unten erklärt.
2.3 Klassentemplates 93
/**
* @brief The Cache class Erzeugen von Zwischenspeicher für
* Funktionswerte
*/
template <class X, class Y, bool DEBUG = false >
class Cache
{
private :
// Speicher für (x,f(x))
std ::map <X, Y> _table ;
// Zeiger auf Funktionsobjekt f(x)
std :: function <Y(X)> _fn;
public :
Der Konstruktor erwartet als Eingabe eine Funktion mit einem Eingabeparameter vom
Typ X und eine Ausgabe vom Typ Y.
/**
* @brief Cache Konstruktor
* @param fn Zeiger auf Funktion
*/
Cache(Y (*fn)(X))
: _fn{ fn } {}
Für ein gegebenes x wird ein Wert y zurückgegeben. Ist x in der internen Tabelle re-
gistriert, wird aus dieser y ermittelt. Andernfalls wird y berechnet, (x, y) in die Tabelle
geschrieben und gleichzeitig y zurückgegeben. Der Template-Parameter DEBUG dient zur
Ausgabe einer Meldung, wenn der ermittelte Wert aus der Tabelle stammt.
/**
* @brief get berechne Funktionenwert
* @param x Variable
* @return Funktionswert
*/
Y get(X x) {
const auto fitr = _table .find(x);
if (fitr != _table .end ()) {
// Wert wurde im Speicher gefunden
if constexpr ( DEBUG ) {
std :: cout << "from cache :";
}
return fitr -> second ;
}
Y val = _fn(x); // Wert wird berechnet
_table [x] = val; // ... und gespeichert
return val;
}
Die Anzahl der gespeicherten Werte erhalten wir mit folgender Funktion
/**
* @brief size
* @return Anzahl der gespeicherten Werte
*/
auto size () const { return _table .size (); }
}; // Cache
Beispielanwendungen
Zur Berechnung von n! programmieren wir eine Funktion, die eine Kombination aus
√ ( )n
rekursiver Berechnung der Fakultät und der Stirlingformel n! ≈ 2πn ne ist. Letztere
berechnet für große n einen Näherungswert für n!.
/**
* @brief fac Berechnung von n! exakt oder mit einer Näherungsformel
* @param n ganze Zahl
* @return n!
*/
double fac( size_t n) {
if (n < 1000) {
return n == 0 ? 1 : n * fac(n - 1);
}
// Stirling - Formel
return std :: sqrt (2 * Math :: PI * n) * std :: pow(n / Math ::E, n);
}
Folgendes Beispiel zeigt die Klasse im Einsatz. Es wird eine Instanz ohne und eine mit
Ausgabe einer Meldung erzeugt.
/**
* @brief cppx6 Beispiel
*/
inline void ex1 () {
Cache <size_t , double > c1{ fac }; // ohne Meldung
for (auto n : { 2, 9, 50, 100 , 2, 9 }) {
std :: cout << n << "! =" << c1.get( static_cast <size_t >(n)) <<
std :: endl;
}
std :: cout << " ---------------------------" << std :: endl;
Cache <size_t , double , true > c2{ fac }; // mit Meldung
for (auto n : { 2, 9, 50, 100 , 2, 9 }) {
std :: cout << n << "! =" << c2.get( static_cast <size_t >(n)) <<
std :: endl;
}
}
2! =2
9! =362880
50! =3.03634 e+64
100! =9.32485 e+157
2! =2
9! =362880
---------------------------
2! =2
9! =362880
50! =3.03634 e+64
100! =9.32485 e+157
2! =from cache :2
9! =from cache :362880
Listing 2.80
∑
n=0 n! ≈ 2.71828 berechnen. Zur
In einem weiteren Beispiel werden wir die Summe 10000 1
Berechnung von n! setzen wir, die in diesem Abschnitt programmierte Klasse ein. Die
Summe soll zwei Mal berechnet und in beiden Fällen die Zeit gemessen werden. Wir
erwarten, dass beim ersten Mal die gemessene Zeit größer sein wird als beim zweiten, da
im ersten Durchlauf alle Werte berechnet werden müssen. Im zweiten Durchlauf werden
sie vom Speicher abgerufen. Für die Zeitmessung setzen wir, die in der Header-Datei
<chrono> definierte Uhr steady_clock ein, welche die Uhrzeit unabhängig von der Sys-
temuhr liefert. Mit steady_clock::now wird der Zeitpunkt wiedergegeben, zu dem diese
Funktion aufgerufen wurde. Mit duration<double, std::milli> berechnen wir die Diffe-
renz zwischen zwei Zeitpunkten in Millisekunden.
/**
* @brief ex2 Berechnung eines Näherungswerts für e mit 1+1/2!+1/3!+...
*/
inline void ex2 () {
// vermeide lange Bezeichner
using namespace std :: chrono ;
Wir sehen wie sich die Rechenzeit, durch die Speicherung der Werte für n! deutlich
reduziert. Die gemessenen Zeiten hängen von der Rechnerkonfiguration ab.
dt1 =43.1981 ms
value =2.71828
---------------------------
dt2 =8.47219 ms
value =2.71828
---------------------------
dt2/dt1 =0.196124
cache size =100000
Listing 2.82
Wir werden in diesem Abschnitt die binären Operatoren +,-,*,/ für rationale Zahlen
implementieren. Es ist abzusehen, dass die Programmierung dieser Operatoren immer
demselben Schema folgen wird, weswegen wir folgende Elementfunktion einführen:
/**
* @brief helpfn Hilfsfunktion zur Implementierung von +,-,*,/
* @param arg1 reelle , ganze oder rationale Zahl
* @param arg2 reelle , ganze oder rationale Zahl
* @param fn lambda Funktion für +,-,*,/
* z.B. []( double &x, double y){ return x+=y; }
*/
template <class X, class Y, class FN >
inline friend auto helpfn ( const X &arg1 , const Y &arg2 , FN fn) {
// wenn eine der beiden Zahlen eine Fließkommazahl ist
if constexpr (std :: is_floating_point_v <X> || //
std :: is_floating_point_v <Y>) {
// ... ist das Ergebnis auch eine Fließkommazahl
double rout = static_cast <double >( arg1);
fn(rout , static_cast <double >( arg2));
return rout;
} else {
2.3 Klassentemplates 97
/**
* Implementierung von +,-,*,/ Funktionen werden nur dann
* vom Compiler erzeugt , wenn Z oder Y eine rationale Zahl ist.
*/
template <class X, class Y, is_candidate_t <X, Y> * = nullptr >
inline friend auto operator +( const X &x, const Y &y) {
return helpfn (x, y, []( auto &a, const auto &b) { a += b; });
}
Die Hilfsfunktion (Listing 2.83) ruft zur Implementierung der Operatoren eine λ-
Funktion auf. Für die Addition wird die Operation += für rationale Zahlen (Listing 2.28)
ausgeführt. Entsprechendes gilt für die restlichen Operatoren.
Ohne jegliche Restriktion würde der Compiler diese Operatoren auf jede Addition, Mul-
tiplikation usw. versuchen anzuwenden, d.h. auch dort, wo keine rationalen Zahlen vor-
kommen. Dies verhindern wir durch den dritten Templateparameter.
/**
* zur Implementierung von +,-,*,/ sind nur rationale Zahlen
* zugelassen . X oder Y muss eine rationale Zahl sein
*/
template <class X, class Y>
using is_candidate_t = //
std :: enable_if_t <std :: is_same_v <X, Rational > ||
std :: is_same_v <Y, Rational >>;
Der Compiler versucht, einen Zeiger vom Typ void als dritten Template-Parameter zu
deklarieren (Listing 2.84 bis Listing 2.87). Dies gelingt aber nur dann, wenn einer der
beiden Datentypen X oder Y eine rationale Zahl ist. Wir fahren mit der Programmierung
der Vergleichsoperatoren fort, die nur dann angewandt werden, wenn eines der beiden
Eingabeargumente eine rationale Zahl ist.
/**
* @brief operator == Vergleichsoperator
* @param r1 rationale Zahl
* @param r2 rationale Zahl
* @return true wenn die rationalen Zahlen gleich sind
*/
template < typename X, typename Y, is_candidate_t <X, Y> * = nullptr >
inline friend bool operator ==( const X &x, const Y &y) { //
if constexpr (std :: is_same_v <X, Y>) {
// beide Eingabeargumente sind rationale Zahlen
return x.num () * y.den () == x.den () * y.num ();
} else if constexpr (std :: is_same_v <Y, Rational >) {
return x * y.den () == y.num ();
} else {
return x.num () == x.den () * y;
}
}
/**
* @brief operator != Vergleichsoperator
* @param r1 rationale Zahl
* @param r2 rationale Zahl
2.3 Klassentemplates 99
Die Potenz einer rationalen Zahl gibt dann eine rationale Zahl zurück, wenn der Exponent
eine ganze Zahl ist; sonst ist das Ergebnis eine Fließkommazahl.
/**
* @brief pow potenziere rationale Zahl
* @param r rationale Zahl
* @param n Potenz
* @return rationale Zahl wenn x ganzzahlig ist
*/
template <class T>
inline friend auto pow( const Rational &r, T n) { //
if constexpr (std :: is_same_v <T, int >) {
Rational r1 = n > 0 ? r : inverse (r);
long long num = //
static_cast <long long >( std :: pow(r1.num (), n));
long long den = //
static_cast <long long >( std :: pow(r1.den (), n));
return Rational { num , den };
} else {
return std :: pow( static_cast <double >(r),
static_cast <double >(n));
}
}
/**
* @brief invert inverse rationale Zahl
* @param r rationale Zahl
* @return 1/r
*/
inline friend Rational inverse ( const Rational &r) { //
return Rational (r.den () , r.num ());
}
/**
* @brief negate eine rationale Zahl wird negiert
* @param r rationale Zahl
* @return -r
*/
inline friend Rational operator -( const Rational &r) { //
100 2 Klassen in C++
/**
* @brief operator << Ausgabe einer rationalen Zahl
* @param os Ausgabestrom
* @param r rationale Zahl
* @return Referenz auf Ausgabestrom
*/
inline friend std :: ostream &operator <<( std :: ostream &os , const
Rational &r) {
return os << r.num () << "/" << r.den ();
}
}; // Rational
2.3.4 Beispiele
Es folgt eine Reihe von einfachen Beispielen. Die dazugehörigen Outputs werden nicht
angezeigt. Im ersten Beispiel wird eine rationale Zahl initialisiert, danach vereinfacht und
dann in eine Fließkommazahl umgewandelt.
/**
* @brief ex2 Initialisierung , Vereinfachung , Umwandlung in eine reelle
* Zahl
*/
inline void ex2 () {
Rational r3{ 5, 100 };
std :: cout << r3 << std :: endl;
r3. simplify ();
std :: cout << " simplified :" << r3 << std :: endl;
std :: cout << "as double :" << static_cast <double >(r3) << std :: endl;
}
/**
* @brief ex3 Anwendung der Vergleichsoperatoren
*/
inline void ex3 () {
Rational r4{ 2, 4 };
Eine Zahl wird potenziert. Ist die Potenz eine Fließkommazahl, wird das Ergebnis in eine
Fließkommazahl umgewandelt.
/**
* @brief ex4 Potenzierung mit ganzer und mit einer reellen Zahl
*/
inline void ex4 () {
Rational r4{ 2, 4 };
std :: cout << pow(r4 , 3) << std :: endl;
std :: cout << pow(r4 , 3.1) << std :: endl;
}
/**
* @brief ex5 Summe aus rationalen Zahlen
*/
inline void ex5 () {
Rational sum{ 0, 1 };
Die Summe einer rationalen Zahl und einer Fließkommazahl ergibt eine Fließkommazahl.
Durch die Implementierung des Plus-Operators als Template wird die Addition einer
Fließkommazahl mit einer rationalen Zahl und umgekehrt berücksichtigt.
/**
* @brief ex6 Summe rationale Zahl mit reeller Zahl Summe ganze Zahl
* mit rationaler Zahl
*/
inline void ex6 () {
Rational r1{ 5, 100 };
auto c = r1 + 0.8;
auto c1 = 1.1 + Rational { 2, 2 };
102 2 Klassen in C++
Zwei rationale Zahlen werden initialisiert. Die eingeführten binären Operatoren +,-,*,/
sowie += werden zum Rechnen mit diesen Zahlen eingesetzt.
/**
* @brief ex1 algebraische Operationen mit rationalen Zahlen
*/
inline void ex1 () {
Rational r1{ 1, 2 }, r2{ 1, 3 };
auto rout = r1 + r2;
std :: cout << r1 << "+" << r2 << "=" << rout << std :: endl;
rout = r1 - r2;
std :: cout << r1 << "-" << r2 << "=" << rout << std :: endl;
rout = r1 * r2;
std :: cout << r1 << "*" << r2 << "=" << rout << std :: endl;
rout = r1 / r2;
std :: cout << r1 << "/" << r2 << "=" << rout << std :: endl;
Rational r5 = r1;
r5 += 1;
std :: cout << r5 << std :: endl;
}
2.4 Vererbung
Das Konzept der Vererbung ist ein wichtiges Hilfsmittel zur Wiederverwendung von be-
reits geschriebenem Quelltext. Eine neue Klasse kann von einer Basisklasse abgeleitet
werden und damit einen Teil oder alle Eigenschaften der Basisklasse erben. Dies bedeu-
tet, dass sowohl Daten als auch Elementfunktionen einer Basisklasse der abgeleiteten
Klasse so zur Verfügung gestellt werden können, als ob diese direkt für die abgeleitete
Klasse programmiert wurden. Durch die Schlüsselwörter public, protected und private
wird geregelt, welche Eigenschaften der Basisklasse die abgeleitete Klasse erbt. Sie ist
damit ein Spezialfall der Basisklasse. Wir wollen anhand eines Beispiels demonstrieren,
wie die Vererbung dazu beiträgt, das Schreiben von Quelltext zu reduzieren.
Es sollen mit einem Programm mithilfe von Potenzreihen Näherungswerte für die Funk-
tionen ex , sin x und cos x berechnet werden.
2.4 Vererbung 103
Theorie Wir schreiben die Taylorpolynome für die folgenden drei Funktionen an der
Entwicklungsstelle x0 = 0:
x x2 x3 xn
ex = 1 + + + + ... + (2.6)
1! 2! 3! n!
x3 x5 x2n+1
sin x = x − + − . . . + (−1)n (2.7)
3! 5! (2n + 1)!
x2 x4 x6 x2n
cos x = 1 − + − + . . . + (−1)n (2.8)
2! 4! 6! (2n)!
xn 2n+1
Entwurf Eine schnelle Lösung wäre, die allgemeinen Terme n! , (−1)n (2n+1)!
x
und
2n
(−1)n (2n)!
x
zu implementieren und dann mithilfe von Schleifen die Summen zu berechnen.
Wir suchen jedoch nach Möglichkeiten, sich wiederholende Arbeitsschritte nur einmal zu
programmieren und diese immer wieder zu verwenden. Eine Möglichkeit wäre, dies durch
Kombination von normalen Funktionen umzusetzen. Mit der Vererbung steht uns jedoch
ein mächtigeres Werkzeug zur Verfügung, welches wir hier einsetzen werden. Wir suchen
durch die Betrachtung der Formeln nach gemeinsamen Elementen, die in die Basisklasse
geschrieben werden können. Ein Kandidat ist die Routine zur Berechnung der Summen.
Dort soll über Terme summiert werden, die erst in den abgeleiteten Klassen spezifiziert
werden. Die Berechnung von Fakultäten soll ebenfalls innerhalb der Basisklasse imple-
mentiert werden.
Quelltext Wir beginnen mit der Basisklasse. Neben dem Konstruktor finden wir im
protected-Bereich eine Variable zur Speicherung einer λ-Funktion. Diese soll in den ab-
geleiteten Klassen initialisiert werden und den allgemeinen Term zur Berechnung der
Summe implementieren. Die Klasse enthält eine Elementfunktion zur Berechnung und
Speicherung von n! und schließlich eine Methode, die in Abhängigkeit von n ein Vor-
zeichen zurückgibt. Als einziger Operator im public-Bereich steht der überladene Klam-
meroperator.
/**
* @brief The PowerSeries class Basisklasse Berechnung
* von Taylorpolynomen von Funktionen
*/
class PowerSeries
{
protected :
// Anzahl der Terme des Polynoms
size_t _n;
// Zwischenspeicher n!
inline static std :: vector <double > cache ;
Der Konstruktor legt die Anzahl der Terme für die Potenzreihe fest (Listing 2.102).
104 2 Klassen in C++
/**
* @brief PowerSeries Konstruktor
* @param n Anzahl der Terme
*/
inline PowerSeries ( size_t n)
: _n{ n } {}
Die Summe wird mit Termen berechnet, die durch das intern gespeicherte Funktionsob-
jekt festgelegt werden.
/**
* @brief sum Summe aller Terme
* @param x Variable
* @return Näherungswert der Funktion
*/
inline double sum( double x) const {
double val = 0;
for ( size_t idx = 0; idx <= _n + 1; idx ++) {
// wende das intern gespeicherte Funktionsobjekt an
val += _fn(idx , x);
}
return val;
}
Werte zur Berechnung von n! werden beim ersten Aufruf gespeichert. Ab einem bestimm-
ten n wird die Stirling-Formel eingesetzt.
/**
* @brief factorial Fakultät
* @param n (ganze Zahl )
* @return Werte aus Speicher oder aus Stirling - Formel
*/
static inline double factorial ( size_t n) {
// maximale Anzahl der gespeicherten Werte für n!
const size_t cachesize = 11;
if (cache . empty ()) {
//n! wird berechnet und gespeichert
double val = 1;
cache . push_back (val);
for ( size_t idx = 1; idx < cachesize ; idx ++) {
val *= idx;
cache . push_back (val);
}
}
if (n < cache .size ()) {
return cache [n]; // lese gespeicherten Wert
}
// Stirling - Formel
return sqrt (2 * Math :: PI * n) * std :: pow(n / Math ::E, n);
}
/**
* @brief sign
* @param n Exponent
* @return Wert ( -1)^n
*/
static inline int sign( size_t n) { return n % 2 == 0 ? 1 : -1; }
public :
Der Klammeroperator wird überladen (Listing 2.106), um das Programmieren mit dieser
Klasse zu vereinfachen.
/**
* @brief operator ()
* @param x Variable
* @return Näherungswert der Funktion
*/
inline double operator ()( double x) const { return sum(x); }
}; // PowerSeries
Wir leiten von der Basisklasse eine neue Klasse zur Berechnung des Taylorpolynoms für
ex ab. Diese erbt alle Eigenschaften der Basisklasse, z.B. die Berechnung der Summen
und auch alle internen Variablen. Die neue Klasse ist mit wenigen Zeilen Quelltext pro-
grammiert. Hierdurch wird es weniger wahrscheinlich fehlerhafte Software zu schreiben.
/**
* @brief The Exp class Taylorpolynom für e^x
*/
class Exp : public PowerSeries
{
private :
// speichere den zuletzt berechneten Term: x^n
double _powterm ;
public :
Der Konstruktor legt die Anzahl der Summenterme fest und spezifiziert die Funktion zur
Berechnung dieser Terme. Dies geschieht mithilfe der Elementfunktion Listing 2.109 der
Klasse und einer anonymen λ-Funktion.
/**
* @brief Exp Konstruktor
* @param n Anzahl der Terme
*/
Exp( size_t n)
106 2 Klassen in C++
: PowerSeries (n) {
// lambda - Funktion ruft Elementfunktion der Klasse auf
_fn = [this ]( size_t np , double x) { return term(np , x); };
}
/**
* @brief term Term des Taylor - Polynoms
* @param n n-ter Term
* @param x Variable
* @return Wert für Term n der Potenzreihe
*/
double term( size_t n, double x) {
_powterm = n == 0 ? 1 : _powterm *= x;
return _powterm / factorial (n);
}
}; // Exp
Es folgen die entsprechenden Implementierungen für sin und cos. Analog zu ex wird ein
Konstruktor und eine Elementfunktion zur Berechnung der einzelnen Terme der Summe
implementiert.
/**
* @brief The Sin class Taylorpolynom für sin(x)
*/
class Sin : public PowerSeries
{
private :
double _powterm ;
public :
/**
* @brief Sin Konstruktor
* @param n Anzahl der Terme
*/
Sin( size_t n)
: PowerSeries (n) {
_fn = [this ]( size_t np , double x) { return term(np , x); };
}
/**
* @brief term Term des Taylor - Polynoms
* @param n n-ter Term
* @param x Variable
* @return Wert des n-ten Terms an der Stelle x
*/
double term( size_t n, double x) {
size_t npow = 2 * n + 1;
_powterm = n == 0 ? x : ( _powterm *= x * x);
return sign(n) / factorial (npow) * _powterm ;
}
}; // Sin
/**
* @brief The Cos class Taylorpolynom für cos(x)
*/
class Cos : public PowerSeries
{
private :
double _powterm ;
public :
/**
* @brief Cos Konstruktor
* @param n Anzahl der Terme
*/
Cos( size_t n)
: PowerSeries (n) {
_fn = [this ]( size_t np , double x) { return term(np , x); };
}
/**
* @brief term Term des Taylor - Polynoms
* @param n n-ter term
* @param x Variable
* @return Wert des n-ten Terms an der Stelle x
*/
double term( size_t n, double x) {
size_t npow = 2 * n;
_powterm = n == 0 ? 1 : ( _powterm *= x * x);
return sign(n) / factorial (npow) * _powterm ;
}
}; // Cos
Beispiel
Es werden zehn Terme der Taylorpolynome für die Funktionen ex , cos x und sin x an der
Stelle x = 0.25 berechnet und die approximierten Werte zusammen mit den von std::sin,
std::cos, std::exp berechneten Werten auf dem Bildschirm geschrieben (Listing 2.117).
/**
* @brief run_power_series Berechnung von Näherungswerten
* für e^x, sin(x), cos(x) 10 Terme
*/
inline void ex1 () {
const size_t nmax = 10; // Anzahl der Terme
double x0 = 0.25;
N=10
x0 =2.5000000e -01
1.2840254 e+00 1.2840254 e+00
2.4740396e -01 2.4740396e -01
9.6891242e -01 9.6891242e -01
Listing 2.117
Von der allgemein formulierten Polynomklasse (Abschnitt 2.3.1) werden wir einen neuen
Datentyp generieren, der ein quadratisches Polynom mit reellen Koeffizienten abbildet.
Insbesondere interessiert uns hier die Berechnung seiner Nullstellen. Wir leiten daher
diese Klasse von der allgemeinen ab (Listing 2.63) und fügen die Elementfunktionen zur
Berechnung der Nullstellen hinzu. Die Implementierung gestaltet sich sehr einfach, da
der Konstruktor mittels einer einfachen Anweisung von der Basisklasse geerbt wird.
/**
* @brief The QPolynom class Nullstellen eines quadratischen Polynoms
2.4 Vererbung 109
public :
using BaseT :: BaseT ; // erbe Konstruktoren
Zur Berechnung der Nullstellen setzen wir Funktionen der GNU Scientific Library ein. Es
wird unterschieden, ob wir nach reellen (gsl_poly_solve_quadratic) oder nach komplexen
(gsl_poly_complex_solve_quadratic) Nullstellen suchen. Die auf die reellen Nullstellen
spezialisierte Funktion gibt neben den Nullstellen auch ihre Anzahl zurück. Wir geben
deswegen als Rückgabewert immer einen Vektor, dessen Länge der Anzahl der Nullstellen
ist, zurück.
/**
* @brief real_roots berechne reelle Nullstellen
* @return Feld mit Nullstellen , die Länge des Feldes ist
* gleich der Anzahl der Nullstellen
*/
auto real_roots () const {
double x0 , x1; // die Nullstellen
int countroots = gsl_poly_solve_quadratic (c(2) , c(1) , c(0) ,
&x0 , &x1);
Es folgt die Berechnung der komplexen Nullstellen. Es wird immer ein Container mit
zwei Nullstellen zurückgegeben.
/**
* @brief complex_roots berechne komplexe Nullstellen
* @return Feld mit berechneten Nullstellen (immer zwei)
*/
auto complex_roots () const {
using CVector = std :: vector <std :: complex <double >>;
gsl_complex z0 , z1;
gsl_poly_complex_solve_quadratic (c(2) , c(1) , c(0) , &z0 , &z1);
return CVector { { GSL_REAL (z0), GSL_IMAG (z0) }, //
{ GSL_REAL (z1), GSL_IMAG (z1) } };
}
Beispiel
/**
* @brief cppx4 Beispiel Berechnung von Nullstellen eines
* quadratischen Polynoms
*/
inline void ex1 () {
// lambda Funktion zur Ausgabe eines Polynoms und seiner Nullstellen
auto showResults = []( const QPolynom &p) {
std :: cout << p << std :: endl;
// suche reelle Nullstellen
auto roots = p. real_roots ();
if (roots . empty ()) { // keine reelle Nullstellen
std :: cout << "no real roots " << std :: endl;
auto croots = p. complex_roots ();
for (const auto &z : croots ) {
std :: cout << z << std :: endl;
}
} else {
std :: cout << "real roots " << std :: endl;
for (const auto &x : roots ) {
std :: cout << x << std :: endl;
}
}
};
// 3*x^2+2* x+1
QPolynom p1{ { 1, 2, 3 } };
auto roots = p1. real_roots ();
showResults (p1);
std :: cout << " -------------------" << std :: endl;
//x^2 -3*x+1
QPolynom p2{ { 1, -3, 1 } };
showResults (p2);
}
Es folgt die Ausgabe des Programms mit den Koeffizienten des Polynoms p1 und seiner
komplexen Nullstellen sowie den Koeffizienten von p2 und seiner zwei reellen Nullstellen.
{1 ,2 ,3}
no real roots
( -0.333333 , -0.471405)
( -0.333333 ,0.471405)
-------------------
{1,-3 ,1}
real roots
0.381966
2.61803
Listing 2.122
2.5 Programmieren mit Komponenten 111
Der zu entwickelnde interaktive Test soll Aufgaben in Form von Fragen, wie z.B. 13 +
2
5 =? generieren und nach jeder Antwort dem Benutzer mitteilen, ob seine Antwort
richtig oder falsch ist. Bei einer falschen Antwort soll das richtige Ergebnis und nach
Abschluss des Tests die Anzahl der richtigen Antworten angezeigt werden. Die Tests
dürfen nicht identisch sein, d.h. beim Starten eines neuen Tests müssen andere Fragen
generiert werden.
Entwurf Ein interaktiver Test setzt voraus, dass eine Schnittstelle zur Kommuni-
kation mit dem Benutzer existiert. Wir haben die Möglichkeit, zwischen einer grafi-
schen Benutzeroberfläche und einer textbasierten Anwendung zu wählen. In der C++-
Standardbibliothek gibt es keine Funktionen zur Programmierung von grafischen Benut-
zeroberflächen, was bedeutet, dass in diesem Fall auf externe Bibliotheken zurückgegriffen
werden müsste. Wir entscheiden uns, um das Programm nicht übermäßig kompliziert zu
machen, für eine textbasierte Lösung. Die Benutzerschnittstelle wollen wir vom eigentli-
chen Test trennen und dafür eine eigene Komponente erstellen. Die beiden Komponenten
werden mittels ihrer Schnittstellen miteinander kommunizieren.
Der Wertebereich der rationalen Zahlen soll über Parameter gesteuert und damit der
Schwierigkeitsgrad des Tests bestimmt werden. Zugelassen sollen Addition, Subtraktion,
Multiplikation und Division sein.
Quelltext Wir beginnen mit der Programmierung der Komponente zur Generierung der
Fragen, die als Klassentemplate realisiert wird. Es besteht somit die Möglichkeit, diese
Komponente in anderen Programmen mit anderen Zahlentypen einzusetzen. Zur Identi-
fizierung der Operatoren +,-,*,/ wird ein Aufzählungstyp eingeführt, dessen Elemente
in einem Feld gespeichert werden. Eine vorgegebene Anzahl von rationalen Zahlen wird
in einem std::vector gespeichert. Aus diesem Vorrat an Zahlen und Operationen sollen
die Fragen generiert werden. Die Fragen selbst bilden wir mit einem std::tuple ab, ei-
nen Container der Daten von unterschiedlichen Datentypen speichern kann. In unserem
112 2 Klassen in C++
{ 12 , 1
3,
1
4,
1
5,
1
6,
1
7,
1
8, 9}
1
{+, −, ∗, /}
{ 14 , 1 1 1 1 1 1 1
} {−, /, +, ∗ }
6, 5, 7, 9, 8, 2, 3
{ 14 , 1
6, −}, { 15 , 1
7, /}
Abb. 2.5: Ein Test mit rationalen Zahlen: Ein Container mit Zahlen und ein Container
mit algebraischen Operationen werden gemischt und zwei Fragen für den Test generiert.
Fall wird jede Frage aus zwei rationalen Zahlen und einer Operation bestehen. Wir spei-
chern innerhalb der Klasse noch zusätzlich die Anzahl der Fragen, die ein Test beinhalten
soll, und zwei Indizes, mit denen über die gespeicherten rationalen Zahlen und über die
gespeicherten Operationen iteriert werden soll.
/**
* @brief The TestGenerator class Erzeugt Fragen ( Aufgaben ) zum Rechnen
* mit Zahlen . T kann eine ganze eine rationale oder auch eine
* Fließkommazahl sein.
*/
template <class T>
class TestGenerator
{
public :
enum class Ops { plus , minus , mul , div };
// Synonym : Speicher für Zahlen
using DataC = std :: vector <T >;
// Synonym : Speicher für mathematische Operationen
using OpsC = std :: array <Ops , 4>;
// Synonym : Testfrage
using Question = std :: tuple <T, T, Ops >;
private :
// rationale Zahlen
DataC _data ;
// mathematische Operationen
OpsC _types ;
// Anzahl der Fragen in einem Test
size_t _maxQuestions ;
size_t _eIdx , _tIdx ;
Für einen mit Zahlen gefüllten Container soll die Wahrscheinlichkeit minimiert werden,
identische Tests zu generieren. Wir erreichen dies durch eine zufällige Änderung der Rei-
henfolge der Elemente bei einem Neustart des Tests oder wenn im Laufe eines Tests
bereits alle Zahlen eingesetzt wurden und wiederverwendet werden müssen. Warum der
Index einen ungültigen Wert zugewiesen bekommt, wird mit der Elementfunktion Lis-
2.5 Programmieren mit Komponenten 113
ting 2.125 klar. Die zufällige Anordnung der Elemente wird mithilfe der stl-Funktion
std::shuffle und eines Pseudo-Zufallsgenerators erreicht.
/**
* @brief shuffle_data mische die gespeicherten Elemente
*/
void shuffle_data () {
// erzeuge Startwert für den Zufallsgenerator
std :: random_device rd;
// Pseudo - Zufallsgenerator
std :: default_random_engine g(rd ());
std :: shuffle ( _data . begin () , _data.end () , g);
Mit dem intern gespeicherten Index _eIdx wird über die im Container platzierten Zahlen
iteriert. Hat der Index einen ungültigen Wert, wird er gleich 0 gesetzt.
/**
* @brief next_rational gebe die nächste gespeicherte rationale Zahl
* @return rationale Zahl
*/
auto next_item () {
// wenn der Index einen ungültigen Wert hat erhält er den Wert 0
// sonst rückt er eine Position weiter
_eIdx = ( _eIdx == _data .size ()) ? 0 : _eIdx + 1;
return _data [ _eIdx ];
}
Ähnlich verfahren wir mit den algebraischen Operatoren. Dadurch, dass sie in einem
Container gespeichert sind, können sie gemischt und somit zufällige Fragen generiert
werden (Listing 2.126, Listing 2.127).
/**
* @brief shuffle_ops mische algebraische Operationen
*/
void shuffle_ops () {
// erzeuge Startwert für den Zufallsgenerator
std :: random_device rd;
// Pseudo - Zufallsgenerator
std :: default_random_engine g(rd ());
std :: shuffle ( _types . begin () , _types .end (), g);
_tIdx = _types .size ();
}
/**
* @brief next_op gebe nächste algebraische Operation
* @return Aufzählung
*/
auto next_op () {
// bei ungültigem Wert setze den Wert auf 0
_tIdx = ( _tIdx == _types .size ()) ? 0 : _tIdx + 1;
return _types [ _tIdx ];
}
Ein Testgenerator wird durch die Angabe der Anzahl der Fragen und einen vorgegebenem
Vorrat an Zahlen erzeugt.
/**
* @brief TestGenerator Konstruktor
* @param maxq Anzahl der Fragen
* @param data Feld mit Zahlen zur Generierung von Fragen z.B.
* {1/2 ,1/3 ,1/4 ,...}
*/
TestGenerator ( size_t maxq , const DataC &data) {
_data = data; // kopiere Zahlen
shuffle_data (); // mische Container mit Zahlen
Eine Frage wird mit zwei Zahlen und einem Operator generiert und dann der interne
Zähler für die Anzahl der Fragen aktualisiert. Die zwei ersten Elemente eines std::tuple
sind Zahlen und das dritte und letzte Element der Operator (Abb. 2.5). Der einfachste
Weg ein std::tuple zu initialisieren ist über die Funktion std::make_tuple.
/**
* @brief next_question generiere eine Frage
* @return Frage (std :: tuple )
*/
auto next_question () {
// Anzahl der noch zu stellenden Fragen
_maxQuestions --;
// generiere Frage
return std :: make_tuple ( next_item (), next_item (), next_op ());
}
/**
* @brief has_questions
* @return true wenn noch Fragen vorhanden sind
*/
bool has_questions () { return _maxQuestions != 0; }
Die korrekte Antwort für eine Frage wird berechnet. Die Elemente des std::tuple werden
mit der Funktion std::get gelesen.
/**
* @brief get_result berechne korrekte Antwort
* @param q Frage
* @return Antwort rationale Zahl
*/
static T calc_result ( const Question &q) {
// lese erste Zahl
auto r1 = std ::get <0 >(q);
// lese zweite Zahl
auto r2 = std ::get <1 >(q);
// das Ergebnis
T r;
// welcher Operator ?
switch (std ::get <2 >(q)) {
case Ops :: plus: {
r = r1 + r2;
} break;
case Ops :: minus : {
r = r1 - r2;
} break;
case Ops :: mul: {
r = r1 * r2;
} break;
case Ops :: div: {
r = r1 / r2;
} break;
}
return r;
}
}; // TestGenerator
Die dritte Komponente neben der Klasse für rationalen Zahlen und dem Testgenerator ist
die Benutzerschnittstelle. Wir benötigen hierfür Funktionen, welche die Fragen in Text
und umgekehrt die Benutzerantworten vom Textformat ins interne Format übersetzen.
/**
* @brief The TestUI class Test mit rationalen Zahlen
* Benutzerschnittstelle
*/
class TestUI
116 2 Klassen in C++
{
public :
using Rational = nmx :: apps :: x017 :: Rational ;
using Test = nmx :: apps :: x017 :: TestGenerator <Rational >;
// Testgenerator
Test test;
// algebraische Operationen
static const std :: array <std :: string , 4> ops;
// Anzahl der richtigen Anttworten
size_t _score = 0;
// maximale Punktzahl
const size_t _maxScore ;
public :
Die Benutzerschnittstelle speichert intern eine Instanz eines Testgenerators sowie Varia-
blen, welche die erreichte und die maximale Punktzahl speichern. Zusätzlich wird ein
statisches Feld angelegt, in dem die Operatoren als Zeichenketten aufgelistet sind, z.B.
für Ops::plus das Zeichen + usw.
/**
* Umwandlung der Operatoren +,-,*,/ in Zeichenketten
*/
inline const std :: array <std :: string , 4> TestUI :: ops{ "+", "-", "*", "/"
};
Eine Instanz der Benutzerschnittstelle wird durch die Angabe der Anzahl der Fragen und
eine Liste von Zahlen erzeugt, die an den Testgenerator weitergereicht werden.
/**
* @brief TestUI Konstruktor
* @param maxq Anzahl der Fragen
* @param data Container mit rationalen Zahlen
*/
TestUI ( size_t maxq , const Test :: DataC &data)
: test{ maxq , data }
, _maxScore { maxq } {}
Die Übersetzung einer Frage in eine Zeichenkette erfolgt mithilfe eines std::stringstream.
Die Implementierung ist einfach, denn es existiert eine Version des Operators << für
rationale Zahlen (Listing 2.94).
/**
* @brief to_string Umwandlung Frage in Zeichenkette
* @param q Frage
* @return Frage als Zeichenkette
*/
2.5 Programmieren mit Komponenten 117
Die Benutzerantwort wird gelesen. Es wird immer eine Eingabe der Form a/b erwartet.
Ist das nicht der Fall, wird eine Ausnahme ausgeworfen. Zum Einsatz kommt die Funktion
std::stoll, welche eine long long Zahl aus einer Zeichenkette extrahiert.
/**
* @brief read Lese Antwort des Benutzers
* @param answer Antwort des Benutzers ( Zeichenkette )
* @return Antwort des Benutzers als rationale Zahl
*/
inline Rational read( const std :: string & answer ) {
size_t l;
// extrahiere Zähler z.B. 2 von 2/3
long long num = std :: stoll (answer , &l);
//l zeigt auf /
if (l == answer .size () || answer .at(l) != '/') {
throw std :: runtime_error (" expected /");
}
// Rest der Zeichenkette z.B. /3
auto part2 = answer . substr (l + 1);
// extrahiere Nenner
long long den = std :: stoll (part2 , &l, 10);
if (l != part2 .size ()) {
throw std :: runtime_error (" syntax error");
}
return { num , den };
}
}; // TestUI
Der Test wird gestartet. Der Testgenerator erzeugt neue Fragen, die als Zeichenketten
auf dem Bildschirm geschrieben werden. Die Benutzereingabe wird gelesen und mit der
richtigen Antwort verglichen. Eine Meldung wird ausgegeben.
/**
* @brief run Interaktion mit dem Benutzer
*/
void run () {
while (test. has_questions ()) {
try {
std :: string answer ;
// generiere Frage
const auto q = test. next_question ();
// zeige Frage auf dem Bildschirm
std :: cout << "q: ";
118 2 Klassen in C++
std :: cout << to_string (q) << "=?" << std :: endl;
// lese Antwort als Zeichenkette
std :: cout << "a:";
getline (std ::cin , answer );
// berechne korrekte Antwort
auto cresult = Test :: calc_result (q);
auto uresult = read( answer );
if ( cresult == uresult ) {
std :: cout << "OK" << std :: endl;
_score ++;
} else {
std :: cout << " correct answer is:" //
<< cresult << std :: endl;
}
} catch (const std :: out_of_range &err) {
std :: cout << " wrong input format " << std :: endl;
std :: cout << " input must be like a/b" << std :: endl;
std :: cout << err.what () << std :: endl;
} catch (const std :: invalid_argument &err) {
std :: cout << " wrong input format " << std :: endl;
std :: cout << " input must be like a/b" << std :: endl;
std :: cout << err.what () << std :: endl;
} catch (const std :: runtime_error &err) {
std :: cout << " wrong input format " << std :: endl;
std :: cout << " input must be like a/b" << std :: endl;
std :: cout << err.what () << std :: endl;
}
}
Beispiel
Ein Vektor mit rationalen Zahlen wird über einer λ-Funktion mit Daten gefüllt. Es sollen
nur solche Zahlen gespeichert werden, deren Nenner größer als der Zähler ist. Dies wird
mit zwei Schleifen und die Vorgabe des maximalen Werts für den Nenner realisiert. Die
äußere Schleife durchläuft alle möglichen Werte für den Zähler, beginnend mit der 1. Die
innere Schleife beginnt mit dem gerade aktuellen Wert des Zählers, erhöht um 1. Der
Test wird mit diesem Vorrat an rationalen Zahlen gestartet.
/**
* @brief cppx3 Beispiel Test mit rationalen Zahlen
*/
inline void ex1 () {
// Erzeuge einen Container mit rationalen Zahlen
// lege den größten Nenner fest.
auto fn = []( long long lmaxden ) {
std :: vector < TestUI :: Rational > data;
for (long long idx = 1; idx < lmaxden ; idx ++) {
2.6 Schnittstellen für Klassen mithilfe von Templates definieren 119
for (long long jdx = idx + 1; jdx < lmaxden ; jdx ++) {
data. push_back ({ idx , jdx });
}
}
return data;
};
// Instanz eines Tests
TestUI quiz{ 5, fn (10) };
// starte Test
quiz.run ();
}
Es folgt der Ablauf eines Tests mit fünf Fragen. Die ersten vier wurden richtig beantwor-
tet. Die korrekte Antwort auf die falsch beantwortete letzte Frage wird angezeigt. Am
Ende erscheint die erreichte Punktzahl in Prozent.
q: 4/9+4/5=?
a :56/45
OK
q: 3/6 -3/4=?
a: -1/4
OK
q: 1/2/6/9=?
a:3/4
OK
q: 2/3*6/7=?
a :11/21
correct answer is :4/7
q: 1/3+2/8=?
a :7/12
OK
score :80%
Listing 2.139
eine Alternative zu virtuellen Funktionen ist. Diese Art zur Erstellung von Schnittstel-
len basiert auf der Tatsache, dass eine Methode eines Klassentemplates nur bei Bedarf
instanziiert wird.
Betrachten wir ein Beispiel, in dem für ein Klassentemplate die zwei Vergleichsopera-
toren =,!= implementiert werden sollen und Objekte von einem noch nicht festgelegten
Typ T miteinander verglichen werden. Da das Klassentemplate nicht wissen kann, wie
zwei Objekte von einem beliebigen Datentyp miteinander verglichen werden, ruft es eine
für den Datentyp T spezielle Elementfunktion is_equal_to auf.
public :
friend bool operator ==( const T &x, const T &y)
{
return x. is_equal_to (y);
}
Nehmen wir nun eine Klasse B welche intern eine ganze Zahl speichert. Sie wird von der
Klasse A abgeleitet und implementiert die Methode is_equal_to. Automatisch stehen der
Klasse B die zwei Vergleichsoperatoren zur Verfügung.
public :
B(int x) : _x{x} { }
Betrachten wir eine weitere Klasse C, die Daten in einem Feld verwaltet. Auch diese imple-
mentiert eine für ihre Zwecke geeignete is_equal_to-Methode. Dadurch, dass C ebenfalls
von A abgeleitet wird, stehen auch dieser Klasse die Vergleichsoperatoren automatisch
zur Verfügung.
public :
C(int x1 , int x2 , int x3) : _data {x1 , x2 , x3} { }
int main(void)
{
std :: cout << std :: boolalpha << (b1 == b2) << " ,\t";
std :: cout << std :: boolalpha << (b1 == b3) << " ,\t";
std :: cout << std :: boolalpha << (c1 == c2) << " ,\t";
std :: cout << std :: boolalpha << (c1 == c3) << std :: endl;
return 0;
}
Listing 2.144
2.6.1 Beispielanwendung
Ausgehend von Gl. 2.6, Gl. 2.7 und Gl. 2.8 sollen die Taylorpolynome für die Funktionen
ex , sin(x) und cos x mithilfe von statischen Schnittstellen implementiert werden. Die
122 2 Klassen in C++
Werte für die Berechnung der Fakultät sollen zwischengespeichert und wiederverwendet
werden.
Kurzbeschreibung Wir werden insgesamt vier Klassen einführen. Eine wird für die
Speicherung der Werte der Fakultät zuständig sein und über einen Template-Parameter
die Größe des Speichers festlegen. Eine zweite Klasse wird die Basisfunktionalität zur
Berechnung der Taylorpolynom-Terme implementieren. Diese Klasse soll in Form einer
Schnittstelle implementiert werden. Ausgehend von dieser Basisklasse werden schließlich
die Klassen für die Berechnung der Taylorpolynome für die drei Funktionen folgen.
Quelltext Die Klasse zur Berechnung und Speicherung der Fakultät ist ein Klassentem-
plate, welches die Werte intern in einem std::array speichert. Die Länge des Feldes wird
über einen Template-Parameter N gesteuert, dessen Standardwert 10 sein wird. Alle Funk-
tionen und Elemente sind statisch. Eine Hilfsfunktion zur Berechnung der Terme wird
bereitgestellt. Sie berechnet und speichert die vorgesehenen Werte beim ersten Aufruf.
/**
* @brief The Factorial class Berechnung von Fakultäten
* @param Anzahl der vorberechneten Werte
*/
template < size_t N = 10>
class Factorial
{
private :
// reserviere Speicher für n!
inline static std :: array <double , N> _data;
// Daten berechnet ?
inline static bool _init = false;
public :
// berechne und speichere n! ( Hilfsfunktion )
inline static void init () {
if (! _init ) {
_data [0] = 1;
for ( size_t idx = 1; idx < N; idx ++) {
_data [idx] = idx * _data[idx - 1];
}
_init = true;
}
}
// berechne n!
inline static double get( size_t n) { //
return n < N ? _data [n] : (n * get(n - 1));
}
};
Es folgt die Komponente zur Berechnung der Taylorpolynome. Diese ist ebenfalls ein
Klassentemplate. Der erste der beiden Template-Parameter gibt den Datentyp der ab-
geleiteten Klasse an und der zweite bestimmt die Anzahl der zu speichernden Fakultäts-
werte.
/**
* @brief The PowerSeries class Schnittstelle zur Entwicklung
* von Funktionen in Potenzreihen Parameter
* @param T die zu entwickelnde Funktion ( Klasse )
*/
template <class T = double , size_t N = 15>
class PowerSeries
{
public :
using Factorial = Factorial <N >;
protected :
size_t _n;
// Variable speichert die Potenzen von x
mutable double xval = 1;
protected :
Über den Konstruktor wird die Anzahl der zu berechnenden Terme festgelegt.
/**
* @brief PowerSeries
* @param n Anzahl der Terme der Reihe
*/
inline PowerSeries ( size_t n)
: _n{ n } {
Factorial :: init ();
}
Die nächste Elementfunktion leitet den Aufruf an die entsprechende Methode der ab-
geleiteten Klasse weiter. Dazu ist es notwendig, über ein static_cast dem Compiler
mitzuteilen, die Methode welcher Klasse aufgerufen werden soll. In diesem Fall ist es die
Elementfunktion der abgeleiteten Klasse.
/**
* @brief term allgemeiner Term ( Funktionsspezifisch )
* @param x Stelle an der die Potenzreihe ausgewertet werden soll
* @param n der Term der ausgewertet werden soll
* @return der berechnete Wert
*/
inline double term( double x, size_t n) const { //
return static_cast <const T *>( this)->term(x, n);
}
Die Terme des Taylorpolynoms werden aufsummiert. Zur Berechnung der Terme wird
die Elementfunktion Listing 2.148 aufgerufen.
/**
* @brief sum Partialsumme ...
* @param x .. an der Stelle
* @return der berechnete Wert der Partialsumme
*/
inline double sum( double x) const {
xval = 1;
double val = 0;
for ( size_t idx = 0; idx < _n + 1; idx ++) {
val += term(x, idx);
}
return val;
}
/**
* @brief sign Berechne (-1)^n
* @param n ganze Zahl
* @return +1 oder -1
*/
inline int sign( size_t n) const { return n % 2 == 0 ? 1 : -1; }
public :
Alternativ kann der Näherungswert einer Funktion mithilfe des überladenen Klammer-
operators berechnet werden.
/**
* @brief operator () berechne Funktionswert
* @param x Variable
* @return Funktionswert
*/
inline double operator ()( double x) const { return sum(x); }
}; // PowerSeries
Es folgt die Implementierung des Taylorpolynoms für ex . Die Umsetzung ist einfach,
denn das Einzige, was neben dem Konstruktor noch implementiert werden muss, ist die
Formel für die einzelnen Terme der Reihe.
/**
* @brief The Exp class Die Funktion e^x als Taylor - Polynom
*/
class Exp : public PowerSeries <Exp >
{
2.6 Schnittstellen für Klassen mithilfe von Templates definieren 125
protected :
/**
* @brief term Term des Taylor - Polynoms
* @param x Variable
* @param n Anzahl der Terme
* @return n-ter Term des Taylor - Polynoms
*/
inline double term( double x, size_t n) const { //
xval *= n == 0 ? 1 : x;
return xval / Factorial :: get(n);
}
public :
/**
* @brief Exp Konstruktor
* @param n Anzahl der Terme
*/
Exp( size_t n)
: PowerSeries (n) {}
}; // Exp
/**
* @brief The Sin class Die Funktion sin(x) als Taylor - Polynom
*/
class Sin : public PowerSeries <Sin >
{
friend PowerSeries ; // Zugriff auf private Elemente
protected :
/**
* @brief term Term des Taylor - Polynoms
* @param x Variable
* @param n Anzahl der Terme
* @return n-ter Term des Taylor - Polynoms
*/
inline double term( double x, size_t n) const { //
const size_t npw = 2 * n + 1;
xval *= n == 0 ? x : (x * x);
126 2 Klassen in C++
public :
/**
* @brief Sin Konstruktor
* @param n Anzahl der Terme
*/
Sin( size_t n)
: PowerSeries (n) {}
}; // Sin
/**
* @brief The Cos class Die Funktion cos(x) als Taylor - Polynom
*/
class Cos : public PowerSeries <Cos >
{
friend PowerSeries ; // Zugriff auf private Elemente
protected :
/**
* @brief term Term des Taylor - Polynoms
* @param x Variable
* @param n Anzahl der Terme
* @return n-ter Term des Taylor - Polynoms
*/
inline double term( double x, size_t n) const { //
const size_t npw = 2 * n;
xval *= n == 0 ? 1 : (x * x);
return sign(n) * xval / Factorial :: get(npw);
}
public :
/**
* @brief Cos Konstruktor
* @param n Anzahl der Terme
*/
Cos( size_t n)
: PowerSeries (n) {}
}; // Cos
Ein Vergleich mit der Version aus Abschnitt 2.4.1 zeigt, dass hier keine Variable für ein
Funktionsobjekt gespeichert wird, mit dem die Terme des Polynoms berechnet werden.
Beispiel
Wir berechnen die Funktionswerte für ex , cos x und sin x an der Stelle x0 = 0.2 unter
Berücksichtigung der ersten zehn Terme. Zu den approximierten Werten werden auch die
von std::sin, std::cos und std::exp berechneten Werte ausgegeben (Listing 2.162).
/**
* @brief run_power_series Beispielanwendung
*/
inline void ex1 () {
const Exp myexp (5);
const Sin mysin (5);
const Cos mycos (5);
// berechne Funktionswert an dieser Stelle
const double x0 = 0.2;
std :: cout << myexp (x0) << std :: setw (10) << std :: exp(x0) <<
std :: endl;
std :: cout << mysin (x0) << std :: setw (10) << std :: sin(x0) <<
std :: endl;
std :: cout << mycos (x0) << std :: setw (10) << std :: cos(x0) <<
std :: endl;
}
1.2214 1.2214
0.198669 0.198669
0.980067 0.980067
Listing 2.162
Es folgt ein weiteres Beispiel mit dem sin x, cos x und ex an der Stelle x = 0.2 berechnet
werden. Die Berechnung erfolgt mehrmals innerhalb einer Schleife, einmal mit den von
uns programmierten Klassen und dann mit std::sin, std::cos und std::exp. In beiden
Fällen wird die benötigte Zeit gemessen.
/**
* @brief ex2
*/
inline void ex2 () {
using namespace std :: chrono ;
constexpr size_t N = 3;
const Exp myexp (N);
const Sin mysin (N);
const Cos mycos (N);
// berechne Funktionswert an dieser Stelle
const double x0 = 0.2;
std :: cout << "dt1=" << diff1 << " ms" << std :: endl;
std :: cout << "dt2=" << diff2 << " ms" << std :: endl;
std :: cout << "dt1/dt2=" << diff1 / diff2 << std :: endl;
std :: cout << " ---------------------------------" << std :: endl;
const size_t w = 15;
std :: cout << "exp(x)" << std :: setw(w) << "sin(x)" << std :: setw(w)
<< "cos(x)" << std :: endl;
std :: cout << y1 << std :: setw(w) << y2 << std :: setw(w) << y3 <<
std :: endl;
std :: cout << y4 << std :: setw(w) << y5 << std :: setw(w) << y6 <<
std :: endl;
}
} // namespace nmx :: apps :: x018
Listing 2.164
2.7.1 Fehlerbehandlung
Selbst gut getestete Programme haben Fehler. Manche von ihnen machen sich erst be-
merkbar, wenn die Software beim Benutzer im Einsatz ist. Solche Probleme können
nur dann systematisch behoben werden, wenn sinnvolle Meldungen bei Fehlverhalten
erzeugt werden. Die besten Fehlermeldungen sind diejenigen, die am schnellsten zur Feh-
lerbehebung führen. Eine generelle Regel, wie Fehlermeldungen auszusehen haben, gibt
es leider nicht. Eine Mindestanforderung jedoch ist, dass die Stelle im Quelltext regis-
triert wird, an der ein Problem aufgetreten ist. Jede zusätzliche Information macht dann
die Identifizierung des Fehlers einfacher. Zu einem Entwicklungsprozess gehört auch der
Entwurf einer Strategie zur Erkennung und Behandlung von Fehlern, die während der
Ausführung eines Programms auftreten. Wir haben im ersten Kapitel die Vorteile der
Ausnahmebehandlung kennengelernt und wollen diesen Mechanismus durch Einführung
von wiederverwendbaren Funktionen systematisieren.
Kurzbeschreibung Wir möchten mit einer Reihe von Funktionen Fehlermeldungen er-
zeugen, die folgende Informationen enthalten:
Dateiname
Funktionsname
Zeile im Quelltext
Text mit zusätzlichen Informationen (optional)
Quelltext Innerhalb einer Struktur definieren wir zwei statische Funktionen (Listing
2.166 und Listing 2.167), die eine Zeichenkette zusammensetzen, welche die oben aufge-
listeten Informationen enthält. Sie unterscheiden sich dadurch, dass der zweiten Funktion
(Listing 2.167) eine zusätzliche Zeichenkette mitgegeben wird, mit der ein Fehler noch
näher beschrieben werden kann. In beiden Fällen werden die Nachrichten durch den
Einsatz von std::stringstream zusammengesetzt.
/**
* @brief The Error struct erzeugt Fehlermeldungen
*/
struct Error {
/**
* @brief msg Text einer Fehlermeldung zusammengesetzt aus
* @param file Dateinamen
* @param line Zeilennummer
* @param func Funktionsnamen
* @return Fehlermeldung als Zeichenkette
*/
inline static std :: string msg( const char *file , int line , const
char *func) {
std :: stringstream myoutput ;
myoutput << file << "\n" << line << "\n" << func;
130 2 Klassen in C++
/**
* @brief info Text einer Fehlermeldung zusammengesetzt aus
* @param msg allgemeine Nachricht
* @param file Dateinamen
* @param line Zeilennummer
* @param func Funktionsnamen
* @return Fehlermeldung als Zeichenkette
*/
template <class T>
inline static std :: string msgx( const T &msg , const char *file , int
line , const char *func) {
std :: stringstream myoutput ;
myoutput << msg << "\n" << file << "\n" << line << "\n" << func;
return myoutput .str ();
}
}; // Error
Mit statischen Funktionen einer weiteren Struktur können Fehler durch das Auswerfen
von Ausnahmen gemeldet werden.
/**
* @brief The Check struct Testfunktionen zum Auffinden von Fehlern
*/
struct Check {
Die ersten beiden Funktionen werfen bei Erfüllung bzw. Nichterfüllung einer Bedin-
gung eine Ausnahme aus. Es ist möglich den Typ der Ausnahme über einen Template-
Parameter festzulegen und somit differenziert auf Fehler zu reagieren.
/**
* @brief error_if Fehler wenn Bedingung erfüllt ist
* @param s Nachricht
* @param arg ( Bedingung ) wenn true wird eine Ausnahme ausgeworfen
*/
template <class X = std :: runtime_error >
void inline static error_if ( const std :: string &s, bool arg) {
if (arg) {
throw X(s);
}
}
/**
* @brief error_if_not Fehler wenn Bedingung nicht erfüllt ist
* @param s Nachricht
* @param arg ( Bedingung ) wenn false wird Ausnahme ausgeworfen
*/
template <class X = std :: runtime_error >
void inline static error_if_not ( const std :: string &s, bool arg) {
if (! arg) {
throw X(s);
}
}
Die nächste Funktion prüft mehrere Bedingungen auf einmal. Sie ist zum Überprüfen
von Eingabeparametern gut geeignet.
/**
* @brief input teste mehrere Bedingungen auf einmal
* @param s Nachricht
* @param args Bedingungen für Eingabeparameter wenn eine nicht
* erfüllt ist wird eine Ausnahme ausgeworfen
*/
template <class X = std :: invalid_argument >
void inline static all( const std :: string &s,
std :: initializer_list <bool > lst) {
size_t counter = 0;
// iteriere über alle Bedingungen . Jede Bedingung ist ein
// Element der Liste und wird deswegen einem Index zugeordnet .
for (auto itr = lst. begin (); itr != lst.end (); itr ++) {
if (!* itr) {
// wenn eine nicht erfüllt , schreibe den Index in
// die Nachricht
std :: stringstream sstream ;
sstream << s << " invalid argument :" << counter ;
throw X( sstream .str ());
}
counter ++;
}
}
Ein ungültiger Zeiger ist oft die Ursache von Fehlern. Mit folgender Funktion kann ein
Zeiger getestet werden.
/**
* @brief null_ptr teste Zeiger auf Gültigkeit
* @param s Nachricht
* @param ptr Zeiger
*/
template <class T, class X = std :: invalid_argument >
void inline static ptr( const std :: string &s, T *ptr) {
if (ptr == nullptr ) {
std :: stringstream sstream ;
sstream << " nullpointer :" << s;
132 2 Klassen in C++
/**
* generiert einen Text mit Dateinamen , Zeile , Funktionsnamen
*/
/**
* generiert einen Text mit Dateinamen , Zeile , Funktionsnamen und einer
* zusätzlichen Nachricht
*/
# define nmx_msg_txt (msg) nmx :: Error :: msgx(msg , __FILE__ , __LINE__ ,
__PRETTY_FUNCTION__ )
/**
* generiert einen Text mit Dateinamen , Zeile , Funktionsnamen
*und einer zusätzliche Nachricht z.B. aus einem bool 'schen Ausdruck
*/
# define nmx_msgx (msg) nmx :: Error :: msgx (#msg , __FILE__ , __LINE__ ,
__PRETTY_FUNCTION__ )
Beispiel
Ein std::array der Länge 5 wird instanziiert und mit Werten gefüllt. Anschließend wer-
den die Elemente auf dem Bildschirm ausgegeben. Bevor jedes Element gelesen wird,
findet eine Bereichsüberprüfung statt. Da versucht wird, mit einem Index außerhalb des
zulässigen Bereichs auf das Feld zuzugreifen, wird eine Ausnahme ausgeworfen. Die Feh-
lermeldung (Listing 2.177) wird mithilfe des Makros nmx_msgx generiert.
/**
* @brief ex1 Fehlerbehandlung Beispielanwendung
*/
2.7 Eigene Werkzeuge bauen 133
Das Programm wird ausgeführt. Die Elemente werden auf dem Bildschirm ausgegeben.
Sobald versucht wird auf das Element mit Index 5, welches nicht existiert, zuzugreifen,
wird eine Ausnahme ausgeworfen. Es wird eine Fehlermeldung angezeigt, in der auch die
Bedingung, die nicht erfüllt wurde, enthalten ist.
Listing 2.177
Die von einem Programm berechneten Daten werden meistens als Tabellen oder Dia-
gramme dargestellt. Dies bedeutet, dass sie in einem bestimmten Format in Dateien
gespeichert werden müssen.
Betrachten wir ein Beispiel, in dem zwei Felder auf dem Bildschirm in jeweils eine Reihe
ausgegeben werden müssen. Die Trennzeichen zwischen den Elementen sowie das Zeichen
am Zeilenende sollen für jedes Feld anders sein. Damit nicht zweimal eine Schleife pro-
134 2 Klassen in C++
grammiert werden muss, führen wir eine λ-Funktion ein, der neben dem Feld auch die
beiden Zeichen für die Trennung der Elemente und das Zeilenende übergeben werden.
/**
* @brief ex1 Beispiel für die formatierte Ausgabe von Arrays
*/
inline void ex1 () {
// Datentyp und Länge von std :: array wird automatisch ermittelt
std :: array x{ 1, 2, 3, 4, 5 }, y{ 6, 7, 8, 9, 10 };
Wir führen die Funktion aus und erhalten das gewünschte Ergebnis.
1, 2, 3, 4, 5
6 &7 &8 &9 &10\\
Listing 2.179
Durch die Einführung der λ-Funktion ist die Programmierung von Schleifen für jede
Ausgabe eines Feldes überflüssig. Es ist jedoch möglich, die Formatierung der Daten wei-
ter zu automatisieren. Die Idee ist, die Daten zusammen mit Formatierungsanweisungen
einem Ausgabestrom zu übergeben.
Quelltext Bei Dateien, die Daten in einem bestimmten Format speichern, ist es üblich,
für die Dateiendung spezielle Bezeichner zu wählen, z.B. für eine LATEX-Datei tex und für
eine CSV-Datei csv. Wir wollen dies in der neuen Klasse berücksichtigen und reservieren
eine Variable _extension, die zusammen mit einer weiteren Variablen _name automa-
tisch einen Dateinamen zusammensetzt. So würde für _extension=tex und _name=plot
der Dateiname plot.tex gebildet werden. Die Anzahl der Nachkommastellen für die ge-
speicherten Zahlen kann über den Ausgabestrom gesteuert werden. Wir definieren einen
Standardwert innerhalb der Klasse.
/**
* @brief The Format class Formatierte Ausgabe von Arrays
*/
class Format
{
// Name , Trennzeichen , Zeichen am Ende jeder Zeile , Zusatzname
std :: string _name , _sep , _lend , _extension ;
public :
// Anzahl der Ziffern
std :: streamsize precision = 3;
Über den Konstruktor werden allen internen Variablen Werte zugewiesen. Die Zahlen
werden standardmäßig in wissenschaftlicher Form und mit drei Nachkommastellen dar-
gestellt (Listing 2.183, Listing 2.184).
/**
* @brief Format Konstruktor
* @param sep Trennzeichen
* @param lend Zeichen am Ende einer Reihe
* @param prec Anzahl der Nachkommastellen
*/
inline Format ( const std :: string &sep , //
const std :: string &lend ,
std :: streamsize prec = 3)
: _name { "none" }
, _sep{ sep }
, _lend { lend }
, _extension { "none" }
, precision { prec } {}
/**
* @brief Format Konstruktor
* @param name Dateiname
* @param sep Trennzeichen
* @param lend Zeichen am Ende einer Reihe
* @param ext Dateisuffix
* @param prec Anzahl der Nachkommastellen
*/
inline Format ( const std :: string &name , //
const std :: string &sep ,
const std :: string &lend ,
const std :: string &ext ,
std :: streamsize prec = 3)
: _name { name }
, _sep{ sep }
, _lend { lend }
, _extension { ext }
, precision { prec } {}
Die Trennzeichen für Spalten und die Zeichen am Zeilenende können als Wertepaar aus-
gegeben werden.
/**
* @brief operator () Ausgabe von
* Trennzeichen , Zeichen am Ende jeder Zeile
*/
inline auto operator () () const { return std :: make_pair (_sep ,
_lend); }
/**
* @brief write_line Daten in einem Array werden in
* einer Zeile ausgegeben
* @param ofs Ausgabestrom
* @param crow Array
*/
template <class T>
void write_line (std :: ostream &ofs , const T &crow) const {
ofs << crow [0];
for ( size_t idx = 1; idx < crow.size (); idx ++) {
ofs << _sep << crow[idx ];
}
ofs << _lend ;
}
Eine Anzahl von Feldern wird einem Ausgabestrom übergeben. Jedes der Felder wird
als Reihe ausgegeben. Das variadische Funktionstemplate erlaubt es mit beliebig vielen
2.7 Eigene Werkzeuge bauen 137
Feldern zu arbeiten. Zur Übersetzungszeit wird die Anzahl der Felder ermittelt und der
entsprechende Code zur Übergabe an den Ausgabestrom generiert.
/**
* @brief as_rows beliebige Anzahl von Arrays
* werden als Reihen ausgegeben
* @param ofs Ausgabestrom
* @param x Array
*/
template <class X, class ... Y>
void as_rows (std :: ostream &ofs , const X &x, const Y &... y) const {
write_line (ofs , x);
// sizeof ... : die Anzahl der Elemente vom Typ Y
if constexpr ( sizeof ...(y) > 0) {
// iteriere über alle Felder
for (const auto &arg : { y... }) {
// eine Zeile für jeden Container
write_line (ofs , arg);
}
}
}
/**
* @brief as_columns beliebige Anzahl von Arrays
* werden als Spalten einer Tabelle dargestellt
* @param ofs Ausgabestrom
* @param x Array
*/
template <class X, class ... Y>
void as_columns (std :: ostream &ofs , const X &x, const Y &... y)
const {
for ( size_t idx = 0; idx < x.size (); idx ++) {
ofs << x[idx ];
if constexpr ( sizeof ...(y) > 0) {
for (const auto &arg : { y... }) {
ofs << _sep << arg[idx ];
}
}
ofs << _lend ;
}
}
/**
* @brief as_columns Array von Arrays wird als Tabelle ausgegeben .
* @param ofs Ausgabestrom
* @param data Array von Arrays
*/
138 2 Klassen in C++
Beispiele
/**
* @brief ex2 Beispiel für die formatierte Ausgabe eines Arrays
*/
inline void ex2 () {
Format fmt1{ "fmt1", " ,\t", "\n", "dat" }, //
fmt2{ "fmt2", "&\t", " \\\\\ n", "dat" };
std :: array x{ 1, 2, 3, 4, 5 }, y{ 6, 7, 8, 9, 10 };
fmt1. set_defaults (std :: cout); // es genügt ein Aufruf
fmt1. write_line (std :: cout , x);
fmt2. write_line (std :: cout , y);
}
Zwei Felder werden in jeweils eine Reihe ausgegeben. Die Formatierung ist für beide
dieselbe. Die Arbeit wird mit einem einzigen Funktionsaufruf ausgeführt.
/**
* @brief ex3 Beispiel für die formatierte Ausgabe eines Arrays
*/
inline void ex3 () {
Format fmt{ "fmt1", "\t", "\n", "dat" };
std :: array x{ 1, 2, 3, 4, 5 }, y{ 6, 7, 8, 9, 10 };
fmt. set_defaults (std :: cout);
fmt. as_rows (std :: cout , x, y);
}
/**
* @brief ex4 Beispiel für die formatierte Ausgabe eines Arrays
*/
2.7 Eigene Werkzeuge bauen 139
/**
* @brief ex5 Beispiel für die formatierte Ausgabe eines Arrays
*/
inline void ex5 () {
Format fmt{ "fmt1", " ,\t", "\n", "csv" };
std :: vector <std :: vector <double >> table{ { 1, 2, 3, 4, 5 },
{ 6, 7, 8, 9, 10 },
{ 11, 12, 13, 14, 15 } };
fmt. set_defaults (std :: cout);
fmt. as_columns (std :: cout , table );
}
Wir haben den Prozess zur formatierten Ausgabe von Feldern automatisiert und führen
in diesem Abschnitt eine weitere Klasse ein, die in Zusammenarbeit mit der im letzten
Abschnitt entwickelten Formatierungsklasse das Schreiben von Daten in Dateien verein-
facht.
Kurzbeschreibung Die Klasse wird überprüfen, ob die Datei geöffnet wurde und über
eine Instanz der Formatierungsklasse (Listing 2.182) einen Dateinamen generieren. Wir
möchten, dass alle Dateien in einem Basisverzeichnis bzw. in einem Unterverzeichnis
des Basisverzeichnisses abgelegt werden. Der Name dieses Basisordners soll über die ein-
geführten Konfigurationsparameter (Abschnitt 1.9, Listing 1.50) festgelegt werden. Die
neue Klasse soll nur aus statischen Funktionen bestehen, die Ausgabeströme erzeugen.
/**
* @brief The Output struct Hilfsklasse zur Erzeugung von Ausgabeströmen
* zum Schreiben in Dateien
*/
class Output
{
140 2 Klassen in C++
public :
inline static const Format latex { " table", "&", "\\\\\n", "tex" };
inline static const Format plot{ "plot", ",", "\n", "csv" };
inline static const Format terminal { " terminal ", "\t", "\n", "out"
};
inline static const Format csv{ "csv", ",", "\n", "csv" };
Mit folgender Elementfunktion wird ein Ausgabestrom erzeugt und zusätzlich überprüft,
ob die Zieldatei korrekt geöffnet wurde.
/**
* @brief get_output_stream Öffnen einer Ausgabedatei
* falls die Datei nicht geöffnet werden kann wird ein Fehler
* ausgeworfen
* @param fname Name der Ausgabedatei
* @return Ausgabestrom
*/
static std :: ofstream get_stream ( const std :: string &fname) {
std :: ofstream _ofs( fname . c_str ());
if (! _ofs) {
std :: string _msg = " error open file:" + fname;
throw std :: runtime_error ( nmx_msg_txt (_msg));
}
return _ofs;
}
Eine Datei wird, in einem Unterverzeichnis des Basisverzeichnisses abspeichert. Der Aus-
gabeordner muss existieren.
/**
* @brief get_output_stream Öffnen einer Ausgabedatei
* in einem Unterverzeichnis , Falls die Datei nicht
* geöffnet werden kann , wird ein Fehler ausgeworfen
* @param dirname Name des Unterverzeichnisses
* @param fname Name der Ausgabedatei
* @return Ausgabestrom
*/
static std :: ofstream get_stream ( const std :: string &dirname , const
std :: string & fname ) {
std :: stringstream _stream ;
_stream << settings :: Config :: data_directory //
<< "/" << dirname //
<< "/" << fname ; //
return get_stream ( _stream .str ());
}
/**
* @brief get_output_stream Öffnen einer Ausgabedatei
* in einem Unterverzeichnis . Falls die Datei nicht
* geöffnet werden kann , wird ein Fehler ausgeworfen
* @param dirname Name des Unterverzeichnisses
* @param fmt Formatierungsanweisung für den Ausgabestrom , der Name
* der Datei erhält einen Zusatz abhängig von diesem Parameter
* @return Ausgabestrom
*/
static std :: ofstream get_stream ( const std :: string &dirname , const
Format &fmt) {
std :: stringstream _stream ;
_stream << settings :: Config :: data_directory //
<< "/" << dirname //
<< "/" << fmt.name () //
<< "." << fmt.ext (); //
std :: ofstream ofs = get_stream ( _stream .str ());
fmt. set_defaults (ofs);
return ofs;
}
Ein Ausgabestrom für eine Zieldatei wird erzeugt. Der Dateiname wird durch eine weitere
Zeichenkette oder Zahl ergänzt. Damit können Namenskonflikte, die in der automatischen
Namensgenerierung ihre Ursache haben, vermieden werden.
/**
* @brief get_output_stream Öffnen einer Ausgabedatei
* in einem Unterverzeichnis . Falls die Datei nicht
* geöffnet werden kann , wird ein Fehler ausgeworfen
* @param dirname Name des Unterverzeichnisses oder der Datei
* @param fmt Formatierungsanweisung für den Ausgabestrom , der Name
* der Datei erhält einen Zusatz abhängig von diesem Parameter
* @param id Namenszusatz
* @return Ausgabestrom
*/
template <class T>
static std :: ofstream get_stream ( const std :: string &dirname , const
Format &fmt , const T &id) {
std :: stringstream _stream ;
_stream << settings :: Config :: data_directory //
<< "/" << dirname //
<< "/" << fmt.name () //
<< "-" << id << "." << fmt.ext ();
// erzeuge Ausgabestrom
std :: ofstream ofs = get_stream ( _stream .str ());
fmt. set_defaults (ofs);
return ofs;
Intern wird ein Formatierungsobjekt für die Ausgabe von Feldern gespeichert. Es existiert
nur eine Kopie, die über den <<-Operator verändert werden kann (Listing 2.199).
142 2 Klassen in C++
/**
* @brief _array internes Formatierungsobjekt
*/
inline static Format _array = Output :: csv;
/**
* @brief operator << überladener Ausgabeoperator internes
* Formatierungsobjekt wird gesetzt .
* @param os Ausgabestrom
* @param fmt Formatierungsobjekt
* @return Referenz auf Ausgabestrom
*/
inline std :: ostream &operator <<( std :: ostream &os , const Format &fmt) {
Output :: _array = fmt;
return os;
}
/**
* @brief array lese internes Formatierungsobjekt
* @return Referens auf gespeichertes Formatierungsobjekt
*/
inline static const Format & array () { return _array ; }
Beispiele
Das folgende Beispiel zeigt, wie eine Tabelle, zusammengesetzt aus zwei verschachtel-
ten std::vector-Containern, in eine Datei geschrieben wird. Einem Formatierungsobjekt
werden zwei Zeichenketten __func__ (Variable für den Funktionsnamen) und csv überge-
ben. Damit wird der Dateiname ex15.csv generiert und in einem Ordner unserer Wahl
innerhalb des Basisverzeichnisses gespeichert.
/**
* @brief ex15 schreibe Tabelle in eine Datei im csv - Format
*/
inline void ex15 () {
Format fmt{ __func__ , ",", "\n", "csv" };
std :: vector <std :: vector <double >> table{ //
{ 1, 2, 3, 4 },
{ 6, 7, 8, 9 },
{ 11, 12, 13, 14 }
};
2.8 Übungsaufgaben
1. Am unteren Ende eines homogenen Stabs der Masse m und Länge l wird eine Kugel
vom Radius R und Masse M befestigt. Das obere Ende des Stabs wird so befestigt,
dass das System um diesen Punkt frei drehbar ist (physikalisches Pendel). Entwerfen
Sie eine Klasse, welche die Eigenschaften des Systems abbildet.
2. Entwerfen Sie friend-Funktionen, welche die Addition, Subtraktion und Multiplika-
tion für Polynome (Abschnitt 2.3.1) implementieren. Ergänzen Sie diese Funktionen
um eine weitere, welche die Ableitung eines Polynoms berechnet.
3. Entwerfen Sie eine Klasse für ein Polynom dritten Grades und implementieren Sie
mithilfe der gsl-Routinen gsl_poly_solve_cubic, gsl_poly_complex_solve_cubic Ele-
mentfunktion zur Berechnung der Nullstellen.
4. Entwerfen Sie eine Basisklasse zur Beschreibung von regelmäßigen Polygonen. Leiten
Sie von dieser Klassen für ein Dreieck, Viereck, Fünf- und Sechseck ab.
5. Entwerfen Sie einen textbasierten interaktiven Test zum Rechnen mit ganzen Zahlen.
Es sollen dabei nur die vier Grundrechenarten zugelassen sein.
6. Ändern Sie die Klasse Factorial (Listing 2.145) so ab, dass die Berechnung der zwi-
schengespeicherten Werte nicht beim ersten Aufruf stattfindet, sondern erst, wenn
diese angefordert werden.
7. Entwerfen Sie eine Klasse, welche für eine vorgegebene Funktion der Form y = f (x)
Funktionswerte im CSV- und LATEX-Format speichert. Verwenden Sie zur Speicherung
der Daten die Klasse Output (Listing 2.194).
8. Entwerfen Sie eine Klasse, welche einen Text aus einer Datei lädt, die im Text vor-
kommenden Wörter zählt und die Ergebnisse im CSV-Format speichert.
9. Ein Teilchen bewegt sich entlang einer geraden Strecke. Der Ort des Teilchens als
Funktion der Zeit lautet x(t) = at3 + bt2 + ct, mit a = 3.2 m/s3 , b = −1 m/s2
und c = 10 m/s. Schreiben Sie ein Computerprogramm, welches für t = 0, . . . , 10 s
mit ∆t = 1 s den Ort, die Geschwindigkeit und die Beschleunigung des Teilchens
berechnet. Setzen Sie die Polynom-Klasse aus Abschnitt 2.3.1 sowie die Funktion zur
Berechnung der Ableitung eines Polynoms aus Aufgabe 2 ein.
3 Tabellen und Views
Übersicht
3.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.2 Eine Klasse für Zahlentabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145
3.3 Eine Klasse für Sichten auf Tabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
3.4 Erstellen von Views auf Zahlentabellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
3.5 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
3.6 Beispiele aus der Physik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
3.7 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
3.1 Einleitung
Die im letzten Kapitel eingeführten Klassen zur Formatierung von Daten und ihre Spei-
cherung in Dateien werden wir in diesem Kapitel um eine weitere Klasse zur Verwaltung
von Zahlentabellen ergänzen. Sie sollen hauptsächlich zum Speichern von Ergebnissen aus
numerischen Berechnungen eingesetzt werden. Das Hinzufügen von Daten, ihre Speiche-
rung in Dateien sowie eine einfache Suche werden Teil der Funktionalität der Klasse sein.
Views sind Sichten auf Daten. Sie sind eng mit Zahlentabellen verknüpft und machen es
möglich, mit Untermengen der Daten zu arbeiten, ohne Kopien anlegen zu müssen.
10 11 12 13
... ...
std::vector 30 31 32 33 std::array<double,4>
... ...
50 51 52 53
Abb. 3.1: Eine Zahlentabelle als ein std::vector mit std::array als Elemente
/**
* @brief The Data class Eine Klasse für Zahlentabellen mit fester
* Anzahl von Spalten . Die Anzahl der Reihen wächst automatisch . Intern
* wird ein std :: vector von std :: array angelegt
* @param N Anzahl der Spalten
*/
template < size_t N>
class Data
{
public :
using Row = std :: array <double , N >;
using Container = std :: vector <Row >;
using Column = std :: vector <double >;
using View = View <Data <N>>;
private :
Container _data ;
public :
/**
* @brief Data Standardkonstruktor die Tabelle enthält keine Daten
*/
inline Data () {}
Wir haben die Möglichkeit, auf den internen Container zuzugreifen und direkt mit ihm
zu arbeiten. Dies ist z.B. bei der Nutzung der stl-Algorithmen von Vorteil; andernfalls
müsste die Klasse zusätzlich stl-konforme Iteratoren definieren.
/**
* @brief data lesender Zugriff auf die interne Struktur
* @return Referenz auf interne Struktur
*/
inline const auto &data () const { return _data; }
/**
* @brief data schreibender Zugriff auf die interne Struktur
* @return Referenz auf interne Struktur
*/
inline auto &data () { return _data ; }
Auf die erste und letzte Zeile der Tabelle kann mittels der Elementfunktionen Listing 3.5
bis Listing 3.8 zugegriffen werden.
/**
* @brief first erste Zeile ( schreibender Zugriff )
* @return Referenz auf erste Zeile
*/
inline Row & first () { return * _data . begin (); }
/**
* @brief first erste Zeile ( lesender Zugriff )
* @return Referenz auf erste Zeile
*/
inline const Row & first () const { return *_data.begin (); }
/**
* @brief last letzte Zeile ( schreibender Zugriff )
* @return Referenz auf letzte Zeile
*/
inline Row &last () { return *( _data .end () - 1); }
/**
* @brief last letzte Zeile ( lesender Zugriff )
* @return Referenz auf letzte Zeile
*/
inline const Row &last () const { return *( _data.end () - 1); }
Auf die Reihen der Tabelle kann mit den Elementfunktionen Listing 3.9 und Listing 3.10
über einen Index zugegriffen werden.
/**
* @brief row Zugriff auf Zeile per Index (lesen)
* @param idx Zeilenindex
* @return Referenz auf Zeile
148 3 Tabellen und Views
*/
inline const auto &row( size_t idx) const {
// wenn ungültiger Index : out_of_range Ausnahme
return _data .at(idx);
}
/**
* @brief row Zugriff auf Zeile per Index ( schreiben )
* @param idx Zeilenindex
* @return Referenz auf Zeile
*/
inline auto &row( size_t idx) {
// wenn ungültiger Index : out_of_range Ausnahme
return _data .at(idx);
}
Wie viele Reihen und Spalten die Tabelle hat, kann über die nächsten beiden Funktionen
abgefragt werden.
/**
* @brief rows
* @return Anzahl der Reihen
*/
inline size_t rows () const { return _data.size (); }
/**
* @brief columns
* @return Anzahl der Spalten
*/
inline size_t columns () const { return N; }
Eine neue Zeile wird in die Tabelle eingefügt. Da Row ein Synonym für ein std::array
gleicher Länge wie die Anzahl der Tabellenspalten ist (Listing 3.1), muss keine Längen-
überprüfung stattfinden.
/**
* @brief add füge eine neue Zeile hinzu
* @param lst Array mit Zahlen
*/
inline void add( const Row &lst) { _data. push_back (lst); }
Eine neue Reihe kann auch über die Angabe einer Liste von Zahlen hinzugefügt werden.
Die Liste darf die vorgesehene Länge N nicht überschreiten, sie kann aber kürzer sein.
Damit besteht die Möglichkeit, die Tabelle in mehreren Etappen zu füllen.
/**
* @brief add füge eine neue Reihe hinzu. Die Anzahl der
* Elemente darf kleiner als die Anzahl der Spalten sein
* @param lst Liste mit Zeilenelementen
*/
inline void add(std :: initializer_list <double > lst) {
Check :: error_if <std :: out_of_range >( nmx_msgx ( __func__ ), //
lst.size () > N);
// instanziiere neue Zeile
Row crow;
// kopiere Elemente
std :: copy(std :: begin (lst), std :: end(lst), std :: begin(crow));
// addiere neue Reihe zur Tabelle
add(crow);
}
Es folgen zwei Versionen des Operators += zum Hinzufügen von Zeilen. Intern werden die
Methoden aus Listing 3.13 und Listing 3.14 aufgerufen.
/**
* @brief operator += Hinzufügen einer Reihe
* @param lst Array mit Zeilenelementen
* @return Referenz auf aufrufendes Objekt
*/
inline auto & operator +=( const Row &lst) {
add(lst);
return *this;
}
/**
* @brief operator += Hinzufügen einer Reihe
* @param lst Liste mit Zeilenelementen
* @return Referenz auf aufrufendes Objekt
*/
inline auto & operator +=( std :: initializer_list <double > lst) {
try {
add(lst);
} catch (...) {
// wenn ein Fehler auftritt , enthält die Nachricht
// den richtigen Funktionsnamen
throw std :: out_of_range ( nmx_msgx ( __func__ ));
}
return *this;
}
Die Daten der Tabelle werden einem Ausgabestrom übergeben. Jede Reihe wird mithilfe
der Elementfunktion Listing 2.186, formatiert ausgegeben.
/**
* @brief save Schreibe Tabelle in Datei
* @param ofs Ausgabestrom
* @param fmt Formatierungsanweisung
*/
inline void save(std :: ostream &ofs , Format fmt) const {
for (const auto &row : _data ) {
fmt. write_line (ofs , row);
}
}
Vor der Übergabe an den Ausgabestrom kann bei Bedarf auf jede Zeile eine Funktion
angewandt werden.
/**
* @brief save Schreibe Tabelle in Datei
* @param ofs Ausgabestrom
* @param fmt Formatierungsanweisung
* @param fn Funktion zur Bearbeitung der Zeilen
* vor dem Schreiben in die Datei
*/
template <class FN >
void save(std :: ostream &ofs , Format fmt , FN fn) const {
for (const auto &row : _data ) {
fmt. write_line (ofs , fn(row));
}
}
Eine Kopie einer Spalte (Listing 3.19) oder eines Teils einer Spalte (Listing 3.20) wird
erstellt.
/**
* @brief column Kopie einer Spalte
* @param cidx Spaltenindex
* @return Spalte als Column ( Synonym für std :: vector )
*/
auto column ( size_t cidx) const {
// erzeuge Vektor mit Länge gleich der Anzahl der Reihen
Column clmn(rows ());
for ( size_t idx = 0; idx < _data.size (); idx ++) {
clmn[idx] = _data [idx ][ cidx ];
}
return clmn;
}
/**
* @brief column erstelle Kopie einer Spalte
* @param cidx Spaltenindex
* @param fn Testfunktion
* @return Kopie der Spalte cidx
*/
template <class FN >
inline Column column ( size_t cidx , const FN &fn) {
Column clmn;
for (const auto &row : _data ) {
if (fn(row)) {
clmn. push_back (row[cidx ]);
}
}
return clmn;
}
}; // Data
Eine ganze Spalte einer Tabelle kann mit einem Aufruf verändert oder mit Werten gefüllt
werden, wenn sie leer ist (Listing 3.21).
/**
* @brief set_column Die Werte einer Spalte werden eingefügt
* oder ersetzt . Wenn die Tabelle leer ist wird erst Speicher
* reserviert
* @param clmn Spalte die kopiert werden soll
* @param idx Spaltenindex
*/
inline void set_column ( const Column clmn , size_t idx) {
if (_data . empty ()) { // Tabelle ist leer
for ( size_t jdx = 0; jdx < clmn.size (); jdx ++) {
Row crow; // erzeuge neue Reihe
crow[idx] = clmn[jdx ]; // setze Wert
_data . push_back (crow); // füge Reihe hinzu
}
} else { // Tabelle nicht leer
if (clmn.size () != rows ()) {
// Fehler : die neue Spalte hat nicht die gleiche Länge
// mit der alten Spalte
throw std :: range_error ( nmx_msgx ( __func__ ));
}
for ( size_t jdx = 0; jdx < rows (); jdx ++) {
_data [jdx ][ idx] = clmn[jdx ]; // ersetze Wert
}
}
}
Alle oder bestimmte Zeilen einer Tabelle können geändert werden. Intern wird über eine
Schleife ein Funktionsobjekt auf jede Zeile der Tabelle angewandt (Listing 3.22). Das
Funktionsobjekt kann so programmiert werden, dass es nur dann aktiviert wird, wenn
eine bestimmte Bedingung erfüllt wird.
152 3 Tabellen und Views
/**
* @brief apply ändere alle oder bestimmte Elemente
* @param fn Funktion , die als Eingabeargument eine
* Tabellenzeile erwartet
*/
template <class FN >
void apply(FN fn) {
for (auto &row : _data ) {
fn(row);
}
}
Daten aus einer CSV-Datei werden in eine Tabelle geladen. Die Implementierung basiert
auf dem Beispiel Listing 1.78 aus Abschnitt 1.13.
/**
* @brief read_csv lese Daten aus csv -Datei
* @param ifs Eingabestrom
*/
inline void read_csv (std :: ifstream &ifs) {
// teste Status von Eingabestrom
Check :: error_if (nmx_msg , !ifs. is_open ());
std :: string line;
while ( getline (ifs , line)) {
std :: stringstream stream (line);
std :: string token ;
size_t idx = 0;
char *ptr;
std :: array <double , N> buffer ;
while ( getline (stream , token , ',')) {
buffer [idx] = std :: strtod ( token. c_str (), &ptr);
idx ++;
}
add( buffer );
}
}
Wir schließen vorerst die Beschreibung der Zahlentabelle ab, um eine Klasse zur Erzeu-
gung von Sichten (Views) auf Zahlentabellen vorzustellen.
10 11 12 13
20 21 22 23 View 10 11 12 13
30 31 32 33 30 31 32 33
40 41 42 43 40 41 42 43
50 51 52 53
Abb. 3.2: Sicht auf eine Zahlentabelle, View zeigt auf die Zeilen 0,2,3
tieren eine View als Klassentemplate mit einem Template-Parameter, der dem Typ des
Containers entspricht, auf dessen Daten die View zeigen soll. Intern wird ein Zeiger auf
eine Instanz einer Tabelle und eine Indexliste gespeichert. Die Indexliste gibt vor, auf
welche Reihen zugegriffen werden darf. Sie muss sortiert sein und darf keine doppelten
Einträge haben. Ein zur Speicherung der Indexliste geeigneter Container ist std::set,
weil dieser Elemente in einer sortierten Reihenfolge nur einmal speichert.
/**
* @brief The View class Sicht auf Teilmenge eines Containers
* @param CONTAINER Typ des Containers
*/
template <class CONTAINER >
class View
{
private :
// Zeiger auf einen Container
const CONTAINER * _data = nullptr ;
public :
Der Konstruktor erwartet eine Instanz eines Containers und setzt einen Zeiger auf sie.
Die Indexliste ist noch leer (Listing 3.25).
/**
* @brief View Konstruktor
* @param input Container
*/
inline View( const CONTAINER & input )
: _data { & input } {}
Eine Kopie einer View kann mithilfe des Kopierkonstruktors erzeugt werden.
/**
* @brief View Kopierkonstruktor
* @param v View
*/
inline View( const View &v)
154 3 Tabellen und Views
: _data { v. _data }
, _indexlist { v. _indexlist } {}
Ein move-Konstruktor ist ebenfalls implementiert. Er übernimmt neben dem Zeiger auf
den Container auch die Indexliste der Vorlage.
/**
* @brief View Move - Konstruktor
* @param v View
*/
inline View(View &&v)
: _data { nullptr } {
// vertausche Zeiger und Indexliste
std :: swap(_data , v. _data );
std :: swap( _indexlist , v. _indexlist );
//v._data ist ungültig und v. _indexlist ist leer
}
/**
* @brief operator = Zuweisungsoperator
* @param v View
* @return Referenz auf aufrufendes Objekt
*/
inline auto & operator =( const View &v) {
if (this != &v) {
_data = v. _data ;
_indexlist = v. _indexlist ;
}
return *this;
}
/**
* @brief operator =
* @param v View
* @return Referenz auf das aufrufende Objekt
*/
inline auto & operator =( View &&v) {
if (this != &v) {
std :: swap(_data , v. _data );
std :: swap( _indexlist , v. _indexlist );
v._data = nullptr ;
v. _indexlist . clear ();
}
return this;
}
/**
* @brief empty
* @return true wenn die View auf keine Daten zeigt
*/
inline bool empty () const { return _indexlist .empty (); }
Zeigt die View auf gültige Daten, so gibt die nächste Funktion true zurück.
/**
* @brief is_valid
* @return true wenn die View auf einen Container zeigt
*/
inline bool is_valid () const { return _data != nullptr ; }
Einer View wird ein einzelner Index (Listing 3.32) oder gleichzeitig mehrere Indizes (Lis-
ting 3.33) übergeben.
/**
* @brief add addiere Elemente
* @param idx Index
*/
inline void add( size_t idx) { _indexlist . insert (idx); }
/**
* @brief add addiere Elemente über Index -Liste
* @param idxlst Indexliste
*/
template <class T>
void add( const T & idxlst ) {
_indexlist . insert ( idxlst . begin () , idxlst .end ());
}
Die Reihen einer Zahlentabelle, auf welche die View zeigt, werden einem Ausgabestrom
übergeben.
/**
* @brief save Speichere Container auf den die View zeigt
* @param ofs Ausgabestrom
* @param fmt Formatierung
*/
inline void save(std :: ofstream &ofs , Format fmt) const {
for (const auto idx : _indexlist ) {
const auto &crow = _data ->row(idx);
156 3 Tabellen und Views
Die Daten einer View werden vor der Übergabe an einen Ausgabestrom von einem Funk-
tionsobjekt bearbeitet.
/**
* @brief save Speichere Container auf den die View zeigt
* @param ofs Ausgabestrom
* @param fmt Formatierung
* @param fn Funktion zur Bearbeitung der Reihe
*/
template <class FN >
void save(std :: ofstream &ofs , Format fmt , FN fn) const {
for (const auto idx : _indexlist ) {
const auto crow = fn(_data ->row(idx));
fmt. write_line (ofs , crow);
}
}
Eine Kopie einer Spalte wird erstellt. Es werden nur die Elemente aufgenommen, auf
welche die View Zugriff hat.
/**
* @brief column Kopie einer Spalte
* @param cidx Spaltenindex
* @return Kopie Spalte (im Standardfall ein std :: vector )
*/
inline auto column ( size_t cidx) const {
// der Datentyp der Spalte wird über den Template - Parameter
// festgelegt . ( reserviere Speicher )
typename CONTAINER :: Column clmn( _indexlist .size ());
auto clmnitr = clmn. begin ();
for (const auto &idx : _indexlist ) {
// idx : Reihe auf welche die View Zugriff hat
// cidx: die gewünschte Spalte
* clmnitr = _data ->row(idx)[cidx ];
// zeige auf nächstes Element
clmnitr ++;
}
return clmn;
}
}; // View
} // namespace nmx
/**
* @brief view die ganze Tabelle als view
* @return eine Instanz vom Typ View
*/
inline auto view () const { return View (* this); }
Wir definieren ein Filter als eine Funktion oder als ein Funktionsobjekt, welches Zeilen
einer Tabelle einliest und diese einer View nach bestimmten Kriterien hinzufügt. Nützlich
ist dies z.B. für die Darstellung von berechneten Daten in Tabellen. Ist die berechnete
Datenmenge zu groß, wird die Tabelle schnell unübersichtlich. Eine Sicht auf eine re-
präsentative Untermenge der Daten könnte dann als Tabelle dargestellt werden. Mit der
Elementfunktion (Listing 3.38) wird eine View auf die ganze oder eine Teilmenge einer
Zahlentabelle erzeugt. Eine Testfunktion liest jede Reihe der Tabelle und fügt, wenn ein
vorgegebenes Kriterium erfüllt wird, den entsprechenden Index zu der View hinzu. Die
Testfunktion, welche die Rolle eines Filters spielt, kann jede Art von Funktionsobjekt
sein, welches eine Tabellenzeile einliest und entweder true oder false zurückgibt. Mit
zwei optionalen Parametern kann festgelegt werden, ob der View die erste und/oder die
letzte Tabellenzeile hinzugefügt wird.
/**
* @brief view Teile der Tabelle als View
* @param fn Funktionsobjekt zur Auswahl der Tabellenreihen
* @param f optional füge erste Tabellenzeile hinzu
* @param l optional füge letzte Tabellenzeile hinzu
* @return eine Instanz vom Typ View
*/
template <class PREDICATE >
auto view( PREDICATE fn , bool f = true , bool l = true) const {
View view (* this);
if (f) { // füge erste Zeile der Tabelle hinzu?
view.add( static_cast <size_t >(0));
}
for ( size_t idx = 0; idx < rows (); idx ++) {
if (fn( _data [idx ])) {
view.add(idx); // addiere Reihenindex zur View
}
}
if (l) { // füge letzte Zeile der Tabelle hinzu?
view.add( _data .size () - 1);
}
return view;
}
Eine View kann auch durch die Angabe einer Liste von Iteratoren, welche auf ausgewählte
Elemente eines Containers zeigen, erzeugt werden. Optional können die erste und letzte
Zeile des Containers hinzugefügt werden. Intern wird der Index eines Elements mit der
stl-Funktion std::distance ermittelt.
/**
* @brief view Teile der Tabelle als View
* @param lst eine Liste mit Iteratoren , welche auf Elemente
* des Containers zeigen
* @param f füge erste Tabellenzeile hinzu ( optional )
* @param l füge letzte Tabellenzeile hinzu ( optional )
* @return eine Instanz vom Typ View
*/
inline auto view(std :: initializer_list < typename
Container :: iterator > lst ,
bool f = true ,
bool l = true) {
View view (* this);
Beginnend ab einer bestimmten Reihe der Zahlentabelle werden, mit einer vorgegebenen
Schrittweite, weitere Reihen einer View hinzugefügt.
/**
* @brief view Teile der Tabelle als View
* @param step Schrittweite
* @param startidx erste Stelle im Container
* @param last füge letzte Tabellenzeile hinzu ( optional )
* @return eine Instanz vom Typ View
*/
inline auto select_rows ( size_t step , size_t startidx = 0, bool last
= false) const {
View view (* this);
for ( size_t idx = startidx ; idx < _data.size (); idx += step) {
// addiere Reihenindex zur View
view.add(idx);
}
Eine View wird, mit einer vorgegebenen Anzahl von äquidistanten Reihen einer Zahlen-
tabelle, aufgebaut.
/**
* @brief get_total_rows generiere View mit eine Instanz vom Typ
View
* bestimmter Anzahl von Spalten
* @param n Anzahl der Reihen
*/
inline auto select_total_rows ( size_t n, size_t start = 0, bool l =
true) const {
// berechne Abstand zwischen den Zeilen
const size_t stepSize = std :: ceil (( rows ()) / (n - 1));
return select_rows (stepSize , start , l);
}
3.5 Beispiele
3.5.1 Generieren einer Tabelle und Speicherung in eine Datei
Eine leere Tabelle wird erzeugt und mit vier Zeilen aus jeweils drei Elementen gefüllt. Ein
Formatierungsobjekt (siehe Abschnitt 2.7.2) sorgt dafür, dass die Daten im Verzeichnis
x020 und unter dem automatisch generierten Namen ex16.csv gespeichert werden.
/**
* @brief ex16 Aufbau einer Tabelle und Ausgabe in Datei
*/
inline void ex16 () {
Data <3> data; // Tabelle mit drei Spalten
// fülle Tabelle mit Daten
data += { 1, 2, 3 }; // Reihe 0
data += { 10, 20, 30 }; // Reihe 1
data += { 100 , 200 , 300 }; // Reihe 2
data += { 1000 , 2000 , 3000 }; // Reihe 3
// Formatierung
Format fmt{ __func__ , ",", "\n", "csv" };
// Ausgabe
auto csvstream = Output :: get_stream ("x020", fmt);
data.save(csvstream , fmt);
}
Eine Tabelle enthält pro Reihe einen Winkel in Bogenmaß und den Sinus und Kosinus
dieses Winkels. Gespeichert werden die Winkel in Gradmaß. Die Daten werden vor dem
Speichern in die Datei umgerechnet. Das Anlegen einer Kopie ist somit nicht nötig, wie
in Listing 3.44 zu sehen ist.
/**
* @brief ex17 Ausgabe einer Tabelle in Datei. Jede Zeile wird vor
* dem Speichern bearbeitet
*/
inline void ex17 () {
using Data = Data <3 >;
// Tabelle mit drei Spalten
Data data;
// Formatierung
Format fmt{ __func__ , ",", "\n", "csv" };
// Ausgabe
auto csvstream = Output :: get_stream ("x020", fmt);
data.save(csvstream , fmt , []( const auto &crow) {
return Data :: Row{ Math :: to_degrees (crow [0]) , crow [1], crow [2] };
});
}
Eine Tabelle mit vier Spalten wird erzeugt. Jede Spalte stellt einen dreidimensionalen
Vektor dar. Wir wollen in einer neuen Tabelle die normierten Vektoren speichern.
/**
* @brief ex18 Bearbeitung einer Tabelle . Berechnung der Normen von
* Vektoren
*/
inline void ex18 () {
// Tabelle mit vier Spalten
using Data = Data <4 >;
Data table;
table += { -4, 3, 7, 1 };
table += { -2, 8, 2, 0 };
table += { 9, 1, 1, 2 };
// Kopiere Tabelle
Data normtable = table ;
// normiere Vektoren
for (auto &crow : normtable .data ()) {
for ( size_t idx = 0; idx < crow.size (); idx ++) {
crow[idx] /= results [idx ];
}
}
// füge als letzte Reihe die Norm hinzu
normtable += results ;
// formatierte Ausgabe
Format fmt{ __func__ , " ,\t", "\n", "csv" };
fmt. precision = 3;
auto csvstream = Output :: get_stream ("x020", fmt);
// ursprüngliche Tabelle
table.save(csvstream , fmt);
csvstream << " -------------------------------" << std :: endl;
// Tabelle mit normierten Vektoren und Normen
normtable .save(csvstream , fmt);
}
Nach Ausführung der Funktion erhalten wir die ursprüngliche Tabelle und die neue mit
den normierten Vektoren. Die letzte Reihe der neuen Tabelle enthält die Normen der
Vektoren.
Alle Elemente einer Tabelle, die größer als 6 sind, werden mit −1 multipliziert. Die
bearbeitete Tabelle wird in eine Datei gespeichert.
/**
* @brief ex19 Suchen und Bearbeiten von Elementen einer Tabelle
*/
inline void ex19 () {
// Tabelle mit vier Spalten
using Data = Data <4 >;
Data table;
table += { 1, 2, 3, 4 };
table += { 5, 6, 7, 8 };
table += { 9, 10, 11, 12 };
// Formatierung
Format fmt{ __func__ , ",", "\n", "csv" };
// Ausgabe
auto csvstream = Output :: get_stream ("x020", fmt);
table.save(csvstream , fmt);
}
Eine Tabelle wird mit Daten aus einer Datei (CSV-Format) gefüllt. Diejenigen Elemente,
die kleiner als −6 sind, werden gleicht 0 gesetzt. Die Tabelle wird anschließend in eine
Datei gespeichert.
/**
* @brief ex20 lade Tabelle aus csv -Datei ändere Werte
* und speichere Tabelle wieder ab
*/
inline void ex20 () {
using Data = Data <4 >;
Data table;
// generiere Namen
std :: string filename = settings :: Config :: data_directory + //
"/x020/ex19.csv";
// lade Tabelle
std :: ifstream ifs( filename );
table. read_csv (ifs);
// Ausgabe
Format fmt{ __func__ , " ,\t", "\n", "csv" }; // Formatierung
auto csvstream = Output :: get_stream ("x020", fmt);
table.save(csvstream , fmt);
}
Eine Wertetabelle für die Funktionen f1 (x) = x3 und f2 (x) = x5 mit −5 ≤ x < 5 und
∆x = 1 wird erzeugt. Die View zeigt nur auf Tabellenreihen mit positiven Funktions-
werten.
/**
* @brief ex21 Filter für Funktionswerte (Sicht auf Tabelle )
*/
inline void ex21 () {
164 3 Tabellen und Views
// Erzeuge Daten
for ( double x = -5; x < 5; x += 1) {
table += { x, pow(x, 3) , pow(x, 5) };
}
// Ausgabe
Format fmt{ __func__ , " ,\t", "\n", "csv" }; // Formatierung
auto csvstream = Output :: get_stream ("x020", fmt);
view.save(csvstream , fmt);
}
In einem weiteren Beispiel wird eine Sicht auf eine Tabelle erzeugt. Jede zweite Reihe
wird in die View aufgenommen. Jede Reihe der Tabelle enthält die Werte x, x3 , sin x mit
−4 ≤ x ≤ 4 und ∆x = 1. Die View zeigt auf die Reihen mit −3 ≤ x ≤ 3 und ∆x = 2.
/**
* @brief ex22 Filter für Funktionswerte : Filter für jede zweite
* Reihe der Tabelle
*/
inline void ex22 () {
using Data = Data <3 >;
Data table;
// Erzeuge Daten
for ( double x = -4; x < 4; x += 1) {
table += { x, pow(x, 3) , sin(x) };
}
// formatierte Ausgabe
Format fmt{ __func__ , " ,\t", "\n", "csv" };
auto csvstream = Output :: get_stream ("x020", fmt);
table.save(csvstream , fmt);
3.5 Beispiele 165
Wir stellen den Sinus und Kosinus für −π ≤ x < π und ∆x = π4 in eine Tabelle dar.
Eine View zeigt auf die Reihen der Tabelle, für welche cos x < 0 ist. Eine Kopie der
Spalte mit den Werten für den Kosinus wird erstellt. Die Zahlentabelle, die View sowie
die Kopie der Spalte werden in eine Datei geschrieben.
/**
* @brief ex23 Kopie einer Spalte
*/
inline void ex23 () {
using Data = Data <3 >;
Data table;
// erzeuge Daten
for ( double x = -Math :: PI; x < Math :: PI; x += 0.25 * Math ::PI) {
table += { x, std :: cos(x), std :: sin(x) };
}
// Ausgabe
Format fmt{ __func__ , " ,\t", "\n", "csv" }; // Formatierung
auto csvstream = Output :: get_stream ("x020", fmt);
166 3 Tabellen und Views
table.save(csvstream , fmt);
csvstream << " --------------" << std :: endl;
view.save(csvstream , fmt);
csvstream << " --------------" << std :: endl;
Data :: Column clmn = view. column (1);
fmt. write_line (csvstream , clmn);
}
Ein Teilchen wird vom Boden mit einer Anfangsgeschwindigkeit v0 = 20 m/s vertikal
nach oben geworfen (g = 9.81 m/s2 ).
1. Wie lange dauert es, bis das Teilchen seine maximale Höhe erreicht? Wie lange braucht
es, um zur Anfangsposition zurückzukehren, und mit welcher Geschwindigkeit kommt
es dort an?
2. Erstellen Sie Bewegungsdaten für Ort und Geschwindigkeit des Teilchens als Funktion
der Zeit. An welcher Stelle im Datensatz wechselt die Geschwindigkeit ihr Vorzeichen?
Finden Sie einen Näherungswert für den Zeitpunkt, zu dem das Teilchen auf den
Boden fällt.
3.6 Beispiele aus der Physik 167
Lösung
Wir legen die positive z-Achse entlang der Bewegungsrichtung. Die Bewegung des Teil-
chens wird durch folgende Gleichungen
Das Computerprogramm
Kurzbeschreibung Wir werden den Ort und die Geschwindigkeit als Funktion der Zeit
mit Gl. 3.1a und Gl. 3.1b berechnen und die Daten in eine Zahlentabelle speichern.
Anschließend werden wir eine View erstellen, welche auf die Stelle zeigt, in der die Ge-
schwindigkeit ihr Vorzeichen wechselt. Zur View werden wir auch die letzte Reihe der
Tabelle hinzufügen.
Quelltext und Daten Wir wählen für die Berechnung sehr kleine Zeitschritte, um gute
Näherungswerte für die Zeitpunkte t1 und t2 zu erhalten. Mithilfe des stl-Algorithmus
std::adjacent_find finden wir, die für uns interessante Reihen der Tabelle und fügen
diese zu einer View hinzu (std::adjacent_find findet die Stelle in einem Container, in
der zwei direkt benachbarte Elemente eine vorgegebene Bedingung erfüllen).
/**
* @brief ex23 Teilchen wird vertikal nach oben geworfen
*/
inline void ex23 () {
using namespace gravitation ;
// Tabelle mit drei Spalten t,z,v
using Data = Data <3 >;
Data table;
// Anfangsbedingungen
const double z0 = 0.0 , v0 = 20.0;
const double g = Earth ::g;
// Ausgabe
output << std :: setprecision (3);
output << std :: setw (5) << "t" << std :: setw (10) << "z" <<
std :: setw (10) << "v" << std :: endl;
output << " -------------------------------" << std :: endl;
view.save(output , Output :: csv);
output << " -------------------------------" << std :: endl;
// die exakten Werte
Data :: Row r1{ v0 / Earth ::g, 0.5 * pow(v0 , 2) / Earth ::g, 0 };
Data :: Row r2{ 2 * v0 / Earth ::g, 0, -v0 };
Output :: csv. as_rows (output , r1 , r2);
}
t z v
-------------------------------
2.039 e+00 ,2.039e+01 ,4.241e -03
2.040 e+00 ,2.039e+01 , -5.566e -03
4.078 e+00 ,1.729e -02 , -1.999e+01
-------------------------------
2.039 e+00 ,2.039e+01 ,0.000e+00
4.079 e+00 ,0.000e+00 , -2.000e+01
Die ersten drei Reihen sind die gefundenen Näherungswerte und die beiden folgenden die
exakten Ergebnisse.
1. Wir groß ist die Beschleunigung des Blocks? Welche Strecke legt der Block innerhalb
der ersten t1 Sekunden zurück? Welche Arbeit leistet die Kraft innerhalb dieser Zeit?
2. Erstellen Sie für m = 2 kg, |F ⃗ | = 10 N, t1 = 10 s und für die Winkel φ =
◦ ◦ ◦ ◦
10 , 30 , 45 , 60 eine Tabelle, die in jeder Reihe die x- und y-Komponente der Kraft,
die von ihr verrichtete Arbeit und die vom Block zurückgelegte Strecke enthält. Spei-
chern Sie die Tabelle im LATEX-Format ab.
Lösung
Das Koordinatensystem Wir legen die positive x-Achse entlang der Bewegungsrichtung
des Blocks und senkrecht zur ihr nach oben zeigend die positive y-Achse.
m⃗r¨ = N
⃗ +F
⃗ + w,
⃗ (3.3)
⃗ die Normalkraft und w
wobei N ⃗ die Gewichtskraft ist. Wir multiplizieren diese Gleichung
nacheinander skalar mit ⃗ex , dann mit ⃗ey und erhalten:
{
mẍ = F cos φ
mÿ = N + F sin φ − mg = 0
oder
F
ẍ = cos φ (3.5a)
m
N F
ÿ = + sin φ − g = 0 (3.5b)
m m
Die Geschwindigkeit als Funktion der Zeit erhalten wir durch Integration von Gl. 3.5a
nach der Zeit unter Berücksichtigung der Anfangsbedingung v(0) = 0:
∫ t
F F
v(t) = cos φ dt = cos φ t. (3.6)
0 m m
Eine weitere Integration nach der Zeit, zusammen mit x(0) = 0, liefert die Ortskoordi-
nate: ∫ t
F 1F
x(t) = cos φ t′ dt′ = cos φ t2 (3.7)
0 m 2m
Die von der Kraft verrichtete Arbeit berechnen wir über folgendes Integral:
∫ x ∫ x
WF = F · d⃗r =
⃗ F cos φ dx′ = F cos φ x (3.8)
0 0
Das Computerprogramm
Kurzbeschreibung
Implementierung der Formeln Gl. 3.6, Gl. 3.7, Gl. 3.8 und Speicherung der Daten in
einer Zahlentabelle mit sieben Spalten
Ausgabe der Daten in eine Datei im LATEX-Format
170 3 Tabellen und Views
Quelltext und Daten Innerhalb einer Schleife werden durch Anwendung der herge-
leiteten Formeln Daten für unterschiedliche Winkel berechnet und einer Zahlentabelle
hinzugefügt.
/**
* @brief ex24 Kraft beschleunigt eine Masse auf reibungsfreien Boden
*/
inline void ex24 () {
const double mass = 2.0;
const double force = 10.0;
Data <7> table ;
const double t = 10.0;
// für jeden Winkel ...
for ( double phi : { 10, 30, 45, 60 }) {
// ... berechne Daten
const double phirad = Math :: to_radians (phi);
const double fx = force * cos( phirad );
const double fy = force * sin( phirad );
const double ax = fx / mass;
const double vx = ax * t;
const double x = 0.5 * ax * pow(t, 2);
const double work = fx * x;
// ... und füge eine neue Reihe zur Tabelle hinzu
table += { phi , fx , fy , ax , vx , x, work };
}
// speichere Daten in Datei
std :: ofstream csvstream = Output :: get_stream ("x021", Output :: latex ,
__func__ );
table.save(csvstream , Output :: latex );
}
Ein Geschoss der Masse m1 bewegt sich geradlinig mit einer horizontalen Geschwindigkeit
v und trifft auf einen ruhenden Holzblock mit der Masse M und bleibt in ihm stecken.
1. Mit welcher Geschwindigkeit ⃗v ′ bewegen sich der Holzblock und das Geschoss direkt
nach dem Stoß?
2. Wenn m = 200 g, erstellen Sie eine LATEX-Tabelle, in der für drei unterschiedliche
Holzblockmassen M1 = 1 kg, M2 = 5 kg und M3 = 10 kg und Geschossgeschwindig-
3.6 Beispiele aus der Physik 171
keiten v1 = 200 m/s, v2 = 400 m/s und v3 = 600 m/s die Geschwindigkeiten direkt
nach dem Stoß des Systems Holzblock und Geschoss aufgelistet werden. Ergänzen Sie
die Daten mit den kinetischen Energien des Systems vor und nach dem Stoß sowie
den Energieverlust in Prozent.
Lösung
Das Koordinatensystem Wir legen die positive x-Achse entlang der Bewegungsrichtung
des Geschosses.
Die Beschreibung des Stoßvorgangs Auf das System wirken keine äußeren Kräfte. Der
Gesamtimpuls des Systems bleibt in der Zeit kurz vor und direkt nach dem Stoß erhalten.
oder
m
v′ = v. (3.10)
m+M
Für die kinetische Energie des Systems vor und nach dem Stoß sowie den Energieverlust
in Prozent gelten die Formeln:
1 1 |K ′ − K|
K= mv 2 , K′ = (m + M )v ′2 , 100 % (3.11)
2 2 K
Das Computerprogramm
Kurzbeschreibung
Quelltext und Daten Mit zwei ineinander geschachtelten Schleifen berechnen wir, in
Abhängigkeit von M und v, Daten und speichern sie in eine Zahlentabelle. Diese wird
anschließend in eine Datei im LATEX-Format geschrieben (Tab. 3.2).
/**
* @brief ex25 Geschoss trifft auf ruhenden Holzblock
*/
inline void ex25 () {
// Datenobjekt
using Data = Data <6 >;
Data table;
// Masse des Geschosses
const double m = 200.0 _g;
// erzeuge Daten für drei Holzblockmassen und
// drei Kugelgeschwindigkeiten
for (auto M : { 1.0 , 5.0 , 10.0 }) {
const double factor = m / (m + M);
172 3 Tabellen und Views
Tab. 3.2: Geschoss mit Masse m = 200 g trifft auf ruhenden Block.
K ′ −K
M [kg] v[m/s] v ′ [m/s] K[J] K ′ [J] K
%
1.000e+00 2.000e+02 3.333e+01 4.000e+03 6.667e+02 8.333e+01
1.000e+00 4.000e+02 6.667e+01 1.600e+04 2.667e+03 8.333e+01
1.000e+00 6.000e+02 1.000e+02 3.600e+04 6.000e+03 8.333e+01
5.000e+00 2.000e+02 7.692e+00 4.000e+03 1.538e+02 9.615e+01
5.000e+00 4.000e+02 1.538e+01 1.600e+04 6.154e+02 9.615e+01
5.000e+00 6.000e+02 2.308e+01 3.600e+04 1.385e+03 9.615e+01
1.000e+01 2.000e+02 3.922e+00 4.000e+03 7.843e+01 9.804e+01
1.000e+01 4.000e+02 7.843e+00 1.600e+04 3.137e+02 9.804e+01
1.000e+01 6.000e+02 1.176e+01 3.600e+04 7.059e+02 9.804e+01
Ein Teilchen der Masse m befindet sich in einer Höhe z über der Erdoberfläche.
1. Welche Kraft übt die Erde auf das Teilchen aus? Wie groß ist die potenzielle Energie
des Systems Erde-Teilchen?
2. Tragen Sie in eine Tabelle die Kraft und die potenzielle Energie als Funktion der Höhe
ein, beginnend von der Erdoberfläche bis zu einem zweifachen Erdradius. Speichern
Sie die Tabelle im LATEX-Format ab.
Lösung
Das Koordinatensystem Wir wählen die positive z-Achse senkrecht zur Erdoberfläche.
Sie soll in Richtung des Teilchens zeigen.
3.6 Beispiele aus der Physik 173
Formeln für die Kraft und die potenzielle Energie Bei großen Abständen von der
Erdoberfläche kann die Gravitationskraft nicht mehr als konstant angesehen werden. Die
Kraft und die potenzielle Energie berechnen wir mit den Formeln
⃗ = −G M m ⃗ez ,
F U = −G
Mm
, (3.12)
z2 z
wobei M die Masse der Erde, G die universelle Gravitationskonstante und z der Abstand
vom Erdmittelpunkt ist.
Das Computerprogramm
Kurzbeschreibung
Quelltext und Daten Wir setzen m = 10 kg und berechnen Daten für z = R bis
z = 2R in Schritten von ∆z = 1000 km, wobei R der Erdradius ist. Die Daten werden
in einer Zahlentabelle gespeichert, die dann im LATEX-Format in eine Datei geschrieben
wird (Tab. 3.3).
/**
* @brief ex26 Kraft und potenzielle Energie im Gravitationsfeld der
Erde
*/
void ex26 () {
using namespace gravitation ; // für die universelle
Gravitationskonstante
Data <3> table ;
// Abstände xmin = R bis xmax = 2* R...
const auto xmin = Earth :: radius ;
const auto xmax = 2 * xmin;
// ... mit Schrittweite
const auto dx = 1000.0 _km;
const auto mass = 10.0;
const auto factor = Astronomy ::G * Earth :: mass * mass;
Tab. 3.3: Gravitationskraft und potenzielle Energie eines Teilchens der Masse m = 10 kg
als Funktion vom Abstand zur Erdoberfläche
z[m] F [N ] U [J]
6.378e+06 -2.449e+01 -3.124e+08
7.378e+06 -2.106e+01 -2.897e+08
8.378e+06 -1.830e+01 -2.701e+08
9.378e+06 -1.605e+01 -2.529e+08
1.038e+07 -1.419e+01 -2.378e+08
1.138e+07 -1.264e+01 -2.244e+08
1.238e+07 -1.133e+01 -2.125e+08
Für ein System von Teilchen, die sich an festen Orten befinden, soll mittels eines Com-
puterprogramms der Massenschwerpunkt für folgende Konfiguration berechnet werden:
Drei Massenpunkte mit Massen m1 = 1 kg, m2 = 2 kg, m3 = 3 kg, und Ortsvektoren
⃗r1 = 0, ⃗r2 = 4 ⃗ex m, ⃗r3 = 2 ⃗ex m + 3.46 ⃗ey m.
Lösung
gegeben, wobei mi die Massen des Systems und ⃗ri ihre Ortsvektoren sind.
Das Computerprogramm
Kurzbeschreibung
Speicherung der Massen der Teilchen und ihrer Koordinaten in eine Tabelle
Anwendung der Formel Gl. 3.13 zur Berechnung des Massenschwerpunktes
Einfügen der Ergebnisse in die Tabelle
Ausgabe der Daten in eine Datei im LATEX-Format (Tab. 3.4)
Quelltext und Daten Eine Tabelle mit drei Spalten wird mit den Daten der drei Mas-
sen gefüllt. Es folgt eine λ-Funktion, welche die Formel (Gl. 3.13) implementiert. Zu-
sätzlich erhält die λ-Funktion eine Referenz auf zwei Variablen, in denen die gesuchten
Ergebnisse gespeichert werden sollen. Die λ-Funktion wird auf jede Reihe der Tabelle
angewandt. Bei jeder zusätzlichen Masse und ihrer Ortskoordinaten werden die Daten
für den Schwerpunkt (Zähler und Nenner in Gl. 3.13) aktualisiert.
3.6 Beispiele aus der Physik 175
/**
* @brief ex27 Massenschwerpunkt von drei Teilchen
*/
void ex27 () {
using Data = Data <3 >;
Data data;
// Ausgabe
std :: ofstream ofs = Output :: get_stream ("x021", Output :: latex ,
__func__ );
data.save(ofs , Output :: latex );
}
Tab. 3.4: Massen und Koordinaten eines Systems von drei Teilchen und ihres Schwer-
punktes. Die letzte Reihe enthält die Masse und die Koordinaten des Massenschwerpunk-
tes
m [kg] x [m] y [m]
1.000e+00 0.000e+00 0.000e+00
2.000e+00 4.000e+00 0.000e+00
3.000e+00 2.000e+00 3.460e+00
6.000e+00 2.333e+00 1.730e+00
176 3 Tabellen und Views
3.7 Übungsaufgaben
1. Erstellen Sie eine Datentabelle für das Polynom f (x) = x5 − x4 − x3 − x2 − x + 1 für
x ∈ [−1, 2] mit ∆x = 0.02.
a) Suchen Sie in der Tabelle Näherungswerte für den maximalen und minimalen Wert.
b) Filtern Sie die Tabellenwerte für die f (x) > 0.
c) Speichern Sie die Werte im CSV- und LATEX-Format in Dateien ab. Für die LATEX-
Daten wählen Sie eine Schrittweite von ∆x = 0.5.
2. Ein Teilchen der Masse m = 2 kg wird aus einer Höhe H = 10 m fallen gelassen. Er-
stellen Sie eine Zahlentabelle mit Bewegungsdaten für das Teilchen. Dabei sollen fol-
gende Größen enthalten sein: Zeit, Höhe, Geschwindigkeit, kinetische und potenzielle
Energie. Berechnen Sie aus dem Mittelwert der Differenzen ∆v ∆t einen Näherungswert
für die Beschleunigung des Teilchens. Setzen Sie die Fallbeschleunigung g als bekannt
voraus.
3. Eine Masse m = 1 kg ist am Ende einer idealen Feder mit der Federkonstante k =
100 N/m befestigt und schwingt reibungsfrei auf horizontalem Boden.
a) Berechnen Sie die Auslenkung, die Geschwindigkeit und die Federkraft sowie die
potenzielle, kinetische und Gesamtenergie als Funktion der Zeit. Erstellen Sie eine
Tabelle für die Dauer einer Periode.
b) Finden Sie mittels einfacher Suche im generierten Datensatz einen Näherungswert
für die Zeit, zu der die potenzielle und die kinetische Energie gleich sind. Verglei-
chen Sie das Ergebnis mit dem exakten Wert.
4. Vier Massenpunkte mit Massen m1 = 2 kg, m2 = 6 kg, m3 = 1 kg und m4 = 10 kg,
befinden sich jeweils auf den Ecken eines Quadrats mit Seitenlänge l = 2 m. Berechnen
Sie mithilfe eines Computerprogramms, die Gesamtmasse und den Ortsvektor des
Massenschwerpunktes.
5. Zwei Autos befinden sich in einem Abstand d und bewegen sich mit konstanten Ge-
schwindigkeiten v1 und v2 geradlinig aufeinander zu. Ein Insekt fliegt mit einer dem
Betrag nach konstanten Geschwindigkeit v3 zwischen den Autos hin und her.
a) Welche Strecke legt das Insekt zurück, bis sich die beiden Autos treffen? Wie lange
dauert seine Bewegung?
b) Zeichnen Sie für d, v1 , v2 und v3 Ihrer Wahl mit einem Computerprogramm die
Bewegungsdaten für die beiden Autos und das Insekt auf. Stellen Sie die Daten in
einer LATEX-Tabelle dar.
4 Klassen zur Modellierung von
physikalischen Systemen
Übersicht
4.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
4.2 Eine Basisklasse für Computermodelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
4.3 Eine Komponente zur Speicherung von Rechenergebnissen . . . . . . . . . . . . . . . . 180
4.4 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
4.5 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193
4.1 Einleitung
Betrachten wir eine Physikaufgabe als eine stark idealisierte Beschreibung eines Na-
turvorgangs und demzufolge eines physikalischen Systems, so sind die bekannten und
unbekannten Größen die Parameter, welche das System beschreiben. Sie bieten damit
eine Vorlage, um Klassen zu programmieren, die diese Prozesse abbilden. In Anlehnung
an die Vorgehensweise im vorherigen Kapitel wären damit die Eigenschaften der Klasse
schnell festgelegt und es bliebe zu klären, welche dieser Eigenschaften des zu beschrei-
benden Systems mit Variablen und welche mit Elementfunktionen abgebildet werden.
Für eine Aufgabe existieren manchmal mehrere Lösungswege, z.B. exakte Lösungen oder
Näherungsverfahren. Es stellt sich dann die Frage, wie eine einzige Klasse von vornherein
so programmiert werden kann, dass sie alle möglichen Lösungswege berücksichtigt bzw.
um Lösungswege erweitert werden kann. Es zeigt sich, dass dies sehr aufwendig ist, wenn
die Klasse nicht ständig Änderungen unterzogen werden soll. Hier ist es sinnvoll über
Komponenten nachzudenken, welche die alternativen Lösungswege implementieren.
Modellklasse
Aufgabe
N Parameter
Abgeleitete Klasse Abgeleitete Klasse
...
Lösungsweg 1 Lösungsweg N
Abb. 4.1: Eine Aufgabe wird mithilfe einer Modellklasse abgebildet. Die Lösungswege
werden in abgeleiteten Klassen implementiert.
/**
* @brief The XModel class Basisklasse speichert eine ID in Form
* einer Zeichenkette enthält Hilfsfunktionen zur Speicherung von Daten
*/
class XModel
{
private :
// speichere ID für alle Instanzen dieser Klasse
static inline std :: string _name ;
public :
Es soll nicht erlaubt sein, ein Modell ohne Namen zu erzeugen. Der Standardkonstruktor
wird deswegen gelöscht.
/**
* @brief XModelBase kein Standardkonstruktor
*/
XModel () = delete ;
Der Name eines Modells wird in Kleinbuchstaben (tolower) konvertiert und abgespei-
chert. Dies brauch nur einmal zu geschehen, da es sich um eine statische Variable handelt.
/**
* @brief XModelBase Konstruktor
* @param name Klassen -ID
*/
XModel ( const char *name) {
if (_name . empty ()) {
_name = name;
std :: transform (ALL( _name ), _name.begin (), tolower );
}
}
Um die Arbeit mit den Modellklassen zu erleichtern, definieren wir statische Funktionen,
die Ausgabeströme zum Schreiben in Dateien generieren. Die Dateinamen setzen sich aus
4.2 Eine Basisklasse für Computermodelle 179
den Namen der Modellklasse und einem Suffix, das dem Format der Daten entspricht,
zusammen. Somit können Dateien erzeugt werden, die dieselben Daten im CSV- oder im
LATEX-Format speichern.
/**
* @brief get_output_stream
* @param fmt Formatierung
* @return Ausgabestrom
*/
inline static std :: ofstream get_output_stream ( Format fmt) {
return Output :: get_stream ( class_name (), fmt);
}
/**
* @brief get_output_stream
* @param fmt Formatierung
* @param id Namenszusatz
* @return Ausgabestrom
*/
template <class T>
inline static std :: ofstream get_output_stream ( Format fmt , const T
&id) {
return Output :: get_stream ( class_name (), fmt , id);
}
Wir gehen einen Schritt weiter und führen Elementfunktionen ein, mit denen Daten in
Dateien gespeichert werden, deren Namen automatisch generiert werden. Hier besteht
die Möglichkeit, analog zu Listing 4.5 und Listing 4.6, dies mit zwei Varianten zu tun.
/**
* @brief save Speicherung von Daten in eine Datei
* @param data Datenobjekt
* @param fmt Formatierung
*/
template <class T>
inline static void save( const T &data , Format fmt) {
std :: ofstream output = get_output_stream (fmt);
data.save(output , fmt);
}
/**
* @brief save Speicherung von Daten in eine Datei
* @param data Datenobjekt
* @param fmt Formatierung
180 4 Klassen zur Modellierung von physikalischen Systemen
* @param id Namenszusatz
*/
template <class T, class ID >
inline static void save( const T &data , Format fmt , const ID &id) {
std :: ofstream output = get_output_stream (fmt , id);
data.save(output , fmt);
}
Es folgt schließlich eine Methode, welche den Namen des Modells liefert.
/**
* @brief class_name
* @return Klassenname oder ID
*/
inline static std :: string class_name () { //
if (_name . empty ()) {
throw std :: invalid_argument ( nmx_msg );
}
return _name ;
}
}; // XModel
Wir haben die Programmierung der kurzen, aber sehr nützlichen Klasse abgeschlossen
und kümmern uns als nächstes um eine Komponente, welche für die Speicherung von
Rechenergebnissen zuständig ist.
/**
* @brief The CResult class Speicherung von Rechenergebnissen
* @param N Anzahl der Ergebnisse
* @param IDX Aufzählung
* @param VTYPE Datentyp der Ergebnisse ( Standardwert double )
*/
template < size_t N, class IDX , class VTYPE = double >
class CResult
{
private :
// speichere Elemente vom Typ VTYPE
4.3 Eine Komponente zur Speicherung von Rechenergebnissen 181
protected :
Es besteht intern aus einem Feld, dessen Länge über den ersten Template-Parameter der
Klasse festgelegt wird. Der zweite Template-Parameter ist ein Platzhalter für einen Auf-
zählungstypen, mit dem auf die einzelnen Elemente des Feldes zugegriffen werden kann.
Der dritte Parameter legt den gemeinsamen Datentyp, der zu speichernden Ergebnisse
fest.
Im protected-Bereich befinden sich Elementfunktionen, mit denen entweder per Index
einzelnen Elementen Werte zugewiesen werden können (Listing 4.10) oder alle Elemente
über die Angabe eines anderen Containers festgelegt werden (Listing 4.11).
/**
* @brief operator () einem Element wird ein Wert zugewiesen
* @param idx Aufzählung
* @param val Werte des Elements an dr Stelle idx
*/
inline void set_result (IDX idx , VTYPE val) { //
_result [ static_cast <size_t >( idx)] = val;
}
/**
* @brief set_result allen Elementen werden Werte zugewiesen
* @param v Container mit N Elementen
*/
template <class T>
inline void set_result ( const T &v) {
// der Container muss die gleiche Länge wie das intern
// gespeicherte Feld haben
Check :: error_if <std :: out_of_range >( nmx_msg , v.size () != N);
std :: copy(v. begin () , v.end () , std :: begin( _result ));
}
Es besteht zusätzlich die Möglichkeit, über eine Liste (Listing 4.12) oder einen assoziati-
ven Container (Listing 4.13), die Elemente des Feldes anzugeben. Im ersten Fall muss die
Liste alle Werte enthalten, während im zweiten Fall über die Angabe des Index gezielt
einzelne Werte festgelegt werden können.
/**
* @brief set_result allen Elementen werden Werte zugewiesen
* @param lst Liste mit Werten
*/
inline void set_result (std :: initializer_list <VTYPE > lst) { //
Check :: error_if <std :: out_of_range >( nmx_msg , lst.size () != N);
182 4 Klassen zur Modellierung von physikalischen Systemen
/**
* @brief set_result setze Werte über assoziativen Container
* @param args Container der Form { { IDX ,VTYPE } ,... }
*/
inline void set_result (std :: unordered_map <IDX , VTYPE > args) {
Check :: error_if <std :: out_of_range >( nmx_msg , args.size () < N);
for (const auto &val : args) {
set_result (val.first , val. second );
}
}
Es folgen der überladene Klammeroperator und zwei Methoden, mit denen die gespei-
cherten Elemente gelesen werden können. Durch die Umwandlung mit static_cast des
Aufzählungstyps in ein size_t ist es möglich auch mit enum class zu arbeiten.
/**
* @brief operator () Zugriff auf Element
* @param idx Aufzählung
* @return Wert des Elements an der Stelle idx
*/
inline auto operator ()(IDX idx) const { //
return _result [ static_cast <size_t >( idx)];
}
/**
* @brief get Zugriff auf Element
* @param idx Aufzählung
* @return Wert des Elements an der Stelle idx
*/
inline auto get(IDX idx) const { //
return _result [ static_cast <size_t >( idx)];
}
/**
* @brief result
* @return Referenz auf Feld mit gespeicherten Daten
*/
inline const auto & result () const { return _result ; }
4.4 Beispiele
Wir werden mithilfe von zwei Beispielen aus der Physik den Einsatz von Modellklassen
demonstrieren. Das erste Beispiel befasst sich mit der Statik eines Stabes, der an ei-
ner glatten Wand gelehnt ist. Das zweite Beispiel behandelt den Fall eines ballistischen
Pendels.
Ein homogener Stab der Länge l und Masse m wird unter einem Winkel ϑ an eine
senkrechte Wand gelehnt. Der maximale Haftreibungskoeffizient (statischer Reibungs-
koeffizient) zwischen Boden und Stab ist µ. Die Reibung zwischen Stab und Wand sei
vernachlässigbar.
B l sin ϑ B ⃗B
N
l
ϑ 2
sin ϑ
ϑ
l
M l cos ϑ M
l
cos ϑ w
⃗ ⃗A
N
2
⃗ ⃗ey
R
A A ⃗e
⃗ez x
(a) (b)
Abb. 4.2: (a) Ein Stab der Länge l lehnt unter einem Winkel ϑ an einer senkrechten
⃗ die Normalkräfte N
Wand und befindet sich im Gleichgewicht. (b) Die Haftreibung R, ⃗ A,
⃗
NB und die Gewichtskraft w⃗
1. Berechnen Sie den maximalen Winkel ϑmax , unter dem der Stab an die Wand gelehnt
werden kann, ohne dass er anfängt zu gleiten.
2. Berechnen Sie für eine Reihe von Haftreibungskoeffizienten, die auf den Stab wir-
kenden Kräfte sowie den Winkel ϑmax . Fassen Sie die Ergebnisse in eine Tabelle
zusammen und erstellen Sie Diagramme für NA , NB , R und ϑmax als Funktionen
des Haftreibungskoeffizienten µ.
Lösung
Das Koordinatensystem Wir legen das Koordinatensystem so, dass ⃗ex parallel zum
Boden und ⃗ey parallel zur Wand verläuft. ⃗ez steht senkrecht zur Buchebene und bildet
mit den beiden anderen Einheitsvektoren ein Rechtssystem (Abb. 4.2b).
184 4 Klassen zur Modellierung von physikalischen Systemen
∑
3
⃗ri × F
⃗i = 0 (4.3)
i=1
⇒ AM
⃗ ×w ⃗ ×N
⃗ + AB ⃗B = 0
− 2l sin ϑ 0 −l sin ϑ NB
⇒ l
2 cos ϑ × −mg + l cos ϑ × 0 = 0 (4.4)
0 0 0 0
mg
R= tan ϑ (4.7)
2
Bedingung für den Winkel ϑ Die Haftreibung hat keinen konstanten Wert. Sie ändert
sich, wenn NB sich ändert, und sorgt dafür, dass der Stab im Gleichgewicht bleibt. Ist
jedoch eine obere Grenze
Rmax = µNA = µmg (4.8)
NB ≤ Rmax (4.9)
4.4 Beispiele 185
mg
tan ϑ ≤ µmg (4.10)
2
bedeutet und schließlich zu
tan ϑ ≤ 2µ (4.11)
führt. Wir stellen fest, dass der Winkel weder von der Masse noch von der Länge des
Stabs abhängt.
Das Computerprogramm
Kurzbeschreibung Wir führen eine neue Modellklasse ein, welche auch für die Berech-
nung der Ergebnisse zuständig sein wird. Es folgt die Liste der Daten, die der Modellklasse
übergeben werden, und derjenigen, die von ihr berechnet werden.
Der Quelltext Die Modellklasse speichert im public-Bereich die Länge und die Masse
des Stabes sowie den Haftreibungskoeffizienten (Listing 4.18). Diese Daten werden über
den Konstruktor initialisiert und können nicht mehr geändert werden (Listing 4.19).
Die Ergebnisse der Berechnungen werden in einem Feld gespeichert, welches durch die
Basisklasse CResult bereitgestellt wird. Der Zugriff auf die Elemente des Feldes erfolgt
über einen Aufzählungstyp.
/**
* @brief The Idx enum Zugriff auf berechnete Daten
*/
enum class Idx {
MAXTHETA = 0, // maximaler Winkel damit der Stab nicht wegrutscht
NA = 1, // Normalkraft : Boden auf Stab
NB = 2, // Normalkraft : Wand auf Stab
STATIC_FRICTION = 3, // Haftreibung
WEIGHT = 4 // Gewicht des Stabs
};
/**
* @brief The X5000 class Stab lehnt gegen eine senkrechte Wand
*/
class X040 : public nmx :: XModel , public CResult <5, Idx >
{
public :
// Eingabeparameter
const double length ; // Länge des Stabs
186 4 Klassen zur Modellierung von physikalischen Systemen
public :
Eine bequeme Art einen Namen für eine Modellklasse zu vergeben, ist die Compiler-
Variable __func__ einzusetzen.
/**
* @brief XModel Konstruktor
* @param l Länge des Stabs
* @param w Masse des Stabs
* @param fc Haftreibungskoeffizient
*/
inline X040( double l, double m, double fc)
: XModel ( __func__ )
, length { l }
, mass{ m }
, friction_coefficient { fc } {
Check :: all(nmx_msg , { length > 0, mass > 0,
friction_coefficient > 0 });
}
/**
* @brief exec interne Parameter werden berechnet
*/
inline void exec () {
using namespace gravitation ;
const auto tanMaxTheta = 2 * ( friction_coefficient );
// rufe Methode der Basisklasse CResult auf , um die Werte
// zu speichern
set_result (Idx :: MAXTHETA , atan( tanMaxTheta ));
set_result (Idx :: WEIGHT , -mass * Earth ::g);
set_result (Idx ::NA , -get(Idx :: WEIGHT ));
set_result (Idx ::NB , 0.5 * (mass) *Earth ::g * tanMaxTheta );
set_result (Idx :: STATIC_FRICTION , -get(Idx :: NB));
}
}; // namespace nmx :: x5000
Die Daten Für l = 2 m und m = 10 kg werden Beispieldaten für eine Reihe von Haft-
reibungskoeffizienten berechnet und einer Zahlentabelle hinzugefügt. Die Daten werden
sowohl im Tabellen- als auch im Grafikformat gespeichert (Tab. 4.1, Abb. 4.3). Die Da-
teinamen werden mit Funktionen der Modellklasse generiert.
4.4 Beispiele 187
/**
* @brief run Berechnung von Beispieldaten Stab lehnt gegen
* eine senkrechte Wand
*/
inline void run () {
Data <5> data;
// Tabellendaten werden berechnet
for ( double mu = 0.1; mu < 0.6; mu += 0.1) {
// Modellklasse wird initialisiert
X040 xobj{ 2.0 , 10.0 , mu };
xobj.exec (); // Ergebnisse werden berechnet
data += { xobj. friction_coefficient ,
xobj(Idx :: NA),
xobj(Idx :: NB),
xobj(Idx :: STATIC_FRICTION ),
xobj(Idx :: MAXTHETA ) };
}
// Schreibe Daten in Datein (LaTeX -, CSV - Format )
X040 :: save(data , Output :: latex );
X040 :: save(data , Output :: plot);
}
Tab. 4.1: Stab lehnt gegen eine senkrechte Wand. Werte der Normalkräfte vom Boden
und der Wand und der maximale Winkel ϑmax in Abhängigkeit vom Haftreibungskoef-
fizienten
µ NA [N] NB [N] R [N] ϑmax [rad]
1.000e-01 9.807e+01 9.807e+00 -9.807e+00 1.974e-01
2.000e-01 9.807e+01 1.961e+01 -1.961e+01 3.805e-01
3.000e-01 9.807e+01 2.942e+01 -2.942e+01 5.404e-01
4.000e-01 9.807e+01 3.923e+01 -3.923e+01 6.747e-01
5.000e-01 9.807e+01 4.903e+01 -4.903e+01 7.854e-01
0.8 100 NA
NB
R
NA , NB , R[N ]
0.6 50
ϑ[rad]
0.4 0
0.2 −50
0.1 0.2 0.3 0.4 0.5 0.1 0.2 0.3 0.4 0.5
µ µ
(a) (b)
Abb. 4.3: (a) Der maximale Winkel und (b) die auf den Stab wirkende Kräfte als Funk-
tion des Haftreibungskoeffizienten
188 4 Klassen zur Modellierung von physikalischen Systemen
Eine Kugel mit der Masse m1 wird mit einer Geschwindigkeit ⃗v0 , die einen Winkel φ0
mit der Horizontalen bildet, auf den Pendelkörper eines ballistischen Pendels der Masse
m2 abgefeuert. Der Pendelkörper ist an einer Stange vernachlässigbarer Masse befestigt,
die sich am anderen Ende frei drehen kann. Die Kugel dringt in die Masse m2 ein und
bleibt stecken, ohne dass das System aus Pendelkörper und Kugel einen Massenverlust
erleidet.
O
O O
ϑmax d
l
m1 m1
⃗j
φ0 ymax
⃗ey ⃗ey m1 ⃗v ′0 U =0
⃗v0 xmax
⃗ex ⃗ex
m2 m2
1. Wie groß ist der maximale Ausschlag des Pendels in Abhängigkeit von der Anfangs-
geschwindigkeit der Kugel?
2. Wie groß ist der Kraftstoß ⃗j, der von der Stange auf den Pendelkörper ausgeübt wird?
3. Erstellen Sie Daten für den maximalen Ausschlag des Systems in Abhängigkeit von
der Anfangsgeschwindigkeit der Kugel.
Lösung
Der Stoßvorgang Im Folgenden werden wir stellvertretend für das System aus Block
und Kugel der Einfachheit halber nur den Block schreiben. Für den zweidimensionalen
Stoßprozess gilt
m1⃗v0 = (m1 + m2 ) ⃗v ′0 + ⃗j, (4.12)
∫t
wobei ⃗v ′0 die Geschwindigkeit des Systems direkt nach dem Stoß und ⃗j = 0 F ⃗ dt′ der
Kraftstoß ist (Abb. 4.4). Multiplizieren wir diese Gleichung skalar mit ⃗ex und ⃗ey , erhalten
wir
{
m1 v0 cos φ0 = (m1 + m2 )v0 ′ (4.13a)
−m1 v0 sin φ0 = |⃗j|, (4.13b)
4.4 Beispiele 189
was bedeutet, dass der Impuls nur entlang der x-Richtung erhalten bleibt. Aus (Gl. 4.13a)
folgt für die Geschwindigkeit des Blocks nach dem Stoß
m1 v0 cos φ0
v0′ = . (4.14)
m1 + m2
Bewegung nach dem Stoß Aus Abb. 4.4c folgt für die Höhe des Blocks, wenn der
maximale Ausschlag erreicht ist
Die mechanische Energie bleibt für den Vorgang nach dem Stoß erhalten. Anfangs hat der
Block nur kinetische Energie (Kmax ). Ist die maximale Höhe erreicht, liegt nur potenzielle
Energie (Umax ) vor.
Das Computerprogramm
Kurzbeschreibung Wir leiten von XModel (Listing 4.1) und CResult (Listing 4.14) eine
neue Modellklasse ab. Es folgt eine Liste aller Parameter, welche der Klasse übergeben
werden, und derjenigen, die von der Klasse berechnet werden.
Eingabe: Masse der Kugel m1 , Masse des Blocks m2 , der Betrag v0 und der Winkel
φ0 der Geschwindigkeit der Kugel; Länge der Stange des Pendels l
Ausgabe: Die maximalen Werte für Ausschlag ϑmax , Höhe ymax und kinetische En-
ergie Kmax sowie der Kraftstoß
/**
* @brief The Idx enum Zugriff auf die Ergebnisse mittels Index
*/
enum Idx {
EKIN_MAX = 0, // maximale kinetische Energie
EPOT_MAX = 1, // maximale potenzielle Energie
Y_MAX = 2, // maximale Höhe
V0P = 3, // Geschwindigkeit direkt nach dem Stoß
THETA_MAX = 4, // maximaler Winkel
IMPULS = 5 // Kraftstoß
};
/**
* @brief The X630 class Das ballistische Pendel
*/
class X032 : public XModel , public CResult <6, Idx >
{
private :
double _mass ; // Gesamtmasse =Masse der Kugel + Masse des Blocks
std :: array <double , 2> _bullet_velocity ; // Kugelgeschwindigkeit
public :
// Eingabeparameter
const double mbullet ; // Masse der Kugel
const double mblock ; // Masse des Blocks
const double length ; // Länge des Pendels
public :
/**
* @brief X630 Konstruktor
* @param m1 Masse der Kugel
* @param m2 Masse des Blocks
* @param l Länge des Pendels
*/
inline X032( double m1 , double m2 , double l)
: XModel { __func__ }
, mbullet { m1 }
, mblock { m2 }
, length { l } {
// sind alle Eingaben gültig ?
Check :: all(nmx_msg , { m1 > 0, m2 > 0, l > 0 });
_mass = m1 + m2; // berechne die Gesamtmasse
}
Das Verhalten des Systems wird für eine vorgegebene Kugelgeschwindigkeit untersucht.
Die Ergebnisse werden berechnet und abgespeichert. Auch die Geschwindigkeit der Kugel
wird registriert und kann mit einer Elementfunktion gelesen werden (Listing 4.26).
4.4 Beispiele 191
/**
* @brief exec berechne alle fehlenden Werte
* @param v0 Geschwindigkeit der Kugel vor dem Stoß
* @param phi0 Winkel der Geschwindigkeit mit der Horizontalen
*/
inline void exec( double v0 , double phi0) {
_bullet_velocity = { v0 , phi0 }; // speichere Geschwindigkeit
const double g = Earth ::g;
// berechne cos( theta_max )
const double tmp = pow (( mbullet * v0 * cos(phi0)) / _mass , 2);
const double cosThetaMax = 1 - 1 / (2 * g * length ) * tmp;
// Wertezuweisung der internen Variablen
set_result (V0P , mbullet * v0 * cos(phi0) / _mass);
set_result (EKIN_MAX , 0.5 * _mass * pow(get(V0P), 2));
set_result (EPOT_MAX , get( EKIN_MAX ));
set_result (Y_MAX , get( EKIN_MAX ) / (g * _mass ));
set_result (THETA_MAX , acos( cosThetaMax ));
set_result (IMPULS , -mbullet * v0 * sin(phi0));
}
/**
* @brief bullet_velocity Kugelgeschwindigkeit
* @return Feld (Betrag , Winkel )
*/
inline const auto & bullet_velocity () const { //
return _bullet_velocity ;
}
}; // X032
Daten Zur Berechnung der Beispieldaten benutzen wir eine Kugelmasse m = 20 g, eine
Pendelmasse m = 4 kg und eine Pendellänge l = 1.5 m. Wir variieren den Einfallswinkel
und den Betrag der Geschwindigkeit der Kugel und berechnen den maximalen Ausschlag,
die maximale Höhe, die maximale kinetische Energie und den Kraftstoß. Für jede Einfalls-
geschwindigkeit wird eine separate Datei erzeugt (Tab. 4.2, Abb. 4.5). Die Dateinamen
werden aus dem Modellnamen und dem Geschwindigkeitsbetrag zusammengesetzt.
/**
* @brief run Berechnung von Beispieldaten ( ballistisches Pendel )
*/
inline void run () {
// Modellklasse : Masse der Kugel , Masse des Blocks , Länge des Pendels
X032 xobj{ 20.0_g , 4.0 , 1.5 };
Tab. 4.2: Ballistisches Pendel: Kraftstoß, maximale Werte für Winkel, kinetische Energie
und Höhe als Funktion der Anfangsgeschwindigkeit der Kugel
v0 [m/s] φ0 [deg] ϑmax [deg] ymax [m] Kmax [J] j [Ns]
6.000e+02 -6.000e+01 2.244e+01 1.136e-01 4.478e+00 1.039e+01
6.000e+02 -5.000e+01 2.897e+01 1.877e-01 7.400e+00 9.193e+00
6.000e+02 -4.000e+01 3.469e+01 2.666e-01 1.051e+01 7.713e+00
6.000e+02 -3.000e+01 3.939e+01 3.407e-01 1.343e+01 6.000e+00
6.000e+02 -2.000e+01 4.290e+01 4.012e-01 1.582e+01 4.104e+00
6.000e+02 -1.000e+01 4.507e+01 4.406e-01 1.737e+01 2.084e+00
6.000e+02 9.542e-15 4.580e+01 4.543e-01 1.791e+01 -1.998e-15
40
20 35
30
ϑmax [deg]
ϑmax [deg]
15
25
10 20
v0 = 200m/s v0 = 400m/s
v0 = 300m/s 15 v0 = 500m/s
−60 −50 −40 −30 −20 −10 0 −60 −50 −40 −30 −20 −10 0
φ0 [deg] φ0 [deg]
(a) (b)
Abb. 4.5: Maximaler Ausschlag des Pendels als Funktion des Einfallswinkels der Kugel
für verschiedene Kugelgeschwindigkeiten
4.5 Übungsaufgaben 193
4.5 Übungsaufgaben
1. Eine Kugel mit der Masse m1 trifft mit einer Geschwindigkeit v0 und unter einem
Winkel φ0 auf einen ruhenden Holzblock der Masse m2 , der auf einem Tisch mit der
Höhe H ruht. Die Kugel bleibt im Holzblock stecken und der Block rutscht eine Strecke
l, bis er vom Tisch fällt. Der Reibungskoeffizient für die Bewegung des Klotzes auf dem
Tisch ist µ. Schreiben Sie ein Computerprogramm, welches als Eingabeparameter v0 ,
φ0 , H, l und µ einließt und die Bahn des Blocks berechnet, nachdem dieser den Tisch
verlassen hat. Erstellen Sie die Daten für unterschiedliche Geschwindigkeitsbeträge
v0 und Einfallswinkel φ0 und für l = 2 m, H = 1.25 m, m1 = 200 g, m2 = 2 kg und
µ = 0.3.
2. Ein homogener Zylinder mit der Masse m und Radius r rollt ohne zu gleiten eine
schiefe Ebene mit dem Neigungswinkel φ hinab. Zum Zeitpunkt t0 = 0 beträgt die
Höhe des Kontaktpunktes des Zylinders mit der schiefen Ebene vom Boden H. Das
Trägheitsmoment des Zylinders bezüglich der Drehachse, die durch den Schwerpunkt
verläuft, lautet Izz = 12 mr2 .
a) Entwerfen Sie ein Modell, welches die Bewegung des Zylinders beschreibt.
b) Erstellen Sie eine Tabelle und Diagramme mit den Bewegungsdaten des Zylinders.
Benutzen Sie folgende Parameter m = 1 kg, r = 50 cm, φ = 30◦ und H = 1 m.
c) Wie muss das Modell programmiert werden, damit auch die Bewegung einer ho-
mogenen Kugel damit beschrieben werden kann?
Teil II
Übersicht
5.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
5.2 Vom gsl-Programm zur Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
5.3 Entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
5.4 Kommunikation zwischen gsl und Komponenten . . . . . . . . . . . . . . . . . . . . . . . . . 201
5.5 Komponente zum Lesen und Schreiben von Vektoren . . . . . . . . . . . . . . . . . . . . . 204
5.6 Komponente zum Rechnen mit Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
5.7 Schnittstellen zur stl . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
5.8 Minima und Maxima . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212
5.9 Eigenschaften von Vektoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
5.10 BLAS-Schnittstelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
5.11 Sicht auf einen Vektor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
5.12 Die Vektor-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
5.13 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
5.14 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
5.1 Einleitung
Ein Feld oder Array ist eine Datenstruktur, in der mehrere Elemente desselben Datentyps
hintereinander im Speicher angelegt werden. Der Speicher kann entweder dynamisch (zur
Laufzeit des Programms) oder statisch (während der Kompilierung) erzeugt und die
Elemente können per Index über ihre Position im Feld angesprochen werden. Vektoren
sind in der GNU Scientific Library Strukturen, die mit einer Reihe von zugehörigen
Funktionen zur Verwaltung von C-Feldern spezialisiert sind. Da es möglich ist, Felder mit
Elementen von jeweils einem speziellen Datentyp (z.B. int oder double) anzulegen, stellt
die gsl pro Datentyp einen Satz von Funktionen zur Verfügung, die einer bestimmten
Namenskonvention folgen. So beginnen alle Funktionen für die Bearbeitung von int-
Feldern mit gsl_vector_int_... und solche, die auf double-Felder spezialisiert sind, mit
gsl_vector_... ohne den Zusatz double. Wir wollen im Rahmen dieses Buches mit double-
Feldern arbeiten und werden die Funktionen dieser Gruppe in Klassen kapseln.
/**
* @brief ex0 Beispiel mit Vektoren (gsl - Programm )
*/
inline void ex0 () {
const size_t n = 5;
// reserviere Speicher für Vektor
gsl_vector *v = gsl_vector_alloc (n);
// initialisiere Elemente
for ( size_t idx = 0; idx < n; idx ++) {
gsl_vector_set (v, idx , idx);
}
// schreibe Elemente auf dem Bildschirm
for ( size_t idx = 0; idx < n; idx ++) {
std :: cout << gsl_vector_get (v, idx) << " ";
}
std :: cout << " -> ";
// vertausche Elemente mit Index 2 und 3
gsl_vector_swap_elements (v, 2, 3);
// schreibe Elemente auf dem Bildschirm
for ( size_t idx = 0; idx < n; idx ++) {
std :: cout << gsl_vector_get (v, idx) << " ";
}
// Speicherfreigabe
gsl_vector_free (v);
}
0 1 2 3 4 -> 0 1 3 2 4
Listing 5.2
Das Studium des Quelltextes zeigt, dass alle Funktionen - unabhängig davon für welchen
Zweck sie bestimmt sind - als erstes Eingabeargument einen Zeiger auf einen gsl_vector
erwarten. Ein gsl_vector ist eine Struktur, die folgendermaßen definiert ist.
typedef struct {
size_t size; // die Länge des Feldes
size_t stride ; // Schrittweite
double * data; // Zeiger auf das erste Element
gsl_block * block ; // C-Feld
int owner; // Eigentümer der Daten
} gsl_vector ;
Mit dieser Struktur werden C-Felder verwaltet. So kann z.B. ein gsl_vector Eigentümer
eines Feldes sein (owner=1), was ihm erlaubt, den Speicher des Feldes freizugeben. Durch
die Angabe einer Schrittweite ist es möglich, nur auf bestimmte Elemente zuzugreifen
usw. Neben dem gsl_vector existiert auch eine gsl_vector_view. Es handelt sich hierbei
um eine Struktur, die Zugriff auf die Daten eines Feldes hat, aber nicht ihr Eigentümer
ist (owner=0).
typedef struct
{
gsl_vector vector ;
} _gsl_vector_view ;
Einer View oder Sicht stehen praktisch dieselben Informationen wie einem gsl_vector
zur Verfügung. Der Unterschied ist, dass eine Sicht als Nichteigentümer der Daten ein
Feld weder erzeugen noch freigeben kann. Eine Methode, um dasselbe wie in Listing 5.1
mit einer View zu tun, zeigt das nächste Beispielprogramm.
/**
* @brief ex00 Beispiel (gsl - Programm )
*/
inline void ex00 () {
const size_t n = 5;
// erzeuge C-Feld
double data [] = { 0, 1, 2, 3, 4 };
Hier wird auf dem C-üblichen Weg statischer Speicher für ein Feld reserviert. Mit einer
gsl_vector_view wird auf die Daten zugegriffen und über dieser Sicht kann dann mit den
gsl-Funktionen genau wie im ersten Beispiel gearbeitet werden. Eine View in der GNU
Scientific Library ist nicht identisch mit der im Abschnitt 3.3 eingeführten Klasse.
200 5 Die Vektor-Klasse
5.3 Entwurf
Nach dem Studium der Beispiele stellen wir fest, dass eine Klasse einen Zeiger auf einen
gsl_vector intern speichern müsste, um über ihn mit der GNU Scientific Library zu kom-
munizieren. Somit würde eine Instanz einer Klasse genau einem gsl_vector entsprechen.
Folgender Ansatz tut genau dies:
/**
* erster Entwurf einer Klasse für gsl - Vektoren
*/
class GslVector
{
private :
// Zeiger auf gsl - Struktur
gsl_vector *_vec;
public :
// Konstruktor
// n : Länge des Vektors
GslVector ( size_t n){
// reserviere Speicher
_vec = gsl_vector_alloc (n);
}
// Destruktor
~ GslVector () {
// gebe Speicher wieder frei
gsl_vector_free (_vec);
}
Die Klasse ist, trotzt ihres kleinen Umfangs voll funktionsfähig und wir können ein Pro-
gramm schreiben, welches einen Vektor erzeugt ihn mit Werten füllt und anschließend
diese auf den Bildschirm ausgibt.
int main(void)
{
const size_t n = 4;
GslVector v(n);
// fülle Vektor
5.4 Kommunikation zwischen gsl und Komponenten 201
return 0;
}
Es fällt sofort auf, dass die Speicherfreigabe automatisch stattfindet, was ein enormer
Vorteil gegenüber der C-Version des Programms ist. Wir könnten nun so fortfahren und
alle gsl-Funktionen dieser Gruppe der Klasse hinzufügen und somit die komplette Funk-
tionalität über die Klasse abbilden. Diese würde wegen des großen Umfangs der gsl
enorm wachsen. Wir wollen die Kapselung der gsl_vector-Struktur beibehalten, jedoch
die Methoden der zu programmierenden Klasse nach Gruppen ordnen und in kleinen
Bausteinen, die auch Klassen sein werden, unterbringen. Aus diesen Komponenten soll
dann ein Vektor-Datentyp zusammengesetzt werden (Abb. 5.1). Wir hätten somit eine
Aufgabeteilung erreicht, die es einfacher macht, diese kleinen Komponenten zu pflegen.
Wir gewinnen aber mehr als das. Mit den erstellten Komponenten werden wir eine Klas-
se zur Abbildung einer gsl_vector_view bauen. Zur Implementierung der Schnittstellen
werden wir wie in Abschnitt 2.6 vorgehen.
Komponente 1
..
.
Vector Komponente k View
Komponente n
Abb. 5.1: Die Klassen für einen Vektor und eine Sicht werden gezielt mithilfe von Kom-
ponenten zusammengesetzt.
Ansatz wäre eine Basisklasse zu erstellen, die einen Zeiger intern enthält, und von dieser
dann alle Komponenten abzuleiten (Abb. 5.2). Wir würden so diesen Teil nur einmal
programmieren müssen.
B
gsl_vector*
I1 I2 ... In−1 In
Vector
Abb. 5.2: Alle Komponenten I1 , I2 , . . . In erben von einer Basisklasse B einen
gsl_vector-Zeiger.
Dieser Versuch führt aber zu dem Problem, dass über jede Komponente die Vektor-
Klasse jeweils eine Kopie eines gsl_vector-Zeigers erhält, was eine Mehrdeutigkeit zur
Folge hat (Diamond-Problem). Eine Lösung wäre, mit virtuellen Basisklassen zu arbei-
ten. Wir haben uns aber vorgenommen, die Komponenten als statische Schnittstellen
zu implementieren, und kehren zur leicht abgeänderten ursprünglichen Idee mit einer
Basisklasse pro Komponente zurück. Da die Komponenten für den Zusammenbau ei-
nes Datentyps gedacht sind, soll die aus den Komponenten zusammengesetzte Klasse
als einzige über einen gsl_vector-Zeiger verfügen. Die Komponenten sollen als Teil des
resultierenden Datentyps mittels einer Methode diesen Zeiger anfordern (Abb. 5.3).
B1 B2 ... Bn−1 Bn
I1 I2 ... In−1 In
Vektor
gsl_vector*
Dieser Ansatz scheint mit sehr viel Programmieraufwand verbunden zu sein, wenn
pro Komponente eine dazugehörige Basisklasse programmiert werden muss. Dies ist aber
nicht der Fall, denn wir werden alle Basisklassen mittels eines einzigen Klassentemplates
vom Compiler generieren lassen und führen zu diesem Zweck einen Template-Parameter N
vom Typ int ein (Listing 5.8). Für jedes N wird vom Compiler eine neue Klasse generiert.
5.4 Kommunikation zwischen gsl und Komponenten 203
/**
* @brief The IGslContainer struct Basisklasse für
* gsl - Vektoren und Matrizen
* @param T : abgeleitete Klasse
* @param GSLOBJ : gsl_vector oder gsl_matrix
* @param N : ganze Zahl ( Diamond Problem )
*/
template <class T, class GSLOBJ , int N>
struct IGslContainer {
Der Template-Parameter T wird wie im Abschnitt 2.6 gezeigt die abgeleitete Klasse sein,
sodass ein Aufruf wie in Listing 2.141 und Listing 2.142 möglich ist. Ausgehend von
dieser Basisklasse werden wir alle Komponenten erstellen. Da eine Matrix ähnlich wie
ein Vektor in der gsl abgebildet ist, denken wir einen Schritt voraus und führen noch
einen weiteren Template-Parameter GSLOBJ ein. Für die Abbildung eines gsl-Vektors
wird dieser Template-Parameter gleich gsl_vector gesetzt.
Die beiden nächsten Elementfunktionen geben einen Zeiger auf einen gsl_vector zu-
rück. Sie leiten den Aufruf an die abgeleitete Klasse weiter.
/**
* @brief ermöglicht direkten Zugriff auf gsl - Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline const GSLOBJ *gsl () const {
// konvertiere Objekt welches diese Methode aufruft in einem
// Objekt vom Typ T
return static_cast <const T *>( this)->gsl ();
}
/**
* @brief gsl_vec ermöglicht direkten Zugriff auf gsl - Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline GSLOBJ *gsl () {
// konvertiere Objekt welches diese Methode aufruft in einem
// Objekt vom Typ T
return static_cast <T *>( this)->gsl ();
}
}; // IGslContainer
/**
* @brief The IVBase struct Basisfunktionalität für eine Vektor - Klasse
*/
template <class T>
class IVBase : public IGslContainer <T, gsl_vector , 1>
{
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_vector , 1>:: gsl;
public :
/**
* @brief size
* @return die Länge des Vektors
*/
inline size_t size () const { //
try {
return gsl () ->size;
} catch (...) {
// wenn kein Speicher angefordert wurde
return 0;
}
}
Mit den beiden folgenden überladenen Klammeroperatoren können die Werte des Vektors
gelesen oder überschrieben werden. Intern nutzen wir eine gsl-Funktion, die mittels Index
einen Zeiger auf die entsprechende Stelle des Feldes liefert.
/**
* @brief operator [] Zugriff auf die Elemente des Vektors
* @param idx Index
* @return Element an der Stelle idx
*/
double operator []( size_t idx) const { //
return * gsl_vector_const_ptr (gsl () , idx);
}
/**
* @brief operator [] Zugriff auf die Elemente des Vektors
* @param idx Index
* @return Referenz Element an der Stelle idx
*/
double & operator []( size_t idx) { //
return * gsl_vector_ptr (gsl () , idx);
}
Die Elemente eines Vektors werden einem Strom zur Ausgabe in einer Datei oder auf
dem Bildschirm übergeben. Der Vektor wird zeilenweise ausgegeben. Die Formatierungs-
anweisung wird im Standardfall über eine globale Variable (Listing 2.201) gesteuert.
/**
* @brief save Ausgabe in Datei oder auf dem Bildschirm
* @param os Ausgabestrom
* @param fmt Formatierung
*/
inline void save(std :: ostream &os , Format fmt = Output :: array ())
const {
// schreibe erstes Element z.B. x0 ...
os << gsl_vector_get (gsl () , 0);
for ( size_t idx = 1; idx < size (); idx ++) {
// ... ,x1 ,x2 ,...
os << fmt.sep () << gsl_vector_get (gsl (), idx);
}
os << fmt.lend ();
}
Wir überladen den Ausgabeoperator für Instanzen dieser Klasse. Intern leiten wir den
Vektor an die Elementfunktion Listing 5.15 weiter.
/**
* @brief operator <<
* @param os Ausgabestrom
* @param p Formatierung
* @return generierter Ausgabestrom
*/
inline friend std :: ostream &operator <<( std :: ostream &os , const T
&p) {
p.save(os);
return os;
}
Zur Transformation der Elemente des Vektors durchlaufen wir diese innerhalb einer
Schleife und wenden auf jedes Element eine Funktion an, die als Eingabeargument über-
geben wird (Listing 5.17). Die Funktion muss die Form f (x) = y haben.
206 5 Die Vektor-Klasse
/**
* @brief transform Transformation der Elemente eines Vektors
* @param fn Funktionsobjekt
* @return Referenz auf das aufrufende Objekt
*/
template <class FN >
inline T & transform (FN fn) {
for ( size_t idx = 0; idx < size (); idx ++) {
double val = gsl_vector_get (gsl (), idx);
gsl_vector_set (gsl () , fn(val), idx);
}
return static_cast <T & >(* this);
}
}; // IVBase
/**
* @brief ex1 Rechnen mit gsl - Vektoren
*/
inline void ex1 () {
const size_t n = 3;
// erzeuge Vektor mit 3 Elementen
gsl_vector *v = gsl_vector_alloc (n);
// erzeuge C-Feld
double f[] = { -10, -20, -30 };
0 1 2
100 110 120
90 90 90
Listing 5.19
Ähnlich wie auch in der ersten Komponente wird diese über die Basisklasse mit Funk-
tionen zum Zugriff auf die gsl_vector-Struktur ausgestattet.
/**
* @brief The IVCalc struct Komponente implementiert Operatoren
* += , -= ,*= ,/=
*/
template <class T>
class IVCalc : public IGslContainer <T, gsl_vector , 2>
{
using IGslContainer <T, gsl_vector , 2>:: gsl;
protected :
Wir wollen die Erstellung von überflüssigem Quelltext vermeiden und führen eine Hilfs-
funktion ein, mit der alle Operatoren implementiert werden.
/**
* @brief apply_fn Hilfsfunktion
* @param v Vektor oder View oder Zahl
* @param fn gsl - Funktion
* @return Referenz auf das aufrufende Objekt
*/
template <class X, class FN >
inline T & apply_fn ( const X &v, FN fn) {
if constexpr (std :: is_same_v <X, double >) {
fn(gsl () , v); //z.B. gsl_vector_add_constant ( , v)
} else {
fn(gsl () , v.gsl ()); //z.B. gsl_vector_add ( , v)
}
return static_cast <T & >(* this);
}
public :
Dieser Hilfsfunktion kann entweder eine Konstante oder ein Vektor bzw. eine View über-
geben werden. Alle diese Fälle werden über den ersten Template-Parameter abgedeckt.
Das zweite Eingabeargument ist eine gsl-Funktion, die intern aufgerufen wird. Dem
Beispiel aus Listing 5.18 entnehmen wir, dass zwei Kategorien von Aufrufen existie-
ren. Beide erwarten als erstes Eingabeargument einen gsl_vector. Sie unterscheiden
sich aber beim zweiten Eingabeargument, denn dieses kann entweder eine Zahl (z.B.
gsl_vector_add_constant) oder ein Vektor (z.B. gsl_vector_add) sein. Wir handeln beide
Fälle mittels der if constexpr-Anweisung ab, mit der schon bei der Übersetzung des Pro-
gramms zwei unabhängige Funktionen durch den Compiler generiert werden. Mithilfe der
eingeführten Hilfsfunktion ist es einfach, die überladenen Operatoren zu implementieren.
/**
* @brief Implementierung der Operatoren += , -= ,*= ,/=
* @param v Vektor oder View
* @return Referenz auf das aufrufende Objekt
*/
inline T & operator +=( const T &v) { //
return apply_fn (v, gsl_vector_add );
}
inline T &operator -=( const T &v) { //
return apply_fn (v, gsl_vector_sub );
}
inline T & operator *=( const T &v) { //
return apply_fn (v, gsl_vector_mul );
}
inline T & operator /=( const T &v) { //
return apply_fn (v, gsl_vector_div );
}
Analog folgt eine Gruppe mit Operationen zwischen aufrufendem Objekt und einer Zahl.
/**
* @brief Implementierung der Operatoren +=,-= ,*=
* @param x Zahl
* @return Referenz auf das aufrufende Objekt
*/
inline T & operator +=( double x) { //
return apply_fn (x, gsl_vector_add_constant );
}
inline T &operator -=( double x) { //
return apply_fn (-x, gsl_vector_add_constant );
}
inline T & operator *=( double x) { //
return apply_fn (x, gsl_vector_scale );
}
Die Division mit einer Konstanten könnte auch wie die anderen Operatoren implementiert
werden. Wir machen hier eine Ausnahme und testen, ob eine Division mit 0 stattfindet.
/**
* @brief operator /= dividiere alle Elemente mit einer Zahl
* @param x Zahl
* @return Referenz auf das aufrufende Objekt
*/
inline T & operator /=( double x) {
Check :: error_if (nmx_msg , x == 0.0);
gsl_vector_scale (gsl () , 1 / x);
return static_cast <T & >(* this);
}
}; // IVCalc
Es folgt eine weitere Komponente zum Rechnen mit Vektoren. Es werden die Operatoren
für das binäre Plus, Minus usw. implementiert.
/**
* @brief The IVCalc1 class Implementierung von +,-,*,/ (binär)
*/
template <class TOUT , class TIN >
struct IVCalc1 {
Auch hier soll eine Hilfsfunktion zur Implementierung der Operatoren eingesetzt werden.
Ihr werden eine Reihe von Vektoren bzw. Sichten zusammen mit einer Rechenvorschrift
210 5 Die Vektor-Klasse
übergeben. Diese wird dann elementweise innerhalb einer Schleife ausgeführt. Für die
Vektoren
a0 b0 c0
a1 b1 c1
a = . , b = . , c = .
.. .. ..
an bn cn
und die Rechenvorschrift
f (x, y, z) = 3x − 4y + z
zi = 3ai − 4bi + ci .
Wir implementieren diese Funktion als variadisches Template, denn so haben wir die
Möglichkeit, mit beliebig vielen Vektoren zu arbeiten.
/**
* @brief apply Anwendung einer Rechenvorschrift auf eine
* beliebige Anzahl von Vektoren
* @param fn Rechenvorschrift
* @param v Vektor oder View
* @param x Vektor oder View
* @return Vektor
*/
template <class FN , class ... X>
inline friend TOUT apply (FN fn , const TIN &v, const X &... x) {
TOUT vout(v.size ());
for ( size_t idx = 0; idx < v.size (); idx ++) {
vout[idx] = fn(v[idx], x[idx ]...);
}
return vout;
}
Der Parameter TIN kann entweder ein Vektor oder eine Sicht auf einen Vektor sein. TOUT
dagegen ist immer ein Vektor. Es wird dadurch möglich gemacht, mit Views zu rechnen.
Das Ergebnis der Rechnung kann jedoch keine View, sondern muss ein Vektor sein, da
das Ergebnis ein neuer Datensatz ist. Es folgt die Implementierung der Operatoren.
/**
* @brief Implementierung der Operatoren +,-,*,/
* @param v1 Vektor oder View
* @param v2 Vektor oder View
* @return Vektor
*/
inline friend TOUT operator +( const TIN &v1 , const TIN &v2) {
auto v = apply (std :: plus <double >() , v1 , v2);
return v;
}
5.7 Schnittstellen zur stl 211
inline friend TOUT operator -( const TIN &v1 , const TIN &v2) {
return apply (std :: minus <double >() , v1 , v2);
}
inline friend TOUT operator *( const TIN &v1 , const TIN &v2) {
return apply (std :: multiplies <double >() , v1 , v2);
}
inline friend TOUT operator /( const TIN &v1 , const TIN &v2) {
return apply (std :: divides <double >() , v1 , v2);
}
Die eingeführte Hilfsfunktion kann auch für die Gruppe von Operatoren eingesetzt wer-
den, in denen die Multiplikation und Division eines Vektors mit einer Zahl implementiert
wird. Hier wird die Zahl über die eckige Klammer (capture-list) an die λ-Funktion über-
geben.
/**
* @brief Implementierung der Operatoren *,/
* @param c Zahl
* @param v2 Vektor oder View
* @return Vektor
*/
inline friend TOUT operator *( double c, const TIN &v2) {
return apply ([c]( double x) { return x * c; }, v2);
}
Wir wollen eine rudimentäre stl-Unterstützung einführen, indem wir einen Zeiger auf
das erste und einen weiteren auf ein Element nach dem letzten setzen.
/**
* @brief The IVStl class Schnittstelle zur Standard Template Library
*/
template <class T>
class IVStl : public IGslContainer <T, gsl_vector , 3>
{
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_vector , 3>:: gsl;
public :
inline double * begin () { return gsl () ->data; }
inline double *end () { return gsl () ->data + gsl () ->size; }
/**
* @brief The IVMinMax class Suche minimales , maximales Element
*/
template < typename T>
class IVMinMax : public IGslContainer <T, gsl_vector , 4>
{
using IGslContainer <T, gsl_vector , 4>:: gsl;
public :
// suche minimales Element
inline double max () const { return gsl_vector_max (gsl ()); }
Mit den zwei letzten Elementfunktionen werden der maximale und minimale Wert sowie
die dazugehörigen Indizes ermittelt. In beiden Fällen erfolgen die Rückgabewerte als
std::pair.
/**
* @brief The IVEqual class Informationen über die Elemente
* eines Vektors und Vergleichsoperatoren
*/
template <class T>
class IVProperties : public IGslContainer <T, gsl_vector , 5>
{
using IGslContainer <T, gsl_vector , 5>:: gsl;
public :
// sind alle Elemente 0?
inline bool is_null () const { //
return gsl_vector_isnull (gsl ()) == 1;
}
/**
* @brief operator == prüfe ob zwei Vektoren gleich sind
* @param v1 Vektor
* @param v2 Vektor
* @return true wenn gleich
*/
inline friend bool operator ==( const T &v1 , const T &v2) {
return gsl_vector_equal (v1.gsl (), v2.gsl ()) == 1;
}
/**
* @brief operator == prüfe ob zwei Vektoren gleich sind
* @param v1 Vektor
* @param v2 Vektor
* @return true wenn gleich
*/
inline friend bool operator !=( const T &v1 , const T &v2) {
return gsl_vector_equal (v1.gsl (), v2.gsl ()) != 1;
}
5.10 BLAS-Schnittstelle
Die BLAS (Basic Linear Algebra Subprograms) ist eine numerische Bibliothek, mit der
Vektor- und Matrixoperationen implementiert werden. Die gsl stellt Schnittstellen zu
dieser Bibliothek zur Verfügung. Mit folgender Komponente werden wir Funktionen der
BLAS wie z.B. die Berechnung des Skalarproduktes oder die Norm eines Vektors, imple-
mentieren. Diese Komponente besteht nur aus friend-Funktionen und hat keinen inneren
Zustand. Es gibt deswegen keinen Grund, eine Struktur oder Klasse von der Basisklas-
se IGslContainer abzuleiten (Listing 5.34). Wir beginnen mit dem Skalarprodukt xT y
zweier Vektoren, wobei xT der transponierte Vektor
√∑ ist (Listing 5.35). Es folgen die Be-
N 2
rechnung der euklidischen Norm eines Vektors i xi (Listing 5.36), des normierten
Vektors ⃗n = |⃗⃗vv| (Listing 5.37) sowie der Summe der Beträge der Elemente eines Vektors
∑N
i |xi | (Listing 5.38).
5.10 BLAS-Schnittstelle 215
/**
* @brief The IVBlas struct Schnittstellen zur BLAS für das Rechnen
* mit Vektoren oder Views
* @param T Vektor oder View
*/
template < typename T>
struct IVBlas {
/**
* @brief dot Skalarprodukt
* @param v1 Vektor oder View
* @param v2 Vektor oder View
* @return double ( Skalarprodukt )
*/
inline friend double dot( const T &v1 , const T &v2) {
double result ;
gsl_blas_ddot (v1.gsl () , v2.gsl (), & result );
return result ;
}
/**
* @brief nrm2 Norm eines Vektors
* @param v Vektor oder View
* @return double (Norm)
*/
inline friend double nrm2( const T &v) { //
return gsl_blas_dnrm2 (v.gsl ());
}
/**
* @brief normalize Vektor wird normiert
* @param v Vektor oder View
* @return Kopie der Vektors ( normiert )
*/
inline friend T normalize ( const T &v) {
// berechne Norm des Vektors
const double magn = nrm2(v);
Check :: error_if (nmx_msg , magn == 0.0);
// Kopie eines Vektors
T vout(v);
// Kopie des Vektors wird normiert
vout /= magn;
return vout;
}
/**
* @brief asum Summe der Beträge der Elemente
* @param v Vector oder View
* @return Summe der Beträge der Elemente
*/
inline friend double asum( const T &v) { //
return gsl_blas_dasum (v.gsl ());
}
In allen Fällen wird es durch den Template-Parameter (Listing 5.34) möglich gemacht,
die Funktionen auf Vektoren und auf Views anzuwenden.
/**
* @brief The VView class Klasse für Sichten zusammengesetzt
* aus Komponenten
*/
class VView : public IVBase <VView >,
public IVCalc <VView >,
public IVProperties <VView >,
public IVCalc1 <Vector , VView >,
public IVBlas <VView >
{
private :
gsl_vector_view _view ; //gsl - Struktur
public :
Instanzen einer View werden über zwei Wege erstellt. Die erste ist, dem Konstruktor,
eine entsprechende gsl-Struktur und die zweite, ein statisch erstelltes Feld zu übergeben.
/**
* @brief VView Konstruktor
* @param v gsl -View
*/
VView( gsl_vector_view v) { _view = v; }
/**
* @brief VView Konstruktor
* @param dim Anzahl der Elemente eines C- Feldes
* @param f C-Feld
*/
VView( size_t dim , double f[]) { //
_view = gsl_vector_view_array (f, dim);
}
Die nächsten beiden Elementfunktion geben den intern gespeicherten gsl_vector zurück.
/**
* @brief gsl_vec ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline const gsl_vector *gsl () const { return &_view. vector ; }
/**
* @brief gsl_vec ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline gsl_vector *gsl () { return &_view . vector ; }
}; // VView
/**
* @brief The Vector class Klasse für gsl - Vektoren zussamengesetzt
* aus Komponenten
*/
class Vector : public IVBase <Vector >,
public IVCalc <Vector >,
public IVStl <Vector >,
public IVProperties <Vector >,
public IVCalc1 <Vector , Vector >,
public IVBlas <Vector >,
public IVMinMax <Vector >
{
public :
using View = VView ; // Synonym
218 5 Die Vektor-Klasse
private :
gsl_vector * _vector = nullptr ; // gsl - Struktur
public :
Der Standardkonstruktor erzeugt einen leeren Vektor. Dieser kann zu einem späteren
Zeitpunkt mit Daten gefüllt werden. Für die Instanz wird kein Speicher reserviert.
/**
* @brief Vector leerer Vektor
*/
inline Vector () {}
Der nächste Konstruktor erzeugt einen Vektor einer bestimmten Länge. Alle Elemente
erhalten denselben Wert.
/**
* @brief Vector Konstruktor : Vektor mit reservierten Speicher
* @param dim Länge des Vektors
*/
inline explicit Vector ( size_t dim , double val = 0) {
// reserviere Speicher
_vector = gsl_vector_alloc (dim);
// setze alle Elemente gleich einem vorgegebenen Wert
gsl_vector_set_all (_vector , val);
}
Eine intuitive Methode, Instanzen eines Vektors zu erzeugen, bietet der nächste Konstruk-
tor. Hier werden die Elemente innerhalb von geschweiften Klammern einfach aufgelistet.
Der Speicher wird durch den Aufruf des Konstruktors aus Listing 5.46 reserviert.
/**
* @brief Vector Konstruktor : kopiere eine Liste von Zahlen
* @param lst Zahlenliste
*/
inline Vector (std :: initializer_list <double > lst)
: Vector (lst.size ()) {
std :: copy(ALL(lst), begin ());
}
Ähnlich arbeitet der nächste Konstruktor, der aber statt eine Liste von Zahlen ein einfa-
ches C-Feld erwartet (Listing 5.48). Hier muss dem Konstruktor zusätzlich die Anzahl der
zu kopierenden Elemente übergeben werden. Diese darf die gesamte Anzahl der Elemente
nicht überschreiten.
5.12 Die Vektor-Klasse 219
/**
* @brief Vector Konstruktor : kopiere Inhalt eines C- Feldes
* @param n Anzahl der Elemente
* @param f C-Feld
*/
inline Vector ( size_t n, const double f[])
: Vector (n) {
std :: copy(f, f + n, begin ());
}
Es folgt die Implementierung des Kopierkonstruktors, mit dem eine Kopie eines bereits
existierenden Vektors erzeugt wird.
/**
* @brief Vector Kopierkonstruktor
* @param v der zu kopierende Vektor
*/
inline Vector ( const Vector &v)
: Vector (v.size ()) {
std :: copy(ALL(v), begin ());
}
Der move-Konstruktor kopiert nicht, sondern übernimmt die Daten eines anderen Vektors.
Die Vorlage ist anschließend leer.
/**
* @brief Vector move - Konstruktor
* @param v übernehme Daten dieses Vektors
*/
inline Vector ( Vector &&v) {
std :: swap(_vector , v. _vector );
v. _vector = nullptr ;
}
Der Destruktor gibt den Speicher, der für den gsl_vector reserviert wurde, wieder frei.
/**
* Destruktor
*/
inline ~ Vector () {
if ( _vector != nullptr ) {
gsl_vector_free ( _vector );
}
}
Es folgen zwei Versionen des Zuweisungsoperators: die eine, um einen anderen Vektor
zu kopieren (Listing 5.52), und die andere, um die Daten eines Vektors zu übernehmen
(Listing 5.53). Genauso wie im Fall des move-Konstruktors ist die Vorlage anschließend
leer.
/**
* @brief operator = kopiere die Werte eines Vektors
* @param v der zu kopierende Vektor
* @return Referenz auf das aufrufende Objekt
*/
inline Vector & operator =( const Vector &v) {
if (&v != this) {
if ( _vector != nullptr ) {
gsl_vector_free ( _vector );
}
_vector = gsl_vector_alloc (v.size ());
std :: copy(ALL(v), begin ());
}
return *this;
}
/**
* @brief operator = übernehme Daten eines Vektors
* @param v dieser Vektor ist anschließend leer
* @return Referenz auf das aufrufende Objekt
*/
inline Vector & operator =( Vector &&v) {
if (&v != this) {
if ( _vector != nullptr ) {
// gebe alten Speicher frei
gsl_vector_free ( _vector );
// der Zeiger ist ab jetzt ungültig
_vector = nullptr ;
}
// vertausche Zeiger
std :: swap(_vector , v. _vector );
}
return *this;
}
Es folgen zwei Elementfunktionen, die einen Zeiger auf den intern gespeicherten
gsl_vector liefern. Alle Komponenten leiten die entsprechenden Aufrufe an diese bei-
den Methoden weiter.
/**
* @brief gsl ermöglicht direkten Zugriff auf gsl - Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline const gsl_vector *gsl () const {
// ist der Zeiger gültig ?
5.12 Die Vektor-Klasse 221
/**
* @brief gsl ermöglicht direkten Zugriff auf gsl - Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline gsl_vector *gsl () {
// ist der Zeiger gültig ?
Check :: ptr(nmx_msg , _vector );
return _vector ;
}
Es wird eine View mit n Elementen erzeugt, die bei der Position offset beginnt.
/**
* @brief view erzeuge View auf Daten
* @param offset beginne ab dieser Position
* @param n die View zeigt auf n Elemente
* @return View auf die Daten des aufrufenden Objekts
*/
inline View view( size_t offset , size_t n) {
return View( gsl_vector_subvector (gsl (), offset , n));
}
Mit dieser Elementfunktion wird ebenfalls eine View mit n Elementen (beginnend bei der
Position offset) erzeugt, allerdings wird mit einer vorgegebenen Schrittweite auf diese
zugegriffen.
/**
* @brief view erzeuge View auf Daten
* @param offset beginne ab dieser Position
* @param stride Schrittweite
* @param n Anzahl der Elemente
* @return View auf die Daten des aufrufenden Objekts
*/
inline View view( size_t offset , size_t stride , size_t n) {
return View( gsl_vector_subvector_with_stride (gsl (), //
offset ,
stride ,
n));
}
Ist ein Vektor leer, so gibt die Elementfunktion Listing 5.58 true zurück. Leer bedeutet,
dass entweder kein Speicher reserviert wurde oder aber der Vektor keine Elemente enthält.
222 5 Die Vektor-Klasse
Ob der interne Zeiger auf gültige Werte zeigt, kann mit der Elementfunktion Listing 5.59
getestet werden.
/**
* @brief empty
* @return true wenn gsl - Struktur instanziiert wurde und Elemente
* beinhaltet
*/
inline bool empty () const { //
return _vector != nullptr && size () != 0;
}
/**
* @brief is_init
* @return true wenn gsl - Struktur instanziiert wurde
*/
inline bool is_init () const { return _vector != nullptr ; }
5.13 Beispiele
Für die Beispiele in diesem Abschnitt werden wir folgende Hilfsfunktion zur Generierung
eines Ausgabestroms verwenden.
/**
* @brief get_output_stream Ausgabestrom für die Beispielprogramme
* @param name Dateiname
* @param fmt Formatierung ( optional )
* @return Ausgabestrom
*/
static inline std :: ofstream get_output_stream ( const std :: string &name ,
const Format &fmt =
Output :: csv) {
return Output :: get_stream ("x029", fmt , name);
}
Beispiel Vektoren werden über Zahlenlisten und den Kopierkonstruktor erstellt. Eine
View wird erzeugt und zeigt auf ein statisches C-Feld. Alle Objekte werden in eine Datei
geschrieben.
5.13 Beispiele 223
/**
* @brief vec1 Konstruktoren Vektor - Klasse
*/
inline void vec0 () {
using Vector = gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed ;
// erzeuge Vektor aus einer Liste von Zahlen
Vector v1{ 1.0 , 2.0 , 3.0 };
ofs << v1;
// erzeuge zweiten Vektor aus einer Liste von Zahlen
Vector v2{ 2, 3, 4 };
ofs << v2;
// Kopierkonstruktor v3 = v1
gsl :: Vector v3{ v1 };
ofs << v3;
v3 = v2;
ofs << v3;
// erzeuge Vektor mit C-Array
double f[] = { 10, 20, 30 };
Vector :: View v4(3, f);
ofs << v4;
}
Beispiel Ein Vektor wird über eine Zahlenliste erzeugt. Anschließend werden seine Da-
ten mit std::move einem anderen Vektor übertragen. Die Vorlage ist anschließend leer.
/**
* @brief vec11 Daten werden von einem Vektor auf einen anderen
* übertragen
*/
inline void vec11 () {
using namespace gsl;
// öffne Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed ;
// erzeuge Vektor und schreibe ihn in die Datei
Vector v1{ 1, 2, 3, 4 };
ofs << v1;
// übertrage Inhalt
Vector v2 = std :: move(v1);
if (v1.empty ()) {
ofs << "v1 is empty " << std :: endl;
} else {
224 5 Die Vektor-Klasse
Beispiel Ein Vektor mit den Elementen 0, 1, 2, 3 . . . 9 wird initialisiert. Es werden zwei
Sichten auf diesen Vektor erzeugt. Die erste beginnt bei dem Element mit Index 3 und
hat die Länge 4 und die zweite bei dem Index 3 mit einer Länge 4 und Schrittweite 2.
/**
* @brief vec1 Konstruktoren Vektor - Klasse und Views
*/
inline void vec1 () {
using Vector = gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed ;
0.000 ,1.000 ,2.000 ,3.000 ,4.000 ,5.000 ,6.000 ,7.000 ,8.000 ,9.000
3.000 ,4.000 ,5.000 ,6.000
3.000 ,5.000 ,7.000 ,9.000
Beispiel Ein Vektor v1 der Länge 4 wird mit den Elementen 1, 2, 3, 4 gefüllt. Es werden
2 2
folgende Rechenoperationen ausgeführt: v2i = sin(v1i ), v3i = cos(v1i ), v4i = v2i + v3i
2 2
und v5i = cos (v2i ) + sin (v3i ), wobei i = 0, 1, 2, 3 ist.
/**
* @brief vec2 mathematische Operationen mit Vektoren
*/
inline void vec2 () {
using Vector = gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
// erzeuge Nullvektor
Vector v1 (4);
ofs << v1;
Beispiel Ein Vektor der Länge 6 wird mithilfe des Klammeroperators mit Werten
sin(xi )/xi gefüllt, wobei xi = 1, 2, . . . 7. Zuerst werden der minimale und maximale
Wert durch Suchen mithilfe einer Schleife gefunden, dann durch Anwendung der stl-
Algorithmen und schließlich durch Aufruf der Elementfunktionen der Vektor-Klasse.
/**
* @brief vec5 minimales maximales Element
*/
inline void vec5 () {
using gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
constexpr size_t dim = 6;
gsl :: Vector v(dim);
// Vektor wird gefüllt
for ( size_t idx = 0; idx < dim; idx ++) {
v[idx] = sin(idx + 1) / (idx + 1);
}
// Suche nach Minimum Maximum mit Schleife
double maxelement = v[0] , minelement = v[0];
for ( size_t jdx = 0; jdx < dim; jdx ++) {
const double jelement = v[jdx ];
maxelement = std :: max( maxelement , jelement );
minelement = std :: min( minelement , jelement );
}
// Suche nach Minimum Maximum mit stl
auto imaxlement = std :: max_element (std :: begin(v), //
std :: end(v));
auto iminlement = std :: min_element (std :: begin(v), //
std :: end(v));
// Ergebnisse
ofs << v;
ofs << "max loop:" << maxelement << std :: endl;
ofs << "min loop:" << minelement << std :: endl;
ofs << "max method :" << v.max () << std :: endl;
ofs << "min method :" << v.min () << std :: endl;
ofs << "max stl:" << * imaxlement << std :: endl;
ofs << "min stl:" << * iminlement << std :: endl;
}
8.415e -01 ,4.546e -01 ,4.704e -02 , -1.892e -01 , -1.918e -01 , -4.657e -02
max loop :8.415e -01
min loop : -1.918e -01
max method :8.415e -01
min method : -1.918e -01
max stl :8.415e -01
min stl : -1.918e -01
Beispiel Werden zwei Punkte ⃗r0 und ⃗r1 einer Geraden gegeben, so lautet ihre Parame-
terform:
⃗r = ⃗r0 + λ (⃗r1 − ⃗r0 ) , λ ∈ R
wäre das
x 3 −4
= + λ .
y 1 5
Das dazugehörige Programm berechnet die Koordinaten von zwei Punkten für λ1 = 1
und λ2 = 2.
/**
* @brief vec6 Parameterdarstellung einer Gerade
*/
inline void vec6 () {
using gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
const gsl :: Vector r0{ 3, 1 };
const gsl :: Vector r1{ -1, 6 };
// Funktion Parameterform
auto paramForm = [r0 , r1 ]( double l) { return r0 + l * (r1 - r0); };
const double l1 = 1, l2 = 2;
const auto v1 = paramForm (l1);
const auto v2 = paramForm (l2);
// Ausgabe
ofs << r0 << r1;
ofs << " lambda 1=" << l1 << std :: endl;
ofs << v1;
ofs << " lambda 2=" << l2 << std :: endl;
ofs << v2;
}
wollen wir
7 1 6 −3
⃗v1 = ⃗a + ⃗b = , ⃗v2 = ⃗a − ⃗b = , ⃗v3 = 2⃗a = , ⃗v4 = −⃗a = (5.2)
3 7 10 −5
vom Computer berechnen lassen und zusätzlich zeigen, dass die Dreiecksungleichungen
/**
* @brief vec3 algebraische Operationen
*/
inline void vec3 () {
using Vector = gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
Format fmt{ ",", "\t" };
ofs << fmt << std :: fixed ;
const Vector a{ 3, 5 };
const Vector b{ 4, -2 };
auto v1 = a + b, v2 = a - b, v3 = 2 * a, v4 = -a;
ofs << a << b << v1 << std :: endl << v2 << v3 << v4 << std :: endl;
// teste Dreiecksungleichungen
if (nrm2(v1) <= abs(nrm2(v1)) + abs(nrm2(v2))) {
ofs << "true , ";
} else {
ofs << "false , ";
}
if (nrm2(v2) >= abs(abs(nrm2(v1)) - abs(nrm2(v2)))) {
ofs << "true" << std :: endl;
} else {
ofs << " false " << std :: endl;
}
}
Beispiel Das Skalarprodukt zweier Vektoren und der Winkel zwischen ihnen wird be-
rechnet. Für die beiden Vektoren
5 −1
⃗c = , d⃗ = (5.3)
2 6
/**
* @brief vec4 Skalarprodukt cos
*/
inline void vec4 () {
using Vector = gsl :: Vector ;
Wir implementieren als erstes eine Funktion zur Berechnung des Kreuzproduktes zweier
dreidimensionalen Vektoren.
/**
* @brief cross_product Kreuzprodukt von dreidimensionalen Vektoren
230 5 Die Vektor-Klasse
/**
* @brief vec7 Kreuzprodukt zweier Vektoren
*/
inline void vec7 () {
using gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
// berechne Kreuzprodukt
const auto c = cross_product (a, b);
// Ausgabe
ofs << a << b;
ofs << "c = a x b=" << c;
}
Eine Kraft F⃗ = 2⃗ex + 3⃗ey wirkt auf ein Teilchen, das sich am Ort ⃗r = 5⃗ex − 5⃗ey be-
züglich des Ursprungs befindet. Es soll mit einem Computerprogramm das Drehmoment
berechnet werden. (Alle Größen in SI-Einheiten.)
5.13 Beispiele 231
Quelltext und Daten Zur Berechnung des Kreuzproduktes setzen wir die Funktion
Listing 5.77 ein.
/**
* @brief vec8 Drehmoment durch Kraft auf Teilchen bezüglich
* des Ursprungs
*/
inline void vec8 () {
using Vector = gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
const Vector r{ 5, -5, 0 }, f{ 2, 3, 0 };
const auto tau = cross_product (r, f);
ofs << r << f;
ofs << "tau = r x f=" << tau;
}
Ein Teilchen der Masse m = 100 g bewegt sich mit einer Geschwindigkeit ⃗v (t) =
(0, 0, −9.81t⃗ez ) und seine Position wird durch ⃗r(t) = (0, 10⃗ey , 30 − 4.905t2⃗ez ) beschrie-
ben. Alle Größen sind in SI-Einheiten angegeben. Es soll der Drehimpuls des Teilchens
bezüglich des Ursprungs für t = 1 s berechnet werden.
Lösung
⃗ = ⃗r × p
L ⃗ = m⃗r × ⃗v (5.6)
oder
⃗ex ⃗ey ⃗ez
⃗ = 0.1 0 10 30 − 4.905t2 = −9.81t⃗ex
L (5.7)
0 0 −9.81t
⃗ = −0.9.81⃗ex kg m2 /s
Für t = 1 s ist L
232 5 Die Vektor-Klasse
Quelltext Wir nutzen die Funktion für das Kreuzprodukt (Listing 5.77).
/**
* @brief vec9 Drehimpuls eines Teilchens bezüglich des Ursprungs
*/
inline void vec9 () {
using gsl :: Vector ;
std :: ofstream ofs = get_output_stream ( __func__ );
const double mass = 100.0 _g;
const double t = 1.0;
1. Berechnen Sie die auf den Massenpunkt wirkende Gesamtkraft in kartesischen Koor-
dinaten.
2. Erstellen Sie ein Computerprogramm, welches die Gesamtkraft berechnet und alle
Kräfte inklusive der Gesamtkraft im LATEX-Format als Tabelle speichert.
⃗ = ∑5 F
Lösung Die Gesamtkraft auf das Teilchen ist F ⃗
i=0 i oder
0 20 5 −10 1 26 10
⃗ = +
F + + + + = . (5.8)
−3 −10 20 0 5 −10 2
Quelltext und Daten Für jede Kraft wird eine gsl::Vector-Instanz angelegt. Alle zu-
sammen werden in einem std::array gespeichert. Es kann somit bequem mithilfe einer
Schleife die Summe aller Kräfte berechnet werden.
5.13 Beispiele 233
/**
* @brief vec12 Kräfte wirken auf Massenpunkt
* ( kartesischen Koordinaten )
*/
inline void vec12 () {
// alle Kräfte werden in einem std :: array gespeichert
std :: array <gsl :: Vector , 7> forces ;
forces [0] = { 0, -3 };
forces [1] = { 20, -10 };
forces [2] = { 1, 20 };
forces [3] = { 10, 0 };
forces [4] = { 5, 5 };
forces [5] = { -10, -10 };
forces [6] = { 0, 0 };
Tab. 5.1: Kräfte, die auf Massenpunkt wirken, und die resultierende Gesamtkraft
i F0 [N] F1 [N] F2 [N] F3 [N] F4 [N] F5 [N] F [N]
x 0.00 20.00 1.00 10.00 5.00 -10.00 26.00
y -3.00 -10.00 20.00 0.00 5.00 -10.00 2.00
Lösung
⃗
und berechnen die Gesamtkraft F
∑
2 ∑
2
⃗ =
F Fi cos φi⃗ex + Fi sin φi⃗ey . (5.10)
i=0 i=0
Quelltext und Daten Die Kräfte werden in Polarkoordinaten angegeben. Zur Be-
rechnung der Gesamtkraft rechnen wir die Polarkoordinaten in kartesische Koordina-
ten um. Dazu führen wir eine λ-Funktion ein. Eine weitere λ-Funktion rechnet kar-
tesische Koordinaten in Polarkoordinaten um. Beide rufen intern die gsl-Funktionen
gsl_sf_polar_to_rect und gsl_sf_rect_to_polar auf.
/**
* @brief vec13 Kräfte wirken auf Massenpunkt (in Polarkoordinaten )
*/
inline void vec13 () {
// Umrechnung von Polarkoordinaten in kartesischen Koordinaten
auto polar_to_rect = []( double r, double phi) {
gsl_sf_result x, y;
gsl_sf_polar_to_rect (r, phi , &x, &y);
return gsl :: Vector ({ x.val , y.val });
};
Tab. 5.2: Kräfte (i = 0, 1, 2), die auf Massenpunkt wirken die resultierende Gesamtkraft
(i = 3)
i Fix [N] Fiy [N] |Fi | [N] φi [deg]
0 61.28 51.42 80.00 40.00
1 41.04 112.76 120.00 70.00
2 -122.87 86.04 150.00 145.00
3 -20.55 250.22 251.06 94.69
Zwei kugelförmige Massen m1 und m2 mit Radien R1 und R2 befinden sich in einem
Abstand 2d voneinander und sind fest an ihrem Ort gebunden. Eine Probemasse mit
Masse m befindet sich innerhalb des Feldes, welches von diesen beiden erzeugt wird.
m m
y F1
r1 F2
r r2 F
ex ex
0 ex x 0 ex
m1 m2 m1 m2
−d d
(a) (b)
Abb. 5.4: Zwei Massen üben eine Kraft auf eine Probemasse aus. (a) Das Koordina-
tensystem und die Ortsvektoren (b) Die auf die Probemasse von den einzelnen Massen
ausgeübten Kräfte und die Gesamtkraft
1. Leiten Sie einen Ausdruck her für die Feldstärke des Gravitationsfeld in einer Ebene,
welches von beiden Massen erzeugt wird.
2. Erstellen Sie ein Computerprogramm, welches die Richtung der Kraft darstellt, die
das Feld auf die Probemasse mit m = 1 kg ausübt.
Lösung
Das Koordinatensystem Wir legen das Koordinatensystem so, dass die x-Achse durch
die Mittelpunkte der beiden Massen verläuft (Abb. 5.4a).
Berechnung der Kräfte m1 soll die Koordinaten (−d, 0) und m2 die Koordinaten (d, 0)
haben. Die Kraft, die von m1 auf m ausgeübt wird, lautet (Abb. 5.4a):
⃗1 = −G mm1 ⃗r1
F (5.11)
|⃗r1 |2 |⃗r1 |
236 5 Die Vektor-Klasse
mit
⃗ = ⃗r + d⃗
⃗r1 = ⃗r − (−d) (5.12)
Analog folgt für m2 und m:
⃗2 = −G mm2 ⃗r2
F (5.13)
|⃗r2 |2 |⃗r2 |
und
⃗r2 = ⃗r − d⃗ (5.14)
In Komponenten gilt für Gl. 5.11 und Gl. 5.13:
m1 m x + d
F⃗1 = −G [ ]3/2 (5.15a)
(x + d)2 + y 2 y
m m x − d
]3/2
⃗2 = −G [ 2
F (5.15b)
(x − d)2 + y 2 y
Das Computerprogramm
Eingabe der beiden Massen m1 und m2 , des Abstands d sowie der Probemasse m
Berechnung der normierten Gesamtkraft an äquidistanten Punkten in einem Bereich
zwischen xmin und xmax für die x-Richtung und zwischen ymin und ymax für die
y-Richtung
Ausgabe der Daten in einer Datei zur grafischen Darstellung im TikZ-Format
Quelltext Die Klasse speichert neben den physikalischen Parametern auch die Grenzen
des Bereichs, in dem die normierten Kraftvektoren gezeichnet werden sollen. Alle diese
Werte werden mit dem Konstruktor (Listing 5.87) initialisiert und können nicht geändert
werden.
/**
* @brief The X130 class Gravitationsfeld zweier Massen
*/
5.13 Beispiele 237
private :
// G m1 m3 and G m2 m3
double _factor1 , _factor2 ;
// Koordinaten der Massen m1 und m2
Vector _d1 , _d2;
// Datenobjekt
Data _data;
public :
// Eingabeparameter
const double m1 , m2 , m3;
const double d, dx , dy;
const double xlim , ylim;
public :
Mit clang-format off und clang-format on wird innerhalb des Quelltextes ein Bereich
markiert, der nicht von clang-format formatiert wird.
/**
* @brief X130 Konstruktor
* @param m1 Masse 1 ( Quelle )
* @param m2 Masse 2 ( Quelle )
* @param d Abstand zwischen m1 und m2
* @param m3 Probemasse
* @param xlim Darstellung von [-xlim ,xlim]
* @param dx Schrittweite in x- Richtung
* @param ylim Darstellung von [-ylim ,ylim]
* @param dy Schrittweite in y- Richtung
*/
// clang - format off
X130( double m1 , double m2 , double d, double m3 = 1,
double xlim = 5, double dx = 1,
double ylim = 5, double dy = 1): XModel ( __func__ )
, m1{ m1 } , m2{ m2 }, m3{ m3 }
, d{ 0.5*d }, dx{ dx } , dy{ dy }
, xlim{ xlim }, ylim{ ylim } {}
// clang - format on
Die Kraft von m1 auf die Probemasse (Gl. 5.15a) wird durch folgende Funktion berechnet:
/**
* @brief force1 Berechnung der Kraft von m1 auf die Probemasse
* @param v Ortsvektor der Probemasse
* @return Kraft
*/
inline auto force1 ( const Vector &v) const {
auto r = v - _d1; // Vektor von m1 zur Probemasse
238 5 Die Vektor-Klasse
// berechne Kraft
double den = pow(norm , 3);
auto fResult = ( _factor1 / den) * r;
return fResult ;
}
Analog gilt für die Berechnung der Kraft von m2 auf die Probemasse (Gl. 5.15b):
/**
* @brief force2 Berechnung der Kraft von m2 auf die Probemasse
* @param v Ortsvektor der Probemasse
* @return Kraft
*/
inline auto force2 ( const Vector &v) const {
auto r = v - _d2; // Vektor von m2 zur Probemasse
const double norm = nrm2(r);
Check :: error_if (nmx_msg , norm == 0.0);
// berechne Kraft
double den = pow(norm , 3);
auto fResult = ( _factor2 / den) * r;
return fResult ;
}
Es folgt die Berechnung der normierten Gesamtkraft für alle Punkte im festgelegten
Zeichenbereich.
/**
* @brief exec Berechnung der normierten Kraftvektoren
*/
inline void exec () {
using namespace gravitation ;
Check :: all(nmx_msg , { m1 > 0, m2 > 0, d > 0, m3 > 0 });
Die Speicherung der Daten erfolgt im TikZ-Format. Es werden die x- und y- Koordinate
der Stelle im Zeichenbereich und die zwei Komponenten des normierten Kraftvektors in
jeweils eine Zeile ausgegeben.
/**
* @brief save_data Speicherung der Daten , z.B. -1 -1 5 8
*/
inline void save_data () const {
std :: ofstream mysplot = get_output_stream ( Output ::plot , m1);
mysplot << std :: scientific << std :: setprecision (2);
Daten Wir erzeugen zwei Grafiken: die erste für das Gravitationsfeld, welches von zwei
gleichen Massen erzeugt wird, und die zweite, in der m1 ein ganzzahliges Vielfaches von
m2 ist (Abb. 5.5).
/**
* @brief run Gravitationsfeld welches von zwei Massen m1 und m2
* erzeugt wird
*/
inline void run () {
// Probemasse
const double m3 = 1.0;
// Erzeugt zusammen mit m1 das Feld
const double m2 = 1e4;
// Abstand zwischen m1 und m2
const double d = 10.0;
// Bereich in dem die Vektoren gezeichnet werden
240 5 Die Vektor-Klasse
// setze m1 = m2
double m1 = 1e4;
X130 xobj0{ m1 , m2 , d, m3 , xlim , dx , ylim , dy };
xobj0.exec ();
xobj0. save_data ();
10 10
5 5
0 0
−5 −5
−10 −10
−10 −5 0 5 10 −10 −5 0 5 10
(a) (b)
Abb. 5.5: Gravitationsfeld zweier Massen (a) m1 = m2 = 104 kg (b) m1 = 16 ×
104 kg, m2 = 104 kg
5.14 Übungsaufgaben
y mit ⃗x = ⃗ex + 2⃗ey − 3⃗ez und ⃗
1. Berechnen Sie für die zwei Vektoren ⃗x und ⃗ y =
12⃗ex − ⃗ey + ⃗ez :
a) den Vektor ⃗r = ⃗x + ⃗
y und den Einheitsvektor parallel zu ⃗r,
b) den Winkel φ zwischen den zwei Vektoren ⃗x und ⃗ y,
c) das normalisierte Kreuzprodukt ⃗x × ⃗
y.
2. Abb. 5.6 zeigt fünf in einer Ebene liegenden Kräfte, welche an einem Massenpunkt
greifen. Erstellen Sie eine CSV-Datei, welche die Beträge für die Kräfte und die einge-
zeichneten Winkel enthält. Lesen Sie mit einem Computerprogramm diese Datei und
berechnen Sie die Gesamtkraft.
5.14 Übungsaufgaben 241
3. Zwei Massenpunkte A und B bewegen sich auf zwei geraden Strecken mit Geschwin-
digkeiten vA = 2 m/s und vB = 4 m/s. Berechnen Sie die relative Geschwindigkeit
(Betrag und Richtung) von A bezüglich B für den Fall, dass der Winkel zwischen den
beiden Strecken φ = 30◦ ist.
⃗2
F
⃗3
F
⃗1
F
ϑ3
ϑ2
⃗4
F
ϑ4
ϑ1 ⃗0
F
Übersicht
6.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
6.2 Ein C-ähnliches Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
6.3 Basisfunktionalität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
6.4 Rechnen mit Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
6.5 Minimales und maximales Element einer Matrix . . . . . . . . . . . . . . . . . . . . . . . . . 252
6.6 Austausch von Reihen und Spalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
6.7 Sichten und Kopien von Reihen und Spalten . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
6.8 Schnittstelle zur BLAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
6.9 Eigenschaften von Matrizen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
6.10 Die Matrix-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263
6.11 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 268
6.12 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
6.1 Einleitung
Eine Matrix ist eine zweidimensionale Struktur von Elementen desselben Datentyps, die
in Reihen und Spalten angeordnet sind und über zwei Indizes angesprochen werden,
wobei der Index für die erste Spalte und Reihe 0 ist.
a00 · · · a0j · · · a0n
.. .. . .. ..
. . .. . .
A = ai1 · · · aij · · · ain (6.1)
.. .. . .. ..
. . .. . .
am0 · · · amj · · · amn
Die GNU Scientific Library verfügt über eine Reihe von Funktionen zum Rechnen mit
Matrizen. Die Namen für Matrixfunktionen werden nach demselben Muster wie bei den
entsprechenden Vektorfunktionen vergeben. Wir beginnen mit einem C-ähnlichen Bei-
spiel und konzentrieren uns dabei auf Matrizen mit double-Elementen.
/**
* @brief ex1 Rechnen mit Matrizen (gsl - Routinen )
*/
inline void ex1 () {
// Spalten und Reihen der Matrix
const size_t nrows = 4, nclmns = 4;
Listing 6.2
Wir stellen als erstes fest, dass auch hier darauf geachtet werden muss, den reservierten
Speicher wieder freizugeben. Es fällt zusätzlich auf, dass die Ein- und Ausgabe der Ele-
mente über Schleifen erfolgt - die obwohl einfach zu programmieren - den meisten Platz
im Quelltext einnehmen. Wir wollen mit einer Klasse das Arbeiten mit gsl-Matrizen
vereinfachen und die Quelltexte übersichtlicher machen.
Eine Matrix wird in der gsl mithilfe folgender Struktur abgebildet.
typedef struct
{
size_t size1; // Reihen
size_t size2; // Spalten
size_t tda; // Anzahl der Elemente einer Reihe im Speicher
double * data; // Zeiger auf das erste Element der Matrix
gsl_block * block ; // reservierter Speicher
int owner; // Eigentümer
} gsl_matrix ;
Listing 6.3
Die Variablen size1 und size2 geben die Anzahl der Reihen und Spalten an. Die Ele-
mente werden in Form eines eindimensionalen Feldes im Speicher abgelegt. Für Matrizen
existieren auch Sichten (Listing 6.4) auf die Daten, die es erlauben, mit dem ganzen
Datensatz oder einen Teil davon zu arbeiten, ohne eine Kopie erstellen zu müssen.
typedef struct
{
gsl_matrix matrix ;
} _gsl_matrix_view ;
Listing 6.4
Der dem Listing 6.1 entsprechende Quelltext, mit einer gsl_matrix_view realisiert, sieht
folgendermaßen aus.
/**
* @brief ex2 Rechnen mit Matrizen (gsl - Routinen und gsl - Sichten )
*/
inline void ex2 () {
// Spalten und Reihen der Matrix
const size_t nrows = 4, nclmns = 4;
246 6 Die Matrix-Klasse
// statisches C-Feld
double data [16];
// eine Sicht auf das C-Feld. Die Daten werden als Matrix verwaltet
gsl_matrix_view m = gsl_matrix_view_array (data , nrows , nclmns );
Für ein statisch erstelltes, eindimensionales C-Feld wird eine Sicht erzeugt, die diese Da-
ten als eine 4 × 4-Matrix verwaltet. Dies führt nicht zu Konflikten, denn die Anordnung
im Speicher sagt nichts darüber aus, wie auf die Daten zugegriffen werden kann. Wenn
r die Anzahl der Reihen und s die der Spalten ist, dann kann jedes Element eines eindi-
mensionalen Feldes über eine Indexpaar (i, j) mit folgender Formel angesprochen werden:
i ∗ s + j.
6.3 Basisfunktionalität
Wir folgen beim Entwurf der Klasse dieselbe Strategie wie im Fall der Vektor-Klasse
und erzeugen eine Matrix bzw. eine Sicht auf eine Matrix durch das Zusammenfügen von
6.3 Basisfunktionalität 247
Komponenten. Alle werden von der Basisklasse IGslContainer abgeleitet (Abschnitt 5.3,
Listing 5.8). Wir beginnen mit der Implementierung von Elementfunktionen zur Abfrage
der Anzahl der Reihen und Spalten sowie einer Methode zum Testen, ob die Matrix leer
ist.
/**
* @brief The IMBase class Basisfunktionalität
* für eine gsl - Matrix in Form einer Komponente
* T ist die Klasse , die mithilfe dieser Komponente
* gebaut wird
*/
template <class T>
class IMBase : public IGslContainer <T, gsl_matrix , 1>
{
// verwende die Funktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 1>:: gsl;
public :
/**
* @brief rows
* @return Anzahl der Reihen
*/
inline size_t rows () const { return gsl () ->size1; }
/**
* @brief columns
* @return Anzahl der Spalten
*/
inline size_t columns () const { return gsl () ->size2; }
Eine leere Matrix ist diejenige, für die kein Speicher reserviert wurde.
/**
* @brief empty
* @return true wenn kein Speicher reserviert wurde
*/
inline bool empty () const { //
return gsl () == nullptr ? true : rows () == 0 && columns () == 0;
}
Durch die Überladung des Klammeroperators kann auf jedes Element über die Angabe
der Reihe und Spalte sowohl lesend als auch schreibend zugegriffen werden.
248 6 Die Matrix-Klasse
/**
* @brief operator () lesender Zugriff
* @param idx Reihe
* @param jdx Spalte
* @return Wert an der Stelle i,j
*/
inline double operator ()( size_t idx , size_t jdx) const {
return gsl_matrix_get (gsl () , idx , jdx);
}
/**
* @brief operator () schreibender Zugriff
* @param i Reihe
* @param j Spalte
* @return Referenz auf das Element i,j
*/
inline double & operator ()( size_t i, size_t j) {
return * gsl_matrix_ptr (gsl () , i, j); //
}
Die Ausgabe einer Matrix auf dem Bildschirm oder in einer Datei erfolgt über einen Aus-
gabestrom. Diesem werden die Elemente (angeordnet nach Spalten und Reihen) mithilfe
von zwei Schleifen übergeben. Die Formatierung der Elemente wird im Standardfall über
die Variable aus Listing 2.201 gesteuert.
/**
* @brief save Speichert reihenweise eine Matrix
* @param os Ausgabestrom
* @param fmt Formatierungsanweisung ( optional )
*/
inline void save(std :: ostream &os , Format fmt = Output :: array ())
const {
for ( size_t _idx = 0; _idx < rows (); _idx ++) {
os << gsl_matrix_get (gsl () , _idx , 0);
for ( size_t _jdx = 1; _jdx < columns (); _jdx ++) {
os << fmt.sep () << gsl_matrix_get (gsl (), _idx , _jdx);
}
os << fmt.lend ();
}
}
Wir überladen den <<-Operator zur Weiterleitung einer Matrix an einen Ausgabestrom.
Intern wird die Methode aus Listing 6.12 aufgerufen.
/**
* @brief operator << leitet die Daten der abgeleiteten Klasse
* an einem Ausgabestrom weiter
6.4 Rechnen mit Matrizen 249
* @param os Ausgabestrom
* @param p Instanz der abgeleiteten Klasse
* @return Referenz auf den Ausgabestrom
*/
inline friend std :: ostream &operator <<( std :: ostream &os , const T
&p) {
p.save(os);
return os;
}
Die Elemente einer Matrix können über die Angabe einer Funktion der Form y = f (x)
transformiert werden. Dadurch, dass eine Referenz auf das aufrufende Objekt zurückge-
liefert wird, können mehrere Aufrufe hintereinandergeschaltet werden.
/**
* @brief transform eine Funktion wird auf alle Elemente der
* Matrix angewandt
* @param fn Funktion der Form f(x) = y
* @return Referenz auf die Instanz der abgeleiteten Klasse
*/
template <class FN >
inline T & transform (FN fn) {
for ( size_t idx = 0; idx < rows (); idx ++) {
for ( size_t jdx = 0; jdx < columns (); jdx ++) {
double val = gsl_matrix_get (gsl (), idx , jdx);
gsl_matrix_set (gsl () , idx , jdx , fn(val));
}
}
return static_cast <T & >(* this);
}
/**
* @brief The IMCalc class kombinierte Rechenoperationen und Zuweisungen
*/
template <class T>
class IMCalc : public IGslContainer <T, gsl_matrix , 2>
{
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 2>:: gsl;
protected :
Die Implementierung von +=,-= usw. erfolgt über eine Hilfsfunktion, die als Template
formuliert sowohl mit Matrizen als auch mit Views und Konstanten arbeiten kann.
/**
* @brief apply_fn Hilfsfunktion
* @param v Instanz einer Matrix oder View oder Zahl
* @param fn gsl - Funktion kombiniert das aufrufende Objekt
* mit einer Instanz v vom Typ X
* @return Referenz auf das aufrufende Objekt
*/
template <class X, class FN >
inline T & apply_fn ( const X &v, FN fn) {
if constexpr (std :: is_same_v <X, double >) {
// v ist eine Zahl
fn(gsl () , v);
} else {
// v ist ein Vektor oder eine View
fn(gsl () , v.gsl ());
}
// Referenz auf das aufrufende Objekt als Instanz vom Typ T
// ( downcast )
return static_cast <T & >(* this);
}
public :
Folgender Quelltextabschnitt zeigt, wie durch Einführung der Hilfsfunktion die Imple-
mentierung dieser Gruppe der Operatoren vereinfacht wird.
/**
* @brief operator += , -= ,*= ,/+
* @param v View oder Vektor
* @return Referenz auf das aufrufende Objekt
*/
inline T & operator +=( const T &v) { //
return apply_fn (v, gsl_matrix_add );
}
inline T &operator -=( const T &v) { //
return apply_fn (v, gsl_matrix_sub );
}
inline T & operator *=( const T &v) { //
return apply_fn (v, gsl_matrix_mul_elements );
}
inline T & operator /=( const T &v) { //
return apply_fn (v, gsl_matrix_div_elements );
}
Ähnliches gilt auch für die nächste Gruppe von Elementfunktionen. Der Compiler erzeugt
aus dem Funktionstemplate die richtige Variante, mit dem Resultat, dass auch für diese
Operatoren kein zusätzlicher Programmieraufwand nötig ist.
6.4 Rechnen mit Matrizen 251
/**
* @brief operator += , -= ,*=
* @param x reelle Zahl
* @return Referenz auf das aufrufende Objekt
*/
inline T & operator +=( double x) { //
return apply_fn (x, gsl_matrix_add_constant );
}
inline T &operator -=( double x) { //
return apply_fn (-x, gsl_matrix_add_constant );
}
inline T & operator *=( double x) { //
return apply_fn (x, gsl_matrix_scale );
}
Das Programmieren des binären Plus und Minus ist eine einfache Sache.
/**
* @brief operator + addiere zwei Matrizen
* @param m1 Matrix
* @param m2 Matrix
* @return m1+m2
*/
inline friend T operator +( const T &m1 , const T &m2) {
T m{ m1 };
m += m2;
return m;
}
/**
* @brief operator - subtrahiere zwei Matrizen
* @param m1 Matrix
* @param m2 Matrix
* @return m1 -m2
*/
inline friend T operator -( const T &m1 , const T &m2) {
T m{ m1 };
m -= m2;
return m;
}
}; // IMCalc
/**
* @brief The IMMinMax class Suche das minimale und maximale Element
* einer Matrix
*/
template <class T>
class IMMinMax : public IGslContainer <T, gsl_matrix , 3>
{
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 3>:: gsl;
public :
Über die beiden folgenden Elementfunktionen wird das größte bzw. kleinste Element
einer Matrix abgefragt.
/**
* @brief min suche minimales Element
* @return das minimale Element
*/
inline double min () const { return gsl_matrix_min (gsl ()); }
/**
* @brief max suche maximales Element
* @return das maximale Element
*/
inline double max () const { return gsl_matrix_max (gsl ()); }
Durch einen Funktionsaufruf können beide Werte auf einmal abgefragt werden. Das Er-
gebnis wird in Form eines std::pair von der Funktion zurückgeliefert.
/**
* @brief min_max suche gleichzeitig minimales und maximales Element
* @return beide Werte als std :: pair
*/
inline auto min_max () const {
double minval , maxval ;
gsl_matrix_minmax (gsl () , &minval , & maxval );
return std :: make_pair (minval , maxval );
}
Es wird die Position des maximalen (Listing 6.25) und des minimalen Werts (Listing
6.26) innerhalb der Matrix ermittelt.
/**
* @brief max_index Position des maximalen Elements
* @return (i,j) als std :: pair
*/
inline auto max_index () const {
size_t idx , jdx;
gsl_matrix_max_index (gsl () , &idx , &jdx);
return std :: make_pair (idx , jdx);
}
/**
* @brief min_index Position des minimalen Elements
* @return (i,j) als std :: pair
*/
inline auto min_index () const {
size_t idx , jdx;
gsl_matrix_min_index (gsl () , &idx , &jdx);
return std :: make_pair (idx , jdx);
}
Mit einem Aufruf können beide Indexpaare abgefragt werden. Da hier insgesamt vier
Indizes benötigt werden, ist der Rückgabewert ein verschachteltes std::pair.
/**
* @brief min_max_idx Position des minimalen und maximalen
* Elements
* @return geschachteltes std :: pair
*/
inline auto min_max_idx () const {
size_t minidx , minjdx , maxidx , maxjdx ;
gsl_matrix_minmax_index (gsl () , //
&minidx ,
&minjdx ,
&maxidx ,
& maxjdx );
return std :: make_pair (std :: make_pair (minidx , minjdx ), //
std :: make_pair (maxidx , maxjdx ));
}
}; // IMMinMax
/**
* @brief The IMExchange struct
*/
template <class T>
struct IMExchange : public IGslContainer <T, gsl_matrix , 4> {
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 4>:: gsl;
Zwei Reihen (Listing 6.29) bzw. Spalten (Listing 6.30) einer Matrix werden vertauscht.
/**
* @brief swap_rows vertausche Reihen
* @param i i-te Reihe
* @param j j-te Reihe
*/
void swap_rows ( size_t i, size_t j) { //
gsl_matrix_swap_rows (gsl () , i, j);
}
/**
* @brief swap_columns vertausche Spalten
* @param i i-te Spalte
* @param j j-te Spalte
*/
void swap_columns ( size_t i, size_t j) { //
gsl_matrix_swap_columns (gsl () , i, j);
}
Für eine quadratische Matrix werden Elemente einer Reihe mit den Elementen eine Spalte
vertauscht.
/**
* @brief swap_row_columns vertausche Reihe mit Spalte
* @param i i-te Reihe
* @param j j-te Spalte
*/
void swap_row_columns ( size_t i, size_t j) { //
gsl_matrix_swap_rowcol (gsl () , i, j);
}
Es folgen zwei Elementfunkionen zur Bildung der transponierten bzw. einer Kopie der
transponierten Matrix.
/**
* @brief transpose Matrix wird transponiert
*/
void transpose () { gsl_matrix_transpose (gsl ()); }
/**
* @brief transpose_copy
* @param m die transponiert Matrix wird auf
* das aufrufende Objekt kopiert
*/
void transpose_copy ( const T &m) { //
gsl_matrix_transpose_memcpy (gsl (), m.gsl ());
}
/**
* @brief The IRowColumns struct erzeugt Sichten auf Reihen
* und Spalten einer Matrix
*/
template <class T>
struct IRowColumnsViews : public IGslContainer <T, gsl_matrix , 5> {
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 5>:: gsl;
Mit den beiden folgenden Elementfunktionen werden Sichten auf ganze Reihen bzw.
Spalten erzeugt.
/**
* @brief row_view Sicht auf eine Reihe
* @param n n-te Reihe
* @return eine Sicht auf die n-te Reihe
*/
Vector :: View row_view ( size_t n) { //
256 6 Die Matrix-Klasse
/**
* @brief column_view erzeugt eine Sicht auf eine Spalte
* @param n n-te Spalte
* @return eine Sicht auf die n-te Spalte
*/
Vector :: View column_view ( size_t n) { return
Vector :: View( gsl_matrix_column (gsl (), n)); }
Eine Sicht auf die Reihe l wird ab der Spalte 1 mit insgesamt k Elementen, erzeugt.
a00 a01 a02 . . . a0k . . . a0n
.. .. ..
.
. .
al0 al1 al2 . . . alk . . . aln
| {z }
View
.. .. ..
.
. .
an0 an1 an2 . . . ank . . . ann
/**
* @brief sub_row_view Sicht auf Reihe einer Matrix
* @param i i-te Reihe ...
* @param offset ... beginnend ab dieser Stelle
* @param n Anzahl der Elemente
* @return Sicht auf Reihe einer Matrix
*/
Vector :: View sub_row_view ( size_t i, size_t offset , size_t n) {
return Vector :: View( gsl_matrix_subrow (gsl () , i, offset , n));
}
Es folgt eine Elementfunktion, die eine Sicht auf einen Teil einer Spalte erzeugt.
/**
* @brief sub_column_view Sicht auf Spalte einer Matrix
* @param i i-te Spalte ...
* @param offset ... beginnend ab dieser Stelle
* @param n Anzahl der Elemente
* @return Sciht auf Spalte der Matrix
*/
Vector :: View sub_column_view ( size_t i, size_t offset , size_t n) {
return Vector :: View( gsl_matrix_subcolumn (gsl (), i, offset , n));
}
Mit folgender Komponente werden Funktionen zum Erstellen von Kopien von Reihen
und Spalten erstellt. Zusätzlich können ganze Reihen und Spalten verändert werden.
/**
* @brief The IRowColumns struct Kopien von Spalten und Reihen erzeugen
*/
template <class T>
struct IRowColumns : public IGslContainer <T, gsl_matrix , 10> {
// benutze Elementfunktionen der Basisklasse
using IGslContainer <T, gsl_matrix , 10 >:: gsl;
/**
* @brief row Kopie einer Reihe erzeugen
* @param n n-te Reihe
* @return Kopie der n-ten Reihe als Vektor
*/
gsl :: Vector row( size_t n) const {
// lese die Anzahl der Spalten über die von dieser Komponenten
// abgeleitete Klasse
size_t clmns = static_cast <const T *>( this)->columns ();
// erzeuge leeren Vektor mit vorgegebener Länge
Vector vout( clmns );
// kopiere Reihe
gsl_matrix_get_row (vout.gsl () , gsl (), n);
return vout;
}
/**
* @brief column Kopie einer Spalte
* @param n n-te Spalte
* @return Kopie der n-ten Spalte als Vektor
*/
gsl :: Vector column ( size_t n) const {
// lese die Anzahl der Reihen über die von dieser Komponenten
// abgeleitete Klasse
size_t rows = static_cast < const T *>( this)->rows ();
// erzeuge leeren Vektor mit vorgegebener Länge
Vector vout(rows);
// kopiere Spalte
gsl_matrix_get_col (vout.gsl () , gsl (), n);
return vout;
}
/**
* @brief set_column Spalte wird den Elementen eines
* Vektors gleichgesetzt
* @param v Vektor
258 6 Die Matrix-Klasse
/**
* @brief set_row Reihe wird den Elementen eines
* Vektors gleichgesetzt
* @param v Vektor
* @param n Index der Spalte
*/
void set_row ( const gsl :: Vector &v, size_t n) { //
gsl_matrix_set_row (gsl () , n, v.gsl ());
}
und Matrix-Matrix-Rechenoperationen
wobei α, β reelle Zahlen, A, B Matrizen und x, y Vektoren sind. Ein Parameter vom Typ
CBLAS_TRANSPOSE_t legt fest, ob mit einer Matrix selbst (CblasNoTrans), der Transponierten
(CblasTrans) oder, wenn die Matrix komplex ist, ihrer Adjungierten (CblasConjTrans)
gerechnet wird. Die Klasse besteht nur aus friend-Funktionen; deswegen gibt es keinen
Grund, diese von der üblichen Basisklasse abzuleiten.
/**
* @brief The IMBlas class Schnittstelle zur BLAS mit Funktionen für
* Matrix -Vektor - und Matrix -Matrix - Multiplikation
*/
template <class M, class V>
struct IMBlas {
Implementierung von Gl. 6.2: Der Vektor y wird der Funktion übergeben, verändert
und als Ergebnis ausgegeben. Für diesen und alle folgenden Aufrufe gilt, dass, wenn
CblasTrans übergeben wird statt CblasNoTrans, die Matrix A transponiert wird.
6.8 Schnittstelle zur BLAS 259
/**
* @brief dgemv y = a*m + b*y
* @param t CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m Matrix
* @param x Vektor
* @param b reelle Zahl
* @param y Vektor Eingabe und Ausgabe
*/
inline friend void dgemv ( CBLAS_TRANSPOSE_t t, //
double a,
const M &m,
const V &x,
double b,
V & yinout ) {
gsl_blas_dgemv (t, a, m.gsl () , x.gsl (), b, yinout .gsl ());
}
/**
* @brief dgemv
* @param t CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m Matrix
* @param v Vektor
* @param b reelle Zahl
* @return Vektor Ergebnis von a m*v + b*y
*/
inline friend V amxby ( CBLAS_TRANSPOSE_t t, //
double a,
const M &m,
const V &x,
double b,
const V &y) {
Vector out(y);
gsl_blas_dgemv (t, a, m.gsl () , x.gsl (), b, out.gsl ());
return out;
}
Es folgt ein weiterer Spezialfall von Listing 6.45. Dieser implementiert die Rechenopera-
tion v = αAx.
/**
* @brief dgemv
* @param t CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m Matrix
* @param x Vektor
260 6 Die Matrix-Klasse
* @return a m x
*/
inline friend V amx( CBLAS_TRANSPOSE_t t, double a, const M &m,
const V &x) { //
Vector out{ x };
gsl_blas_dgemv (t, a, m.gsl () , x.gsl (), 0, out.gsl ());
return out;
}
/**
* @brief dot Matrix - Vektor Multiplikation
* @param m Matrix
* @param v Vektor
* @param type CblasNoTrans oder CblasTrans oder CblasConjTrans
* @return m v
*/
inline friend V dot( const M &m, const V &v,
CBLAS_TRANSPOSE_t type = CblasNoTrans ) { //
return amx(type , 1, m, v);
}
Es folgt die Gruppe von Rechenoperationen (Gl. 6.3). Auch hier werden zur einfacheren
Handhabung Spezialfälle implementiert. Wir beginnen mit der Umsetzung von Gl. 6.3,
in der die Matrix C übergeben und das Ergebnis in dieser geschrieben wird.
/**
* @brief dgemm
* @param t1 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param t2 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m1 Matrix
* @param m2 Matrix
* @param b reelle Zahl
* @param minout Matrix Eingabe Ausgabe
* @return m = a m1 m2 + b m
*/
inline friend void dgemm ( CBLAS_TRANSPOSE_t t1 ,
CBLAS_TRANSPOSE_t t2 ,
double a,
const M &m1 ,
const M &m2 ,
double b,
M & minout ) {
gsl_blas_dgemm (t1 , t2 , a, m1.gsl (), m2.gsl (), b, minout .gsl ());
}
Es folgt eine leicht abgeänderte Version der Routine Listing 6.49. Hier wird das Ergebnis
nicht in die Matrix C geschrieben, sondern als neue Matrix D von der Elementfunktion
zurückgegeben (D = αAB + βC).
/**
* @brief dgemm
* @param t1 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param t2 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m1 Matrix
* @param m2 Matrix
* @param b reelle Zahl
* @param m3 Matrix
* @return m = a m1 m2 + b m3
*/
inline friend M dgemm ( CBLAS_TRANSPOSE_t t1 , //
CBLAS_TRANSPOSE_t t2 ,
double a,
const M &m1 ,
const M &m2 ,
double b,
const M &m3) {
M out{ m3 };
gsl_blas_dgemm (t1 , t2 , a, m1.gsl (), m2.gsl (), b, out.gsl ());
return out;
}
/**
* @brief dgemm
* @param t1 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param t2 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param a reelle Zahl
* @param m1 Matrix
* @param m2 Matrix
* @return a * m1 * m2
*/
inline friend M dgemm ( CBLAS_TRANSPOSE_t t1 ,
CBLAS_TRANSPOSE_t t2 , //
double a,
const M &m1 ,
const M &m2) {
M out(m1.rows () , m2. columns ());
gsl_blas_dgemm (t1 , t2 , a, m1.gsl (), m2.gsl (), 0, out.gsl ());
return out;
}
/**
* @brief dot Matrix - Matrix Multiplikation
* @param m1 Matrix
* @param m2 Matrix
* @param t1 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @param t2 CblasNoTrans oder CblasTrans oder CblasConjTrans
* @return Matrix
*/
inline friend M dot( const M &m1 , //
const M &m2 ,
CBLAS_TRANSPOSE_t t1 = CblasNoTrans ,
CBLAS_TRANSPOSE_t t2 = CblasNoTrans ) {
return dgemm (t1 , t2 , 1, m1 , m2);
}
/**
* @brief The IMProperties struct
*/
template <class M>
struct IMProperties : public IGslContainer <M, gsl_matrix , 6> {
// benutze Elementfunktionen der Basisklasse
using IGslContainer <M, gsl_matrix , 6>:: gsl;
/**
* @brief operator == prüfe ob zwei Vektoren gleich sind
* @param v1 Vektor
* @param v2 Vektor
* @return true wenn gleich
*/
inline friend bool operator ==( const M &v1 , const M &v2) {
return gsl_matrix_equal (v1.gsl (), v2.gsl ()) == 1;
}
/**
* @brief operator == prüfe ob zwei Vektoren gleich sind
* @param v1 Vektor
* @param v2 Vektor
* @return true wenn gleich
*/
inline friend bool operator !=( const M &m1 , const M &m2) {
return gsl_matrix_equal (m1.gsl (), m2.gsl ()) != 1;
}
/**
* @brief The MView class Sicht auf eine gsl - Matrix
*/
class MatrixView : public IMBase < MatrixView >, public IMCalc <MatrixView >
{
private :
gsl_matrix_view _view ;
public :
Eine Instanz dieser Klasse kann entweder durch die Übergabe einer gsl_matrix_view-
Struktur oder über ein vorhandenes Feld erzeugt werden (Listing 6.57, Listing 6.58).
264 6 Die Matrix-Klasse
/**
* @brief MView Konstruktor
* @param v View
*/
MatrixView ( gsl_matrix_view v) { _view = v; }
/**
* @brief MView Konstruktor Sicht auf C-Feld
* @param nr Anzahl der Reihen
* @param nc Anzahl der Spalten
* @param f C-Feld
*/
MatrixView ( size_t nr , size_t nc , double f[]) { //
_view = gsl_matrix_view_array (f, nr , nc);
}
Mit den beiden folgenden Elementfunktionen wird ein Zeiger auf eine gsl_matrix zurück-
gegeben.
/**
* @brief gsl ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline const gsl_matrix *gsl () const { return &_view. matrix ; }
/**
* @brief gsl ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline gsl_matrix *gsl () { return &_view . matrix ; }
}; // MatrixView
Die Matrix-Klasse speichert einen Zeiger auf einer gsl_matrix. Damit wird auch hier eine
Zuordnung zwischen einer Instanz dieser Klasse und einer gsl-Matrix sichergestellt.
/**
* @brief The Matrix class Schnittstelle zur gsl Bibliothek
*/
class Matrix : public IMBase <Matrix >,
public IMProperties <Matrix >,
public IMCalc <Matrix >,
public IMBlas <Matrix , Vector >,
public IMExchange <Matrix >,
public IMMinMax <Matrix >,
6.10 Die Matrix-Klasse 265
private :
gsl_matrix * _matrix = nullptr ;
public :
/**
* @brief Matrix Konstruktor erzeugt eine leere Matrix ohne
* Speicherreservierung
*/
inline Matrix () {}
Eine Nullmatrix mit reserviertem Speicher für eine vorgegebene Anzahl von Reihen und
Spalten wird erzeugt.
/**
* @brief Matrix Konstruktor erzeugt eine Nullmatrix
* @param rows Anzahl der Reihen
* @param columns Anzahl der Spalten
*/
explicit inline Matrix ( size_t rows , size_t columns ) { //
_matrix = gsl_matrix_calloc (rows , columns );
}
Eine Matrix mit reserviertem Speicher für eine vorgegebene Anzahl von Reihen und
Spalten wird erzeugt. Alle Elemente werden einem vorgegebenen Wert gleichgesetzt.
/**
* @brief Matrix Konstruktor in der alle Elemente denselben
* Wert haben
* @param rows Anzahl der Reihen
* @param columns Anzahl der Spalten
* @param x alle Element erhalten den Wert x
*/
explicit inline Matrix ( size_t rows , size_t columns , double val)
: Matrix (rows , columns ) {
gsl_matrix_set_all (gsl () , val);
}
Durch die explizite Angabe der Elemente einer Matrix in Form von verschachtelten Listen
kann mit diesem Konstruktor eine Instanz der Klasse erzeugt werden. Die Anzahl der
Spalten und Reihen sowie der benötigte Speicher werden automatisch ermittelt.
/**
* @brief Matrix Konstruktor
* @param lst verschachtelte Liste von Vektoren werden zu Reihen
* der Matrix z.B. { {1 ,2 ,3} ,{4 ,5 ,6} ,{6 ,7 ,8}}
*/
inline Matrix (std :: initializer_list <std :: initializer_list <double >>
lst)
: Matrix () {
// Anzahl der verschachtelten Listen :
// für { {1 ,2 ,3} ,{4 ,5 ,6} ,{6 ,7 ,8}} ist die Anzahl 3
size_t irows = lst.size ();
size_t iclmns = 0;
// suche maximale Spaltenzahl
for (const auto &row : lst) {
iclmns = std :: max(iclmns , row.size ());
}
// erzeuge Speicher , alle Element sind 0
_matrix = gsl_matrix_calloc (irows , iclmns );
// fülle Vektor
size_t i = 0, j = 0;
// jedes l ist eine verschachtelte Liste
for (const auto &l : lst) {
for (const auto &val : l) {
// setze Element (i,j) = val
gsl_matrix_set (gsl () , i, j, val);
++j;
}
j = 0;
++i;
}
}
/**
* @brief Matrix Kopierkonstruktor
* @param m Matrix Vorlage
*/
inline Matrix ( const Matrix &m)
: Matrix (m.rows () , m. columns ()) {
gsl_matrix_memcpy (gsl () , m.gsl ());
}
/**
* @brief Matrix move Konstruktor
* @param m Matrix Vorlage
*/
6.10 Die Matrix-Klasse 267
/**
* @brief ~ Matrix
*/
inline ~ Matrix () {
if ( _matrix != nullptr ) {
gsl_matrix_free ( _matrix );
}
}
Mit den beiden folgenden Elementfunktionen wird der Zeiger auf die intern gespeicherte
gsl_matrix-Struktur angefordert. Alle Komponenten leiten die entsprechenden Aufrufe
an diese beiden Methoden weiter.
/**
* @brief gsl_vec ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline const gsl_matrix *gsl () const { return *& _matrix ; }
/**
* @brief gsl_vec ermöglicht direkten Zugriff auf gsl Funktionen
* @return Zeiger auf gsl - Struktur
*/
inline gsl_matrix *gsl () { return *& _matrix ; }
/**
* @brief operator = erzeuge Kopie
* @param m Matrix die kopiert wird
* @return Referenz auf sich selbst
*/
inline Matrix & operator =( const Matrix &m) {
if (this != &m) {
if ( _matrix != nullptr ) {
// gebe Speicher frei
gsl_matrix_free (gsl ());
268 6 Die Matrix-Klasse
}
// erzeuge neuen Speicher
_matrix = gsl_matrix_alloc (m.rows () , m. columns ());
// kopiere Matrix
gsl_matrix_memcpy (gsl () , m.gsl ());
}
return *this;
}
/**
* @brief operator = Move - Zuweisungsoperator
* @param m Matrix Vorlage
* @return Referenz auf sich selbst
*/
inline Matrix & operator =( Matrix &&m) {
if (this != &m) {
if ( _matrix != nullptr ) {
// gebe Speicher frei
gsl_matrix_free (gsl ());
_matrix = nullptr ;
}
// vertausche Zeiger auf gsl - Matrix
std :: swap(_matrix , m. _matrix );
}
return *this;
}
/**
* @brief identity Erzeugt eine NxN Einheitsmatrix
* @param N Anzahl der Reihen und Spalten
* @return NxN Einheitsmatrix
*/
inline static Matrix identity ( size_t N) {
Matrix m{ N, N };
gsl_matrix_set_identity (m.gsl ());
return m;
}
6.11 Beispiele
Alle Beispiele in diesem Abschnitt rufen zur Speicherung der Daten folgende Funktion
auf.
6.11 Beispiele 269
/**
* @brief get_output_stream
* @param name Name der Datei
* @param fmt Formatierung der Daten
*/
inline auto get_output_stream ( const char *name , Format fmt =
Output :: csv) {
auto ofs = Output :: get_stream ("x030", fmt , name);
fmt. set_defaults (ofs);
return ofs;
}
Beispiel Eine 2×2-Matrix wird erzeugt. Alle Elemente erhalten den Wert 5. Die Matrix
wird mithilfe von Schleifen und dem Klammeroperator in eine Datei gespeichert.
/**
* @brief mat1 Initialisierungen und Zuweisungen
*/
inline void mat1 () {
std :: ofstream ofs = get_output_stream ( __func__ );
const size_t rows = 2, columns = 2;
// erzeuge eine 2x2 Matrix und setze alle Elemente gleich 5
gsl :: Matrix m(2, 2, 5.0);
// schreibe Matrix in Datei
for ( size_t i = 0; i < rows; i++) {
for ( size_t j = 0; j < columns ; j++) {
ofs << m(i, j) << "\t";
}
ofs << std :: endl;
}
}
Beispiel Eine 3 × 3-View wird für ein statisches C-Feld der Länge 9 erzeugt. Die Daten
werden über die View mithilfe von Schleifen und des Klammeroperators in eine Datei
geschrieben.
/**
* @brief mat2 Erzeuge View aus C-Feld
*/
270 6 Die Matrix-Klasse
Beispiel Eine 3 × 3-Matrix wird über eine std::initializer_list erzeugt. Die Matrix
wird in eine Datei geschrieben.
/**
* @brief mat3 Erzeuge eine Matrix aus verschachtelten
* Listen von Zahlen
*/
inline void mat3 () {
std :: ofstream ofs = get_output_stream ( __func__ );
// drei Vektoren ergeben folgende 3x3 - Matrix
gsl :: Matrix m{ { 1, 2, 3 }, { 10, 20, 30 }, { 100, 200, 300 } };
ofs << m;
}
Beispiel Eine 3 × 3-Matrix wird durch die Eingabe ihrer Elemente mittels einer std::
initializer_list erzeugt und in eine Datei geschrieben. Die Matrix wird kopiert und
die Kopie wird in dieselbe Datei geschrieben.
6.11 Beispiele 271
/**
* @brief mat4 Einsatz des Kopierkonstruktors
*/
inline void mat4 () {
std :: ofstream ofs = get_output_stream ( __func__ );
// erzeuge eine 3x3 Matrix
gsl :: Matrix m1{ { 1, 2, 3 }, { 10, 20, 30 }, { 100, 200, 300 } };
// schreibe Matrix in Datei
ofs << m1;
// kopiere m1
gsl :: Matrix m2{ m1 };
ofs << " ---------------------" << std :: endl << m2;
}
Beispiel Wir erzwingen durch eine std::move-Anweisung die Übertragung des komplet-
ten Inhalts der Matrix m1 auf die Matrix m2. m1 enthält anschließend keine Daten.
/**
* @brief mat5 Einsatz des move - Konstruktors
*/
inline void mat5 () {
std :: ofstream ofs = get_output_stream ( __func__ );
// erzeuge 3x3 Matrix
gsl :: Matrix m1{ { 1, 2, 3 }, { 10, 20, 30 }, { 100, 200, 300 } };
ofs << m1;
// übertrage Inhalt der Matrix
gsl :: Matrix m2 = std :: move(m1);
ofs << " ---------------------" << std :: endl;
if (m1.empty ()) {
ofs << "m1 is empty " << std :: endl;
} else {
ofs << "m1 is not empty " << std :: endl;
}
ofs << " ---------------------" << std :: endl << m2;
}
---------------------
m1 is empty
---------------------
1.000 e+00 ,2.000e+00 ,3.000e+00
1.000 e+01 ,2.000e+01 ,3.000e+01
1.000 e+02 ,2.000e+02 ,3.000e+02
Beispiel Die Inhalte zweier Matrizen werden mithilfe einer temporären Matrix ver-
tauscht. Die Matrizen werden vor und nach der Vertauschung mit einer λ-Funktion in
eine Datei geschrieben.
/**
* @brief mat6 Einsatz des Zuweisungsoperators
*/
inline void mat6 () {
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
// vertausche Matrizen
gsl :: Matrix tmp = m2;
m2 = m1;
m1 = tmp;
// Ausgabe
print("m1", m1);
print("m2", m2);
}
--------m1 ----------
1.00 ,2.00 ,3.00
4.00 ,5.00 ,6.00
7.00 ,8.00 ,9.00
--------m2 ----------
10.00 ,20.00 ,30.00
40.00 ,50.00 ,60.00
70.00 ,80.00 ,90.00
--------m1 ----------
10.00 ,20.00 ,30.00
40.00 ,50.00 ,60.00
70.00 ,80.00 ,90.00
6.11 Beispiele 273
--------m2 ----------
1.00 ,2.00 ,3.00
4.00 ,5.00 ,6.00
7.00 ,8.00 ,9.00
Beispiel Das folgende Beispiel imitiert die Funktionalität der std::swap-Funktion, die
genauso wie im vorherigen Beispiel die Daten zweier Matrizen vertauscht. Die letzten
Zeilen zeigen, wie derselbe Datenaustausch mit std::swap programmiert werden kann.
/**
* @brief mat7 swap - Funktion
*/
inline void mat7 () {
using Matrix = gsl :: Matrix ;
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
Matrix m1{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } };
ofs << " ----------m1 -----------" << std :: endl;
m1.save(ofs);
ofs << " ----------m1 -----------" << std :: endl << m1;
ofs << " ----------m2 ----------" << std :: endl << m2;
std :: swap(m1 , m2);
ofs << " ----------m1 -----------" << std :: endl << m1;
ofs << " ----------m2 ----------" << std :: endl << m2;
}
----------m1 -----------
1.00 ,2.00 ,3.00
4.00 ,5.00 ,6.00
7.00 ,8.00 ,9.00
----------m2 ----------
10.00 ,20.00 ,30.00
40.00 ,50.00 ,60.00
70.00 ,80.00 ,90.00
----------m1 -----------
10.00 ,20.00 ,30.00
40.00 ,50.00 ,60.00
70.00 ,80.00 ,90.00
----------m2 ----------
274 6 Die Matrix-Klasse
Beispiel Speicher wird für eine 3×3-Matrix reserviert und daraus mittels direktem Auf-
ruf einer gsl-Funktion eine Einheitsmatrix erzeugt und, wieder mittels gsl-Funktionen,
in eine Datei geschrieben. Zur Ermittlung der Anzahl der Reihen und Spalten wird auf
die gsl_matrix-Struktur (Listing 6.3) zugegriffen.
/**
* @brief mat8 direkter Zugriff auf die gsl - Routinen
*/
inline void mat8 () {
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
gsl :: Matrix m{ 3, 3 };
gsl_matrix_set_identity (m.gsl ());
Beispiel Die Elemente einer 3 × 3-Einheitsmatrix werden verändert. Die Matrix wird
vor und nach der Änderung der Elemente in eine Datei geschrieben.
/**
* @brief mat9 verändere Elemente mithilfe des Klammeroperators
*/
inline void mat9 () {
std :: ofstream ofs = get_output_stream ( __func__ );
// erzeuge Einheitsmatrix
gsl :: Matrix m = gsl :: Matrix :: identity (3);
---------------------
1.000 e+00 ,0.000e+00 ,0.000e+00
0.000 e+00 ,1.000e+00 ,0.000e+00
0.000 e+00 ,0.000e+00 ,1.000e+00
---------------------
1.000 e+00 ,0.000e+00 ,0.000e+00
0.000 e+00 ,1.000e+00 ,1.200e+01
0.000 e+00 ,0.000e+00 ,2.200e+01
Beispiel Eine 3 × 4-Matrix wird durch explizite Angabe der Elemente erzeugt. Es wird
jeweils der Index für das kleinste und das größte Element ermittelt. Der Rückgabewert
der Funktionen ist jedes Mal ein std::pair. Mithilfe des Structured Bindings können dem
Rückgabewert gleichzeitig eigene Variablen zugeordnet werden.
/**
* @brief mat10 suche Minimum und Maximum
*/
inline void mat10 () {
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: scientific << std :: setprecision (2);
// erzeuge Matrix
gsl :: Matrix m{ { 0, 20, 129 , 3, 300 }, //
{ 311 , 0, 40, -1, -600 },
{ -100, 3, 0, 5, 123 },
{ -10, -342, -90, 1 } };
Beispiel Es folgt ein einfaches Beispielprogramm für die Addition und Subtraktion zwei-
er Matrizen. Sowohl die Matrizen als auch die Ergebnisse werden in eine Datei geschrie-
ben.
/**
* @brief mat11 algebraische Operationen mit Matrizen
*/
inline void mat11 () {
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
---------- m ----------
1.00 ,2.00 ,3.00
5.00 ,6.00 ,7.00
8.00 ,9.00 ,10.00
----------- m1+m1 ---------
6.11 Beispiele 277
/**
* @brief mat12 algebraische Operationen
*/
inline void mat12 () {
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (2);
// erzeuge Matrix
gsl :: Matrix m1{ { 1, 2, 3 }, { 5, 6, 7 }, { 8, 9, 10 } };
---------------------
1.00 ,2.00 ,3.00
5.00 ,6.00 ,7.00
8.00 ,9.00 ,10.00
---------------------
101.00 ,102.00 ,103.00
105.00 ,106.00 ,107.00
108.00 ,109.00 ,110.00
/**
* @brief mat13 BLAS Matrix - Matrix Multiplikation
*/
inline void mat13 () {
using Matrix = gsl :: Matrix ;
std :: ofstream ofs = get_output_stream ( __func__ );
// erzeuge Matrizen
Matrix m1{ { 4, 3 }, { 1, 1 } }, m2{ { 1, -3 }, { -1, 4 } };
278 6 Die Matrix-Klasse
// berechne Matrixprodukt
Matrix m3 = dot(m1 , m2);
/**
* @brief mat14 BLAS Matrix -Vektor - Multiplikation
*/
inline void mat14 () {
std :: ofstream ofs = get_output_stream ( __func__ );
// Koeffizientenmatrix
gsl :: Matrix m{ { 1, 3, -2 }, { 3, 5, 6 }, { 2, 4, 3 } };
---------------------
1.000 e+00 ,3.000e+00 , -2.000e+00
3.000 e+00 ,5.000e+00 ,6.000e+00
2.000 e+00 ,4.000e+00 ,3.000e+00
---------------------
-1.500e+01 ,8.000e+00 ,2.000e+00
---------------------
5.000 e+00 ,7.000e+00 ,8.000e+00
---------------------
5.000 e+00 ,7.000e+00 ,8.000e+00
true
Jede Drehmatrix kann als Produkt von drei einfachen Drehmatrizen, Dx (φx ) (Drehung
um die x-Achse), Dy (φy ) (Drehung um die y-Achse), Dz (φz ) (Drehung um die z-Achse)
geschrieben werden, wobei
1 0 0 cos φ 0 − sin φ
Dx (φ) =
0 cos φ − sin φ, Dy (φ) = 0 1 0 ,
0 sin φ cos φ sin φ 0 cos φ
cos φ − sin φ 0
Dz (φ) = sin φ cos φ 0
. (6.5)
0 0 1
eine Drehung um die z-Achse um 60◦ , dann eine Drehung um die y-Achse um 45◦ und
schließlich eine Drehung um die x-Achse um 30◦ darstellt.
Lösung Wir müssen zeigen, dass Dx (0.5235)Dy (0.7853)Dz (1.0471) = D ist. Die Win-
kel sind im Bogenmaß angegeben.
Quelltext Wir implementieren innerhalb einer Funktion für jede Drehmatrix eine λ-
Funktion (Gl. 6.5). Der Compiler kann den Typ des Rückgabewerts nicht erraten, da nur
verschachtelte Zahlenlisten angegeben werden. Deswegen schreiben wir den Rückgabe-
typ explizit hin. Es folgen die Matrixmultiplikation der drei Rotationsmatrizen und der
Vergleich mit der Matrix (Gl. 6.6)
280 6 Die Matrix-Klasse
/**
* @brief mat15 Euler 'sche Drehmatrizen
*/
inline void mat15 () {
using Matrix = gsl :: Matrix ;
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: setprecision (8);
// Drehung um x-Achse
auto Dx = []( double phi) -> Matrix {
return { { 1, 0, 0 }, //
{ 0, cos(phi), -sin(phi) },
{ 0, sin(phi), cos(phi) } };
};
// Drehung um y-Achse
auto Dy = []( double phi) -> Matrix {
return { { cos(phi), 0, -sin(phi) }, //
{ 0, 1, 0 },
{ sin(phi), 0, cos(phi) } };
};
// Drehung um z-Achse
auto Dz = []( double phi) -> Matrix {
return { { cos(phi), -sin(phi), 0 }, //
{ sin(phi), cos(phi), 0 },
{ 0, 0, 1 } };
};
// Drehmatrix
Matrix D = { { 0.3536 , -0.6124 , -0.7071 },
{ 0.5732 , 0.7392 , -0.3536 },
{ 0.7392 , -0.2803 , 0.6124 } };
// Winkel
const auto phix = 30.0 _deg;
const auto phiy = 45.0 _deg;
const auto phiz = 60.0 _deg;
auto DxDyDz = dot(dot(Dx(phix), Dy(phiy)), Dz(phiz));
Der Vergleich der Elemente mit dem ==-Operator zeigt nicht das gewünschte Ergebnis an.
Dies liegt daran, dass die Elemente der Drehmatrix nicht genau genug angegeben wurden.
6.11 Beispiele 281
Die berechnete Differenz zeigt jedoch, dass die Ergebnisse genügend nahe beieinander
liegen.
false
----------------------------
3.53600000e -01 , -6.12400000e -01 , -7.07100000e -01
5.73200000e -01 ,7.39200000e -01 , -3.53600000e -01
7.39200000e -01 , -2.80300000e -01 ,6.12400000e -01
----------------------------
3.53553391e -01 , -6.12372436e -01 , -7.07106781e -01
5.73223305e -01 ,7.39198920e -01 , -3.53553391e -01
7.39198920e -01 , -2.80330086e -01 ,6.12372436e -01
----------------------------
-4.66094067e -05 ,2.75643042e -05 , -6.78118655e -06
2.33047034e -05 , -1.08025988e -06 ,4.66094067e -05
-1.08025988e -06 , -3.00858899e -05 , -2.75643042e -05
Die einzelnen Schritte im Quelltext bestehen hauptsächlich aus der Erzeugung von Kopi-
en von Reihen der Matrix. Diese Kopien sind Vektoren. Mit diesen werden algebraische
Operationen durchgeführt und wieder in die Matrix eingesetzt.
/**
* @brief mat16 Manipulation von Reihen und Spalten einer Matrix
*/
inline void mat16 () {
using Matrix = gsl :: Matrix ;
std :: ofstream ofs = get_output_stream ( __func__ );
Matrix m{ { 2, -1, 5, 10 }, { 1, 1, -3, -2 }, { 2, 4, 1, 1 } };
282 6 Die Matrix-Klasse
ofs << m;
Es folgt die Ausgabe des Programms. Die Matrizen werden in der gleichen Reihenfolge
ausgegeben wie in Gl. 6.7.
6.12 Übungsaufgaben
1. Zeigen Sie mit einem Computerprogramm, dass A2 − 4A + 5I2 = 02 gilt, wobei
1 2 1 0 0 0
A= , I 2 = , 0 2 = .
−1 3 0 1 0 0
A2 (x) + B 2 (x) = I ist, wobei I die 2 × 2-Einheitsmatrix ist. Erstellen Sie ein Compu-
terprogramm, welches für eine Reihe von Zufallswerten für x die Gleichung bestätigt.
3. Berechnen Sie für die Matrix A und den Vektor x
1 13 6 7 6
17 15 5 9 15
A= , x =
10 16 20 20 5
11 1 4 19 9
Übersicht
7.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
7.2 LU-Zerlegung mit der GNU Scientific Library . . . . . . . . . . . . . . . . . . . . . . . . . . . 287
7.3 Entwurf einer C++-Klasse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 289
7.4 Beispielanwendungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
7.5 Beispiele aus der Physik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
7.6 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
7.1 Einleitung
Viele Probleme aus der Mathematik und Physik werden durch lineare Gleichungssyste-
me beschrieben. Dabei geht es um die Berechnung von n unbekannten Größen, wenn
m lineare Gleichungen vorliegen. Wenn n = m ist, haben wir es mit einem linearen
quadratischen Gleichungssystem zu tun, welches die allgemeine Form
a11 x1 + a12 x2 + · · · + a1n xn = b1
a21 x1 + a22 x2 + · · · + a2n xn = b2
..
.
an1 x1 + an2 x2 + · · · + ann xn = bn
Die am häufigsten verwendete Methode zur Lösung solcher Gleichungssysteme ist der
Gauß-Algorithmus. Ziel ist, das System durch Transformationen in eine Dreiecksform zu
überführen,
∗ ∗
a11 a∗12 · · · a∗1n x1 b1
0 a∗22 · · · a∗2n x2 b∗2
. .. . . .. . = . ,
.. . . . . .
. .
0 0 · · · a∗nn xn b∗n
wobei die Koeffizienten a∗ij und die Komponenten b∗i aus den ursprünglichen Koeffizienten
aij und Komponenten bi durch äquivalente Umformungen hervorgegangen sind. Diese
bestehen im Wesentlichen darin, die Gleichungen mit konstanten Faktoren zu multipli-
zieren und sie miteinander zu addieren. Zusätzlich können auch Zeilenvertauschungen
vorgenommen werden, z.B. wenn a11 = 0. Dies hat zur Folge, dass die Koeffizienten-
matrix und die rechte Seite des Systems nicht mehr die ursprünglichen sind, aber das
Gleichungssystem, welches sie repräsentieren, dasselbe bleibt. Es ist sinnvoll, durch Zei-
lenvertauschungen eine Matrix zu bilden, in der das Element a11 durch das betragsgrößte
Element der Spalte ersetzt wird, welches wir auch Pivot-Element nennen. Ist das Glei-
chungssystem in eine Dreiecksgestalt überführt worden, berechnen wir die Lösung durch
Rücksubstitution. Beginnend mit der letzten Gleichung wird xn berechnet und in die
vorletzte Gleichung eingesetzt, was zur Berechnung von xn−1 führt, usw.
Mittels des Gauß-Algorithmus lässt sich die Koeffizientenmatrix in zwei Dreiecksma-
trizen L und U zerlegen, wobei L die untere Dreiecksmatrix ist, in der alle Elemente über
der Hauptdiagonalen null und die Diagonalelemente eins sind und U die obere Dreiecks-
matrix ist, in der alle Elemente unterhalb der Hauptdiagonalen null sind. Zwischen L, U
und A gilt die Relation
P A = LU, (7.1)
wobei P eine Permutationsmatrix ist, die jede Reihenvertauschung während der Berech-
nung der beiden Dreiecksmatrizen registriert.
Eine Permutationsmatrix ist eine Einheitsmatrix, in der die Zeilen bzw. Reihen ver-
tauscht sind. Wird eine m × m-Matrix mit einer m × m-Permutationsmatrix von links
multipliziert, so hat dies zur Folge, dass eine Zeilenvertauschung bei A stattfindet. Eine
Multiplikation der Matrix mit der Permutationsmatrix von rechts, hat eine Spaltenver-
tauschung von A zur Folge. Wir wollen dies anhand eines einfachen Beispiels deutlich
machen und multiplizieren eine 3 × 3-Permutationsmatrix P von links mit einer 3 × 3-
Matrix A:
0 0 1 1 2 3 7 8 9
PA =
0 1 0 4 5 6 = 4 5 6
1 0 0 7 8 9 1 2 3
P ist aus einer 3 × 3-Einheitsmatrix hervorgegangen, bei der die erste und letzte Reihe
vertauscht wurden mit dem Resultat, dass die erste und dritte Reihe von A vertauscht
7.2 LU-Zerlegung mit der GNU Scientific Library 287
werden. Entsprechend hat die Multiplikation von rechts von A mit P eine Vertauschung
der ersten und dritten Spalte zur Folge:
1 2 3 0 0 1 3 2 1
AP =
4 5 6 0 1 0 = 6 5 4
7 8 9 1 0 0 9 8 7
Eine Faktorisierung der Koeffizientenmatrix dieser Art führt dann folgendermaßen zur
Lösung des Gleichungssystems
Ax = b ⇒ LU x = b ⇒ L (U x) = b ⇒ Ly = b. (7.2)
| {z }
=y
mittels einer LU-Zerlegung gelöst. Die Daten der Koeffizientenmatrix werden in einem
linearen Feld mit sechzehn Elementen und die der rechten Seite in einem Feld der Länge
vier gespeichert. Die Routinen der gsl benötigen jedoch als Eingabe gsl-Matrizen und
Vektoren. Es werden deswegen Views auf diese Daten erzeugt, die es erlauben, die Felder
wie eine Matrix und wie einen Vektor zu behandeln. Für den Lösungsvektor wird ein
gsl-Vektor reserviert. Es folgt die LU-Zerlegung der Koeffizientenmatrix. Die Daten der
oberen und unteren Dreieckmatrix werden in die ursprüngliche Koeffizientenmatrix ge-
schrieben. Das Gleichungssystem wird gelöst und der Lösungsvektor auf dem Bildschirm
ausgegeben. Anschließend werden die angeforderten Ressourcen freigegeben.
288 7 Lösung von linearen Gleichungssystemen
/**
* @brief lu0 LU - Zerlegung einer Matrix Originalbeispiel mit
* Kommentaren . Lösung eines linearen Gleichungssystems A x= b
* https :// www.gnu.org/ software /gsl/doc/html/ linalg .html# examples
*/
inline void lu0 () {
// die Koeffizientenmatrix A
double a_data [] = { 0.18 , 0.60 , 0.57 , 0.96 , //
0.41 , 0.24 , 0.99 , 0.58 , //
0.14 , 0.30 , 0.97 , 0.66 , //
0.51 , 0.13 , 0.19 , 0.85 };
// der Lösungsvektor
gsl_vector *x = gsl_vector_alloc (4);
// speichere Informationen zur Erzeugung der
// Permutationsmatrix
gsl_permutation *p = gsl_permutation_alloc (4);
Es stellt sich die Frage nach der Notwendigkeit der Einführung einer Klasse, denn das
Programm ist einfach zu verstehen und leicht zu programmieren. Wir möchten als erstes
durch die Klasse die Anforderung und Freigabe der Ressourcen automatisieren und da-
mit eine potenzielle Fehlerquelle eliminieren. Wenn zusätzlich mit viel weniger Quelltext
dasselbe Resultat erreicht wird, dann haben wir zwei sehr starke Argumente, welche für
die Einführung einer Klasse sprechen.
7.3 Entwurf einer C++-Klasse 289
/**
* @brief The LU_Decomposition class Lösung eines linearen
* Gleichungssystems mittels der LU - Zerlegung
*/
class LU_Decomposition
{
public :
using Matrix = gsl :: Matrix ;
using Vector = gsl :: Vector ;
private :
// linke Seite des Gleichungssystems
Matrix _left ;
// rechte Seite des Gleichungssystems
Vector _right ;
// Lösungsvektor
Vector _solution ;
public :
// Reihen - und Zeilenvertauschung (gsl)
gsl_permutation * _permutation ;
// (-1)^n Anzahl der Vertauschungen (gsl)
int _permutation_sign ;
public :
Der Standardkonstruktor erzeugt eine leere Instanz der Klasse. Die Koeffizientenmatrix
und die rechte Seite des Gleichungssystems müssen über Elementfunktionen festgelegt
werden.
/**
* @brief LUDecomposition erzeuge eine leere Instanz
*/
inline LU_Decomposition () {}
Mit diesem Konstruktor wird mit der Erzeugung einer Instanz auch das Gleichungssystem
angegeben.
/**
* @brief LUDecomposition Konstruktor
* @param m Koeffizientenmatrix
* @param v rechte Seite des Gleichungssystems
*/
inline LU_Decomposition (const Matrix &m, const Vector &v)
: _left { m }
290 7 Lösung von linearen Gleichungssystemen
, _right { v }
, _solution ( _right .size ()) {}
Für die Freigabe des Speichers sorgen die Klassen Matrix und Vektor selbst, nicht aber
die Permutationsstruktur. Der Destruktor gibt den Speicher dieser gsl-Struktur frei.
/**
* Destruktor
*/
inline ~ LU_Decomposition () {
if ( _permutation != nullptr ) {
//gsl - Struktur wird freigegeben
gsl_permutation_free ( _permutation );
}
}
Das Gleichungssystem kann mit den beiden folgenden Funktionen festgelegt werden.
/**
* @brief set_left
* @param m die Koeffizientenmatrix
*/
inline void set_left ( const Matrix &m) { _left = m; }
/**
* @brief set_right
* @param v die rechte Seite des Gleichungssystems
*/
inline void set_right ( const Vector &v) { _right = v; }
Ein lesender Zugriff auf die Koeffizientenmatrix und die rechte Seite des Gleichungssys-
tems ist über die Methoden (Listing 7.8) und (Listing 7.9) möglich. Die Koeffizienten-
matrix enthält nach der LU-Zerlegung nicht mehr die ursprünglichen Daten, sondern die
beiden Dreiecksmatrizen.
/**
* @brief get_left
* @return Referenz auf die Koeffizientenmatrix
*/
inline const Matrix & get_left () const { return _left; }
/**
* @brief get_right
* @return Referenz auf die rechte Seite des
* Gleichungssystems
*/
const Vector & get_right () const { return _right ; }
/**
* @brief operator () Zugriff auf die Elemente des Lösungsvektors
* @param idx Index
* @return Lösung an der Stelle idx
*/
inline double operator ()( size_t idx) const { //
return _solution [idx ];
}
Auf die einzelnen Elemente der Koeffizientenmatrix wird mit dieser Version des Klam-
meroperators lesend zugegriffen.
/**
* @brief operator () Zugriff auf die Koeffizientenmatrix
* @param i Index Reihe
* @param j Index Spalte
* @return das Element i,j 0 ,1 ,2 ,3 ,...
*/
inline double operator ()( size_t i, size_t j) const { //
return _left (i, j);
}
Eine LU-Zerlegung der Koeffizientenmatrix kann auch unabhängig von der Lösung des
Gleichungssystems durchgeführt werden.
/**
* @brief decompose Zerlegung der Koeffizientenmatrix
* in obere und untere Dreiecksmatrix
*/
inline void decompose () {
_permutation = gsl_permutation_alloc (std :: min(_left.rows (), //
_left. columns ()));
gsl_linalg_LU_decomp ( _left .gsl (),
_permutation , //
& _permutation_sign );
}
Ein Gleichungssystem, dessen Koeffizientenmatrix und rechte Seite bereits festgelegt wur-
den, wird mit dieser Elementfunktion gelöst.
/**
* @brief solve Berechnung des Lösungsvektors
*/
inline void solve () {
// zerlege Koeffizientenmatrix in obere und untere Dreiecksmatrix
decompose ();
// Lösungsvektor muss die gleiche Länge haben wie die
// rechte Seite
if ( _solution .size () != _right .size ()) {
_solution = Vector ( _right .size ());
}
// löse das Gleichungssystem
gsl_linalg_LU_solve ( _left .gsl () , //
_permutation ,
_right .gsl () ,
_solution .gsl ());
}
Das Gleichungssystem kann in einem Schritt mit einer Elementfunktion festgelegt und
gelöst werden.
/**
* @brief solve Berechnung des Lösungsvektors eines
* Gleichungssystems
* @param m die Koeffizientenmatrix
* @param v rechte Seite des Gleichungssystems
*/
inline void solve ( const Matrix &m, const Vector &v) {
set_left (m);
set_right (v);
solve ();
}
Eine Referenz auf den Lösungsvektor erhalten wir mit der nächsten Funktion. Es versteht
sich, dass kein schreibender Zugriff erlaubt ist, da sonst die Lösung geändert werden
könnte.
/**
* @brief solution
* @return Referenz auf den Lösungsvektor
*/
inline const Vector & solution () const { return _solution ; }
Eine Kopie der unteren und der oberen Dreiecksmatrix erhalten wir mit zwei weiteren
Elementfunktionen (Listing 7.16 und Listing 7.17). Auch hier gilt, dass zuvor eine LU-
Zerlegung durchgeführt werden muss.
/**
* @brief lower_matrix
* @return Kopie der unteren Dreiecksmatrix
*/
inline Matrix lower_matrix () const {
// reserviere Speicher fur das Ergebnis
Matrix mout( _left .rows () , _left . columns ());
return mout;
}
/**
* @brief upper_matrix
* @return Kopie der oberen Dreiecksmatrix
*/
inline Matrix upper_matrix () const {
// reserviere Speicher fur das Ergebnis
Matrix mout( _left .rows () , _left . columns ());
return mout;
}
/**
* @brief permutation_matrix
* @return Permutationsmatrix
*/
inline Matrix permutation_matrix () {
// Anzahl der Elemente der Permutation
size_t psize = static_cast <size_t >( _permutation ->size);
Wir führen eine weitere Elementfunktion der Klasse ein, welche die Elemente einer Per-
mutationsmatrix als Vektor ausgibt.
/**
* @brief permutation_vector Permutationsvektor wird aus
* Permutationstruktur generiert
* @return Permutationsvektor
*/
inline Vector permutation_vector () const {
// erzeuge Vektor für die Elemente der Permutation
gsl :: Vector vec( _permutation ->size);
Die Determinante einer Matrix wird mit einer statischen Funktion berechnet. Sie benötigt
deswegen keine Instanz der Klasse, um aufgerufen zu werden (Listing 7.20). Dasselbe gilt
auch für die Berechnung einer inversen Koeffizientenmatrix (Listing 7.21).
/**
* @brief det Berechnung der Determinante
* @param m die Matrix
* @return die Determinante
*/
inline static double det( const Matrix &m) {
LU_Decomposition lu;
lu._left = m;
//LU - Zerlegung der Matrix
lu. decompose ();
// Berechnung der Determinante
7.4 Beispielanwendungen 295
/**
* @brief inverse Berechnung der Inversen einer Matrix
* @param m die Matrix
* @return die inverse Matrix
*/
inline static Matrix inverse ( const Matrix &m) {
// reserviere Speicher für die Inverse Matrix
Matrix mInverse (m.rows () , m. columns ());
LU_Decomposition lu;
lu._left = m;
//LU - Zerlegung der Matrix
lu. decompose ();
// Berechnung der Inversen Matrix
gsl_linalg_LU_invert (lu. get_left ().gsl (), //
lu. _permutation ,
mInverse .gsl ());
return mInverse ;
}
7.4 Beispielanwendungen
Für die Beispiele in diesem Abschnitt definieren wir folgende Synonyme.
/**
* @brief get_output_stream generiere Ausgabestrom
* @param name Dateiname
* @param fmt Formatierungsanweisung
*/
inline auto get_output_stream ( const char *name , Format fmt =
Output :: csv) {
auto ofs = Output :: get_stream ("x038", fmt , name);
ofs << std :: fixed << std :: setprecision (2);
return ofs;
}
Quelltext Die Struktur des Programms wird durch Nutzung der Klassen für Vekto-
ren und Matrizen sowie der LU-Zerlegung einfach. Selbst die Ausgabe erfolgt ohne die
Programmierung von Schleifen.
/**
* @brief lu1 Lösung eines linearen Gleichungssystems durch LU - Zerlegung
*/
inline void lu1 () {
// öffne Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
// Koeffizientenmatrix
Matrix left{ { { 3, -0.1, -0.2 }, //
{ 0.1 , 7, -0.3 },
{ 0.3 , -0.2, 10 } } };
Daten Wir erhalten der Reihe nach die linke, die rechte Seite und den Lösungsvektor
von Gl. 7.5.
Es folgen die untere und die obere Dreiecksmatrix zusammengefasst als eine Matrix und
aufgeteilt in zwei Matrizen.
Ist die Determinante der Koeffizientenmatrix ungleich null, so kann ein Gleichungssystem
mit der Cramer’schen Regel gelöst werden. Die einzelnen Lösungen werden mit der Formel
det(Ai )
xi = , i = 0, 1, 2 (7.8)
det(A)
berechnet, wobei A die Koeffizientenmatrix ist und jedes der Ai , i = 0, 1, 2 durch den
Austausch der i-ten Spalte der Koeffizientenmatrix mit dem Vektor der rechten Seite
entsteht.
Quelltext In der GNU Scientific Library ist die Cramer’sche Regel nicht implemen-
tiert, da alternative Methoden existieren, die weniger rechenintensiv sind. Wir wollen
dennoch das Gleichungssystem Gl. 7.5 mit dieser Methode lösen, um die Berechnung von
Determinanten mittels unserer Klasse zu demonstrieren.
/**
* @brief lu2 Lösung eines Gleichungssystems mit der Cramer 'schen Regel
*/
inline void lu2 () {
// die Cramer 'sche Regel für x_i als lambda - Funktion
auto cramer_i = []( size_t i, const Matrix &m, const Vector &v) {
// Kopiere linke Seite des GL - Systems
auto m1 = m;
// ersetze i-te Spalte durch rechte Seite
m1. set_column (v, i);
// Determinante Nenner
auto det = LU :: det(m);
// Determinante Zähler
auto det1 = LU :: det(m1);
// Lösung x_i
return det1 / det;
};
// der Lösungsvektor
const size_t N = right .size ();
7.4 Beispielanwendungen 299
Daten Wie erwartet, gibt das Programm, neben der Koeffizientenmatrix und der rech-
ten Seite des Gleichungssystems, den korrekten Lösungsvektor aus.
---------------------
3.00 , -0.10 , -0.20
0.10 ,7.00 , -0.30
0.30 , -0.20 ,10.00
---------------------
7.85 , -19.30 ,71.40
---------------------
3.00 , -2.50 ,7.00
Eine weitere Möglichkeit ein Gleichungssystem zu lösen ist, von links mit der inversen
Koeffizientenmatrix, vorausgesetzt sie existiert, zu multiplizieren.
Quelltext und Daten Die Berechnung der inversen Matrix gehört zum Funktionsum-
fang unserer Klasse und ist daher einfach zu programmieren. Das Programm wird neben
der Lösung von Gl. 7.5 auch die inverse Koeffizientenmatrix und das Ergebnis der Mul-
tiplikation A−1 A ausgeben (Listing 7.31).
/**
* @brief lu3 Lösung eines Gleichungssystems mittels der inversen Matrix
*/
inline void lu3 () {
// erzeuge Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
// Koeffizientenmatrix
Matrix left{ { { 3, -0.1, -0.2 }, //
{ 0.1 , 7, -0.3 },
300 7 Lösung von linearen Gleichungssystemen
{ 0.3 , -0.2, 10 } } };
// Lösung
Vector solution = dot( leftinverse , right);
---------------------
3.00 , -0.10 , -0.20
0.10 ,7.00 , -0.30
0.30 , -0.20 ,10.00
---------------------
7.85 , -19.30 ,71.40
---------------------
0.33 ,0.00 ,0.01
-0.01 ,0.14 ,0.00
-0.01 ,0.00 ,0.10
---------------------
3.00 , -2.50 ,7.00
---------------------
1.00 , -0.00 ,0.00
0.00 ,1.00 ,0.00
-0.00 ,0.00 ,1.00
Eine Kugel mit der Masse m ist, wie in Abb. 7.1a gezeigt, an Seilen mit vernachlässigbarer
Masse aufgehängt. Das System befindet sich im Gleichgewicht.
T2
T2y
ϕ
ϕ A
T2x T3
T1
A
T1
ey m B
ex m B
w
(a)
(b)
Abb. 7.1: (a) Masse im Gleichgewicht, befestigt an einem System von drei Seilen (b)
Kräfte, die auf die Masse und auf den Punkt A wirken.
Lösung
Die Kräfte an den Enden von masselosen Seilen sind vom Betrag gleich und haben ent-
gegengesetzte Richtungen. Somit gilt für das Seil AB:
oder { {
T3⃗ex − T2 cos φ⃗ex = 0 T3 − T2 cos φ = 0
⇒ (7.14)
−T1⃗ey + T2 sin φ⃗ey = 0 −T1 + T2 sin φ = 0.
Fassen wir alle Ergebnisse zusammen, erhalten wir das Gleichungssystem
{
−T2 cos φ + T3 = 0 (7.15a)
T2 sin φ + 0 = mg. (7.15b)
302 7 Lösung von linearen Gleichungssystemen
Die Berechnung der Seilspannungen Zur Lösung des Gleichungssystems wenden wir
die Cramer’sche Regel an:
− cos φ 1
D= = − sin φ
sin φ 0
und
0 1 − cos φ 0
D1 = = −mg, D2 = = −mg cos φ
mg 0 sin φ mg
Das Computerprogramm
Kurzbeschreibung Die Beschreibung des Systems wird über eine Modellklasse und die
Berechnung der Seilspannungen über zwei abgeleitete Rechenmodelle realisiert. Wir ha-
ben so die Freiheit, mit Methoden unserer Wahl, unter Verwendung derselben Modell-
klasse, die Ergebnisse zu berechnen.
Modellklasse
– Eingabe: Winkel φ und die Masse m (Abb. 7.1)
– Ausgabe: Die Seilspannungen T1 , T2 und T3
Quelltext Die Modellklasse liest und speichert über den Konstruktor (Listing 7.34) die
Programmparameter m und φ. Das Gewicht der Masse wird innerhalb des Konstruktors
berechnet und kann über eine Elementfunktion (Listing 7.35) gelesen werden. Auf die
berechneten Seilspannungen wird mithilfe der folgenden Aufzählung zugegriffen.
/**
* @brief The Idx enum Zugriff auf die Seilspannungen
*/
enum class Idx { T1 , T2 , T3 };
/**
* @brief The X1800 class Masse im Gleichgewicht befestigt an
* einem System mit drei Seilen
*/
class X1800 : public XModel , public CResult <3, Idx >
{
private :
// das Gewicht der Masse
double _weight = 0;
public :
const double mass; // Masse der Kugel
const double phi; // Winkel
/**
* @brief X1800 Konstruktor
* @param xmass Masse
* @param xphi Winkel
*/
inline X1800 ( double mass , double phi)
: XModel ( __func__ )
, mass{ mass }
, phi{ phi } {
Check :: all(nmx_msg , { mass > 0, phi > 0, phi < 90 });
_weight = mass * gravitation :: Earth ::g;
}
/**
* @brief weight Zugriff auf das intern gespeicherte Gewicht
* @return das Gewicht
*/
inline double weight () const { return _weight ; }
Die Seilspannungen können per Index (Listing 7.32) gelesen werden. Mit Initialisierung
einer Instanz sind noch keine gültigen Werte vorhanden.
/**
* @brief tension lese Seilspannung über Index ( Aufzählung )
* @param idx der Index ( Element der Aufzählung )
* @return die Seilspannung
*/
inline double tension (Idx idx) const { return get(idx); }
}; // X1800
Es folgt die Implementierung des ersten Rechenmodells. Die Realisierung reduziert sich
auf die Programmierung einer einzigen Elementfunktion, da die Konstruktoren durch
eine using-Anweisung von der Basisklasse geerbt werden. Wir schreiben Gl. 7.11, Gl.
7.12 und Gl. 7.14 als Matrixgleichung und lösen diese mittels LU-Zerlegung.
1 0 0 T1 mg
0 − cos φ 1 T2 = 0 (7.17)
−1 sin φ 0 T3 0
/**
* @brief The CModel1 struct Berechnung der Seilspannungen durch
* direkte Lösung des Gleichungssystems
*/
struct CModel1 : public X1800 {
// erbe Konstruktoren der Basisklasse
using X1800 :: X1800 ;
Das zweite Rechenmodell implementiert die Formeln Gl. 7.16a, Gl. 7.16b und Gl. 7.16c.
Auch hier handelt es sich um eine einfache Struktur, welche eine Elementfunktion imple-
mentiert und die gesamte Funktionalität der Basisklasse erbt.
/**
* @brief The CModel2 struct Berechnung der Seilspannungen
* über die theoretisch hergeleitete Formeln
*/
struct CModel2 : public X1800 {
// erbe Konstruktoren der Basisklasse
using X1800 :: X1800 ;
Daten Für eine Reihe von Massen m werden, für alle Winkel φ, die Seilspannungen
mit den zwei Rechenmodellen berechnet und in einer gemeinsamen Datei abgespeichert
(Tab. 7.1, Abb. 7.2).
/**
* @brief run Berechnung von Beispieldaten mit zwei Rechenmodellen
*/
inline void run () {
using Data = nmx :: Data <7 >;
for (auto m : { 2.0 , 4.0 , 6.0 }) {
Data data;
for ( double phi = 10.0 _deg; phi < 90.0 _deg; phi += 10.0 _deg) {
// initialisiere zwei Rechenmodelle
CModel1 xobj1 (m, phi);
CModel2 xobj2 (m, phi);
// Lösung über LU - Zerlegung
xobj1 . apply ();
// Lösung über hergeleitete Formeln
xobj2 . apply ();
// füge Ergebnisse einer gemeinsamen Zahlentabelle hinzu
data += { Math :: to_degrees (phi), //
xobj1 . tension (Idx ::T1), xobj1. tension (Idx ::T2), //
xobj1 . tension (Idx ::T3), xobj2. tension (Idx ::T1),
xobj2 . tension (Idx ::T2), xobj2. tension (Idx ::T3) };
}
// speichere Daten im CSV - und LaTeX - Format . Dateinamenskonflikte
// werden durch das Hinzufügen der Masse m vermieden
X1800 :: save(data , Output :: plot , m);
X1800 :: save(data , Output :: latex , m);
}
}
Tab. 7.1: Die Beträge der Seilspannungen T1 , T2 und T3 , berechnet mittels LU-
Zerlegung, und den Formeln aus der Aufgabenlösung T1c , T2c und T3c in Abhängigkeit
vom Winkel φ für m = 2 kg
φ [deg] T1 [N] T2 [N] T3 [N] T1c [N] T2c [N] T3c [N]
1.000e+01 1.961e+01 1.129e+02 1.112e+02 1.961e+01 1.129e+02 1.112e+02
2.000e+01 1.961e+01 5.735e+01 5.389e+01 1.961e+01 5.735e+01 5.389e+01
3.000e+01 1.961e+01 3.923e+01 3.397e+01 1.961e+01 3.923e+01 3.397e+01
4.000e+01 1.961e+01 3.051e+01 2.337e+01 1.961e+01 3.051e+01 2.337e+01
5.000e+01 1.961e+01 2.560e+01 1.646e+01 1.961e+01 2.560e+01 1.646e+01
6.000e+01 1.961e+01 2.265e+01 1.132e+01 1.961e+01 2.265e+01 1.132e+01
7.000e+01 1.961e+01 2.087e+01 7.139e+00 1.961e+01 2.087e+01 7.139e+00
8.000e+01 1.961e+01 1.992e+01 3.458e+00 1.961e+01 1.992e+01 3.458e+00
306 7 Lösung von linearen Gleichungssystemen
m = 2kg m = 2kg
m = 4kg 300 m = 4kg
300
m = 6kg m = 6kg
200
T2 [N ]
T3 [N ]
200
100
100
0
0
20 40 60 80 20 40 60 80
φ[deg] φ[deg]
(a) (b)
Abb. 7.2: Die Seilspannungen (a) T2 und (b) T3 als Funktionen des Winkels φ für
Massen m = 2 kg, m = 4 kg und m = 6 kg
Das System (Abb. 7.3a) befindet sich im Gleichgewicht. Der maximale Haftreibungsko-
effizient zwischen m2 und der starren Unterlage U ist µ. Die Masse der Seile sei vernach-
lässigbar.
φ φ
⃗
N T⃗3
m2 ⃗
R T⃗2 T⃗2′ A
A
T⃗1
w
⃗2
U T⃗1′
m1 w
⃗1
ey ey
ex ex
(a) (b)
Abb. 7.3: (a) Das System im Gleichgewicht (b) Auf die Masse m2 wirken die Haftreibung
⃗ das Gewicht w
R, ⃗ 2 , die Seilspannung T⃗2 und die Normalkraft N
⃗ . Auf die Masse m1 wirken
′
die Seilspannung T⃗1 und das Gewicht w ⃗ 1.
1. Wie groß darf die Masse m1 werden, damit das System bei vorgegebener Masse m2
und Haftreibungskoeffizienten µ sich noch im Gleichgewicht befindet?
2. Berechnen Sie für eine Masse m2 = 1 kg und µ = 0.1, 0.2, 0.3, 0.4 sowie φ =
10◦ , 30◦ , 40◦ den maximalen Wert für m1 . Stellen Sie die errechneten Daten in Ta-
bellen und Diagrammen dar.
7.5 Beispiele aus der Physik 307
Lösung
Das Koordinatensystem Wir legen die positive y-Achse entgegengesetzt zur Schwer-
kraft.
|R| ⃗|
⃗ = µ|N (7.20)
Formeln für die Masse m1 und die Seilspannungen Die Bedingungen Gl. 7.20 und Gl.
7.21 eingesetzt in Gl. 7.19, geben folgenden Satz von Gleichungen:
T1 = m1
(7.22a)
T2 m2 µ
T1
= tan φ (7.22b)
T2
Damit folgt für die Masse m1
m1 = µm2 tan φ (7.23)
Das Computerprogramm
Kurzbeschreibung Wir verfolgen wie auch in allen anderen Fällen einen modularen
Ansatz und überlassen die Berechnung der unbekannten Größen einem Rechenmodell.
Genauer gesagt werden wir zwei Rechenmodelle erstellen, um zwei alternative Lösungs-
wege vorzustellen. Beide Rechenmodelle werden von einer gemeinsamen Modellklasse
abgeleitet.
Modellklasse (Basisklasse)
– Eingabe: Haftreibungskoeffizient µ, Masse m2 und Winkel φ (Abb. 7.3)
– Ausgabe: Seilspannungen T1 , T2 , T3 , Normalkraft N und Masse m1
Quelltext Alle Eingabeparameter werden über den Konstruktor (Listing 7.42) eingele-
sen und im public-Bereich als konstante Attribute gespeichert (Listing 7.42). Die Lösun-
gen werden in einem Feld gespeichert auf dessen Elemente über einen Aufzählungstyp
zugegriffen werden kann.
/**
* @brief The Idx enum Aufzählung , Zugriff auf gespeicherte Variablen
*/
enum Idx { T1 , T2 , T3 , N, M1 };
/**
* @brief The X1900 class Massen befestigt an Seilen und Haftreibung
* ( Modellklasse )
*/
class X1900 : public XModel , public CResult <5, Idx >
{
public :
// Eingabeparameter
const double mu; // Haftreibungskoeffizient
const double mass2 ; // Masse m2
const double phi; // Winkel
public :
/**
* @brief X1900 Konstruktor
* @param muin Haftreibungskoeffizient
* @param mass2in Masse m2
* @param phiin Winkel
*/
inline X1900 ( double muin , double mass2in , double phiin)
: XModel ( __func__ )
, mu{ muin }
, mass2 { mass2in }
, phi{ phiin } {
Check :: all(nmx_msg , { mu > 0, mass2 > 0, phi > 0 });
}
}; // X1900
Es folgt das erste Rechenmodell, welches die Formeln Gl. 7.23 und Gl. 7.24a, Gl. 7.24b,
Gl. 7.24c innerhalb einer Elementfunktion implementiert.
/**
* @brief The CModel struct berechnet die Daten auf Basis hergeleiteter
* Formeln ( Rechenmodell )
*/
struct CModel1 : public X1900 {
// erbe Konstruktoren der Basisklasse
using X1900 :: X1900 ;
Das zweite Rechenmodell löst folgendes Gleichungssystem (Gl. 7.19, Gl. 7.20 und Gl.
7.21) mittels LU-Zerlegung.
0 1 0 −µ 0 T1 0
0 0 1 0
0 T 2 m 2 g
0 1 − cos φ 0 0 T3 = 0 (7.25)
1 0 − sin φ 0 0 N 0
1 0 0 0 −g m1 0
310 7 Lösung von linearen Gleichungssystemen
/**
* @brief The CModel2 struct löst das Gleichungssystem direkt mittels
* LU - Zerlegung ( Rechenmodell )
*/
struct CModel2 : public X1900 {
// erbe Konstruktoren der Basisklasse
using X1900 :: X1900 ;
// löse Gleichungssystem
inline void apply () {
using LU = gsl :: LU_Decomposition ;
//LU - Zerlegung : Koeffizientenmatrix
LU:: Matrix left{ { 0, 1, 0, -mu , 0 },
{ 0, 0, 0, 1, 0 },
{ 0, 1, -cos(phi), 0, 0 },
{ 1, 0, -sin(phi), 0, 0 },
{ 1, 0, 0, 0, -Earth ::g } };
//LU - Zerlegung : rechte Seite des Gleichungssystems
LU:: Vector right { 0, mass2 * Earth ::g, 0, 0, 0 };
//LU - Zerlegung : Berechnung der Lösung
LU lusolver { left , right };
lusolver . solve ();
// speichere Ergebnisse
set_result ({ lusolver (Idx :: T1),
lusolver (Idx :: T2),
lusolver (Idx :: T3),
lusolver (Idx ::N),
lusolver (Idx :: M1) });
}
}; // CModel2
Daten Für einen festen Wert der Masse m2 berechnen wir Daten für verschiedene Haft-
reibungskoeffizienten und Winkel mithilfe der beiden Rechenmodelle. Die Ergebnisse wer-
den in Dateien abgespeichert (Tab. 7.2, Tab. 7.3 Abb. 7.4).
/**
* @brief run Massen befestigt an Seilen und statische
* Reibung ( Berechnung von Beispieldaten )
*/
inline void run () {
using Data = Data <7 >;
const double mass2 = 1.0;
for (auto mu : { 0.1 , 0.2 , 0.3 , 0.4 }) {
// reservierter Speicher für Daten
Data data;
for (auto phi : { 10.0 _deg , 30.0 _deg , 40.0 _deg }) {
// verwende hergeleitete Formeln
CModel1 xobj1 { mu , mass2 , phi };
xobj1 . apply ();
// löse Gleichungssystem
CModel2 xobj2 { mu , mass2 , phi };
xobj2 . apply ();
// füge Ergebnisse einer gemeinsamen Zahlentabelle hinzu
// clang - format off
7.5 Beispiele aus der Physik 311
data += { phi ,
xobj2 (Idx :: T1), xobj2(Idx ::T2), xobj2(Idx ::T3),
xobj2 (Idx ::N), xobj2(Idx ::M1), xobj1(Idx ::M1) };
// clang - format on
}
// speichere Daten im CSV - und LaTeX - Format . Dateinamenskonflikte
// werden durch das Hinzufügen des Haftreibungskoeffizienten
// vermieden
X1900 :: save(data , Output :: plot , mu);
X1900 :: save(data , Output :: latex , mu);
}
}
Tab. 7.2: Daten für m2 = 1 kg, µ = 0.1 und unterschiedlichen Winkeln. Die beiden
letzten Spalten sind die Werte für die Masse m1 aus den beiden Rechenmodellen.
φ [rad] T1 [N] T2 [N] T3 [N] N [N] m1 [kg] m1 [kg]
1.745e-01 1.729e-01 9.807e-01 9.958e-01 9.807e+00 1.763e-02 1.763e-02
5.236e-01 5.662e-01 9.807e-01 1.132e+00 9.807e+00 5.774e-02 5.774e-02
6.981e-01 8.229e-01 9.807e-01 1.280e+00 9.807e+00 8.391e-02 8.391e-02
Tab. 7.3: Daten für m2 = 1 kg, µ = 0.4 und unterschiedlichen Winkeln. Die beiden
letzten Spalten sind die Werte für die Masse m1 aus den beiden Rechenmodellen.
φ [rad] T1 [N] T2 [N] T3 [N] N [N] m1 [kg] m1 [kg]
1.745e-01 6.917e-01 3.923e+00 3.983e+00 9.807e+00 7.053e-02 7.053e-02
5.236e-01 2.265e+00 3.923e+00 4.529e+00 9.807e+00 2.309e-01 2.309e-01
6.981e-01 3.292e+00 3.923e+00 5.121e+00 9.807e+00 3.356e-01 3.356e-01
µ = 0.1 µ = 0.3
µ = 0.2 µ = 0.4
0.15 0.3
m1 [kg]
m1 [kg]
0.1 0.2
5 · 10−2
0.1
0.2 0.3 0.4 0.5 0.6 0.7 0.2 0.3 0.4 0.5 0.6 0.7
φ[rad] φ[rad]
Abb. 7.4: Die Masse m1 in Abhängigkeit vom Winkel φ und dem Haftreibungskoeffizi-
enten µ
312 7 Lösung von linearen Gleichungssystemen
Eine schiefe Ebene mit Neigungswinkel φ und der Masse m2 ruht auf reibungsfreiem
horizontalen Boden. Ein Holzblock der Masse m1 gleitet auf der rauen, schrägen Seite
der Ebene herunter. Der Reibungskoeffizient zwischen Holzblock und Ebene sei µ.
⃗1
N
⃗1
R
⃗v1
⃗2
N
⃗
r1 ⃗′
R 1
⃗e2y ⃗e2y
ϑ ⃗e1x ⃗e1y w
⃗1 ⃗e1x ⃗e1y
⃗e2x w
⃗2 ⃗′
N 1 ϑ
⃗
r2 ⃗e2x
(a) (b)
Abb. 7.5: Ein Holzblock bewegt sich entlang einer beweglichen schiefen Ebene. Die rot
eingezeichneten Kräfte wirken auf die schiefe Ebene und die blauen auf den Holzblock.
⃗ 1 ist die Normalkraft von der schiefen Ebene auf den Block, N
N ⃗ 2 die Normalkraft vom
Boden auf die schiefe Ebene und R ⃗ 1 die Gleitreibung.
1. Berechnen Sie die Beschleunigungen für die schiefe Ebene und den Holzblock sowie
die Normalkraft von der schiefen Ebene auf den Holzblock. Wie groß ist die vom
Holzblock auf die schiefe Ebene ausgeübte Kraft?
2. Für einen festen Neigungswinkel der schiefen Ebene variieren Sie das Massenverhältnis
m2
λ = m1 sowie den Gleitreibungskoeffizienten µ und berechnen die Beschleunigungen
der beiden Objekte sowie die Kräfte, die diese gegenseitig aufeinander ausüben.
Lösung
oder
{
m1 (ẍ1⃗e1x + ẍ2⃗e2x ) = −m1 g⃗e2y + N1⃗e1y + R1⃗e1x (7.29a)
m2 x¨2⃗e2x = N2⃗e2y − m2 g⃗e2y − R1′ ⃗e1x − N1′ ⃗e1y (7.29b)
Aus dem dritten Newton’schen Gesetz folgt
⃗ 1′ | = |N
|N ⃗ 1| (7.30)
′
|R
⃗ 1 | = |R⃗ 1 | = µN1 . (7.31)
⃗ 1′ und N
Wir ersetzen R ⃗ 1′ in (Gl. 7.29b):
{
m1 x¨1⃗e1x + m1 x¨2⃗e2x = −m1 g⃗e2y + N1⃗e1y + µN1⃗e1x (7.32a)
m2 x¨2⃗e2x = N2⃗e2y − m2 g⃗e2y − µN1⃗e1x − N1⃗e1y . (7.32b)
Durch skalare Multiplikation dieser Gleichungen mit ⃗e2x und ⃗e2y erhalten wir
m1 x¨1 cos ϑ + m1 x¨2 = −N1 sin ϑ + µN1 cos ϑ (7.33a)
m1 x¨1 sin ϑ = −m1 g + N1 cos ϑ + µN1 sin ϑ (7.33b)
m2 x¨2 = −µN1 cos ϑ + N1 sin ϑ (7.33c)
0 = N2 − m2 g − µN1 sin ϑ − N1 cos ϑ (7.33d)
oder
N1
ẍ1 cos ϑ + ẍ2 = −
(sin ϑ − µ cos ϑ) (7.34a)
m 1
ẍ1 sin ϑ = −g + N1 (cos ϑ + µ sin ϑ) (7.34b)
m1
N 1
ẍ2 = m2 (sin ϑ − µ cos ϑ)
(7.34c)
N2 = m2 g + N1 (µ sin ϑ + cos ϑ) . (7.34d)
und damit [ ]
m2 cos ϑ (µ cos ϑ − sin ϑ) +
m1
g cos ϑ µ
ẍ1 = −g sin ϑ + . (7.39)
1 −mm1
2
sin ϑ (µ cos ϑ − sin ϑ)
Das Computerprogramm
Die Modellklasse Wir möchten vermeiden, unnötige Elementfunktionen für den Zugriff
auf die bekannten Attribute der Modellklasse zu programmieren und speichern diese als
konstante Variablen im public-Bereich ab (Listing 7.46). Die einzige Möglichkeit, dieses
Vorhaben umzusetzen, ist die Variablen über den Konstruktor zu initialisieren (Listing
7.47). Für den Zugriff auf die Ergebnisse, die in einem Feld gespeichert werden, führen
wir einen Aufzählungstypen ein (Listing 7.48). Statt eines Index kann dadurch bequem
mittels eines Bezeichners auf die Elemente des Feldes zugegriffen werden. Es ist aber
nicht nur das, sondern wir sichern uns auch gleichzeitig von falschen Zugriffen ab, denn
wir garantieren durch die Einführung des Aufzählungstypen, dass nur gültige Indizes
verwendet werden können.
7.5 Beispiele aus der Physik 315
/**
* @brief The X410 class Masse gleitet auf bewegliche schiefe Ebene
* ( Modellklasse )
* @param XModel Basisklasse fur alle Modellklassen
* @param CResult Klasse zur Speicherung der Rechenergebnisse
*/
class X410 : public XModel , public CResult <4, Idx >
{
public :
// Eingabeparameter
const double m1 , m2;
const double theta ;
const double mu;
/**
* @brief X410 Konstruktor
* @param m1 Masse des Blocks
* @param m2 Masse der schiefen Ebene
* @param theta Neigung
* @param mu Reibungskoeffizient
*/
X410( double m1 , double m2 , double theta , double mu)
: XModel ( __func__ )
, m1{ m1 }
, m2{ m2 }
, theta { theta }
, mu{ mu } {
// sind alle Eingaben gültig ?
Check :: all(nmx_msg , { m1 > 0, m2 > 0, theta > 0, mu > 0 });
}
}; // X410
/**
* @brief The Idx enum Zugriff auf die einzelnen Elemente
*/
enum class Idx { x1ddot , x2ddot , N1 , N2 };
Das erste Rechenmodell Ziel dieses Rechenmodells ist, die gesuchten Größen direkt
über den Einsatz der hergeleiteten Formeln für ẍ1 (Gl. 7.39), ẍ2 (Gl. 7.37), N1 (Gl. 7.36)
und N2 (Gl. 7.34d) zu berechnen. Da die Ausdrücke kompliziert sind, wollen wir für jede
einzelne Formel eine Elementfunktion einführen. Die Klasse wird dadurch übersichtlicher.
Terme wie der Sinus und Kosinus des Neigungswinkels, die in allen Formeln vorkommen,
wollen wir nur einmal berechnen und innerhalb der Klasse speichern.
316 7 Lösung von linearen Gleichungssystemen
/**
* @brief The CModel1 class Rechenmodell
* Berechnet Daten durch Anwendung der hergeleiteten Formeln
*/
class CModel1 : public X410
{
private :
double _cTheta , _sTheta ;
double _m12 , _trm1 , _den;
Es folgen die Implementierungen der Formeln. Diese Elementfunktionen können nur in-
nerhalb der Klasse aufgerufen werden.
/**
* @brief N1
* @return Flächennormale auf Block
*/
double N1() const { //
return m1 * Earth ::g * _cTheta / (1 - _m12 * _sTheta * _trm1);
}
/**
* @brief N2
* @return Flächennormale auf schiefe Ebene
*/
double N2() const { //
return m2 * Earth ::g - N1 () * (mu * _sTheta + _cTheta );
}
/**
* @brief x1_ddot
* @return Beschleunigung des Blocks im System der schiefen Ebene
*/
double x1_ddot () const {
double num = Earth ::g * _cTheta * (_m12 * _cTheta * _trm1 + mu);
double den = (1 - _m12 * _sTheta * _trm1);
return -Earth ::g * _sTheta + (num / den);
}
/**
* @brief x2_ddot
* @return Beschleunigung der schiefen Ebene im festen
* Koordinatensystem
*/
7.5 Beispiele aus der Physik 317
public :
// erbe Konstruktoren von der Basisklasse
using X410 :: X410;
Bei jedem Aufruf der Elementfunktion Listing 7.54 werden zuerst, die in allen Formeln
vorkommenden Terme berechnet und zwischengespeichert. Diese werden in Kombinati-
on mit den Elementfunktionen der Klasse dazu benutzt, die Beschleunigungen und die
Normalkräfte zu berechnen.
/**
* @brief exec Berechnung und Speicherung der Normalkräfte und
* der Beschleunigungen
* @param xmodel Modellklasse
*/
inline void apply () {
_cTheta = cos( theta );
_sTheta = sin( theta );
_trm1 = mu * _cTheta - _sTheta ;
_den = 1 - _m12 * _sTheta * _trm1;
_m12 = m1 / m2;
// speichere Ergebnisse
set_result ({ x1_ddot () , x2_ddot (), N1 (), N2 () });
}
}; // CModel1
Das zweite Rechenmodell Statt die hergeleiteten Formeln für die gesuchten Größen
zu übernehmen, löst dieses Rechenmodell das lineare Gleichungssystem (Gl. 7.34, hier in
Matrixform):
cos ϑ 1 sin ϑ−µ cos ϑ
0 ẍ1 0
m 1
sin ϑ 0 − cos ϑ+µ sin ϑ 0 ẍ −g
m1 2
= (7.40)
0 1 − sin ϑ−µ cos ϑ 0 N1 0
m2
0 0 µ sin ϑ + cos ϑ 1 N2 m2 g
Mithilfe einer LU-Zerlegung erhalten wir eine Lösung, die identisch ist mit der des vorhe-
rigen Rechenmodells, ohne die komplizierten Formeln zu implementieren. Ein Blick auf
den Quelltext zeigt, dass dies wesentlich einfacher zu programmieren ist.
/**
* @brief The CModel2 struct Rechenmodell
* Berechnet Daten durch direktes Lösen des Gleichungssystems
*/
struct CModel2 : public X410 {
318 7 Lösung von linearen Gleichungssystemen
/**
* @brief run Beschleunigte schiefe Ebene Erstellung von
* Beispieldaten
*/
inline void run () {
// Masse gleitet auf schiefe Ebene
const double m1 = 1.0;
// Neigungswinkel der schiefen Ebene
const double theta = 30. _deg;
// Datenobjekt speichert berechnete Daten
Data <8> data;
// variiere lambda und Gleitreibungskoeffizienten
for ( double lambda : { 10, 100 , 1000 }) {
for ( double mu : { 0., 0.1 , 0.2 }) {
const double m2 = lambda * m1;
// Modellklasse und Rechenmodelle
CModel1 cobj1 { m1 , m2 , theta , mu };
CModel2 cobj2 { m1 , m2 , theta , mu };
cobj1 . apply ();
cobj2 . apply ();
data += { lambda ,
mu ,
cobj1 (Idx :: x1ddot ),
cobj1 (Idx :: x2ddot ),
cobj1 (Idx :: N1),
7.6 Übungsaufgaben 319
Wie erwartet nimmt die Beschleunigung der schiefen Ebene, bei wachsender Masse, ab.
7.6 Übungsaufgaben
1. Lösen Sie das lineare Gleichungssystem
2x − 3y + 2z + 3w =1
x − 2z + 2w =6
3x + y − z + 3w =8
4x − z =1
2. Entwerfen Sie für die Aufgabe aus Abschnitt 7.5.1 ein Rechenmodell, welches das
Gleichungssystem Gl. 7.15 mit der Cramer’schen Regel löst.
3. Die Rolle in einer idealen Atwood’schen Fallmaschine (Abb. 7.6) wird mit einer kon-
stanten Beschleunigung ⃗a nach oben beschleunigt. Die Masse der Seile kann vernach-
lässigt werden.
320 7 Lösung von linearen Gleichungssystemen
⃗a
m1
m2
a) Mit welchen Beschleunigungen bewegen sich die Massen m1 und m2 ? Wie groß ist
die Seilspannung?
b) Erstellen Sie eine Tabelle, in der für unterschiedliche Massenverhältnisse m1 /m2
die Beschleunigungen der Massen m1 und m2 und die Seilspannung in Abhängig-
keit von der Beschleunigung der Rolle dargestellt werden.
8 Nullstellenberechnung reeller
Funktionen
Übersicht
8.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
8.2 Zwei Beispielprogramme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
8.3 Eine Klasse für das Bisektionsverfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 327
8.4 Eine Klasse für das Newton-Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
8.5 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 335
8.6 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
8.1 Einleitung
Es existieren zwei Gruppen von Algorithmen zur Berechnung von Nullstellen reeller Funk-
tionen. Zur ersten Gruppe gehören Verfahren, die ohne Kenntnis der Ableitung einer
Funktion auskommen, und zur zweiten Gruppe solche, bei denen die Ableitung einer
Funktion bekannt sein muss. Alle Verfahren arbeiten iterativ.
Beim Bisektionsverfahren wird die Ableitung einer Funktion nicht vorausgesetzt. Ein
Intervall [x1 , x2 ] (Abb. 8.1a) wird so gewählt, dass f (x1 )f (x2 ) < 0 gilt. Dies garantiert,
dass die gesuchte Nullstelle xr innerhalb des Intervalls liegt. In einem nächsten Schritt
wird die Mitte des Intervalls x3 = x1 +x 2
2
gewählt und getestet, ob f (x1 )f (x3 ) < 0 oder
f (x3 )f (x2 ) < 0 ist und dadurch die Position der Nullstelle eingegrenzt. Dies wird so
lange fortgesetzt, bis das Intervall kleiner als ein vorgegebener Wert wird und damit ein
Näherungswert der Nullstelle mit einer gewünschten Genauigkeit feststeht.
Zur Anwendung des Newton-Verfahrens wird die Ableitung einer Funktion vorausge-
setzt. Von einem gegebenen Näherungswert xn (Abb. 8.1b) wird eine verbesserte Nähe-
rung für die Nullstelle bestimmt, indem die Tangente an der Stelle f (xn ) an die Funktion
gelegt und der Schnittpunkt mit der x-Achse xn+1 bestimmt wird. Dies wird gemäß der
Vorschrift xn+1 = xn − ff′(x n)
(xn ) so lange wiederholt, bis die Nullstelle gefunden wird, d.h
bis |xn+1 − xn | < ϵ, wobei das ϵ der Routine vorgegeben wird.
y y
f (xn )
f (x2 )
x1 +x2
x3 = 2
x1 xr x2 x xn+1 xn x
f (x1 )
(a) (b)
Abb. 8.1: (a) Bisektionsverfahren (b) Newton-Verfahren
struct gsl_function_struct
{
double (* function )( double x, void * params ); // Zeiger auf eigene Funktion
void * params ; // optionaler Parameter
};
typedef struct gsl_function_struct gsl_function ;
Listing 8.1
Diese beinhaltet neben einen Zeiger auf f (x) auch einen zusätzlichen optionalen Para-
meter, mit dem die Funktion f (x) näher spezifiziert werden kann oder ihr zusätzliche
Daten übergeben werden können. Der Grund ist, dass die gsl in C geschrieben ist und
innerhalb dieser Sprache nach Möglichkeiten gesucht wird, die Algorithmen so flexibel
wie möglich zu entwerfen. Für g(x) = 3x2 − 2x + 1 könnten wir z.B., ohne vom zweiten
Eingabeparameter Gebrauch zu machen, Folgendes schreiben:
gsl_function_struct F;
8.2 Zwei Beispielprogramme 323
Listing 8.2
/**
* @brief quadratic quadratisches Polynom
* @param x Variable
* @param params zusätzliche Daten
* @return reelle Zahl
*/
double quadratic_fn ( double x, void * params ) {
// Konvertierung des void -Zeigers , die Koeffizienten des Polynoms
// werden über diesen Parameter festgelegt
struct quadratic_params *p = static_cast < quadratic_params
*>( params );
// Koeffizienten a x^2 + b x + c
double a = p->a;
double b = p->b;
double c = p->c;
return (a * x + b) * x + c;
}
Das zweite Eingabeargument in Listing 8.3 ist ein Zeiger auf folgende Struktur:
/**
* @brief The quadratic_params struct
*/
struct quadratic_params {
double a, b, c;
};
/**
* @brief ex1 Bisektionsverfahren mit gsl - Routinen
*/
inline void ex1 () {
// Definition der Funktion , für welche die Nullstelle gesucht wird
// Polynomkoeffizienten
quadratic_params params = { 1.0 , 0.0, -5.0 };
324 8 Nullstellenberechnung reeller Funktionen
//gsl - Funktion
gsl_function F;
F. function = & quadratic_fn ;
F. params = & params ;
Im zweiten Teil werden alle nötigen gsl-Strukturen initialisiert. Dazu gehören der
gsl_root_fsolver_type, der festlegt, welcher Algorithmus zur Nullstellensuche benutzt
werden soll. Für das Bisektionsverfahren muss dieser gsl_root_fsolver_bisection lauten.
Mit gsl_root_fsolver werden die einzelnen Iterationsschritte koordiniert. Anschließend
wird die Funktion, für welche eine Nullstelle gesucht wird, registriert (Listing 8.6).
Im dritten Teil wird die Iteration durchgeführt. Hier werden die Intervallgrenzen nach je-
dem Schritt neu gesetzt und es wird geprüft, ob das Verfahren gestoppt werden kann, weil
eine Nullstelle gefunden wurde (Listing 8.7). Im vierten Teil werden die vom Programm
angeforderten Ressourcen wieder freigegeben (Listing 8.8).
// Iteration
printf ("using %s method \n", gsl_root_fsolver_name (s));
printf ("%5s [%9s, %9s] %9s %10s %9s\n", "iter", "lower", "upper",
"root", "err", "err(est)");
if ( status == GSL_SUCCESS ) {
printf (" Converged :\n");
}
8.2 Zwei Beispielprogramme 325
// Speicherfreigabe
gsl_root_fsolver_free (s);
}
Da wir vorhaben auch das Newton-Verfahren anzuwenden, implementieren wir die Ab-
leitung f ′ (x) = 2x.
/**
* @brief quadratic_deriv Ableitung des quadratischen Polynoms
* @param x variable
* @param params zusätzliche Daten
* @return reelle Zahl
*/
double quadratic_deriv ( double x, void * params ) {
// Konvertierung des void -Zeigers , die Koeffizienten des Polynoms
// werden über diesen Parameter festgelegt
struct quadratic_params *p = static_cast < quadratic_params
*>( params );
// Koeffizienten a x^2 + b x + c
double a = p->a;
double b = p->b;
return 2.0 * a * x + b;
}
Mit einer Funktion können zur schnelleren Ausführung des Programms, sowohl f (x) als
auch f ′ (x) aufgerufen werden.
/**
* @brief quadratic_fdf quadratisches Polynom und Ableitung
* @param x Variable
* @param params zusätzliche Daten
* @param y Rückgabewert quadratisches Polynom
* @param dy Rückgabewert Ableitung des
* quadratischen Polynoms
*/
void quadratic_fdf ( double x, void *params , double *y, double *dy) {
// Konvertierung des void -Zeigers , die Koeffizienten des Polynoms
// werden über diesen Parameter festgelegt
326 8 Nullstellenberechnung reeller Funktionen
Auch das zweite Beispielprogramm besteht aus vier logischen Einheiten. Der erste Teil
initialisiert die Funktion und ihre Ableitung (Listing 8.11). Im zweiten Teil werden, analog
zum ersten Beispiel, die Strukturen der gsl initialisiert und es wird der Algorithmus
festgelegt, mit dem die Nullstelle gesucht werden soll (Listing 8.12).
/**
* @brief ex2 Nullstellensuche mit dem Newton - Verfahren und gsl - Routinen
*/
inline void ex2 () {
// Definition der Funktion , für welche die Nullstelle gesucht
// wird , Ableitung der Funktion , kombinierter Aufruf Funktion
// und Ableitung
quadratic_params params = { 1.0 , 0.0, -5.0 };
gsl_function_fdf FDF;
FDF.f = & quadratic_fn ;
FDF.df = & quadratic_deriv ;
FDF.fdf = & quadratic_fdf ;
FDF. params = & params ;
Im dritten Teil wird die Iteration ausgeführt. Nach jedem Iterationsschritt wird mit gsl-
Funktionen getestet, ob die gewünschte Genauigkeit erreicht wurde und gegebenenfalls
die Iteration beendet (Listing 8.13). Im letzten Teil werden die angeforderten Ressourcen
wieder freigegeben (Listing 8.14).
// Iteration
int status ;
int iter = 0, max_iter = 100;
8.3 Eine Klasse für das Bisektionsverfahren 327
if ( status == GSL_SUCCESS )
printf (" Converged :\n");
// Speicherfreigabe
gsl_root_fdfsolver_free (s);
}
/**
* @brief The RootBracketing class gsl - Klasse für das
* Bisektionsverfahren
*/
class RootBracketing
{
private :
// gsl interne Struktur
const gsl_root_fsolver_type * _solver_type = nullptr ;
// legt Algorithmus fest
gsl_root_fsolver * _solver = nullptr ;
// Intervallgrenzen
double _xmin , _xmax ;
/**
* @brief next_step einzelner Iterationsschritt
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return GSL_SUCCESS wenn die Bedingung erfüllt ist
*/
inline int next_step ( double epsabs , double epsrel ) {
// nach jeder Iteration ...
if ( gsl_root_fsolver_iterate ( _solver ) != GSL_SUCCESS ) {
throw std :: runtime_error ( nmx_msgx ( _status ));
}
// ... ermittelte Nullstelle
_currentRoot = gsl_root_fsolver_root ( _solver );
// untere Grenze des Intervalls
_xmin = gsl_root_fsolver_x_lower ( _solver );
// obere Grenze des Intervalls
_xmax = gsl_root_fsolver_x_upper ( _solver );
// ist die Bedingung
//|xmin -xmax| < eposabs + epsrel min (| xmin |,| xmax |)
// erfüllt ?
_status = gsl_root_test_interval (_xmin , _xmax , epsabs , epsrel );
return _status ;
}
public :
Neben dem Algorithmus zur Nullstellensuche erwartet der Konstruktor die Funktion,
für die eine Nullstelle gesucht wird. Diese kann eine λ-Funktion oder eine Klasse mit
überladenem Klammeroperator sein.
/**
* @brief RootBracketing Konstruktor
* @param pobj Objekt mit überladenem () -operator oder lambda
* @param type Algorithmus z.B. das klassische Bisektionsverfahren
* gsl_root_fsolver_bisection ( default )
*/
template <class T>
8.3 Eine Klasse für das Bisektionsverfahren 329
/**
* Ressourcen werden freigegeben
*/
~ RootBracketing () {
if ( _solver != nullptr ) {
gsl_root_fsolver_free ( _solver );
}
}
Es folgen drei Elementfunktionen zum Lesen von intern gespeicherten Parametern wie
Status der Iteration, Intervallgrenzen usw.
/**
* @brief converged Status der Nullstellensuche
* @return true wenn das Verfahren erfolgreich war
*/
inline bool converged () const { return _status == GSL_SUCCESS ; }
/**
* @brief xminmax obere und untere Intervallgrenze
* @return [xmin ,xmax]
*/
inline auto xminmax () const { return std :: make_pair (_xmin , _xmax); }
/**
* @brief method_used
* @return Name des Algorithmus
*/
330 8 Nullstellenberechnung reeller Funktionen
Jede der beiden nächsten Funktionen führt eine Nullstellensuche komplett durch. Der
Rückgabewert vom Typ std::optional, ist nur dann gültig, wenn eine Nullstelle gefun-
den wurde. Die erste der beiden Methoden erlaubt es, über ein Funktionsobjekt die
Iterationsschritte aufzuzeichnen.
/**
* @brief apply startet Nullstellensuche
* @param xmin untere Intervallgrenze
* @param xmax obere Intervallgrenze
* @param fn Funktion zum Aufzeichnen der Iterationsschritte
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return gefundene Nullstelle
*/
template <class FN >
auto apply( double xmin , double xmax , FN fn , double epsabs , double
epsrel ) {
_xmin = xmin;
_xmax = xmax;
gsl_root_fsolver_set (_solver , &F, _xmin , _xmax);
for ( size_t i = 1; i <= MAX_STEPS && _status == GSL_CONTINUE ;
++i) {
next_step (epsabs , epsrel );
if ( _status == GSL_SUCCESS ) {
_result = _currentRoot ;
}
fn(i, _xmin , _xmax , _currentRoot );
}
return _result ;
}
/**
* @brief apply startet Nullstellensuche
* @param xmin untere Intervallgrenze
* @param xmax obere Intervallgrenze
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return gefundene Nullstelle
*/
inline auto apply ( double xmin , double xmax , double epsabs , double
epsrel ) {
_xmin = xmin;
_xmax = xmax;
gsl_root_fsolver_set (_solver , &F, _xmin , _xmax);
for ( size_t i = 1; i <= MAX_STEPS && _status == GSL_CONTINUE ;
++i) {
next_step (epsabs , epsrel );
8.4 Eine Klasse für das Newton-Verfahren 331
if ( _status == GSL_SUCCESS ) {
_result = _currentRoot ;
}
}
return _result ;
}
Die ermittelte Nullstelle kann über eine Elementfunktion abgefragt werden. Diese interne
Variable ist vom Typ std::optional. Dies bedeutet, dass falls keine Nullstelle gefunden
wurde, die Variable keinen gültigen Wert besitzt. Der Status der Variablen kann durch
eine einfache Abfrage getestet werden.
/**
* @brief result liefert die Nullstelle nur falls eine gefunden
* wurde , sonst wird eine Ausnahme ausgeworfen .
* @return gefundene Nullstelle
*/
inline double result () const {
// teste Status der Variablen (für die Nullstelle )
Check :: error_if (nmx_msg , ! _result );
return * _result ;
}
}; // RootBracketing
/**
* @brief The FDFSolver class Nullstellensuche Funktion und ihre
* Ableitung werden benötigt
*/
class FDFSolver
{
private :
// gsl interne Struktur
const gsl_root_fdfsolver_type * _solverType ;
// Algorithmus
gsl_root_fdfsolver * _solver = nullptr ;
// suche Nullstelle für diese Funktion (gsl - Funktion )
gsl_function_fdf FDF;
// x_(i+1) ,x_i
double _xnew , _xold ;
// Status der Iteration
int _status ;
// Anzahl der Iterationen
332 8 Nullstellenberechnung reeller Funktionen
size_t _iteration = 0;
// gefundene Nullstelle
std :: optional <double > _foundRoot ;
public :
size_t MAX_STEPS = 100;
private :
/**
* @brief next_step einzelner Iterationsschritt
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return GSL_SUCCESS wenn die Bedingung erfüllt ist
*/
inline void next_step ( double epsabs , double epsrel ) {
_iteration ++;
_status = gsl_root_fdfsolver_iterate ( _solver );
// speichere zuletzt bekannte Nullstelle
_xold = _xnew ;
// berechne neue Nullstelle
_xnew = gsl_root_fdfsolver_root ( _solver );
// teste Konvergenz
_status = gsl_root_test_delta (_xnew , _xold , epsabs , epsrel );
if ( _status == GSL_SUCCESS ) {
_foundRoot = _xnew ;
}
}
public :
Dem Konstruktor wird ein Objekt übergeben, welches die Funktion und ihre Ableitung
definiert. Optional kann die Variante des Algorithmus zur Nullstellensuche angegeben
werden. Sein Name kann mittels einer Elementfunktion abgefragt werden (Listing 8.27).
/**
* @brief FDFSolver Konstruktor
* @param obj enthält Funktion und ihre Ableitung
*/
template < typename T>
FDFSolver (T &obj , const gsl_root_fdfsolver_type *type =
gsl_root_fdfsolver_newton ) {
_solverType = type;
_solver = gsl_root_fdfsolver_alloc ( _solverType );
// verbinde obj mit gsl
FDF. params = &obj;
// Funktion
FDF.f = []( double x, void * params ) { //
8.4 Eine Klasse für das Newton-Verfahren 333
/**
* @brief method_used
* @return Name des Algorithmus
*/
inline const char * method_used () const { //
return gsl_root_fdfsolver_name ( _solver );
}
Der Destruktor gibt die von der gsl benötigten Ressourcen wieder frei.
/**
* Destruktor
*/
inline ~ FDFSolver () {
if ( _solver != nullptr ) {
gsl_root_fdfsolver_free ( _solver );
}
}
Der Status und die gefundene Nullstelle können mit der beiden nächsten Elementfunk-
tionen abgefragt werden.
/**
* @brief converged Status der Nullstellensuche
* @return true wenn eine Nullstelle gefunden wurde
*/
inline bool converged () const { return _status == GSL_SUCCESS ; }
/**
* @brief getRoot Abfrage der gefundenen Nullstelle
* @return gesuchte Nullstelle
334 8 Nullstellenberechnung reeller Funktionen
*/
inline double result () const {
// teste Status der Variablen (für die Nullstelle )
Check :: error_if (nmx_msg , ! _foundRoot );
return * _foundRoot ;
}
Es folgen zwei Methoden, mit denen die Nullstellensuche durchgeführt wird. Mit der
zweiten Variante können die einzelnen Schritte aufgezeichnet werden (Listing 8.33).
/**
* @brief apply Nullstellensuche
* @param startval Startwert
* @param epsabs absoluter Fehler ( optional )
* @param epsrel relativer Fehler ( optional )
* @return gefundene Nullstelle
*/
inline auto apply ( double startval , double epsabs = 0, double epsrel
= 1e -3) {
_xnew = startval ;
gsl_root_fdfsolver_set (_solver , &FDF , _xnew);
do {
next_step (epsabs , epsrel );
} while ( _status == GSL_CONTINUE && _iteration < MAX_STEPS );
return _foundRoot ;
}
/**
* @brief apply Nullstellensuche
* @param startval Startwert
* @param epsabs absoluter Fehler ( optional )
* @param epsrel relativer Fehler ( optional )
* @return gefundene Nullstelle
*/
template <class FN >
auto apply( double startval , FN fn , double epsabs = 0, double epsrel
= 1e -3) {
_xnew = startval ;
gsl_root_fdfsolver_set (_solver , &FDF , _xnew);
do {
next_step (epsabs , epsrel );
fn( _iteration , _xold , _xnew );
return _foundRoot ;
}
8.5 Beispiele
Alle Beispiele in diesem Abschnitt verwenden zwei Hilfsfunktionen zur Präsentation der
Ergebnisse. Die Funktion Listing 8.34 erzeugt einen Ausgabestrom, welcher alle Dateien
in einem Verzeichnis schreibt. Der erzeugte Datenstrom wird der Funktion Listing 8.35
übergeben, welche dann die Ergebnisse des Programms zur Nullstellensuche zusammen-
fasst.
/**
* @brief get_output_stream
* @param name Name der Datei
* @param fmt Formatierung der Daten
*/
inline auto get_output_stream ( const char *name , Format fmt =
Output :: terminal ) {
auto ofs = Output :: get_stream ("x034", fmt , name);
fmt. set_defaults (ofs);
return ofs;
}
/**
* @brief show_results Präsentation der Ergebnisse
* @param ofs Ausgabestrom
* @param obj Klasse zur Nullstellensuche
* @param expected exakt berechneter Wert
*/
template <class T>
inline void show_results (std :: ofstream &ofs , const T &obj , double
expected ) {
ofs << " method used :" << obj. method_used () << std :: endl;
if (obj. converged ()) {
ofs << " success \n";
ofs << "t:" << obj. result () << std :: endl;
ofs << "t ( exact ):" << expected << std :: endl;
ofs << " error :" << abs(obj. result () - expected ) << std :: endl;
} else {
ofs << " failed " << std :: endl;
}
}
√
8.5.1 Berechnung von 5 mit dem Bisektionsverfahren
√
Gesucht wird der Wert von 5. Wir definieren die Funktion f (x) = x2 − 5 und su-
chen ihre Nullstellen. Wir wenden das Bisektionsverfahren an und zeichnen die einzelnen
Iterationsschritte auf.
336 8 Nullstellenberechnung reeller Funktionen
/**
* @brief root1 Berechnung von sqrt {5} mit dem Bisektionsverfahren
*/
inline void root1 () {
// Suche Nullstelle dieser Funktion
auto function = []( double x) { return pow(x, 2) - 5; };
// öffne Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: fixed << std :: setprecision (7);
// Ausgabe
show_results (ofs , rootFinder , std :: sqrt (5));
}
Bei beiden Ausgaben werden die einzelnen Iterationsschritte und der Name des Verfah-
rens, dass angewandt wurde, ausgegeben.
√
8.5.2 Berechnung von 5 mit dem Newton-Verfahren
√
Wir greifen noch einmal das Beispiel aus Abschnitt 8.5.1 auf und berechnen 5 mit dem
Newton-Verfahren. Die Funktion f (x) = x2 − 5 und ihre Ableitung f ′ (x) = 2x werden
innerhalb eines Objekts definiert. Eine Instanz dieses Objekts und eine λ-Funktion zur
Aufzeichnung der Iterationsschritte werden der neuen Klasse übergeben.
/**
* @brief root4 Berechnung von sqrt (5) mit dem Newton - Verfahren
*/
inline void root4 () {
// Funktion und ihre Ableitung
struct MyFN {
inline double f( double x) const { return pow(x, 2) - 5; }
inline double df( double x) const { return 2 * x; }
} myFN;
// Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
ofs << std :: setprecision (6) << std :: fixed ;
m1 m2
⃗1
F ⃗2
F
x
x0 = 0 t1 =?, x1 =? x2 = d
Abb. 8.2: Zwei Teilchen bewegen sich unter dem Einfluss von konstanten Kräften auf-
einander zu.
Lösung
Die Bewegungsgleichungen Wir legen die positive x-Achse entlang der Bewegungs-
richtung des Teilchens m1 . Die beiden Teilchen bewegen sich mit konstanten Beschleu-
nigungen
F1 F2
a1 = , a2 = . (8.1)
m1 m2
Die jeweiligen Ortskoordinaten werden durch
1 1
x1 = a 1 t2 , x 2 = d − a 2 t2 (8.2)
2 2
gegeben. Zum Zeitpunkt der Begegnung t1 gilt
1 1 1
x1 = x2 ⇒ a1 t21 = d − a2 t21 ⇒ d = (a1 + a2 ) t21
2 2 2
und damit √
2d
t1 = ± , (8.3)
a1 + a2
wobei wir nur das positive Vorzeichen beibehalten. Den Ort erhalten wir durch Einsetzen
von t1 in eine der beiden Gleichungen aus Gl. 8.2.
Das Computerprogramm
Kurzbeschreibung Wir werden mit einem kurzen Programm den Zeitpunkt der Begeg-
nung berechnen. Dazu definieren wir eine Funktion f (t) = x1 (t) − x2 (t) (siehe Gl. 8.2).
Der gesuchte Zeitpunkt ist die Nullstelle von f (t).
Quelltext und Daten Wir implementieren f (x) mithilfe einer λ-Funktion. Die Nullstelle
berechnen wir mit unserer gsl-Klasse.
/**
* @brief root3 zwei Teilchen bewegen sich aufeinader zu
*/
inline void root3 () {
// Bewegungsparameter
const double d = 100;
const double m1 = 1, m2 = 2;
const double f1 = 10, f2 = 15;
const double a1 = f1 / m1;
const double a2 = f2 / m2;
// Ausgabestrom
std :: ofstream ofs = get_output_stream ( __func__ );
340 8 Nullstellenberechnung reeller Funktionen
//gsl - Klasse
gsl :: RootBracketing rootFinder { fn , gsl_root_fsolver_brent };
rootFinder . apply (0, 100 , 0, 1e -6);
// Ergebnisse
const auto expected = sqrt (2 * d / (a1 + a2));
show_results (ofs , rootFinder , expected );
}
8.6 Übungsaufgaben
1. Implementieren Sie einen eigenen Algorithmus für das Bisektionsverfahren und be-
√
nutzen Sie es, um 5 zu berechnen.
2. Schreiben Sie den Quelltext aus Listing 8.39 so um, dass die aufgezeichneten Daten
im LATEX-Tabellenformat abgespeichert werden.
3. Bestimmen Sie die Nullstellen der Funktion f (x) = e3x − x − 6 mittels der beiden in
diesem Kapitel vorgestellter Verfahren.
4. Lösen Sie die Gleichung sin(x) = ex .
5. Ein Teilchen wird aus einer Höhe H mit einer Geschwindigkeit v0 unter einem Winkel
φ abgeworfen. Berechnen Sie den Winkel φ0 für eine Wurfweite xw = 17.55 m, H =
10 m und v0 = 10 m/s. (Hinweis: Stellen Sie die Gleichung für die Wurfparabel auf.)
9 Numerische Integration
Übersicht
9.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
9.2 Numerische Integration mit der GNU Scientific Library . . . . . . . . . . . . . . . . . . . 342
9.3 Klassen als Schnittstellen zur GNU Scientific Library . . . . . . . . . . . . . . . . . . . . . 346
9.4 Berechnung von Beispielintegralen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
9.5 Beispiele aus der Physik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
9.6 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
9.1 Einleitung
Durch Anwendung der Ableitungsregeln lässt sich praktisch jede Funktion differenzieren.
Dies gilt nicht für die Integration. Nicht jedes Integral kann analytisch berechnet werden
und selbst wenn eine Lösung existiert, ist diese manchmal nur durch eine Reihe von
geschickten und oftmals komplizierten Substitutionen zu berechnen. Schließlich treten
auch Fälle auf, in denen der Integrand nur in tabellarischer Form vorliegt. In all diesen
Situationen bietet es sich an, numerisch zu integrieren.
Die numerische Integration basiert auf der Idee, ein Integral durch eine Summe zu
approximieren.
∫ b ∑N
f (x)dx ≈ wi f (xi ), (9.1)
a i=i
wobei die Stützstellen xi Werte innerhalb des Intervalls [a, b] sind. Zu jeder Stützstel-
le existiert ein Integrationsgewicht wi , welches eine reelle Zahl ist. Es existieren zwei
Strategien, Integrale numerisch auszuwerten. Die erste ist, die Stützstellen äquidistant
über das Integrationsintervall zu verteilen und die Gewichte zu berechnen, indem gefor-
dert wird, f (x) durch ein Interpolationspolynom zu ersetzen und dieses zu integrieren.
Dieser Ansatz führt zu den Newton-Cotes-Formeln. Die andere Strategie ist, nicht nur
die Integrationsgewichte, sondern auch die Stützstellen zu berechnen mit dem Ziel, die
Genauigkeit zu optimieren. Diese Methode heißt Gauß-Quadratur. In diesem Fall fordern
∑
wir, dass N i=i wi f (xi ) dasselbe Ergebnis liefert wie der exakte Wert des Integrals eines
Polynoms von Grad 2N − 1. Aus dieser Forderung entsteht ein nichtlineares Gleichungs-
system, dessen Lösung die Integrationsgewichte und Stützstellen sind. Wenn zusätzlich
in Bereichen, in denen eine Funktion stark variiert, das Integrationsintervall in kleinere
y y
f (x2 ) p(x)
f (x1 )
f (x2 )
f (x1 )
f (x)
f (x3 )
x x
x1 x2 x3 x1 x2 a
(a) (b)
Abb. 9.1: (a) Newton-Codes Quadratur (b) Gauß-Quadratur
unterteilt wird, so spricht man von einem adaptiven Verfahren. Pro Teilintervall werden
zwei Sätze von Stützstellen berechnet, wobei der zweite Satz mehr Stützstellen enthält als
der erste und somit ein Ergebnis mit höherer Genauigkeit berechnet wird. Wenn die Dif-
ferenz der beiden Ergebnisse unter einer vorgegebenen Schranke liegt, wird das Ergebnis
akzeptiert, sonst wird das Intervall weiter unterteilt. Dieser Prozess wird im Normal-
fall von einem Algorithmus gesteuert. Der Preis dieser intelligenten Art zu integrieren
ist, dass pro Teilintervall ein doppelter Satz von Stützstellen und Integrationsgewich-
ten berechnet werden muss, denn bei einer Gauß-Quadratur sind n Stützstellen nicht
in n + 1 Stützstellen enthalten. Die gute Nachricht ist, dass Techniken wie die Gauß-
Kronrod-Formeln existieren, die diesen Aufwand reduzieren. Hier werden bestehende n
Stützstellen wiederverwendet, um zusätzliche n + 1 zu berechnen.
Wir beginnen mit der Erstellung eines Programms zur numerischen Auswertung von
∫ 1
ln (ax)
√ dx = −4. (9.2)
0 x
9.2 Numerische Integration mit der GNU Scientific Library 343
Wir orientieren uns dabei an einem Beispiel, dass auf der Webseite der GNU Scien-
tific Library zu finden ist. Das Programm beginnt mit der Definition des Integranden
f (x) = ln√ax
x
, allerdings in einer ungewohnten Form, da die Funktion statt einem Ein-
gabeargument zwei erwartet. Über diesen zweiten Parameter können der Funktion zu-
sätzliche Daten wie hier der Wert von a übergeben werden. Erreicht wird dadurch, dass
nicht nur eine Funktion, sondern gleich eine ganze Funktionsfamilie implementiert wird.
/**
* @brief f Integrand (gsl - Funktion )
* @param x Variable
* @param params zusätzliche Parameter
* @return Funktionswert
*/
double f( double x, void * params ) {
double alpha = *( double *) params ;
double f = log( alpha * x) / sqrt(x);
return f;
}
Wir fahren mit dem Quelltext zur numerischen Berechnung des Integrals (Gl. 9.2) fort.
/**
* @brief gslex numerische Integration Beispiel mit Kommentaren
* https :// www.gnu.org/ software /gsl/doc/html/ integration .html# examples
*/
void gslex () {
// gsl Struktur : Verwaltung von Daten
gsl_integration_workspace *w =
gsl_integration_workspace_alloc (1000) ;
// exaktes Ergebnis
double expected = -4.0;
// Parameter des Integranden
double alpha = 1.0;
// Ausgabewerte der Integrationsroutine
double result , error ;
//gsl - Funktion ( übergibt Integranden an die Integrationsroutine )
gsl_function F;
F. function = &f;
F. params = & alpha ;
// numerische Integration , Integrand hat Singularität
gsl_integration_qags (&F, 0, 1, 0, 1e-7, 1000 , w, &result , &error);
// Ausgabe der Daten
printf (" result = % .18f\n", result );
printf ("exact result = % .18f\n", expected );
printf (" estimated error = % .18f\n", error);
printf (" actual error = % .18f\n", result - expected );
printf (" intervals = %zu\n", w->size);
Die Funktion beginnt mit der Definition eines Zeigers auf eine Datenstruktur vom
Typ gsl_integration _workspace. Diese Struktur, die wir der Einfachheit halber auch
workspace nennen werden, dient zur Verwaltung der Teilintervalle im Fall einer adapti-
ven Integration und den dazugehörigen Ergebnissen und Fehlerabschätzungen.
Nachdem der Wert von a und der exakte Wert des Integrals initialisiert wird, folgt die
Definition einer gsl_function. Es handelt sich hierbei nicht um eine Funktion, sondern
vielmehr um einer Datenstruktur (siehe Abschnitt 8.2), die dazu dient, den Integranden
an die Integrationsroutine zu übergeben .
Die numerische Integration erfolgt mittels der Routine gsl_integration_qags, die auf
Integranden mit Singularitäten innerhalb des Integrationsintervalls spezialisiert ist. Die-
ser Routine wird der Zeiger auf das workspace, die gsl_function, die Fehlerschranken
sowie eine obere Grenze für die Anzahl der Intervalle, in denen das Integrationsintervall
aufgeteilt werden soll, übergeben. Nach der Ausgabe der Ergebnisse wird der für das
workspace reservierte Speicher wieder freigegeben.
Die Logik hinter dem Aufbau des Programms ist nicht schwierig zu verstehen, sie ist
aber gewöhnungsbedürftig, denn sie folgt nicht dem von der Mathematik gewohnten Mus-
∫b
ter, eine Funktion f (x) zu definieren und dann das Integral a f (x)dx zu berechnen. Wir
denken hier an die zwei Eingabeparameter des Integranden oder seine indirekte Überga-
be an die Integrationsroutine. Die manuelle Speicherfreigabe ist ein weiterer Punkt, der
eine mögliche Fehlerquelle darstellt. Es sei an dieser Stelle betont, dass trotz Kritik diese
Vorgehensweise einem C-Programm einen hohen Grad an Flexibilität verleiht. Die Frage
ist, ob durch den Einsatz von C++ Verbesserungen möglich sind, die den Quelltext kürzer
machen, die Programmierung vereinfachen und die Speicherverwaltung automatisieren.
Die Antwort ist Ja und das C++-Programm könnte dann so aussehen:
/**
* @brief ex1
*/
inline void ex1 () {
// Integrand
auto fn = []( double x) { return log(x) / sqrt(x); };
//gsl - Klasse
gsl :: Integral integral (fn);
// adaptive Integration von fn (mit Singularitäten )
// im Intervall [0 ,1]
integral .qags (0, 1);
// Ausgabe
const double expected = -4;
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
integral . show_results (sout , expected );
}
Der Quelltext ist deutlich kürzer geworden und die manuelle Speicherverwaltung ver-
schwunden. Der Integrand wird im gewohnten Stil als λ-Funktion mit einem Eingabepa-
rameter definiert und ohne Umwege integriert.
9.2 Numerische Integration mit der GNU Scientific Library 345
Bei CQUAD handelt es sich um eine allgemeine gsl-Routine, die auch für die Integration
singulärer Integranden geeignet ist. Das Integrationsintervall wird in kleinere unterteilt.
Jedes der Subintervalle wird mit 4 Clenshaw-Curtis-Punkte integriert und das Ergebnis
mit dem aus der Integration mit 5 Punkten verglichen. Liegt die Differenz nicht unter
einer vorgegebenen Fehlerschranke, wird die Prozedur mit 8 und 9 Punkten wiederholt.
Ist auch dieser Versuch nicht erfolgreich, folgt eine Integration mit 16 und 17 Punkten.
Wenn die Integration auch mit 32 und 33 Punkten nicht zum Erfolg führt, wird das Inter-
vall halbiert und die Prozedur für jedes Subintervall wiederholt. Der folgende Quelltext
zeigt, wie das Integral Gl. 9.2 mit dieser Methode berechnet wird.
/**
* @brief gslex_cquad numerische Integration mit CQUAD
* (gsl - Beispiel mit Kommentaren )
*/
void gslex_cquad () {
// gsl Struktur : Verwaltung von Daten
gsl_integration_cquad_workspace *w =
gsl_integration_cquad_workspace_alloc (100);
// analytisch berechnetes Ergebnis
double expected = -4;
// Parameter des Integranden
double alpha = 1.0;
// Ausgabewerte der Integrationsroutine
double result , error ;
// Anzahl der Funktionsauswertungen
size_t nevals ;
//gsl - Funktion ( übergibt Integranden an die Integrationsroutine )
gsl_function F;
F. function = &f;
F. params = & alpha ;
// numerische Integration , Integrand hat Singularität
gsl_integration_cquad (&F, 0, 1, 0, 1e-7, w, &result , &error ,
& nevals );
// Ausgabe der Daten
printf (" result = % .18f\n", result );
printf ("exact result = % .18f\n", expected );
printf (" estimated error = % .18f\n", error);
printf (" actual error = % .18f\n", result - expected );
printf (" intervals = %zu\n", w->size);
Wir stellen eine große Ähnlichkeit zum ersten Beispielprogramm (Listing 9.2) fest. Auch
hier wird ein workspace initialisiert und einer Routine übergeben, welche die Integration
durchführt und anschließend die angeforderten Ressourcen wieder freigibt.
346 9 Numerische Integration
/**
* @brief The BaseIntegral class Basisklasse zur numerischen
* Integration
* @param T workspace -Typ
*/
template <class T>
class BaseIntegral
{
protected :
T * _workspace = nullptr ;
gsl_function _gslFunction ;
double _result , _error ;
int _errorCode ;
Der Klasse wird der Integrand über ihren Konstruktor zugewiesen. Wir erinnern uns,
dass diese Zuweisung im C-Programm (Listing 9.1) indirekt geschieht. Diesen Vorgang
wollen wir innerhalb des Konstruktors so implementieren, dass er automatisch ausge-
führt wird. Wir platzieren diesen einzigen Konstruktor im protected-Bereich, sodass er
nur von abgeleiteten Klassen aufgerufen werden kann. Er erwartet als Eingabe die zu
integrierende Funktion.
/**
* @brief BaseIntegral Konstruktor
* @param fn Integrand ( Funktionsobjekt muss über einen
* überladenen Klammeroperator verfügen )
*/
9.3 Klassen als Schnittstellen zur GNU Scientific Library 347
public :
/**
* @brief show_results einfache Ausgabe des Ergebnisses
* mit Zusatzinformationen
* @param sout Ausgabestrom
* @param expected das exakte Ergebnis
*/
inline void show_results (std :: ofstream &sout , double expected ) {
sout << " result = " << result () << "\n";
sout << " exact result = " << expected << "\n";
sout << " estimated error = " << error () << "\n";
sout << " actual error = " << abs( result () - expected ) << "\n";
sout << " intervals = " << intervals () << "\n";
}
Der Rest der Klasse enthält Methoden zur Abfrage des Integrationsergebnisses, der Feh-
lerabschätzung und der Anzahl der verwendeten Teilintervalle.
/**
* @brief result
* @return Ergebnis der Integration
*/
inline double result () const { return _result ; }
/**
* @brief error
* @return Fehlerabschätzung
348 9 Numerische Integration
*/
inline double error () const { return _error ; }
/**
* @brief intervals
* @return Anzahl der eingesetzten Teilintervalle
*/
inline size_t intervals () const { return _workspace ->size; }
}; // BaseIntegral
/**
* @brief The Integral class erste Varianten der gsl - Klasse
* zur numerischen Integration . Der Template - Parameter ist
* der spezielle workspace -Typ für diese Gruppe von Routinen
*/
class Integral : public BaseIntegral < gsl_integration_workspace >
{
public :
Der Konstruktor der ersten abgeleiteten Klasse erwartet als Eingabe den Integranden
und die maximale Anzahl der Teilintervalle, in die das Integrationsintervall aufgeteilt
werden darf, wenn eine Schrittweitenanpassung erfolgt.
/**
* @brief Integral Konstruktor
* @param fn der Integrand
* @param size [a,b] maximale Anzahl der Intervalle
* [x_i ,x_i +1]
*/
template <class FN >
inline Integral (FN &fn , size_t size = 1000)
: BaseIntegral (fn) {
_workspace = gsl_integration_workspace_alloc (size);
}
Ein Kopierkonstruktor und ein Zuweisungsoperator sind für diese Klasse nicht zugelassen,
denn dies würde zu unnötigem Programmieraufwand führen.
/**
* @brief Integral
* @param i
*/
Integral ( const Integral &i) = delete ;
/**
* @brief operator =
* @param i
* @return
*/
Integral & operator =( const Integral &i) = delete ;
Der Destruktor gibt den Speicher, der für den workspace reserviert wurde, wieder frei.
Dies geschieht automatisch und ist an die Lebensdauer der dazugehörigen Klassenin-
stanz gebunden. Der Programmierer braucht sich nicht um die Speicherverwaltung zu
kümmern.
/**
* gsl - Speicher wird freigegeben
**/
~ Integral () {
if ( _workspace != nullptr ) {
gsl_integration_workspace_free ( _workspace );
}
}
Die erste Elementfunktionen ruft eine gsl-Routine auf, die keinen workspace benötigt.
Wir nehmen dennoch diese Funktion in die Klasse mit auf, um von der automatischen
Zuweisung des Integranden zu profitieren. Obwohl intern nicht mit einem adaptiven Ver-
fahren gearbeitet wird, erfolgt die Integration in mindestens zwei Schritten. Es wird der
Reihe nach mit 10, 21, 43, 87 Gauss-Kronrod-Punkte über das ganze Integrationsinter-
vall integriert, bis die Differenz zweier nacheinander folgenden Ergebnisse unterhalb einer
vorgegebenen Schranke liegt.
/**
* @brief qng Integration von stetigen Funktionen keine
* automatische Schrittweitenanpassung
* @param xmin untere Intervallgrenze
* @param xmax obere Intervallgrenze
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
350 9 Numerische Integration
/**
* @brief qag numerische Integration mit automatischer
* Schrittweitenanpassung (Gauß - Kronrod )
* @param xmin untere IntervallGrenze
* @param xmax obere IntervallGrenze
* @param key Anzahl der Gauß - Punkte
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return Fehlercode
*/
inline int qag( double xmin , double xmax , int key , double epsabs =
1e-9, double epsrel = 1e -9) {
Check :: error_if_not (nmx_msg , key >= 1 && key <= 6);
// clang - format off
_errorCode = gsl_integration_qag (& _gslFunction , xmin , xmax ,
epsabs , epsrel , _workspace ->limit ,
key , _workspace , &_result , & _error );
// clang - format on
return _errorCode ;
}
Wenn innerhalb des Integrationsintervalls eine Singularität des Integraden vorliegt, so ist
die nächste Methode die richtige Wahl.
/**
* @brief qags numerische Integration mit automatischer
* Schrittweitenanpassung von singulären Funktionen
* @param xmin untere Intervallgrenze
* @param xmax obere Intervallgrenze
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return Fehlercode
*/
inline int qags( double xmin , double xmax , double epsabs = 1e-9,
double epsrel = 1e -9) {
// clang - format off
_errorCode = gsl_integration_qags (& _gslFunction , xmin , xmax ,
epsabs , epsrel ,_workspace ->limit ,
9.3 Klassen als Schnittstellen zur GNU Scientific Library 351
Es folgen drei Elementfunktionen, welche für die Integration von Funktionen über ganz
R (Listing 9.19) und halbendliche Intervalle (Listing 9.20, Listing 9.21) zuständig sind.
/**
* @brief qagi numerische Integration mit automatischer
* Schrittweitenanpassung von - Unendlich bis + Unendlich
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return Fehlercode
*/
inline int qagi( double epsabs = 1e-9, double epsrel = 1e -9) {
// clang - format off
_errorCode = gsl_integration_qagi (& _gslFunction , epsabs ,
epsrel ,_workspace ->limit ,_workspace ,
&_result , & _error );
// clang - format on
return _errorCode ;
}
/**
* @brief qagiu numerische Integration mit automatischer
* Schrittweitenanpassung von xmin bis + Unendlich
* @param xmin untere Integrationsgrenze
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return Fehlercode
*/
inline int qagiu ( double xmin , double epsabs = 0, double epsrel =
1e -9) {
// clang - format off
_errorCode = gsl_integration_qagiu (& _gslFunction , xmin ,
epsabs , epsrel ,
_workspace ->limit , _workspace ,
&_result , & _error );
// clang - format on
return _errorCode ;
}
/**
* @brief qagil numerische Integration mit automatischer
* Schrittweitenanpassung von - Unendlich bis xmax
* @param xmax obere Integrationsgrenze
* @param epsabs absoluter Fehler
352 9 Numerische Integration
Die zweite von der Basisklasse abgeleitete Klasse enthält nur eine Elementfunktion. Über
diese Methode werden alle Funktionen in einem endlichen Integrationsintervall integriert.
Dies gilt auch für singuläre Funktionen und solche, die an bestimmten Stellen NaN (Not a
Number) als Rückgabewert liefern z.B. sinx x an der Stelle x = 0. Die Initialisierung und
Freigabe des workspace sowie die Zuweisung des Integranden erfolgt wie gewohnt über
den Konstruktor und den Destruktor.
/**
* @brief The CQUADIntegral class adaptive numerische Integration
* geeignet für Integranden mit Singularitäten oder solchen die
* als Funktionswert an einigen Stellen Unendlich oder NaN zurückgeben
*/
class CQUADIntegral : public
BaseIntegral < gsl_integration_cquad_workspace >
{
private :
size_t _nFunctionEvals ; // Anzahl der Funktionsaufrufe
public :
/**
* @brief CQUADIntegral Konstruktor
* @param dim minimale Anzahl der Intervalle 3 in den meisten
* Fällen ist 100 ausreichend
*/
template <class FN >
CQUADIntegral (FN &fn , size_t dim = 100)
: BaseIntegral (fn) {
_workspace = gsl_integration_cquad_workspace_alloc (dim);
// Überprüfung der minimalen Anzahl der Intervalle
Check :: error_if_not (nmx_msg , dim >= 3);
}
/**
* Destruktor , Freigabe des von der gsl reservierten Speichers
*/
~ CQUADIntegral () {
if ( _workspace != nullptr ) {
gsl_integration_cquad_workspace_free ( _workspace );
}
}
/**
* @brief apply Ausführung der numerischen Integration
* @param fn die zu integrierende Funktion
* @param xmin untere Intervallgrenze
* @param xmax obere Intervallgrenze
* @param epsabs absoluter Fehler
* @param epsrel relativer Fehler
* @return Fehlercode
*/
int apply( double xmin , double xmax , double epsabs = 1e-9, double
epsrel = 1e -9) {
// clang - format off
_errorCode = gsl_integration_cquad (& _gslFunction , xmin , xmax ,
epsabs , epsrel ,
_workspace , &_result ,
&_error , & _nFunctionEvals );
// clang - format on
return _errorCode ;
}
/**
* @brief get_output_stream generiere Ausgabestrom
* @param name Name der Datei
* @param fmt Formatierungsanweisung
*/
static inline auto get_output_stream ( const char *name , Format fmt) {
auto ofs = Output :: get_stream ("x039", fmt , name);
return ofs;
}
Sind die Schnittstellen zur gsl einmal erstellt, kümmern wir uns nicht mehr um die Im-
plementierungsdetails, sondern nutzen sie, um Integrale zu berechnen. Als erstes Beispiel
werden wir mit einem Programm folgendes Integral auswerten:
∫ 1
4
dx = π (9.3)
0 1 + x2
/**
* @brief ex0 nicht adaptive Gauß - Kronrod Integration
*/
inline void ex0 () {
// Integrand
auto fn = []( double x) { return 4.0 / (1 + pow(x, 2)); };
//gsl - Klasse
gsl :: Integral integral (fn);
// nicht adaptive Integration von fn im Intervall [0 ,1]
integral .qng (0, 1);
// Ausgabe von Ergebnissen
const double expected = Math :: PI;
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
integral . show_results (sout , expected );
}
Wir haben hier ein nichtadaptives Verfahren (Listing 9.16) eingesetzt. Folgende Ergeb-
nisse werden in eine Datei geschrieben:
/**
* @brief ex1
*/
inline void ex15 () {
// Integrand
auto fn = []( double x) { return 1 / sqrt(sin(x)); };
//gsl - Klasse
gsl :: Integral integral (fn);
// adaptive integration (mit Singularitäten ) im Intervall [0 ,1]
9.4 Berechnung von Beispielintegralen 355
Der Programmaufbau folgt dem gewohntem Muster. Nach Ausführung des Programms
erhalten wir
und lösen es mit einem Programm, welches ebenfalls eine Elementfunktion (Listing 9.19)
der Klasse nutzt und sehr einfach zu implementieren ist.
/**
* @brief ex2
*/
inline void ex2 () {
// Integrand
auto fn = []( double x) { return exp (-.5 * pow(x, 2)); };
//gsl - Klasse
gsl :: Integral integral (fn);
// adaptive Integration von fn von -Unendlich bis + Unendlich
integral .qagi ();
// Ausgabe
const double expected = 2.5066282746310002;
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
integral . show_results (sout , expected );
}
/**
* @brief ex3
*/
inline void ex3 () {
// Integrand
auto fn = []( double x) { return exp (-.5 * pow(x, 2)); };
//gsl - Klasse
gsl :: Integral integral (fn);
// adaptive Integration von fn von 0 bis + Unendlich
integral .qagiu (0);
// Ausgabe
const double expected = 1.2533141373155001;
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
integral . show_results (sout , expected );
}
Anhand des nächsten Beispiels soll demonstriert werden, wie Integrale im Bereich
(−∞, a) numerisch berechnet werden.
∫ 0 x2
e− 2 dx ≈ 1.25331 (9.7)
−∞
/**
* @brief ex4
*/
inline void ex4 () {
// Integrand
auto fn = []( double x) { return exp (-.5 * pow(x, 2)); };
//gsl - Klasse
gsl :: Integral integral (fn);
// adaptive Integration von fn von -Unendlich bis 0
integral .qagil (0);
// Ausgabe
const double expected = 1.2533141373155001;
9.4 Berechnung von Beispielintegralen 357
Es folgt die bequeme Art numerisch zu integrieren. Mithilfe der Klasse CQUADIntegral
(Listing 9.22) werden wir folgendes Integral berechnen:
∫ 5 x2
e− 2 dx ≈ 5.70254 × 10−2 (9.8)
2
/**
* @brief ex5
*/
inline void ex5 () {
// Integrand
auto fn = []( double x) { return exp (-.5 * pow(x, 2)); };
//gsl - Klasse
gsl :: CQUADIntegral integral (fn);
integral .apply (2, 5);
// Ausgabe
const double expected = 5.7025405463957006e -2;
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
integral . show_results (sout , expected );
}
Ein Teilchen erhält nach einem kurzen Stoß eine Geschwindigkeit v0 und bewegt sich
geradlinig auf horizontalen Boden, bis es nach einer Strecke s zum Stillstand kommt.
Der Gleitreibungskoeffizient zwischen Teilchen und Boden sei µ:
Lösung
Wir legen die x-Achse entlang der Bewegungsrichtung und wenden den Arbeitssatz an
∫ s
Kf − Ki = ⃗ x
Rd⃗ (9.9)
0
oder ∫
1 s √
0 − mv02 = ⃗ x ⇒ − 1 mv02 = −µmgs ⇒ v0 =
Rd⃗ 2µgs, (9.10)
2 0 2
⃗ die Reibungskraft ist.
wobei R
Das Computerprogramm
Quelltext und Daten Wir lösen das Integral aus Gl. 9.9 numerisch und berechnen
anschließend die Anfangsgeschwindigkeit über die Formel
√
∫s
2 Rd⃗⃗ x
v0 = − 0 . (9.11)
m
Für feste Werte m = 10 kg und s = 50 m berechnen wir v0 für µ = 0.1, µ = 0.4 und
µ = 0.8. Die Ergebnisse schreiben wir in eine Datei im LATEX-Tabellen Format.
/**
* @brief ex6 Teilchen bremst auf geradliniger Strecke
*/
inline void ex6 () {
using Data = Data <5 >;
// Masse
const auto mass = 10.0;
// ... kommt nach einer Strecke zum Stillstand
const auto s = 50.0;
const auto g = gravitation :: Earth ::g;
// Datenobjekt zur Speicherung der berechneten Daten
Data data;
// Schleife über Werte des Reibungskoeffizienten
for (auto mu : { 0.1 , 0.4 , 0.8 }) {
9.5 Beispiele aus der Physik 359
Ein Block der Masse m1 ist an einer horizontalen Feder mit der Federkonstanten k
befestigt (Abb. 9.2b). Die Feder befindet sich im entspannten Zustand. Ein zweiter Block
der Masse m2 wird gegen die Masse m1 gedrückt, sodass die Feder um die Länge x0
gestaucht wird. Das System wird losgelassen und bewegt sich reibungsfrei.
x=0 ⃗1
N
⃗ex m1 ⃗2
⃗
x0 N
m1 ⃗k m2
⃗
N F
m2
k ⃗
N
w
⃗1 w
⃗2
⃗ex
(a) (b)
Abb. 9.2: System aus zwei Blöcken und eine Feder. (a) Die Feder wird um x0 gestaucht.
(b) Auf die Masse m1 wirken die Federkraft, die Kontaktkraft N⃗ , die von m2 ausgeübt
⃗ 1 und das Gewicht w
wird, die Normalkraft N ⃗ 1 . Auf m2 wirken die Kontaktkraft N⃗ , die
⃗
Normalkraft N2 und das Gewicht w ⃗ 2.
360 9 Numerische Integration
1. Wann werden sich die zwei Blöcke trennen? Wie groß wird zu diesem Zeitpunkt ihre
Geschwindigkeit sein?
2. Erstellen Sie ein Computerprogramm, mit dem Sie durch Anwendung eines numeri-
schen Verfahrens die gesuchte Geschwindigkeit berechnen.
Lösung
Wir legen die positive x-Achse parallel zur Federachse und in Richtung der Anfangsaus-
lenkung. Da die zwei Blöcke sich berühren, übt jeweils der eine Block auf den anderen
eine Kontaktkraft aus. Die Kräfte sind nach dem dritten Newton’schen Gesetz vom glei-
chen Betrag und entgegengesetzter Richtung. Solange die Blöcke sich berühren, haben sie
eine gemeinsame Beschleunigung. Wir wenden das zweite Newton’sche Gesetz auf jeden
Block an.
m ẍ = −kx + N (9.12a)
1 1
m2 ẍ2 = −N (9.12b)
ẍ = ẍ = ẍ, (9.12c)
1 2
wobei wir für die gemeinsame Beschleunigung ẍ schreiben werden. Wir addieren Gl. 9.12a
und Gl. 9.12b
(m1 + m2 ) ẍ = −kx
k
⇒ ẍ = − x. (9.13)
m1 + m2
Aus (Gl. 9.12b) folgt für die Kontaktkraft
k m2
N= x. (9.14)
m1 + m2
Wenn die Blöcke sich trennen, ist N = 0 und damit x = 0. Wir schreiben für Gl. 9.13
dv dv dx dv k
= =v =− x.
dt dx dt dx m1 + m2
Nach Trennung der Variablen integrieren wir beide Seiten:
∫ v ∫ 0
kx′
v ′ dv ′ = − dx′
0 x 0 m1 + m2
[ ]v [ ]
2 x0
v ′2 k x′
⇒ =
2 m1 + m2 2
0 0
v2 kx20
⇒ =
2 2 (m1 + m2 )
Wir erhalten: √
k
v= x0 (9.15)
m1 + m2
9.5 Beispiele aus der Physik 361
/**
* @brief ex14 Zwei Blöcke und eine Feder
*/
inline void ex14 () {
// Federkonstante
const double k = 100;
// Massen der Blöcke
const double m1 = 10, m2 = 5;
// Feder wird um diese Länge gestaucht
const double l = 20. _cm;
// exakte Formel für die Geschwindigkeit
auto vex = [k, m1 , m2 ]( double x) { //
return sqrt(k / (m1 + m2)) * x;
};
// Integrand
auto fn = [k, m1 , m2 ]( double x) { //
return k / (m1 + m2) * x;
};
// numerische Berechnung des Integrals ohne den Faktor 2
gsl :: Integral integral (fn);
integral .qng (0, l);
// Ausgabe in Datei
std :: ofstream ofs = get_output_stream (__func__ , Output :: terminal );
ofs << "exact :" << vex(l) << std :: endl;
ofs << " numerical :" << sqrt (2 * integral . result ()) << std :: endl;
}
Ein Block der Masse m bewegt sich geradlinig und reibungsfrei auf horizontalem Boden.
Eine zeitabhängige horizontale Kraft der Form F (t) = F0 cos ωt wirkt in Richtung der
Geschwindigkeit des Teilchens für ein Zeitintervall ∆t.
1. Berechnen Sie die Geschwindigkeit des Teilchens als Funktion der Zeitdauer des Kraft-
stoßes.
2. Berechnen Sie die gesuchte Geschwindigkeit durch Einsatz eines numerischen Algo-
rithmus.
362 9 Numerische Integration
Lösung
Wir legen die positive x-Achse parallel zur Anfangsgeschwindigkeit des Teilchens und
wenden den Impulssatz an. Die Zeitmessung soll beginnen, wenn die Kraft auf den Block
wirkt, und enden, wenn die Kraft nicht mehr angewandt wird:
∫ t ∫
F0 t
mv = mv0 + F (t′ )dt′ ⇒ v = v0 + cos ωt′ dt′
0 m 0
[ ]t
F0 ′
⇒ v = v0 + sin ωt
mω
0
oder
F0
v = v0 + sin ωt (9.16)
mω
Quelltext und Daten Wir implementieren Gl. 9.16 mittels einer λ-Funktion und berech-
∫t
nen das Integral 0 cos ωt′ dt′ mit einer Elementfunktion der gsl-Klasse. Die Berechnung
der Geschwindigkeit erfolgt für drei Werte für ∆t und die Daten werden im LATEX-Format
in eine Datei geschrieben.
/**
* @brief ex12 Geschwindigkeitsänderung durch Kraftstoß
*/
inline void ex12 () {
nmx ::Data <5> data;
const double v0 = -6;
const double m = 2;
const double F0 = 25, omega = Math :: PI / 10;
// Ausgabe in Datei
std :: ofstream ofs = get_output_stream (__func__ , Output :: latex);
data.save(ofs , Output :: latex );
}
Es folgt die Ausgabe des Programms als LATEX-Tabelle. Die beiden letzten Spalten ent-
halten den numerisch berechneten und den exakten Wert für die Geschwindigkeit.
9.5 Beispiele aus der Physik 363
Ein Teilchen bewegt sich auf gerader Strecke mit einer Beschleunigung der Form a =
√
(c1 t + c2 t) m/s2 , wobei t die Zeit gemessen in Sekunden ist. Für das Teilchen gelten
die Anfangsbedingungen x(0) = x0 und v(0) = v0 .
1. Wie lauten der Ort und die Geschwindigkeit als Funktion der Zeit?
2. Berechnen Sie durch numerische Integration den Ort und die Geschwindigkeiten für
drei unterschiedliche Zeitpunkte und für c2 > 0. Wiederholen Sie die Rechnung für
c2 < 0. Vergleichen Sie jedes mal die Daten mit den exakten Ergebnissen.
Lösung
Wir legen die positive x-Achse entlang der Bewegungsrichtung des Teilchens. Die Ge-
schwindigkeit erhalten wir durch Integration nach der Zeit:
∫ ∫ (
√ t
dv ′ t √)
ẍ = c1 t + c2 t ⇒ dt = c1 t + c2 t dt′
0 dt′ 0
[ ]t
c1 ′2 2 ′3/2
⇒ v(t) − v(0) = t + c2 t
2 3
0
c1 2
⇒ v(t) = t2 + c2 t3/2 + v0 . (9.17)
2 3
Eine erneute Integration nach der Zeit berechnet den Ort als Funktion der Zeit:
∫ t ∫ t( )
dx ′ c1 ′2 2 ′3/2
′
dt = t + c2 t + v0 dt′
0 dt 0 2 3
[ ]t
c1 ′3 4 ′5/2 ′
⇒ x(t) − x(0) = t + c2 t + v0 t
6 15
0
c1 4
⇒ x(t) = t3 + c2 t5/2 + v0 t + x0 (9.18)
6 15
Das Computerprogramm
Quelltext und Daten Für die Anfangsbedingungen x(0) = 0 und v(0) = 0 und einen
festen Wert c1 = 10 rechnen wir mit c2 = 1 und dann mit c2 = −3. Zur exakten Be-
364 9 Numerische Integration
rechnung der Ergebnisse implementieren wir Gl. 9.17 und Gl. 9.18 als λ-Funktionen. Die
numerische Berechnung der Geschwindigkeit als Integral über die Beschleunigung ist kein
Problem, im Gegensatz zur Berechnung des Ortes. Hier muss über die Geschwindigkeit
numerisch integriert werden. Wenn wir aber nicht die exakte Formel Gl. 9.17 einsetzen
wollen, liegen nur diskrete Daten für v(t) vor. Die numerische Routine wird aber nicht
unbedingt Stützstellen verwenden, für die v(t) bekannt ist. Wir definieren deshalb eine
λ-Funktion, die für jedes t eine numerische Integration über die Beschleunigung ausführt
und somit die Werte der Geschwindigkeit an den gewünschten Stützstellen zurückgibt.
Dies ist nicht die performanteste Lösung. Solange aber keine Berechnung mit sehr großen
Datenmengen erforderlich ist, bleibt der Aufwand vertretbar.
/**
* @brief ex10 Teilchen bewegt sich geradlinig mit zeitabhängiger
* Beschleunigung
*/
inline void ex10 () {
nmx ::Data <7> data;
// Anfangsbedingungen
const double x0 = 0, v0 = 0;
const double c1 = 10;
for (auto c2 : { 1., -3. }) {
// Beschleunigung
auto afn = [c1 , c2 ]( double t) { //
return c1 * t + c2 * sqrt(t);
};
// Funktionen zur exakten Berechnung von
// Geschwindigkeit ...
auto vex = [c1 , c2 , v0 ]( double t) {
const auto static f1 = 2. / 3.;
const auto static f2 = 3. / 2.;
return 0.5 * c1 * pow(t, 2) + f1 * c2 * pow(t, f2) + v0;
};
// ... und Ort
auto xex = [c1 , c2 , v0 , x0 ]( double t) {
const auto static f1 = 1. / 6.;
const auto static f2 = 4. / 15.;
const auto static f3 = 5. / 2.;
return f1 * c1 * pow(t, 3) //
+ f2 * c2 * pow(t, f3) + v0 * t + x0;
};
Es folgen die berechneten Daten. Die beiden letzten Spalten enthalten die exakt berech-
neten Werte für Geschwindigkeit und Ort.
Ein ruhendes Teilchen wird entlang einer geraden Strecke mit einer geschwindigkeits-
abhängigen Beschleunigung der Form a = (c1 − c2 v) m/s2 beschleunigt, wobei v die
Geschwindigkeit in m/s gemessen wird.
1. Leiten Sie eine Formel her, welche den Zeitpunkt t berechnet, zu dem die Geschwin-
digkeit einen Wert v erreicht.
2. Schreiben Sie ein Computerprogramm, welches durch den Einsatz eines numerischen
Verfahrens die Zeit berechnet. Variieren Sie die Konstanten c1 und c2 sowie die An-
fangsgeschwindigkeit und stellen die Ergebnisse in eine LATEX-Tabelle dar. Fügen Sie
zum Vergleich die exakten Ergebnisse hinzu.
366 9 Numerische Integration
Lösung
Wir legen die positive x-Achse entlang der Bewegungsrichtung des Teilchens. Wir schrei-
ben den Ausdruck für die Beschleunigung des Teilchens und integrieren durch Trennung
der Variablen: ∫ v ∫ t
dv dv ′
= c1 − c2 v ⇒ ′
= dt′
dt 0 c1 − c2 v 0
[ 1 ( ) ]v
⇒ − ln c1 − c2 v ′ =t
c2 0
1 [ ]
⇒t= ln c1 − ln (c1 − c2 v)
c2
1 c1
⇒t= ln (9.19)
c2 c1 − c2 v
Das Computerprogramm
Quelltext und Daten Das Programm ist schnell erstellt. In geschachtelten Schleifen
durchlaufen wir die Werte für c1 und c2 sowie die Werte für die Geschwindigkeiten.
Innerhalb der Schleifen werden die Funktionen zur exakten Berechnung der Zeit (Gl.
∫ v dv′
9.19) und eine λ-Funktion zur numerischen Berechnung des Integrals 0 c1 −c 2v
′ definiert.
Die Ergebnisse werden in einer Zahlentabelle gespeichert und anschließend in eine Datei
im LATEX-Tabellen-Format geschrieben.
/**
* @brief ex11 Teilchen bewegt sich geradlinig mit einer
* geschwindigkeitsabhängigen Beschleunigung
*/
inline void ex11 () {
nmx ::Data <5> data;
// Ausgabe in Datei
9.5 Beispiele aus der Physik 367
Es folgen die berechneten Daten als LATEX-Tabelle. In den beiden letzten Spalten sind
die numerisch berechneten und die exakten Werte für die Zeit dargestellt.
Eine Perle der Masse m ist an eine Feder mit der Federkonstanten k befestigt und kann
sich horizontal entlang einer Schiene reibungsfrei bewegen (Abb. 9.3). Zum Zeitpunkt
t0 = 0 hat die Feder eine vertikale Ausrichtung. Sie befindet sich im entspannten Zustand
und hat die Länge l0 . Die Perle erhält durch einen Stoß eine horizontale Geschwindigkeit
v0 .
x x
⃗
N
v0 v0
⃗x
F
Fk ⃗y
F
l l
l0 l0
ϑ
⃗ey
⃗ex
0 0
(a) (b)
Abb. 9.3: Perle bewegt sich reibungsfrei entlang einer Schiene unter den Einfluss einer
Feder
1. Wir groß ist die Geschwindigkeit der Perle, wenn diese eine Strecke x entlang der
Schiene zurückgelegt hat?
368 9 Numerische Integration
2. Berechnen Sie die gesuchte Geschwindigkeit für eine Reihe von Beispielwerten für
x mittels eines numerischen Verfahrens und vergleichen Sie das Ergebnis mit den
exakten Werten.
Lösung
Wir legen den Ursprung des Koordinatensystems an den unteren Befestigungsort der
Feder (Abb. 9.3b). Aus der Geometrie des Systems folgt:
√
l0 x
∆l = |l − l0 | = l02 + x2 − l0 , cos ϑ = √ 2 , sin ϑ = √ 2 (9.20)
l 0 + x2 l 0 + x2
Wir multiplizieren die Gleichung einmal skalar mit ⃗ex und dann mit ⃗ey :
( )
k l0 x
ẍ = − x− √ 2 (9.22a)
m l0 + x2
( )
0 = N − kl0 1 − √ l 0
(9.22b)
l02 + x2
oder √ ( √ )
2k x2
v= v02 − − l0 l02 + x2 + l02 (9.23)
m 2
9.5 Beispiele aus der Physik 369
Das Computerprogramm
/**
* @brief ex13 Perle bewegt sich reibungsfrei entlang einer Schiene
* unter dem Einfluss einer Feder
*/
inline void ex13 () {
nmx ::Data <5> data;
const double m = 2;
const double l0 = 1, v0 = 15;
const double k = 4;
// Ausgabe in Datei
std :: ofstream ofs = get_output_stream (__func__ , Output :: latex);
ofs << std :: setprecision (4);
data.save(ofs , Output :: latex );
}
Nach Ausführung des Programms erhalten wir folgende Daten, wobei die beiden letzten
Spalten die numerisch berechneten und die exakten Werte für die Geschwindigkeiten
enthalten.
Tab. 9.5: Perle bewegt sich reibungsfrei entlang einer Schiene unter dem Einfluss einer
Feder
v0 [m/s] x [m] F [N] v [m/s] vex [m/s]
1.5000e+01 1.0000e+00 2.9289e-01 1.4989e+01 1.4989e+01
1.5000e+01 2.5000e+00 1.5715e+00 1.4808e+01 1.4808e+01
1.5000e+01 4.0000e+00 3.0299e+00 1.4335e+01 1.4335e+01
1. eines homogenen dünnen zylindrischen Stabs mit Dichte ρS , Länge l und einen Durch-
messer, der im Vergleich zur Länge vernachlässigbar ist,
2. eines homogenen Halbkreisrings mit Dichte ρK und Ri ≈ Ra ≈ R, wobei Ri der
innere und Ra der äußere Radius ist.
3. Wenn ρS = ρK = 7.7 kg/dm2 und l = 1 m, berechnen Sie mit einem Computerpro-
gramm den Schwerpunkt und das Gewicht für einen dünnen Draht, der einmal die
Form einer Geraden und die eines dünnen Halbkreisrings hat.
dl
l dϕ
dx
ey R dm = ρK dl
ex dm = ρS dx
ex
(a)
(b)
Abb. 9.4: (a) Ein dünner homogener Stab und (b) ein homogener dünner Kreisring. In
beiden Fällen ist die infinitesimale Masse vergrößert dargestellt.
Lösung
Der Massenschwerpunkt eines dünnen Stabes Der Ortsvektor des Schwerpunkts wird
über die Formel ∫l ∫l
xdm xρS dx⃗ex
⃗rcm = ∫0 l = 0∫ l (9.25)
0
dm 0 S
ρ dx
9.5 Beispiele aus der Physik 371
und erhalten
l
⃗rcm = ⃗ex . (9.27)
2
Quelltext und Daten Die Integranden in Gl. 9.26 werden im Programm als λ-
Funktionen implementiert. Die gsl-Klasse berechnet die Integrale über diese beiden
Funktionen. Anschließend werden die Masse und der Ortsvektor berechnet und in ei-
ne Datei ausgegeben.
/**
* @brief ex7 Massenmittelpunkt eines homogenen dünnen
* zylindrischen Stabs
*/
inline void ex7 () {
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
const double l = 1.0;
const double density = 7.7 / 1e -3;
// Ausgabe
sout << "mass =" << m << std :: endl;
sout << "xcm=" << xcm << std :: endl;
sout << "xcm=" << 0.5 * l << " ( exact)" << std :: endl;
}
und erhalten für die Koordinaten des Schwerpunkts (xcm , ycm ) = (0, 2R
π ).
Quelltext und Daten Das Programm implementiert die Integranden in Gl. 9.29 und
Gl. 9.30 mit drei λ-Funktionen. Diese werden von der gsl-Klasse integriert. Anschließend
werden die Ortskoordinaten des Schwerpunkts berechnet und die Ergebnisse zusammen
mit den exakten Werten in einer Datei ausgegeben.
/**
* @brief ex8 Massenmittelpunkt eines homogenen Halbkreisrings
*/
inline void ex8 () {
std :: ofstream sout = get_output_stream (__func__ , Output :: terminal );
// der Nenner
auto fnden = [density , radius ]( double x) {
(void) x; // avoid warning
return density * radius ;
};
// Ausgabe in Datei
sout << "w =" << w << std :: endl;
sout << "xcm=" << xcm << std :: endl;
sout << "ycm=" << ycm << std :: endl;
sout << "xcm=" << 0 << " ( exact )" << std :: endl;
sout << "ycm=" << 2 * radius / Math ::PI << " (exact)" << std :: endl;
}
w =7.700 e+03
xcm =7.375e -18
ycm =2.026e -01
xcm =0 (exact)
ycm =2.026e -01 ( exact )
Ein Teilchen mit der Masse m = 1000 kg befindet sich in einem Abstand z0 vom Erdmit-
telpunkt. Berechnen Sie mit einem Computerprogramm die potenzielle Energie von m als
Arbeit der Gravitationskraft entlang einer Strecke von z0 bis ins Unendliche. Vergleichen
Sie die Ergebnisse mit den exakten Werten.
Lösung
Mm
U = −G (9.31)
z0
gegeben. Die Gravitationskraft ist konservativ, deswegen ist die von ihr verrichtete Arbeit
vom Weg unabhängig. Wir berechnen die potenzielle Energie des Teilchens als die Arbeit
der Gravitationskraft F entlang einer geraden Strecke von z0 bis +∞.
∫ ∞ ∫ ∞
dz
W = F dz = −GM m 2
(9.32)
z0 z0 z
374 9 Numerische Integration
Wir wollen für die Abstände in Einheiten von R (Erdradius) rechnen und für die Energien
in Einheiten von λ = GM m
R , was dem Betrag der potenziellen Energie eines Teilchens
mit der Masse m auf der Erdoberfläche entspricht. Wenn z = uR, folgt für Gl. 9.31
Mm U 1
U = −G ⇒ =− . (9.33)
uR λ u
Für das Integral (Gl. 9.32) erhalten wir mit dz = Rdu:
∫ ∫ ∞
M m ∞ du W du
W = −G ⇒ = − (9.34)
R z0 u2 λ z0 u2
R R
Das Computerprogramm
Quelltext und Daten Wir werden das Integral in Gl. 9.34 für vier Werte für z0 nume-
risch berechnen und dann W /λ mit U /λ (Gl. 9.33) vergleichen.
/**
* @brief ex9 Potenzielle Energie einer Masse im Gravitationsfeld
* der Erde
*/
inline void ex9 () {
using Data = nmx :: Data <4 >;
const double mass = 1e3;
// Ausgabe in Datei
data.save(ofs , Output :: latex );
}
Wir führen das Programm aus und erhalten folgende Resultate zusammengefasst in einer
LATEX-Tabelle:
9.6 Übungsaufgaben 375
Tab. 9.6: Arbeitsintegral (numerisch berechnet), potenzielle Energie (exakte Werte) ei-
nes Teilchens im Gravitationsfeld der Erde in Einheiten von λ = G MR
z0 /R W /λ U /λ Fehler
1.000e+01 -1.000e+02 -1.000e+02 0.000e+00
1.000e+02 -1.000e+01 -1.000e+01 5.329e-15
1.000e+03 -1.000e+00 -1.000e+00 1.332e-14
1.000e+04 -1.000e-01 -1.000e-01 3.068e-14
9.6 Übungsaufgaben
∫ 4 6x
1. Erstellen Sie Programme zur numerischen Berechnung folgender Integrale: 0 16−x 2 dx,
∫ 10 √ ∫ 2 sin x
0
xdx, 0 x dx.
2. Ein Teilchen bewegt sich mit einer konstanten Geschwindigkeit und einer geschwin-
digkeitsabhängigen Beschleunigung a = vc m/s2 geradlinig, wobei v die momentane
Geschwindigkeit und c eine reelle Konstante ist. Zum Zeitpunkt t0 = 0 gilt v(0) = v0
und x(0) = x0 .
a) Berechnen Sie den Ort und die Geschwindigkeit als Funktion der Zeit.
b) Erstellen Sie ein Computerprogramm, welches mittels numerischer Integration die
Geschwindigkeit für drei bestimmte Zeitpunkte Ihrer Wahl berechnet.
3. Ein Massenpunkt bewegt sich mit einer konstanten Geschwindigkeit v0 in einem Me-
dium hinein und wird mit einer zeitabhängigen Beschleunigung a = −ct m/s2 abge-
bremst, wobei t die Zeit und c eine reelle Konstante ist.
a) Wie lange braucht das Teilchen, bis es zum Stillstand kommt?
b) Berechnen Sie, für ein c Ihrer Wahl, die gesuchte Zeit mittels eines Computerpro-
gramms.
10 Anfangswertprobleme für
gewöhnliche
Differenzialgleichungen
Übersicht
10.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
10.2 Ein gsl-Programm zur numerischen Lösung einer Differenzialgleichung . . . . . . 379
10.3 Die Schnittstelle zur GNU Scientific Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
10.4 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 386
10.5 Beispiele aus der Physik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
10.6 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 411
10.1 Einleitung
Viele Probleme aus den Naturwissenschaften, wie z.B. Wachstumsmodelle, chemische
Reaktionen oder die Bewegung von Objekten werden mithilfe von Differenzialgleichungen
beschrieben. Wenn keine analytische Lösung für diese Gleichungen gefunden werden kann
oder diese zu kompliziert ist, können numerische Methoden eingesetzt werden.
Liegt eine Differenzialgleichung der Form
mit y(x0 ) = y0 vor, handelt es sich um ein Anfangswertproblem. Eine Gruppe von Algo-
rithmen zur Lösung solcher Probleme basiert auf dem Euler-Verfahren, welches wir hier
kurz vorstellen wollen. Ausgehend von Gl. 10.1 versuchen wir über geometrische Über-
legungen y(x) zu berechnen (Abb. 10.1a). Bekannt sind dabei ein bestimmter Wert der
Kurve y(x0 ) = y0 (Anfangswert) und die Steigung für jedes x, welche aus der Differenzi-
algleichung hervorgeht. Wir wählen eine Anzahl von äquidistanten Punkten xi innerhalb
eines Intervalls mit xi+1 = xi + h und zeichnen die Tangente an der Stelle (xi , yi ). Der
Punkt yi+1 der Tangente soll ein Näherungswert der Kurve y(x) an der Stelle xi+1 sein.
Mithilfe der Trigonometrie schreiben wir folgende Relation für den Winkel φ:
yi+1 − yi
tan φ = (10.2)
xi+1 − xi
Dieser Ausdruck ist gleichzeitig auch die Ableitung von y an der Stelle xi . Es gilt dem-
zufolge
yi+1 − yi
y ′ (xi ) = f (xi , yi ) = (10.3)
xi+1 − xi
⇒ yi+1 = yi + hf (xi , yi ), (10.4)
was eine Iterationsvorschrift zur Berechnung der yi ist.
y
y
(xi+1 , yi+1 )
yi+1 − yi
(xi , yi ) φ
0 xi xi+1 x x
h
(b)
(a)
Abb. 10.1: (a) Geometrische Deutung des Euler-Verfahrens. (b) Das Verhalten der Kur-
ve wird an Stellen, an denen die Funktion stärker variiert durch Schrittweitenanpassung
berücksichtigt.
Je kleiner die Schrittweite h gewählt wird, um so mehr nähert sich yi dem gesuchten
exakten Wert an. Da die Funktion entlang des Definitionsbereichs unterschiedlich vari-
ieren kann, ist es sinnvoll mit einer automatischen Schrittweitenanpassung zu arbeiten.
Dies bedeutet, dass der Algorithmus selbst die Schrittweise steuert. Ein sehr einfaches
Verfahren in diesem Fall wäre ein Ergebnis für h zu berechnen und dann dasselbe über
zwei Schritte, d.h mit h/2 zu tun. Liegen die beiden Resultate nahe genug beieinander,
kann mit der Berechnung des nächsten Punktes fortgefahren werden.
Eine Weiterentwicklung des Euler-Verfahrens ist die Familie der Runge-Kutta-
Verfahren. Statt die Steigung am Punkt xi zu nehmen, wird ein (gewichteter) Mittelwert
von Steigungen innerhalb des Intervalls [xi , xi+1 ] ermittelt und damit eine bessere Appro-
ximation für yi berechnet. Nehmen wir als Beispiel das Runge-Kutta-Verfahren zweiter
Ordnung. Hier lautet die Iterationsvorschrift
1
yi+1 = yi + h (f (xi , yi ) + f (xi + h, yi+1 )) , (10.5)
2
wobei in f (xi + h, yi+1 ), yi+1 = yi + hf (xi , yi ) ist. Statt die Steigung der Tangente
an der Stelle xi für die Iterationsvorschrift zu wählen, berechnen wir den Mittelwert der
Steigungen an den Stellen xi und xi+1 . Auch für diesen Fall gilt, dass die Anpassung
der Schrittweite h zu besseren Ergebnissen führt. Die Halbierung der Schrittweite erfor-
dert jedoch zusätzliche Rechenschritte, die wir durch einen anderen Ansatz versuchen
zu reduzieren und mithilfe der beiden oben beschriebenen Verfahren erklären wollen.
10.2 Ein gsl-Programm zur numerischen Lösung einer Differenzialgleichung 379
Ein genauere Betrachtung der Formeln Gl. 10.4 und Gl. 10.5 zeigt, dass zur Berech-
nung von Gl. 10.5 die Berechnung von Gl. 10.4 erforderlich ist. Wir berechnen für eine
bestimmte Schrittweite h mit beiden Formeln zwei Näherungswerte. Wenn die Differenz
der Ergebnisse innerhalb einer vorgegeben Schranke liegt, so wird das Ergebnis akzeptiert
und weitergerechnet. Dies ist die Idee des Runge-Kutta-Fehlberg-Verfahrens, welches mit
Formeln höherer Ordnung realisiert wird.
Wenn y(x) in bestimmten Bereichen stärker variiert als in anderen, liegt unter Um-
ständen eine steife Differenzialgleichung vor. In diesem Zusammenhang wird eine Jacobi-
Matrix berechnet, welche das lokale Verhalten der gesuchten Funktion zu bestimmen
hilft. Wir werden im Rahmen dieses Buchs nicht mit steifen Differenzialgleichungen ar-
beiten.
/**
* @brief func rechte Seite der Differenzialgleichung
* @param x unabhängige Variable
* @param yin Eingabe
* @param yout Ausgabe
* @param params zusätzlicher Parameter
* @return GSL_SUCCESS
*/
int func( double x, const double yin [], double yout [], void * params ) {
(void) params ;
yout [0] = yin [0] - 2 * x / yin [0];
return GSL_SUCCESS ;
}
Wir bemerken an dieser Stelle, dass der Funktion nicht Zahlen, sondern Felder übergeben
werden. Dies liegt daran, dass die Routinen der gsl auch zur Lösung von Differenzial-
380 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
gleichungssystemen gedacht sind und jede Differenzialgleichung höherer Ordnung sich als
ein System von Differenzialgleichungen erster Ordnung formulieren lässt:
y1′ = f1 (x1 , y1 , . . . , yn )
y2′ = f2 (x1 , y1 , . . . , yn )
..
.
′
yn = fn (x1 , y1 , . . . , yn )
In unserem Beispiel handelt es sich um den Spezialfall eines Systems, das aus nur einer
Gleichung besteht. Wir implementieren zusätzlich die exakte Lösung der Differenzialglei-
chung (Gl. 10.6).
/**
* @brief fexact exakte Lösung
* @param x Variable
* @return Funktionswert
*/
double fexact ( double x) {
return std :: sqrt (2 * x + 1);
}
Das Programm zur Lösung von Gl. 10.6 besteht aus vier logischen Teilen.
/**
* @brief ex1 Beispiel ( Vorlage )
*/
inline void ex1 () {
// Teil 1: Definition des Dgl - Systems
gsl_odeiv2_system sys = {
func , // rechte Seite des Dgl - Systems
nullptr , // Jacobi - Matrix
1, // Anzahl der Gleichungen des Dgl - Systems
nullptr // zusätzliche Parameter
};
// Teil2 : Name des Lösungsalgorithmus
const gsl_odeiv2_step_type *type = gsl_odeiv2_step_rk4 ;
// Initialisierung des Lösungsalgorithmus
gsl_odeiv2_driver *d = //
gsl_odeiv2_driver_alloc_y_new (&sys , // Dgl - System
type , // Lösungsalgorithmus
1e-6, // Schrittweite ( Start )
1e-6, // absoluter Fehler
0.0); // relativer Fehler
// Teil 3: Berechnung der Lösung
double x = 0;
double y[] = { 1.0 };
for ( double xi = 0; xi < 2; xi += 0.25) {
int status = gsl_odeiv2_driver_apply (d, &x, xi , y);
if ( status != GSL_SUCCESS ) {
printf ("error , return value =%d\n", status );
break;
10.2 Ein gsl-Programm zur numerischen Lösung einer Differenzialgleichung 381
Im ersten Teil wird die Differenzialgleichung (Gl. 10.6, Listing 10.1) als ein System von
Differenzialgleichungen erster Ordnung definiert. Die nötigen Informationen werden der
gsl über eine Struktur (Listing 10.4) mitgeteilt. Neben dem Differenzialgleichungssystem
muss zusätzlich die Anzahl der Gleichungen des Systems übergeben werden. Die Angabe
einer Jacobi-Matrix sowie des letzten Parameters ist optional.
typedef struct {
// rechte Seite des Differenzialgleichungssystems
int (* function ) ( double t, const double y[], double dydt [], void
* params );
// Jacobi - Matrix
int (* jacobian ) ( double t, const double y[], double *dfdy ,
double dfdt [], void * params );
// Anzahl der Gleichungen
size_t dimension ;
// zusätzliche Parameter
void * params ;
} gsl_odeiv2_system ;
Listing 10.4
Im zweiten Teil werden die Parameter für die Lösungsroutine festgelegt. Hierzu gehören
die Wahl des Lösungsalgorithmus, in diesem Fall ist es das Runge-Kutta-Verfahren vierter
Ordnung, und die Angabe des Differenzialgleichungssystems.
Es folgt der dritte Teil mit der Festlegung der Anfangsbedingungen und dem Beginn
der Iteration. Wir geben der Lösungsroutine die Anweisung, die Lösungen an den Stellen
x = 0, . . . 2 mit ∆x = 0.25 zu berechnen. Die Ergebnisse werden auf dem Bildschirm
ausgegeben und im letzten Teil werden die angeforderten Ressourcen wieder freigegeben.
Nach Ausführung der Funktion erhalten wir folgende Daten:
Listing 10.5
382 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
/**
* @brief The Odeiv2 class numerische Lösung eines Systems von N
* Differenzialgleichungen Schnittstelle zur gsl
* @param N Anzahl der Differenzialgleichungen
* @param HASCALLOP Funktion wird über (...) aufgerufen
* @param Jacobi - Matrix wird benötigt
*/
template < size_t N, bool HASCALLOP = false , bool JACOBIAN = false >
class Odeiv2
{
public :
// Synonym für den Lösungsvektor
using Array = std :: array <double , N >;
// Synonym für Anzahl der Differenzialgleichungen
static constexpr size_t DIM = N;
private :
int _status ; // Status gsl - Meldung
Wir möchten flexibel sein und das Differenzialgleichungssystem mit eigenen Objekten
festlegen können und führen den Konstruktor als Template ein. Damit die gsl-Routinen
eingesetzt werden können, muss das Objekt über eine gsl_odeiv2_system-Struktur (Lis-
ting 10.4) registriert werden. Dies geschieht, indem die Struktur einen Zeiger auf das
Objekt speichert. Statt der gsl_odeiv2_system das Differenzialgleichungssystem direkt
anzugeben, wird eine Funktion registriert, die jeden Aufruf an das Objekt weiterleitet.
Ist das Objekt eine λ-Funktion , so verfügt es über einen überladenen Klammeropera-
tor und der zweite Template-Parameter der Klasse HASCALLOP muss gleich true gesetzt
werden; sonst muss das Objekt eine Schnittstelle odefn implementieren.
10.3 Die Schnittstelle zur GNU Scientific Library 383
/**
* @brief Odeiv2
* @param p enthält das DGL - System
*/
template <class T>
Odeiv2 (T &p)
: odeSystem { nullptr , nullptr , N, static_cast <void *>(&p) }
, odeDriver { nullptr } {
if constexpr (! HASCALLOP ) {
//DGL - System wird über eine Schnittstelle odefn aufgerufen
// gsl ruft diese Funktion auf
odeSystem . function = []( double t, //
const double y[],
double f[],
void * params ) -> int {
T * myObj = static_cast <T *>( params );
// Objekt muss diese Funktion definieren
return myObj -> odefn (t, y, f);
};
} else {
//DGL - System wird über () -Operator aufgerufen
// gsl ruft diese Funktion auf
odeSystem . function = []( double t, //
const double y[],
double f[],
void * params ) -> int {
T * myObj = static_cast <T *>( params );
// Objekt muss diesen () -Operator definieren
return (* myObj )(t, y, f);
};
}
Wir werden innerhalb dieser Klasse nur Algorithmen einsetzen, die mit Schrittweitenan-
passung arbeiten.
/**
* @brief init Initialisierung der gsl - Schnittstelle
* @param initstepsize Anfangsschrittweite
* @param abserr absoluter Fehler
* @param relerr relativer Fehler
* @param steptype Lösungsalgorithmus ( optional )
* standardmäßig Runge -Kutta - Fehlberg
384 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
*/
inline void init( double initstepsize ,
double abserr ,
double relerr ,
const gsl_odeiv2_step_type * steptype =
gsl_odeiv2_step_rkf45 ) {
odeDriver = gsl_odeiv2_driver_alloc_y_new (& odeSystem , steptype ,
initstepsize , abserr , relerr );
}
public :
Eine Instanz dieser Klasse kann nur über folgenden Konstruktor erzeugt werden.
/**
* @brief Odeiv2 Konstruktor
* @param obj Implementierung des DGL - Systems
* @param istepsize anfägliche Schrittweite
* @param abserror absoluter Fehler
* @param relerror relativer Fehler
* @param steptype Algorithmus (Runge -Kutta Familie )
*/
template <class T>
Odeiv2 (T &obj ,
double istepsize ,
double abserror ,
double relerror ,
const gsl_odeiv2_step_type * steptype = gsl_odeiv2_step_rkf45 )
: Odeiv2 (obj) {
init(istepsize , abserror , relerror , steptype );
}
/**
* Destructor
*/
inline ~ Odeiv2 () {
if ( odeDriver != nullptr ) {
gsl_odeiv2_driver_free ( odeDriver );
}
}
/**
* @brief set_init_conditions lege Anfangsbedingungen fest
* @param t unabhängige Variable
* @param y Lösungsvektor
*/
10.3 Die Schnittstelle zur GNU Scientific Library 385
/**
* @brief applyStep berechnet Lösung
* @param t für einen Wert der unabhängigen Variablen
*/
inline void solve ( double t) {
_status = gsl_odeiv2_driver_apply (odeDriver , & currentTime , t,
& currentState [0]);
if ( _status != GSL_SUCCESS ) {
throw std :: runtime_error ( nmx_msgx (std :: to_string ( _status )));
}
}
Die Anzahl der Gleichungen des Gleichungssystems sowie die Elemente des Lösungsvek-
tors können über die nächsten beiden Elementfunktionen gelesen werden.
/**
* @brief operator [] Zugriff auf einzelnen Elemente des
* Lösungsvektors mit Index
* @param idx Index
* @return Element des Lösungsvektors
*/
inline double operator []( size_t idx) const {
if (idx < 0 || idx >= N) {
throw std :: range_error ( nmx_msgx (std :: to_string (idx)));
}
return currentState [idx ];
}
/**
* @brief size
* @return Anzahl der Gleichungen im Gleichungssystem
*/
inline constexpr size_t size () const { return N; }
10.4 Beispiele
Beispiel Es soll die Differenzialgleichung dx dt = x − x ,
2t
x(0) = 1 numerisch gelöst
√
werden. Ihre analytische Lösung ist bekannt und lautet x(t) = 2t + 1. Im folgen-
den Quelltext wird über den zweiten Parameter des Klassentemplates festgelegt, wel-
che Schnittstelle die Implementierung der Differenzialgleichung haben muss, die wenige
Zeilen weiter definiert ist. Es folgt die analytisch hergeleitete Formel und anschließend
die numerische Lösung. Hier werden innerhalb einer Schleife Lösungen an den Stellen
ti+1 = ti + dt berechnet.
/**
* @brief ode Lösung einer DGL
*/
inline void ode1 () {
// eindimensionale Gleichung , kein () -Operator
// zweiter Template -Parameter false
using ODESolver = gsl :: Odeiv2 <1, false >;
using Data = Data <4 >;
// Datenobjekt
Data data;
Beispiel Wir wollen als nächstes eine lineare Differenzialgleichung zweiten Grades nu-
merisch lösen. Dazu ist es erforderlich, die Gleichung in ein System von linearen Diffe-
renzialgleichungen ersten Grades umzuwandeln. Wir betrachten folgendes Beispiel:
d2 x dx
= 2, x(0) = 0, = 0. (10.9)
dt2 dt t=0
Diese Gleichung kann als System von linearen Differenzialgleichungen ersten Grades for-
muliert werden:
dx
=u
dt
du
=2
dt
Für die Implementierung des Differenzialgleichungssystems soll diesmal ein Klammer-
operator erforderlich sein. Dieser wird durch die Wahl des zweiten Template-Parameters
der Odeiv2-Klasse festgelegt. Der weitere Ablauf des Programms erfolgt analog zum vor-
herigen Beispiel.
/**
* @brief ex2 DGL 2. Grades
*/
inline void ode2 () {
// zweiter Template - Parameter true
// die Implementierung des DGL - Systems erfordert
// einen () -Operator
using ODESolver = gsl :: Odeiv2 <2, true >;
using Data = nmx :: Data <7 >;
Data data;
// Implementierung des DGL - Systems
auto obj = []( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [1];
fout [1] = 2;
388 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
return GSL_SUCCESS ;
};
// exakte Lösung
auto v = [& acceleration ]( double t) { //
return acceleration * t;
};
auto x = [& acceleration ]( double t) { //
return 0.5 * acceleration * pow(t, 2);
};
// numerische Lösung
ODESolver myOde { obj , 1e-2, 1e-9, 0 };
myOde. set_init_conditions (0, { x0 , v0 });
double t = 0, dt = 0.5;
while (t < 5) {
const double xnum = myOde [0];
const double xexact = x(t);
const double vnum = myOde [1];
const double vexact = v(t);
const double verror = abs(vnum - vexact );
const double xerror = abs(xnum - xexact );
// registriere Daten
data += { t, xnum , xexact , xerror , vnum , vexact , verror };
t += dt;
// berechne Lösung für den nächsten Schritt
myOde.solve (t);
}
// Ausgabe
std :: ofstream mystable = get_output_stream (__func__ , Output :: latex);
data.save(mystable , Output :: latex );
}
1. Wie viel Zeit braucht der Holzblock, bis er eine Strecke s zurückgelegt hat? Welche
Geschwindigkeit hat er zu diesem Zeitpunkt?
2. Erstellen Sie mit einem Computerprogramm Bewegungsdaten für m = 40 kg, µ = 0.4,
|F
⃗ | = 200 N, s = 5 m. Lösen Sie die Aufgabe durch Anwendung des Arbeitssatzes
und erstellen Sie die Bewegungsdaten durch die numerische Lösung der Bewegungs-
gleichung. Vergleichen Sie die Ergebnisse.
Lösung
Lösung über den Arbeitssatz Auf den Holzblock wirken neben F ⃗ die Gewichtskraft w,
⃗
die Normalkraft N⃗ und die Gleitreibung R.
⃗ Wir legen fest, dass sich der Holzblock zu
Beginn der Bewegung an der Stelle x = 0 befindet. Es gelten somit folgende Anfangsbe-
dingungen: v(0) = 0 und x(0) = 0. Wir wenden den Arbeitssatz an:
Ks − K 0 = W g + W R + W N + W F , (10.10)
wobei Ks die kinetische Energie des Holzblocks am Ende und K0 jene am Anfang der
Strecke s ist. Es gilt Wg = 0 und WN = 0 für die Arbeit der Gewichtskraft und der
Normalkraft, da beide senkrecht zur Bewegungsrichtung stehen.
∫ s ∫ s [ ]s
WF = ⃗
F d⃗
x= F dx = F x = F s (10.11)
0 0 0
∫ s ∫ s [ ]s
WR = ⃗ x=−
Rd⃗ µN dx = −µN x = −µN s. (10.12)
0 0 0
N ⃗ = 0 ⇒ N = mg.
⃗ +w (10.13)
wobei t die Zeit ist, die der Holzblock braucht, bis er die Strecke s zurückgelegt hat.
Lösung über die Bewegungsgleichung Die Aufgabe kann auch direkt über die Integra-
tion der Bewegungsgleichung, welche aus dem zweiten Newton’schen Gesetz hervorgeht,
gelöst werden:
{
mẍ = F − µN, entlang der Bewegungsrichtung (10.17a)
0 = N − mg, senkrecht zur Bewegungsrichtung. (10.17b)
Das Computerprogramm
Kurzbeschreibung Wir wollen zeigen, dass mit C++ kurze Programme möglich sind
und programmieren alles innerhalb einer einzigen Funktion. Dies ist praktisch, wenn
schnell ein Prototyp erstellt werden muss. Zur Lösung der Differenzialgleichung sowie
zur Speicherung und Ausgabe der Daten in Dateien werden wir die von uns erstellten
Klassen einsetzen.
Quelltext Die Funktion beginnt mit der Initialisierung der Daten, die durch die Aufgabe
vorgegeben werden.
/**
* @brief ode3 Konstante Kraft wirkt auf Holzblock
*/
void ode3 () {
// Daten der Aufgabe
const double mass = 40.0;
const double mu = 0.4;
const double force = 200.0;
const double s = 5.0;
const double g = gravitation :: Earth ::g;
// Anfangsbedingungen
const double x0 = 0, v0 = 0;
Es folgt die Implementierung der Formeln Gl. 10.15 und Gl. 10.16.
std :: cout << std :: setw (3) << tval << "," //
<< std :: setw (3) << vs << std :: endl;
Das Differenzialgleichungssystem wird als λ-Funktion formuliert. Dies wird der Klasse
zur numerischen Lösung der Differenzialgleichung über den zweiten Template-Parameter,
der auf true gesetzt wird, mitgeteilt. Als System von Differenzialgleichungen formuliert
lautet Gl. 10.18
χ̇ = ψ
ψ̇ = F − µg,
m
wobei χ = x und ψ = v ist.
Wir berechnen solange Lösungen, bis die Strecke s erreicht ist. Die Ergebnisse werden in
eine Zahlentabelle geschrieben.
// lege Dgl fest , der zweite Template - Parameter muss true sein ,
// da das System als lambda implementiert ist.
using Ode = gsl :: Odeiv2 <2, true >;
using Data = nmx :: Data <4 >;
Data data;
Ode ode{ acceleration , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { x0 , v0 });
double t = 0, dt = 0.01;
// berechne solange Lösungen bis die Strecke s ist
while (ode [0] < s) {
data += { t, ode [0] , ode [1] , 0.5 * mass * pow(ode [1], 2) };
t += dt;
ode.solve (t);
}
// Ausgabe
std :: ofstream mystable = get_output_stream (__func__ , Output :: latex);
data. select_total_rows (5).save(mystable , Output :: latex);
392 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
Daten Die Werte für Zeit und Geschwindigkeit lauten, nachdem der Holzblock eine
Strecke von s = 5 m zurückgelegt hat, t = 3.04666 s und v = 3.28229 m/s. Wir erzeugen
mithilfe der abgespeicherten Daten eine Tabelle (Tab. 10.3) und Diagramme (Abb. 10.2).
Tab. 10.3: Bewegungsdaten für den Holzblock, der sich unter dem Einfluss einer hori-
zontalen konstanten Kraft und der Gleitreibung bewegt
t [s] x [m] v [m/s] K [J]
0.000e+00 0.000e+00 0.000e+00 0.000e+00
6.000e-01 1.939e-01 6.464e-01 8.357e+00
1.210e+00 7.887e-01 1.304e+00 3.399e+01
1.820e+00 1.784e+00 1.961e+00 7.689e+01
2.430e+00 3.181e+00 2.618e+00 1.371e+02
3.040e+00 4.978e+00 3.275e+00 2.145e+02
5
3
4
v[m/s]
3 2
x[m]
2
1
1
0 0
0 0.5 1 1.5 2 2.5 3 0 0.5 1 1.5 2 2.5 3
t[s] t[s]
(a) (b)
Abb. 10.2: Ein Holzblock bewegt sich unter dem Einfluss einer horizontalen konstanten
Kraft und der Gleitreibung. (a) Orts-Zeit-Diagramm (b) Geschwindigkeit-Zeit-Diagramm
Ein Teilchen mit der Masse m wird mit einer Anfangsgeschwindigkeit v0 unter einem
Winkel ϑ0 von einer Höhe H schräg nach oben geworfen (Abb. 10.3).
1. Wie lauten der Ort und die Geschwindigkeit des Teilchens als Funktion der Zeit?
2. Welche maximale Höhe erreicht das Teilchen und wo schlägt es auf den Boden auf?
Für welchen Winkel wird die horizontale Reichweite maximal?
10.5 Beispiele aus der Physik 393
y
ymax
⃗v0
⃗g w
⃗ vx
H ϑ0
vy
y ⃗ey
⃗ex
⃗ey ⃗
r
(b)
0 ⃗ex x xmax x
(a)
Abb. 10.3: (a) Schiefer Wurf eines Teilchens ohne Luftwiderstand aus einer Höhe H
unter einem Winkel ϑ0 (b) Geschwindigkeiten und Kräfte auf das Teilchen
3. Lösen Sie die Bewegungsgleichung numerisch. Tragen Sie in einer Tabelle zusätzlich
zu den Bewegungsdaten auch die potenzielle, die kinetische und die Gesamtenergie
ein.
Lösung
oder in Komponenten {
mẍ⃗ex = 0
(10.20)
mÿ⃗ey = −mg⃗ey .
Nach elementarer Integration beider Gleichungen erhalten wir
{
ẋ = c1 x = c1 t + c3
⇒
ẏ = c2 − gt y = c4 + c2 t − 1 gt2 .
2
Es ist leicht, mithilfe der Anfangsbedingungen x(0) = 0, y(0) = H, vx (0) = v0 cos ϑ0 ,
vy (0) = v0 sin ϑ0 die Integrationskonstanten c1 , c2 , c3 , c4 zu bestimmen. Ist dies gesche-
hen, folgt für die Orts- und die Geschwindigkeitskoordinaten als Funktion der Zeit:
x = v0 cos ϑ0 t (10.21a)
y = H + v0 sin ϑ0 t − 1 gt2 (10.21b)
2
und
{
vx = v0 cos ϑ0 (10.22a)
vy = v0 sin ϑ0 − gt (10.22b)
394 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
Berechnung der maximalen Steighöhe Hat das Teilchen seine maximale Steighöhe
erreicht, ist die vertikale Komponente der Geschwindigkeit vy (Gl. 10.22b) 0.
v0 sin ϑ0
v0 sin ϑ0 − gt = 0 ⇒ t = . (10.23)
g
v0 sin ϑ0 1 v 2 sin2 ϑ0
y = H + v0 sin ϑ0 − g 0 2
g 2 g
oder
v02 sin2 ϑ0
y=H+ . (10.25)
2g
Die Wurfweite Wir setzen in Gl. 10.21b y = 0 ein und berechnen die gesamte Bewe-
gungszeit:
1
H + v0 sin ϑ0 t − gt2 = 0
2
2 2v0 sin ϑ0 2H
⇒t − t− =0 (10.26)
g g
Die Diskriminante dieser quadratischen Gleichung lautet:
4v02 sin2 ϑ0 8H
∆= + (10.27)
g2 g
d
Der optimale Wurfwinkel ϑ0 Wir suchen ein ϑ0 , für das dϑ 0
xmax = 0 wird. Damit
die Formeln überschaubar bleiben, führen wir folgende Bezeichnungen ein:
√ 2gH
sin ϑ0 = u, cos ϑ0 = 1 − u2 , λ = 2 (10.30)
v0
mit u > 0 weil 0 ≤ ϑ0 < π
2. Wir schreiben für (Gl. 10.29)
v02 √ ( √ )
xmax = 1 − u2 u + u2 + λ .
g
dxmax
=0
dϑ0
( )
−u ( √ ) √ u
⇒ √ u + u2 + λ + 1 − u2 1 + √ =0
1 − u2 u2 + λ
( ( ))
√ −u ( √ )
2 +λ + 1+ √
u
⇒ 1 − u2 u + u =0
1 − u2 2
u +λ
( )
√ −u ( √ )
2 +λ + 1+ √
u
⇒ 1 − u2 = 0 ∨ u + u =0 (10.31)
1 − u2 u2 + λ
√
Wenn 1 − u2 = 0, haben wir einen senkrechten Wurf nach oben und damit die minimale
Reichweite. Wir rechnen mit
−u ( √ ) ( u
)
u+ u +λ + 1+ √
2 =0
1 − u2 u2 + λ
weiter.
( √ ) ( −u 1
)
u+ +λ u2 +√ =0
} 1−u
2
| {z u2 + λ
>0
1 u2
⇒ =
u2 +λ (1 − u2 )2
⇒ 1 − 2u2 + u4 = u4 + λu2 ⇒ u2 (λ + 2) = 1
√
1
⇒u=± . (10.32)
λ+2
Da u > 0, folgt
1
u= √ . (10.33)
λ+2
Durch Rücksubstitution erhalten wir den gesuchten Wert für ϑ0 :
( )
v0
ϑ0 = arcsin √ 2 (10.34)
2v0 + 2gH
Das Computerprogramm
Kurzbeschreibung Wir führen eine Modellklasse ein und leiten von ihr ein Rechenmo-
dell ab. Die Daten für die Flugbahn werden mit dem Rechenmodell berechnet und in
diesem gespeichert. Es folgt eine Liste mit den wichtigsten Eigenschaften beider Klassen:
396 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
Modellklasse
– Eingabewerte: Masse des Teilchens m, Anfangshöhe H, Betrag der Anfangsge-
schwindigkeit v0 und der Wurfwinkel ϑ0
– Implementierung theoretisch hergeleiteter Formeln
Rechenmodell abgeleitet von der Modellklasse
– Numerische Lösung der Bewegungsgleichungen
– Speicherung der Daten in einer Zahlentabelle
– Ausgabe der Zahlentabelle im Grafik- und Tabellenformat
Quelltext Eine Instanz der Modellklasse wird durch die Angabe der Masse des Teilchens
und der Anfangsbedingungen Anfangshöhe, Geschwindigkeitsbetrag und Winkel über den
Konstruktor erzeugt.
/**
* @brief The X710 class schiefer Wurf ohne Luftwiderstand
* ( Modellklasse )
*/
class X710 : public XModel
{
protected :
// Komponenten der Anfangsgeschwindigkeit
double _v0y = 0, _v0x = 0;
public :
// Eingabeparameter : Masse
const double mass;
// Anfangsbedingungen : Anfangshöhe ,
// Geschwindigkeit , Winkel
const double H, v0 , theta0 ;
/**
* @brief Konstruktor
* @param obj Zeiger auf Modellklasse
*/
inline X710( double m, double yinit , double vinit , double angle)
: XModel { __func__ }
, mass{ m }
, H{ yinit }
, v0{ vinit }
, theta0 { angle } {
// teste Eingabeparameter
Check :: all(nmx_msg , { mass > 0, H > 0, v0 > 0, theta0 > 0 });
// berechne und speichere Anfangsbedingungen
_v0x = v0 * std :: cos( theta0 );
_v0y = v0 * std :: sin( theta0 );
}
Es folgen die Formeln (Gl. 10.29, Gl. 10.25) für die Wurfweite und die maximale Höhe
sowie Gl. 10.28, Gl. 10.23 für die dazugehörigen Zeiten.
/**
* @brief xmax
* @return Wurfweite
*/
inline double xmax () const {
double term0 = pow(v0 , 2) / Earth ::g * cos( theta0 );
double term1 = pow(sin( theta0 ), 2) + (2 * Earth ::g * H) /
pow(v0 , 2);
return term0 * (sin( theta0 ) + sqrt(term1 ));
}
/**
* @brief ymax
* @return Steighöhe
*/
inline double ymax () const { //
return H + 0.5 * pow(_v0y , 2) / Earth ::g;
}
/**
* @brief t4xmax
* @return Zeit für maximale Wurfweite
*/
inline double t4xmax () const {
const double _tmp = _v0y + sqrt(pow(_v0y , 2) + 2 * Earth ::g *
H);
return (_tmp) / Earth ::g;
}
/**
* @brief t4ymax
* @return Zeit für Steighöhe
*/
inline double t4ymax () const { return _v0y / Earth ::g; }
Für gegebene Anfangsbedingungen wird der optimale Winkel für die maximale Reichweite
berechnet (Gl. 10.34).
/**
* @brief xmax_angle
* @return Winkel für maximale Wurfweite
398 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
*/
inline double xmax_angle () const { //
return asin(v0 / sqrt (2 * pow(v0 , 2) + 2 * Earth ::g * H));
}
/**
* @brief vx x- Komponente der Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return x- Komponente der Geschwindigkeit
*/
inline double vx( double t) const {
(void) t;
return _v0x;
}
/**
* @brief vy y- Komponente der Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return y- Komponente der Geschwindigkeit
*/
inline double vy( double t) const { //
return v0 * sin( theta0 ) - Earth ::g * t;
}
/**
* @brief x -Koordinate als Funktion der Zeit
* @param t Zeit
* @return x zum Zeitpunkt t
*/
inline double x( double t) const { return _v0x * t; }
/**
* @brief y -Koordinate als Funktion der Zeit
* @param t Zeit
* @return y zum Zeitpunkt t
*/
inline double y( double t) const { //
return H + vy(t) * t - 0.5 * Earth ::g * pow(t, 2);
}
}; // X710
Das Rechenmodell implementiert die Bewegungsgleichungen (Gl. 10.20) als ein System
von linearen Differenzialgleichungen erster Ordnung
χ̇1 = ψ1 (10.35a)
χ̇2 = ψ2 (10.35b)
ψ̇1 = 0 (10.35c)
ψ̇2 = −g, (10.35d)
/**
* @brief The C710 class Rechenmodell schiefer Wurf ohne Luftwiderstand
*/
class C710 : public X710
{
public :
using Ode = gsl :: Odeiv2 <4 >;
friend Ode;
using Data = Data <9 >;
private :
// Speicher für die berechneten Daten
Data _data;
Das Gleichungssystem Gl. 10.35 wird implementiert. Damit muss dieses Rechenmodell
der numerischen Routine (Instanz der Klasse gsl::Odeiv2) übergeben werden.
/**
* @brief odefn Schnittstelle zur gsl - Klasse
* @param t Zeit
* @param fin , Ort Geschwindigkeit
* @param fout Geschwindigkeit , Beschleunigung
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [2];
fout [1] = fin [3];
fout [2] = 0;
fout [3] = -Earth ::g;
return GSL_SUCCESS ;
}
public :
// Konstruktoren der Modellklasse werden geerbt
using X710 :: X710;
Der Ort und die Geschwindigkeit werden als Funktion der Zeit berechnet. Wenn sich das
Teilchen dem Boden nähert, werden die Zeitschritte kleiner gemacht. Wir kommen somit
400 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
näher an das theoretisch berechnete Ergebnis für Zeit und Geschwindigkeit. Es werden
Daten berechnet, bis die Bedingung y ≥ 0 nicht mehr gültig ist.
/**
* @brief solve_ode numerische Lösung der DGL
*/
inline void exec () {
// Anfangsbedingungen Ort
double x0 = 0.0 , y0 = H;
// Anfangsbedingungen Geschwindigkeit
double vx0 = _v0x , vy0 = _v0y;
/**
* @brief save_data speichert Daten im LaTeX - und im CSV - Format
*/
inline void save_data () {
const auto id = Math :: to_degrees ( theta0 );
save(_data , Output :: plot , id);
Daten Ein Teilchen der Masse m1 wird unter verschiedenen Abwurfwinkeln von einer
Höhe H = 10 m mit einer Geschwindigkeit v0 = 10 m/s geworfen. Für jeden Abwurfwin-
kel werden Bewegungsdaten berechnet (Tab. 10.4, Abb. 10.4).
/**
* @brief run Berechnung von Beispieldaten
*/
inline void run () {
const double H = 10.0;
const double mass = 1.0;
const double v0 = 10.0;
for ( double angle : { 10.0 , 30.0 , 45.0 , 60.0 }) {
const double theta0 = Math :: to_radians (angle);
C710 cobj{ mass , H, v0 , theta0 };
cobj.exec ();
cobj. save_data ();
}
Tab. 10.4: Schiefer Wurf für ϑ0 = 30◦ , v0 = 10 m/s für ein Teilchen mit der Masse
m = 1 kg
t [s] x [m] y [m] vx [m/s] vy [m/s] v [m/s]
0.000e+00 0.000e+00 1.000e+01 8.660e+00 5.000e+00 1.000e+01
1.917e+00 1.660e+01 1.566e+00 8.660e+00 -1.380e+01 1.629e+01
1.953e+00 1.691e+01 1.063e+00 8.660e+00 -1.415e+01 1.659e+01
1.989e+00 1.723e+01 5.469e-01 8.660e+00 -1.451e+01 1.689e+01
2.025e+00 1.754e+01 1.830e-02 8.660e+00 -1.486e+01 1.720e+01
2.026e+00 1.755e+01 3.440e-03 8.660e+00 -1.487e+01 1.721e+01
402 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
Tab. 10.5: Schiefer Wurf für ϑ0 = 30◦ , v0 = 10 m/s für ein Teilchen mit der Masse
m = 1 kg
t [s] x [m] y [m] K [J] U [J] E [J]
0.000e+00 0.000e+00 1.000e+01 5.000e+01 9.807e+01 1.481e+02
1.917e+00 1.660e+01 1.566e+00 1.327e+02 1.536e+01 1.481e+02
1.953e+00 1.691e+01 1.063e+00 1.376e+02 1.042e+01 1.481e+02
1.989e+00 1.723e+01 5.469e-01 1.427e+02 5.363e+00 1.481e+02
2.025e+00 1.754e+01 1.830e-02 1.479e+02 1.795e-01 1.481e+02
2.026e+00 1.755e+01 3.440e-03 1.480e+02 3.373e-02 1.481e+02
12 15
ϑ0 = 10◦ ϑ0 = 45◦
10 ϑ0 = 30◦ ϑ0 = 60◦
8 10
y[m]
y[m]
4 5
0 0
0 5 10 15 0 5 10 15
x[m] x[m]
(a) (b)
Abb. 10.4: Flugbahnen eines Teilchens mit der Anfangsgeschwindigkeit v0 = 10 m/s für
unterschiedliche Winkel
Eine Rakete wird senkrecht nach oben geschossen. Sie stößt Gase mit einer konstanten
Geschwindigkeit vR aus. Die Brennrate (Gasmenge, die pro Zeiteinheit ausgestoßen wird)
sei λ. Die Rakete hat zusammen mit dem Treibstoff eine Anfangsmasse m0 . Gehen Sie
von einem konstanten Gravitationsfeld aus und vernachlässigen Sie den Luftwiderstand.
1. Wie lautet die Bewegungsgleichung für die Rakete bis zum Zeitpunkt, zu dem der
gesamte Treibstoff verbraucht ist (Brennschluss)? Berechnen Sie die Geschwindigkeit
und die Höhe der Rakete zum Zeitpunkt des Brennschlusses.
2. Lösen Sie die Bewegungsgleichungen numerisch und vergleichen Sie die Daten mit den
exakten Ergebnissen.
10.5 Beispiele aus der Physik 403
Lösung
Das Koordinatensystem Wir legen das Koordinatensystem so, dass die Flugrichtung
mit der positiven z-Achse übereinstimmt.
Das zweite Newton’sche Gesetz Die Bewegungsgleichung für die Bewegung der Rakete
lautet
⃗ext + dm ⃗vR = m d⃗v ,
F (10.36)
dt dt
⃗ext = m⃗g und vR die Geschwindigkeit der
wobei die Masse m eine Funktion der Zeit, F
ausgestoßenen Gase relativ zur Rakete ist. Da die Brennrate konstant ist, folgt für die
Masse als Funktion der Zeit
m = m0 − λt (10.37)
dm
⇒ = −λ. (10.38)
dt
Wir setzen diesen Ausdruck in (Gl. 10.36) und erhalten
dv
m ⃗ez = −mg⃗ez − λ(−vR⃗ez )
dt
dv λ λvR
⇒ = −g + vR = − g. (10.39)
dt m m0 − λt
Die Geschwindigkeit als Funktion der Zeit Wir integrieren Gl. 10.39 nach der Zeit
und erhalten: ∫ v ∫ t( )
λvR
dv ′ = − g dt′
0 0 m0 − λt
[ ]t
⇒v= − vR ln(m0 − λt′ ) − gt′
( ) 0
m0
⇒ v = vR ln − gt. (10.40)
m0 − λt
Die Höhe als Funktion der Zeit Integration von Gl. 10.40 nach der Zeit liefert die
Höhe der Rakete als Funktion der Zeit:
∫ z ′ ∫ t ( ) ∫ t
dz ′ m0 ′
′
dt = v R ln dt − g t′ dt′
0 dt 0 m0 − λt′ 0
∫ t ( )
m0 1
⇒ z = vR ln ′
dt′ − gt2 (10.41)
0 m0 − λt 2
und mit Gl. 10.41 die Höhe als Funktion der Zeit:
( ) ( )
m0 λ λ 1
z = vR t + 1− t ln 1 − t vR − gt2 (10.46)
λ m0 m0 2
Höhe und Geschwindigkeit der Rakete, wenn der gesamte Treibstoff verbraucht ist
Wenn zum Zeitpunkt tB der gesamte Treibstoff verbraucht und die Masse der Rakete zu
diesem Zeitpunkt M ist, gilt
M = m0 − λtB
m0 − M
⇒ tB = . (10.47)
λ
Die gesuchte Geschwindigkeit zum Zeitpunkt tB (Gl. 10.40) ist somit:
(m ) g
0
vB = vR ln − (m0 − M ) (10.48)
M λ
Entsprechend folgt aus (Gl. 10.46) für die Höhe:
( ) ( )2
vR M 1 m0 − M
zB = m0 − M + M ln − g (10.49)
λ m0 2 λ
Das Computerprogramm
Kurzbeschreibung Wir führen eine Modellklasse und ein von ihr abgeleitetes Rechen-
modell ein mit folgenden Eigenschaften:
Modellklasse
– Speicherung von Eingabewerten m0 , M , λ, vR und Anfangsbedingungen z(0), v(0)
– Implementierung der hergeleiteten Formeln
Rechenmodell (abgeleitet von der Modellklasse)
– Numerische Lösung der Bewegungsgleichung
– Speicherung der berechneten Daten in einer Zahlentabelle
– Ausgabe der Daten in Dateien
10.5 Beispiele aus der Physik 405
Quelltext Die Modellklasse enthält alle für die Beschreibung der Bewegung relevan-
ten Parameter. Sie werden mit dem Konstruktor initialisiert und dürfen nicht geändert
werden. Der Zeitpunkt, zu dem der gesamte Treibstoff verbraucht ist, die Geschwindig-
keit und die Höhe für diesen Zeitpunkt werden im Konstruktor berechnet und intern
gespeichert (Gl. 10.47 bis Gl. 10.49).
/**
* @brief The X1200 class Bewegung einer Rakete in einem konstanten
* Gravitationsfeld ( Modellklasse )
*/
class X1200 : public XModel
{
private :
// Brennschluss : Zeit , Geschwindigkeit , Höhe
double _zB , _vB , _tB;
public :
// Anfangswerte
using IValues = std :: array <double , 2>;
// Eingabewerte
const double mass0 ; // Masse der Rakete plus Treibstoff t=0
const double massRocket ; // Masse der Rakete ohne Treibstoff
const double lambda ; // Brennrate
const double vRelative ; // Relativgeschwindigkeit
const double z0 , v0; // Anfangsbedingungen
public :
/**
* @brief X1200 Konstruktor mit Liste aller Eingabeparameter .
* @param minit Masse der Rakete plus Treibstoff t=0
* @param mrocket Masse der Rakete ohne Treibstoff
* @param l Brennrate
* @param vrocket Relativgeschwindigkeit
* @param zinit Anfangsbedingungen (Höhe)
* @param vinit Anfangsbedingungen ( Geschwindigkeit )
*/
massRocket > 0, //
lambda > 0,
vRelative > 0,
z0 >= 0,
v0 >= 0 });
_tB = ( mass0 - massRocket ) / lambda ;
_vB = vRelative * log( mass0 / massRocket ) - Earth ::g * _tB;
_zB = vRelative / lambda * ( mass0 - massRocket + massRocket *
log( massRocket / mass0 ))
- 0.5 * Earth ::g * pow(_tB , 2);
}
/**
* @brief is_propellant Treibstoff als Funktion der Zeit
* @param t Zeit
* @return false wenn Treibstoff verbraucht ist
*/
inline bool is_propellant ( double t) const { return t <= _tB; }
Es folgen die Formeln Gl. 10.38 und Gl. 10.39 für die Masse und die Beschleunigung als
Funktion der Zeit.
/**
* @brief mass Masse als Funktion der Zeit
* @param t Zeit
* @return Momentanwert der Masse
*/
inline double mass( double t) const { //
if ( is_propellant (t)) {
return mass0 - lambda * t;
}
return massRocket ;
}
/**
* @brief acceleration Beschleunigung als Funktion der Zeit
* @param t Zeit
* @return momentaner Wert der Beschleunigung
*/
inline double acceleration ( double t) const { //
return lambda / mass(t) * vRelative - Earth ::g;
}
Die Höhe und Geschwindigkeit als Funktion der Zeit (Gl. 10.46, Gl. 10.40) werden im-
plementiert:
/**
* @brief height Höhe als Funktion der Zeit
* @param t Zeit
* @return Höhe zum Zeitpunkt t
*/
inline double height ( double t) const {
const double term0 = mass(t) / mass0 ;
const double term1 = term0 * log(term0) * vRelative ;
const double term2 = 0.5 * Earth ::g * pow(t, 2);
return vRelative * t + ( mass0 / lambda ) * term1 - term2;
}
/**
* @brief velocity Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
inline double velocity ( double t) const {
const double term = mass0 / mass(t);
return vRelative * log(term) - Earth ::g * t;
}
Den Zeitpunkt, zu dem der gesamte Treibstoff verbraucht ist, die Höhe und die Geschwin-
digkeit geben folgende Funktionen zurück.
/**
* @brief time Zeitpunkt zu dem der gesamte Treibstoff
* verbraucht ist
* @return Zeit
*/
inline double time () const { return _tB; }
/**
* @brief velocity Geschwindigkeit zum Zeitpunkt zu dem der gesamte
* Treibstoff verbraucht ist
* @return Geschwindigkeit
*/
inline double velocity () const { return _vB; }
/**
* @brief height Höhe zum Zeitpunkt zum dem der gesamte Treibstoff
408 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
* verbraucht ist
* @return Höhe
*/
inline double height () const { return _zB; }
}; // X1200
/**
* @brief The C1200 class Bewegung einer Rakete in einem
* konstanten Gravitationsfeld ( Rechenmodell )
*/
class C1200 : public X1200
{
public :
using Ode = gsl :: Odeiv2 <3 >;
using Data = Data <7 >;
friend Ode;
protected :
Data _data; // speichere numerisch berechnete und exakte Werte
public :
using X1200 :: X1200 ; // erbe Konstruktoren der Modellklasse
/**
* @brief odefn Schnittstelle zur gsl
* @param t Zeit
* @param fin Eingabe Feld der Länge 3
* @param fout Ausgabe Feld der Länge 3
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = -lambda ;
fout [1] = fin [2];
fout [2] = lambda / fin [0] * vRelative - Earth ::g;
return GSL_SUCCESS ;
}
Es werden solange Daten berechnet, bis der Treibstoff verbraucht ist. Die numerisch
berechneten Lösungen werden um die exakt berechneten Werten ergänzt.
/**
* @brief exec Berechnung der numerischen Lösung
* @param t0 Zeit
* @param tstep Zeitschritte
*/
void exec( double t0 = 0, double tstep = 1) {
// Instanz zur Lösung der Differenzialgleichung
Ode myOde { *this , 1e-2, 1e-6, 0 };
myOde. set_init_conditions (0, { mass0 , z0 , v0 });
double t = t0 , dt = tstep ;
// exakte Werte
for (auto &crow : _data .data ()) {
const double t = crow [0];
crow [4] = mass(t);
crow [5] = height (t);
crow [6] = velocity (t);
}
}
Das Schreiben der Daten in Dateien erfolgt über die Elementfunktion Listing 10.51.
Für die Darstellung der Daten in Diagrammen erzeugen wir eine CSV-Datei mit allen
berechneten Daten. Wir treffen eine Auswahl dieser Daten und speichern diese im LATEX-
Tabellenformat ab, damit die Tabelle übersichtlich ist.
/**
* @brief save_data schreibe berechnete Daten
*/
inline void save_data () {
save(_data , Output :: plot);
save(_data . select_total_rows (8) , Output :: latex);
}
}; // C1200
Daten Ein Satz von Eingabeparametern wird einer Instanz eines Rechenmodells über-
geben. Es werden Bewegungsdaten berechnet und im Tabellen- und Grafik-Format in
Dateien gespeichert (Tab. 10.6, Abb. 10.5).
/**
* @brief run Bewegung einer Rakete in einem konstanten Gravitationsfeld
* ( Berechnung von Beispielwerten )
*/
inline void run () {
// Programmparameter
const double m0 = 2.85 e6;
const double M = 0.27 * m0;
const double lambda = 13.84 e3;
// Rechenmodell
C1200 cobj{ m0 , M, lambda , vR , { z0 , v0 } };
cobj.exec ();
cobj. save_data ();
}
Tab. 10.6: Bewegung einer Rakete im konstanten Gravitationsfeld der Erde. Die drei
letzten Spalten beinhalten die exakten Ergebnisse.
t [s] m [kg] z [m] v [m] mex [kg] zex [m] vex [m/s]
0.000e+00 2.850e+06 0.000e+00 0.000e+00 2.850e+06 0.000e+00 0.000e+00
2.100e+01 2.559e+06 5.662e+02 5.866e+01 2.559e+06 5.662e+02 5.866e+01
4.200e+01 2.269e+06 2.687e+03 1.493e+02 2.269e+06 2.687e+03 1.493e+02
6.300e+01 1.978e+06 7.119e+03 2.806e+02 1.978e+06 7.119e+03 2.806e+02
8.400e+01 1.687e+06 1.484e+04 4.655e+02 1.687e+06 1.484e+04 4.655e+02
1.050e+02 1.397e+06 2.719e+04 7.246e+02 1.397e+06 2.719e+04 7.246e+02
1.260e+02 1.106e+06 4.603e+04 1.093e+03 1.106e+06 4.603e+04 1.093e+03
1.470e+02 8.155e+05 7.429e+04 1.636e+03 8.155e+05 7.429e+04 1.636e+03
1.500e+02 7.740e+05 7.935e+04 1.736e+03 7.740e+05 7.935e+04 1.736e+03
10.6 Übungsaufgaben 411
·106 ·104
3
8
2.5
6
a[m/s2 ]
2
m[kg]
1.5
2
1
0
(a) (b)
·104
8
1,500
6
v[m/s] 1,000
z[m]
500
2
0
0
0 50 100 150
0 50 100 150
t[s]
t[s]
(c) (d)
Abb. 10.5: Bewegung einer Rakete im konstanten Gravitationsfeld der Erde. (a) m − t-
Diagramm, (b) a − t-Diagramm (c) z − t-Diagramm, (d) v − t-Diagramm
10.6 Übungsaufgaben
1. Implementieren Sie das Euler- und das Runge-Kutta-Verfahren zweiter Ordnung ohne
Schrittweitenanpassung. Lösen Sie mit beiden Methoden
a) die Differenzialgleichung (Gl. 10.6) an den Stellen x = 0, . . . 2 mit ∆x = 0.25,
b) die Differenzialgleichung (Gl. 10.9) an den Stellen x = 0, . . . 5 mit ∆x = 0.5.
Stellen Sie die Ergebnisse in Tabellen zusammen mit den Abweichungen von der
exakten Lösung dar.
2. Erstellen Sie mit den Daten aus Abschnitt 10.5.2 Energie-Zeit-Diagramme.
3. Entwerfen Sie ein Rechenmodell für die Aufgabe aus Abschnitt 10.5.2, welches Da-
ten für die Flugbahn eines Teilchens durch Anwendung der analytisch hergeleiteten
Formeln berechnet.
4. Eine Perle ist an einem Ende einer Feder befestigt und kann entlang einer Schiene
reibungsfrei gleiten (Abb. 10.6). Die Perle wird aus ihrer Ruhelage ausgelenkt und
losgelassen.
a) Stellen Sie die Bewegungsgleichung für die Perle auf und lösen Sie diese numerisch.
412 10 Anfangswertprobleme für gewöhnliche Differenzialgleichungen
b) Stellen Sie den Ort, die Geschwindigkeit und die Beschleunigung als Funktion der
Zeit in einer Tabelle und in Diagrammen dar.
Implementieren Sie alles innerhalb einer Funktion in Form eines Prototyps und über-
legen Sie, wie Sie daraus ein Modell erzeugen können.
l ⃗k
l0 F
0
Abb. 10.6: Perle bewegt sich reibungsfrei entlang einer Schiene nur unter dem Einfluss
der Federkraft. l0 ist die Länge der entspannten Feder.
11 Interpolation
Übersicht
11.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
11.2 Interpolation mit der GNU Scientific Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . 414
11.3 Die Schnittstelle zur GNU Scientific Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
11.4 Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
11.5 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
11.1 Einleitung
Bei einer eindimensionalen Interpolation handelt es sich um die Aufgabe, Werte einer
Funktion f (x) zu ermitteln, von der nur N Wertepaare (x0 , y0 ), . . . (xN , yN ) bekannt
sind. Dies können z.B. Messdaten eines Experiments oder Ergebnisse einer numerischen
Berechnung sein. Die xi heißen Stützstellen, die yi Stützwerte und die Punkte (xi , yi )
Stützpunkte. In der Praxis werden die Daten gruppenweise bzw. paarweise durch ein
Polynom approximiert. Wenn das Polynom zwischen zwei benachbarten Stützpunkten
ersten Grades ist, so spricht man von einer linearen Interpolation (Abb. 11.1a). Eine
derartige Approximation ist zwar stetig, ihre erste und zweite Ableitung an den Stütz-
punkten aber nicht. Bei kubischen Splines handelt es sich um Polynome dritten Gerades,
die zwei benachbarte Stützpunkte verbinden. Zusätzlich sind die erste und zweite Ablei-
tung der zwei Polynome an den Verbindungspunkten gleich (Abb. 11.1b).
y y
(x1 , y1 ) s1 (x)
s0 (x) (x1 , y1 )
y (x2 , y2 )
(x2 , y2 )
(x3 , y3 )
(x0 , y0 )
(x0 , y0 )
0 x
x x
0
(a) (b)
Abb. 11.1: (a) Lineare Interpolation und (b) Spline-Interpolation. s0 (x) und s1 (x) sind
kubische Polynome, welche die Punkte (x0 , y0 ) und (x1 , y1 ) verbinden.
Die Berechnung der Spline-Koeffizienten basiert auf einem Verfahren, das wir hier kurz
skizzieren möchten. Sei s0 ein Polynom dritten Grades mit s0 (x) = a0 +b0 x+c0 x2 +d0 x3 ,
welches die Punkte (x0 , y0 ) und (x1 , y1 ) verbindet, und s1 mit s1 (x) = a1 + b1 x + c1 x2 +
d1 x3 , dass die Punkte (x1 , y1 ) und (x2 , y2 ) miteinander verbindet. Wir fordern, dass
folgende Bedingungen erfüllt werden:
s0 (x0 ) = y0 (11.1)
s0 (x1 ) = y1 (11.2)
s′0 (x1 ) = s′1 (x1 ) (11.3)
s′′
0 (x1 ) = s′′
1 (x1 ), (11.4)
/**
* @brief ex1 Interpolation mit gsl - Routinen
*/
inline void ex1 () {
// Anzahl der Daten
constexpr size_t N = 10;
// Felder mit diskreten Daten
double xi , yi , x[N], y[N];
// berechne Werte
11.2 Interpolation mit der GNU Scientific Library 415
// Speicherfreigabe
gsl_spline_free ( spline );
gsl_interp_accel_free (acc);
}
Die Struktur gsl_spline ist das Objekt, mit dem die zu interpolierenden Daten verwaltet
werden. Diesem Objekt wird der erzeugte Datensatz zugeordnet und über diesem Ob-
jekt wird mittels einer Funktion jeder beliebige Zwischenwert berechnet. Die Struktur
gsl_interp_accel hilft die Berechnung der Werte zu beschleunigen. Das Programm muss
dafür sorgen, dass die angeforderten Ressourcen wieder freigegeben werden.
Bevor wir darüber nachdenken, wie mithilfe dieses Programms eine Klasse entwor-
fen werden kann, betrachten wir ein weiteres Beispiel (Listing 11.2). In diesem Beispiel
werden periodische Daten interpoliert.
/**
* @brief ex2 Interpolation periodischer Daten
*/
inline void ex2 () {
// Anzahl der Daten
size_t N = 4;
// berechne Werte
for ( size_t i = 0; i <= 100; i++) {
double xi = (1 - i / 100.0) * x[0] + (i / 100.0) * x[N - 1];
double yi = gsl_spline_eval (spline , xi , acc);
std :: cout << xi << "," << yi << std :: endl;
}
// Speicherfreigabe
gsl_spline_free ( spline );
gsl_interp_accel_free (acc);
}
Wir stellen fest, dass die Berechnung der Daten nach demselben Muster erfolgt. Was sich
geändert hat, ist das Verfahren, mit dem interpoliert wird, welches über den Parameter
gsl_interp_type festgelegt wird. Die gsl-Funktionen sind so konstruiert, dass es jeder-
zeit möglich ist, das Interpolationsverfahren zu ändern. Wir bemerken an dieser Stelle
zusätzlich, dass im Fall von periodischen Daten der erste und letzte Stützwert identisch
sein müssen.
/**
* @brief The Spline class Interpolation von diskreten Daten
*/
class Interpolation
{
private :
gsl_spline * _workspace = nullptr ; //gsl - interne Datenstruktur
gsl_interp_accel * _accel = nullptr ; // effiziente Suche im Datensatz
public :
Die Klasse kann ohne gültigen gsl_spline-Zeiger nicht korrekt arbeiten, deswegen soll
es nicht erlaubt sein, Instanzen mittels des Standardkonstruktors anzulegen, da hier
notwendige Informationen zum Anlegen eines gsl_spline fehlen.
/**
* @brief InterpolationX Einsatz des
* Standardkonstruktors nicht erlaubt
*/
Interpolation () = delete ;
Durch Angabe des Interpolationsalgorithmus und der Daten wird eine funktionsfähige
Instanz der Klasse erzeugt. Der Typ des Daten-Containers wird an dieser Stelle offen
11.3 Die Schnittstelle zur GNU Scientific Library 417
gelassen. Infrage kommen alle Container-Typen, die Daten intern in ein eindimensionales
C-Feld speichern.
/**
* @brief Interpolation
* @param type Interpolationsalgorithmus
* @param x Daten z.B. C-Feld oder std :: vector ( monoton wachsend )
* @param y Daten z.B. C-Feld oder std :: vector
*/
template <class X, class Y>
inline Interpolation ( const gsl_interp_type *type , const X &x, const
Y &y) {
size_t _dim = x.size ();
_workspace = gsl_spline_alloc (type , _dim);
_accel = gsl_interp_accel_alloc ();
gsl_spline_init ( _workspace , &x[0], &y[0], _dim);
}
Es folgt ein weiterer Konstruktor für den Fall, dass die Daten in gsl::Vector-Containern
gespeichert sind.
/**
* @brief Interpolation
* @param type Interpolationsalgorithmus
* @param x gsl :: Vector
* @param y gsl :: Vector
*/
inline Interpolation ( const gsl_interp_type *type , const gsl :: Vector
&x, const gsl :: Vector &y) {
size_t _dim = x.size ();
_workspace = gsl_spline_alloc (type , _dim);
_accel = gsl_interp_accel_alloc ();
gsl_spline_init ( _workspace , x. begin (), y.begin (), _dim);
}
Die Klasse besitzt keinen Kopierkonstruktor, dadurch wird ihre Implementierung einfa-
cher.
/**
* @brief InterpolationX Einsatz des Kopierkonstruktors
* nicht erlaubt
* @param obj
*/
Interpolation ( const Interpolation &obj) = delete ;
Der Destruktor sorgt dafür, dass die Klasse, den von der gsl reservierten Speicher wieder
freigibt.
418 11 Interpolation
/**
* @brief ~ Interpolation Destruktor gsl - Strukturen werden
* freigegeben
*/
inline ~ Interpolation () {
if ( _accel != nullptr ) {
gsl_interp_accel_free ( _accel );
}
if ( _workspace != nullptr ) {
gsl_spline_free ( _workspace );
}
}
Wenn das Kopieren von Instanzen dieser Klasse verboten ist, so muss dies auch für die
Anwendung des Zuweisungsoperators gelten.
/**
* @brief operator = Zuweisungsoperator nicht erlaubt
* @param obj
* @return
*/
Interpolation & operator =( const Interpolation &obj) = delete ;
Die Berechnung eines y-Wertes für ein gegebenes x erfolgt über diese Funktion. Sollten die
x-Werte nicht monoton wachsend sein, wird seitens der gsl eine Fehlermeldung erzeugt.
/**
* @brief eval Funktionswert ...
* @param val ... an der Stelle val
* @return Funktionswert
*/
inline double eval( double val) {
double result ; //y-Wert wird hier gespeichert
// Rückgabewert enthält Fehlermeldung
const int errorcode = gsl_spline_eval_e ( _workspace ,
val , // x-Wert
_accel ,
& result );
// wenn der Wert val sich außerhalb der vorgegebenen Werte
// befindet meldet die gsl einen Fehler
Check :: error_if (nmx_msg , errorcode == GSL_EDOM );
return result ;
}
Es können Näherungswerte für die erste und zweite Ableitung berechnet werden (Listing
11.11, Listing 11.12).
11.3 Die Schnittstelle zur GNU Scientific Library 419
/**
* @brief deriv erste Ableitung der Funktion ...
* @param val ... an der Stelle val
* @return Rückgabewert erste Ableitung
*/
inline double deriv ( double val) {
double result ; //y-Wert
const int errorcode = gsl_spline_eval_deriv_e (_workspace , //
val ,
_accel ,
& result );
Check :: error_if (nmx_msg , errorcode == GSL_EDOM );
return result ;
}
/**
* @brief deriv2 zweite Ableitung der Funktion ...
* @param val ... an der Stelle val
* @return Rückgabewert zweite Ableitung
*/
inline double deriv2 ( double val) {
double result ; //y-Wert
const int errorcode = gsl_spline_eval_deriv2_e (_workspace , //
val ,
_accel ,
& result );
Check :: error_if (nmx_msg , errorcode == GSL_EDOM );
return result ;
}
Diese Funktion berechnet das Integral über den diskreten Datensatz, wobei die x-Werte
die Stützstellen und die y-Werte die Funktionswerte sind.
/**
* @brief integral numerische Integration
* @param xmin untere Grenze
* @param xmax obere Grenze
* @return Ergebnis der Integration
*/
inline double integral ( double xmin , double xmax) {
double result ;
const int errorcode = gsl_spline_eval_integ_e (_workspace , //
xmin ,
xmax ,
_accel ,
& result );
Check :: error_if (nmx_msg , errorcode == GSL_EDOM );
return result ;
}
Die Anzahl der gespeicherten Datenpaare kann über folgende Elementfunktion gelesen
werden.
/**
* @brief size
* @return Anzahl der Datenpaare im Datensatz
*/
inline size_t size () const { return _workspace ->size; }
11.4 Beispiele
Für die Ausgabe der Daten in Dateien werden wir in diesem Abschnitt folgende Hilfs-
funktion einsetzen.
/**
* @brief get_output_stream
* @param name Name der Datei ohne Suffix
* @param fmt Formatierungsobjekt
*/
inline auto get_output_stream ( const char *name , Format fmt =
Output :: csv) {
auto ofs = Output :: get_stream ("x033", fmt , name);
ofs << std :: fixed << std :: setprecision (2);
return ofs;
}
Für vorgegebene Werte von f (x) = sin(x) mit 0 < x < 2π wird eine interpolieren-
de Funktion berechnet. Mittels einer Schleife wird ein Datensatz erzeugt und in zwei
std::vector-Containern gespeichert. Zwei Instanzen der Klasse berechnen mittels linea-
rer und Spline-Interpolation y-Werte für beliebige x im Intervall 0 < x < 2π. Die Ausgabe
erfolgt im Tabellen- und Grafikformat (Tab. 11.1, Abb. 11.2).
/**
* @brief interp1 lineare und spline Interpolation
* eines diskreten Datensatzes
*/
inline void interp1 () {
using Data = Data <4 >;
// Datenobjekt
Data data;
11.4 Beispiele 421
// Datenreihe
std :: vector <double > xvalues , yvalues ;
// lineare Interpolation
gsl :: Interpolation linear ( gsl_interp_linear , xvalues , yvalues );
// spline Interpolation
gsl :: Interpolation spline ( gsl_interp_cspline , xvalues , yvalues );
// Ausgabe
std :: ofstream ofsplot = get_output_stream (__func__ , Output :: plot);
data.save(ofsplot , Output :: plot);
std :: ofstream ofstable = get_output_stream (__func__ , Output :: latex);
ofstable << std :: setprecision (4);
1 1
0.5 0.5
fs (x)x
fl (x)
0 0
−0.5 −0.5
−1 −1
1 2 3 4 5 6 1 2 3 4 5 6
x x
(a) (b)
Abb. 11.2: (a) Lineare Interpolation und (b) Spline-Interpolation der Daten. Die Punkte
stellen die mit std::sin berechneten Werte dar.
422 11 Interpolation
Tab. 11.1: Lineare Interpolation fl (x) und Spline-Interpolation fs (x). In der letzten
Spalte stehen die mit std::sin berechneten Werte.
x fl (x) fs (x) sin(x)
0.9425 0.7133 0.8067 0.8090
2.8274 0.2939 0.3104 0.3090
4.7124 -0.8602 -0.9949 -1.0000
5.6549 -0.4755 -0.5823 -0.5878
Der Ort eines sich geradlinig bewegenden Massenpunktes wird durch die Funktion ⃗x(t) =
(−0.75t2 + 3t + 1)⃗ex (alle Angaben im SI) beschrieben.
1. Wie lauten die Formeln für die Geschwindigkeit v(t) und die Beschleunigung a(t)?
2. Erstellen Sie mit den hergeleiteten Formeln Daten für x(t) für Zeiten 0 ≤ t ≤ 4 mit
∆t = 0.1. Berechnen Sie auf Basis dieses Datensatzes Werte für x, v und a mittels
Interpolation für die Zeiten t = 0.3, 1.7, 2.2, 3.8. Vergleichen Sie die Ergebnisse mit
den exakten Werten.
Formeln für Geschwindigkeit und Beschleunigung Es lässt sich schnell feststellen, dass
es sich um eine geradlinige Bewegung mit konstanter Beschleunigung handelt. Die For-
meln für Geschwindigkeit und Beschleunigung lassen sich durch einmaliges bzw. zweima-
liges Differenzieren der Ortsfunktion gewinnen.
Quelltext und Daten Wir werden die Funktionalität der in diesem Abschnitt imple-
mentierten Klasse nutzen, nicht nur um Werte für den Ort, sondern auch für die erste
und zweite Ableitung und damit die Geschwindigkeit und Beschleunigung zu berechnen
(Tab. 11.2).
/**
* @brief interp2 Beschreibung einer geradlinigen Bewegung
* durch Interpolation
*/
inline void interp2 () {
using Data = Data <7 >;
Data data; // Datenobjekt
// Ort
auto x = []( double t) { return -0.75 * pow(t, 2) + 3 * t + 1; };
// Geschwindigkeit
auto xdot = []( double t) { return -1.5 * t + 3; };
// Beschleunigung
auto xddot = []( double t) {
(void) t;
return -1.5;
};
11.5 Übungsaufgaben 423
Tab. 11.2: Ort, Geschwindigkeit und Beschleunigung exakte (Spalten 2, 3, 4) und in-
terpolierte Werte (Spalten 5, 6, 7)
t [s] x [m] v [m/s] a [m/s2 ] xapp [m] vapp [m/s] aapp [m/s2 ]
0.3200 1.8832 2.5200 -1.5000 1.8832 2.5203 -1.5215
1.7500 3.9531 0.3750 -1.5000 3.9531 0.3750 -1.5000
2.2200 3.9637 -0.3300 -1.5000 3.9637 -0.3300 -1.5000
3.1800 2.9557 -1.7700 -1.5000 2.9557 -1.7700 -1.5001
11.5 Übungsaufgaben
1. Der diskrete Datensatz aus (Tab. 11.3) wurde mit der Funktion f (x) = ecos x erzeugt.
Finden Sie die Werte y1 , y2 an den Stellen x1 = 0.34 und x2 = 0.62 mittels linearer
und Spline-Interpolation. Vergleichen Sie die berechneten Daten mit den exakten
Ergebnissen.
2. Ein Teilchen bewegt sich geradlinig mit einer Geschwindigkeit, welche in Abb. 11.3
dargestellt wird.
a) Stellen Sie für 0 ≤ t ≤ 10 s mit ∆t = 0.25 s die Geschwindigkeit und die Beschleu-
nigung als Funktion der Zeit in einer Tabelle dar.
b) Berechnen Sie die Strecke, welche das Teilchen in den ersten 10 s zurückgelegt hat.
424 11 Interpolation
3. Benutzen Sie das Modell für die Bewegung einer Rakete im homogenen Schwerefeld
(Abschnitt 10.5.3) um Tab. 10.6 zu generieren. Berechnen Sie mittels Interpolation
t −t
für Zeiten t = i+12 i Werte für die Masse m und die Höhe z der Rakete. Nutzen Sie
die Funktionen der Klasse, um die Geschwindigkeit der Rakete zu berechnen.
30
20
v[m/s]
10
−10
0 2 4 6 8 10 12
t[s]
Abb. 11.3: Geschwindigkeit-Zeit-Diagramm
Teil III
Übersicht
12.1 Geradlinige Bewegung mit konstanter äußerer Kraft und Gleitreibung . . . . . . 427
12.2 Teilchen bewegt sich unter dem Einfluss einer ortsabhängigen Kraft . . . . . . . . 434
12.3 Teilchen bewegt sich unter dem Einfluss einer zeitabhängigen Kraft . . . . . . . . . 440
12.4 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 446
⃗
N ⃗′
N
⃗
F
⃗ey
⃗
R φ ⃗′
R
⃗ex
(t0 , x0 , v0 ) = (0, 0, 0) w
⃗ (t1 , x1 , v1 ) w
⃗ (t2 , x2 , v2 = 0)
Abb. 12.1: Bewegung eines Objekts auf horizontaler Ebene. Ab den Zeitpunkt t1 wirkt
⃗ nicht mehr.
die äußere Kraft F
1. Stellen Sie die Bewegungsgleichungen für das Objekt auf. Leiten Sie eine Formel für
die Zeit, die das Objekt braucht, bis es zum Stillstand kommt sowie eine Formel für
die insgesamt zurückgelegte Strecke her.
2. Beschreiben Sie die Bewegung mithilfe eines Computerprogramms und vergleichen
Sie die Daten mit den exakt ermittelten Ergebnissen.
Lösung
Das Koordinatensystem Wir legen die positive x-Achse entlang der Bewegungsrichtung
des Objekts (Abb. 12.1). Die Anfangsbedingungen sind x(0) = 0, v(0) = 0.
m ⃗r¨ = F
⃗ +R
⃗ +N
⃗ + w,
⃗ (12.1)
Wir multiplizieren Gl. 12.1 einmal skalar mit ⃗ex dann mit ⃗ey und erhalten
mẍ = F cos φ − µN ẋ (12.3a)
|⃗x˙ |
mÿ = F sin φ + N − mg, (12.3b)
⃗ = −µN ẋ ⃗ex gesetzt haben. Das Objekt wird sich nur in die positive
wobei wir R x˙ |
|⃗
ẋ
Richtung bewegen, deswegen ist x˙ |
|⃗
= 1.
{
mẍ = F cos φ − µN (12.4a)
mÿ = F sin φ + N − mg. (12.4b)
Entlang der y-Achse findet keine Bewegung statt. Wir erhalten deswegen aus Gl. 12.4b
N = mg − F sin φ. (12.5)
Daraus folgt für die Bewegung entlang der x-Richtung
F µ
ẍ = cos φ − (mg − F sin φ)
m m
F
⇒ ẍ = (cos φ + µ sin φ) − µg (12.6)
m
Der erste Bewegungsabschnitt t ≤ t1 Wir erhalten die Geschwindigkeit, indem wir
Gl. 12.6 nach der Zeit integrieren:
∫ t[ ]
F
v(t) = (cos φ + µ sin φ) − µg dt′ (12.7)
0 m
und damit [ ]
F
v(t) = (cos φ + µ sin φ) − µg t. (12.8)
m
Die Ortskoordinate erhalten wir durch erneute Integration nach der Zeit:
∫ t[ ]
F
x(t) = (cos φ + µ sin φ) − µg t′ dt′
m
[ 0 ]
1 F
= (cos φ + µ sin φ) − µg t2 . (12.9)
2 m
12.1 Geradlinige Bewegung mit konstanter äußerer Kraft und Gleitreibung 429
Der zweite Bewegungsabschnitt t > t1 In diesem Fall ist F = 0 und aus Gl. 12.6 wird
ẍ = −µg. (12.10)
oder
v(t) = −µg (t − t1 ) + v1 , (12.11)
wobei v1 = v(t1 ) (Gl. 12.8). Wir integrieren noch einmal und erhalten die Ortskoordinate:
∫ t
( )
x(t) − x(t1 ) = −µg(t′ − t1 ) + v1 dt′
t1
oder
1
x(t) = x1 + v1 (t − t1 ) − µg (t − t1 )2 . (12.12)
2
Die Bewegungszeit und die gesamte zurückgelegte Strecke Wir setzen in Gl. 12.11
v = 0 ein und erhalten die Zeit t2 vom Anfang der Bewegung bis das Objekt zum
Stillstand kommt:
v1
v1 − µg(t2 − t1 ) = 0 ⇒ t2 = + t1 . (12.13)
µg
Setzen wir dieses Ergebnis in Gl. 12.12, erhalten wir für die Ortskoordinate und damit
für die gesamte zurückgelegte Strecke
v12
x2 = x1 + . (12.14)
2µg
Das Computerprogramm
Kurzbeschreibung Zur Beschreibung der Bewegung des Objekts führen wir eine Mo-
dellklasse und ein Rechenmodell ein. Es folgen die Eigenschaften der beiden Modelle.
Modellklasse
– Eingabe und Speicherung der Bewegungsparameter µ, m, F, φ, t1
– Implementierung der theoretisch hergeleiteten Formeln
Rechenmodell (abgeleitet von der Modellklasse)
– Numerische Lösung der Bewegungsgleichungen
– Ergänzung der numerisch berechneten Daten (optional) mit exakten Werten
– Speicherung der Daten in Dateien
430 12 Eindimensionale Bewegungen
Quelltext Die Beschleunigung des Objekts verändert sich nach dem Wegfall der äuße-
ren Kraft. Innerhalb der Zeitintervalle [0, t1 ] und (t1 , t2 ] bewegt sich das Objekt aber mit
konstanten Beschleunigungen (Gl. 12.6 und Gl. 12.10). Wir werden diese im Konstruktor
berechnen und intern speichern. Dies vereinfacht die Implementierung der Elementfunk-
tionen der Modellklasse.
/**
* @brief The X200 class Geradlinige Bewegung eines Objekts mit
* konstanter äußerer Kraft und Gleitreibung ( Modellklasse )
*/
class X200 : public XModel
{
private :
// konstante Beschleunigungen
double _accel1 = 0, _accel2 = 0;
// Anfangsort und Geschwindigkeit für den zweiten Teil der Bewegung
double _v1final = 0, _x1final = 0;
public :
// Eingabeparameter
const double mass , mu;
const double force , phi;
const double t1;
/**
* @brief X200 Konstruktor
* @param inmu Gleitreibungskoeffizient
* @param inforce Betrag der Kraft
* @param inphi Winkel
*/
inline X200( double inmass , double inmu , double inforce , double
inphi , double t1)
: XModel ( __func__ )
, mass( inmass )
, mu{ inmu }
, force { inforce }
, phi{ inphi }
, t1{ t1 } {
Check :: all(nmx_msg ,
{ inmass > 0, //
inmu > 0,
inforce > 0,
inphi > 0,
t1 > 0 });
_accel1 = ( force ) / (mass) * (cos(phi) + (mu) *sin(phi)) - (mu)
*Earth ::g;
_accel2 = -(mu) * Earth ::g;
_v1final = v(t1);
_x1final = x(t1);
}
Es folgen die Elementfunktionen zur Berechnung des Betrags der äußeren Kraft und der
Beschleunigung des Objekts (Gl. 12.2).
/**
* @brief get_force die zeitabhängige Kraft
* @param t Zeit
* @return Momentanwert der Kraft
*/
inline double get_force ( double t) const { //
return t <= (t1) ? force : 0;
}
/**
* @brief acceleration die zeitabhängige Kraft Beschleunigung
* @param t Zeit
* @return Momentanwert der Beschleunigung
*/
inline double acceleration ( double t) const { //
return t <= t1 ? _accel1 : _accel2 ;
}
Der Ort als Funktion der Zeit wird implementiert (Gl. 12.9 und Gl. 12.12).
/**
* @brief v Ortsvektor als Funktion der ...
* @param t .. Zeit
* @return Ortsvektor zum Zeitpunkt t
*/
inline double x( double t) const {
if (t <= t1) {
return 0.5 * _accel1 * pow(t, 2);
}
const double deltat = (t - t1);
return _x1final + _v1final * deltat + 0.5 * _accel2 *
pow(deltat , 2);
}
Schließlich berechnet eine Elementfunktion mit Gl. 12.8 und Gl. 12.11 die Geschwindig-
keit.
/**
* @brief v Geschwindigkeit als Funktion der ...
* @param t .. Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
inline double v( double t) const {
if (t <= t1) {
return _accel1 * t;
432 12 Eindimensionale Bewegungen
}
return _v1final + _accel2 * (t - t1);
}
/**
* @brief The C200 class Rechenmodell ( numerisch berechnete Werte )
*/
template <bool EXACTDATA = true >
class C200 : public X200
{
public :
using Data = Data <5 >;
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
private :
// Speicher für Bewegungsdaten
Data _data;
Die Schnittstelle zur gsl-Klasse implementiert Gl. 12.6 als ein System von linearen Dif-
ferenzialgleichungen erster Ordnung.
χ̇ = ψ
ψ̇ = F (cos φ + µ sin φ) − µg
m
Als abgeleitete Klasse verfügt das Rechenmodell über alle Elementfunktionen der Mo-
dellklasse. Zur Berechnung der Beschleunigung wird die Elementfunktion Listing 12.4
aufgerufen.
/**
* @brief odefn gsl - Schnittstelle
* @param t Zeit
* @param yin Eingabe
* @param yout Ausgabe
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double yin [], double yout []) {
yout [0] = yin [1];
yout [1] = acceleration (t);
return GSL_SUCCESS ;
}
public :
using X200 :: X200; // erbe Konstruktoren der Basisklasse
/**
* @brief exec numerische Lösung der Bewegungsgleichungen
* @param dt Zeitschritte ( optional )
* @param tmax Zeitpunkt bis zu dem maximal gerechnet wird
*/
inline void exec( double dt = 0.1 , double tmax = 500) {
Ode ode{ *this , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { 0, 0 });
double t = 0;
// rechne , bis die Geschwindigkeit 0 wird. Abbruch spätestens
// bei tmax
while (ode [1] >= 0 && t < tmax) {
if constexpr ( EXACTDATA ) {
_data += { t,
ode [0] , // numerisch berechnete ...
ode [1] , // ... Werte
x(t), // exakte Werte Ort und
v(t) }; // Geschwindigkeit
} else {
_data += { t, ode [0] , ode [1], 0, 0 };
}
t += dt;
ode. solve (t);
}
}
/**
* @brief save_data speichere Daten in Dateien
*/
inline void save_data () {
//csv -Datei
save(_data , Output :: plot);
// LaTeX - Tabelle mit Auswahl an Daten inklusive erster
// und letzter Reihe
auto view = _data . select_total_rows (5);
save(view , Output :: latex );
}
}; // C200
Daten Eine Instanz des Rechenmodells wird initialisiert. Die Bewegungsdaten werden
numerisch berechnet und um die exakt berechneten Werte ergänzt. Die Daten werden
im LATEX-Tabellenformat und im CSV-Format gespeichert (Tab. 12.1, Abb. 12.2).
/**
* @brief run Berechnung von Beispieldaten Geradlinige Bewegung eines
434 12 Eindimensionale Bewegungen
Tab. 12.1: Bewegung eines Objekts mit Gleitreibung und äußerer Kraft. Die Werte in
den Spalten 2 und 3 sind numerisch berechnete Daten.
t [s] x [m] v [m/s] xex [m] vex [m/s]
0.000e+00 0.000e+00 0.000e+00 0.000e+00 0.000e+00
8.000e+00 2.387e+02 5.967e+01 2.387e+02 5.967e+01
1.610e+01 7.823e+02 5.963e+01 7.823e+02 5.963e+01
2.420e+01 1.185e+03 3.977e+01 1.185e+03 3.977e+01
3.230e+01 1.427e+03 1.991e+01 1.427e+03 1.991e+01
4.040e+01 1.507e+03 5.538e-02 1.507e+03 5.538e-02
80
1,500
60
1,000
v[m/s]
x[m]
40
500
20
0 0
0 10 20 30 40 0 10 20 30 40
t[s] t[s]
(a) (b)
Abb. 12.2: Bewegung eines Objekts mit Gleitreibung und äußerer Kraft. (a) Ort-Zeit-
Diagramm (b) Geschwindigkeit-Zeit-Diagramm
1. Wie lauten der Orts- und der Geschwindigkeitsvektor als Funktion der Zeit?
12.2 Teilchen bewegt sich unter dem Einfluss einer ortsabhängigen Kraft 435
2. Lösen Sie die Newton’sche Bewegungsgleichung numerisch und erstellen Sie Diagram-
me für den Ort, die Geschwindigkeit, die Beschleunigung und die kinetische Energie
als Funktion der Zeit. Vergleichen Sie die Daten mit den exakt berechneten Werten.
Lösung
Die Bewegungsgleichung Wir legen die positive x-Achse entlang der Bewegungsrich-
tung. Das zweite Newton’sche Gesetz lautet:
¨ = b⃗ b
m⃗x x ⇒ ẍ⃗ex = x⃗ex (12.15)
m
oder √
2 b
ẍ = µ x, µ= . (12.16)
m
Dass der Ansatz
x(t) = c1 sinh (µt) + c2 cosh (µt) , c1 , c2 ∈ R (12.17)
eine Lösung der Differenzialgleichung ist, lässt sich schnell durch einmaliges bzw. zwei-
maliges Differenzieren des Ortes nach der Zeit
v0
c 2 = x0 , c 1 = (12.20)
µ
Damit erhalten wir für die Lösung der Bewegungsgleichung
v0
x(t) = sinh (µt) + x0 cosh (µt) (12.21a)
µ
ẋ(t) = v0 cosh µt + x0 µ sinh µt (12.21b)
ẍ(t) = bx(t). (12.21c)
Das Computerprogramm
Kurzbeschreibung Wir führen eine Modellklasse und ein von ihr abgeleitetes Rechen-
modell ein. Es folgt eine Liste mit den wichtigsten Eigenschaften beider Modelle.
Modellklasse:
– Eingabe und Speicherung der Bewegungsparameter b, m und der Anfangsbedin-
gungen x0 und v0
– Implementierung der theoretisch hergeleiteten Formeln für x(t) und v(t)
436 12 Eindimensionale Bewegungen
Quelltext Die Modellklasse liest die Programmparameter über den Konstruktor ein.
Der Parameter µ (Gl. 12.16) wird berechnet und gespeichert.
/**
* @brief The X9000 class Teilchen bewegt sich unter dem Einfluss einer
* ortsabhängigen Kraft ( Modellklasse )
*/
class X9000 : public XModel
{
public :
// Eingabeparameter
const double mass , b;
const double mu; // sqrt(b/m)
const double x0 , v0;
public :
/**
* @brief X9000 Konstruktor
* @param b Konstante
* @param m Masse
* @param x0 Ort zur Zeit t=0
* @param v0 Geschwindigkeit zur Zeit t=0
*/
X9000( double m, double b, double x0 , double v0)
: XModel ( __func__ )
, mass{ m }
, b{ b }
, mu{ sqrt ((b) / (mass)) }
, x0{ x0 }
, v0{ v0 } {
Check :: all(nmx_msg , { b > 0, mass > 0 });
}
Es folgen die Formeln für die Beschleunigung und die Kraft als Funktion des Ortes und
für die Geschwindigkeit und den Ort als Funktion der Zeit.
/**
* @brief acceleration Beschleunigung als Funktion von x
* @param x Koordinate
* @return Beschleunigung an der Stelle x
*/
inline double a( double x) const { return pow(mu , 2) * x; }
/**
* @brief force Kraft als Funktion von x
* @param x Koordinate
* @return Kraft an der Stelle x
*/
inline double force ( double x) const { return b * x; }
/**
* @brief v Geschwindigkeit als Funktion von t
* @param t Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
inline double v( double t) const { //
return v0 * cosh(mu * t) + mu * (x0) *sinh(mu * t);
}
/**
* @brief x Ort als Funktion von t
* @param t Zeit
* @return Ort zum Zeitpunkt t
*/
inline double x( double t) const { //
return (x0) *cosh(mu * t) + (v0) / mu * sinh(mu * t);
}
}; // X9000
/**
* @brief The CModel class Teilchen bewegt sich unter dem Einfluss
* einer ortsabhängigen Kraft ( Rechenmodell )
*/
class C9000 : public X9000
{
public :
using Data = Data <7 >;
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
private :
// Datenobjekt
Data _data;
Die Differenzialgleichung Gl. 12.19 wird als System von linearen Differenzialgleichungen
erster Ordnung implementiert.
{
χ̇ = ψ (12.22a)
ψ̇ = µ2 χ, (12.22b)
wobei χ = x und ψ = ẋ ist.
/**
* @brief odefn Schnittstelle zur gsl -Klasse
* @param t Zeit
* @param fin Eingabe
* @param fout Ausgabe
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [1];
fout [1] = a(fin [0]);
return GSL_SUCCESS ;
}
public :
// benutze Konstruktoren der Basisklasse
using X9000 :: X9000 ;
Die Bewegungsgleichung wird numerisch gelöst. Der numerischen Routine wird die auf-
rufende Instanz des Rechenmodells übergeben, da diese Gl. 12.22 implementiert.
/**
* @brief solve numerische Lösung der Bewegungsgleichung
* @param tmax Zeitintervall [0, tmax]
*/
void exec( double tmax) {
Ode ode{ *this , 1e-2, 1e-6, 0 };
ode. set_init_conditions (0, { x0 , v0 });
double t = 0, dt = 0.1;
// berechne Daten für die tmax s
while (t <= tmax) {
// speichere nur numerisch berechnete Werte
_data += { t,
ode [0] ,
ode [1] ,
a(ode [0]) , //
Mechanics :: kinetic_energy (mass , ode [1]) ,
x(t),
v(t) };
t += dt;
ode. solve (t);
}
}
Die Daten werden über diese Elementfunktion im CSV- und LATEX-Tabellenformat gespei-
chert.
/**
* @brief save_data Daten werden im CSV - und
* LaTeX - Format gespeichert .
*/
inline void save_data () {
// eine Auswahl der Daten wird zur Darstellung
// als LaTeX - Tabelle gespeichert
auto view = _data . select_total_rows (5);
save(view , Output :: latex );
// alle berechneten Daten werden im CSV - Format gespeichert
save(_data , Output :: plot);
}
/**
* @brief run Teilchen bewegt sich unter dem Einfluss einer
* ortsabhängigen Kraft ( Berechnung von Beispieldaten )
*/
inline void run () {
// Rechenmodell
C9000 cobj (1, 2, 10, 10);
// berechne Lösungen von t=0 s bis t = 2 s
cobj.exec (2);
cobj. save_data ();
Tab. 12.2: Teilchen bewegt sich unter dem Einfluss einer ortsabhängigen Kraft mit
b = 2 s−2 . Die beiden letzten Spalten sind die exakten Werte
t [s] x [m] v [m/s] a [m/s2 ] K [J] xex [m] vex [m/s]
0.000e+00 1.000e+01 1.000e+01 2.000e+01 5.000e+01 1.000e+01 1.000e+01
3.000e-01 1.400e+01 1.710e+01 2.801e+01 1.461e+02 1.400e+01 1.710e+01
7.000e-01 2.351e+01 3.171e+01 4.703e+01 5.029e+02 2.351e+01 3.171e+01
1.100e+00 4.075e+01 5.676e+01 8.150e+01 1.611e+03 4.075e+01 5.676e+01
1.500e+00 7.138e+01 1.005e+02 1.428e+02 5.045e+03 7.138e+01 1.005e+02
1.900e+00 1.255e+02 1.772e+02 2.509e+02 1.569e+04 1.255e+02 1.772e+02
440 12 Eindimensionale Bewegungen
120
150
100
80
v[m/s]
x[m]
100
60
40 50
20
0 0
0 0.5 1 1.5 2 0 0.5 1 1.5 2
t[s] t[s]
(a) (b)
·104
250
1.5
200
a[m/s2 ]
150 1
K[J]
100
0.5
50
0 0
0 0.5 1 1.5 2
0 0.5 1 1.5 2
t[s]
t[s]
(c) (d)
Abb. 12.3: Teilchen bewegt sich unter dem Einfluss einer ortsabhängigen Kraft. (a) Der
Ort, (b) die Geschwindigkeit, (c) die Beschleunigung und (d) die kinetische Energie als
Funktionen der Zeit
1. Geben Sie den Ort, die Geschwindigkeit und die Beschleunigung als Funktion der Zeit
an.
2. Lösen Sie die Newton’sche Bewegungsgleichung numerisch und erstellen Sie x − t,
v − t, a − t und K − t-Diagramme, wobei K die kinetische Energie des Teilchens ist.
Vergleichen Sie die Daten mit den exakten Werten.
12.3 Teilchen bewegt sich unter dem Einfluss einer zeitabhängigen Kraft 441
Lösung
Die Bewegungsgleichung Wir legen die positive x-Achse entlang der Bewegungsrich-
tung des Teilchens. Die Newton’sche Bewegungsgleichung lautet:
¨ = F0 sin ωt⃗ex
m⃗x (12.23)
oder
F0
ẍ = f0 sin ωt, f0 = (12.24)
m
Die Geschwindigkeit als Funktion der Zeit Wir integrieren Gl. 12.24 nach der Zeit
∫ v ∫ t
dv
= f0 sin ωt ⇒ dv ′ = f0 sin ωt′ dt′
dt v0 0
[ ]t
f0 ′
⇒ v − v0 = − cos ωt
ω
0
und damit ( )
f0 f0
v= v0 + − cos ωt, (12.25)
ω ω
wobei v(0) = v0 ist.
Der Ort als Funktion der Zeit Wir integrieren Gl. 12.25 nach der Zeit
∫ x ∫ t [( ) ]
′ f0 f0
dx = v0 + − cos ωt dt′
′
x0 0 ω ω
[( ) ]t
f0 ′ f0 ′
⇒ x − x0 = v0 + t − 2 sin ωt
ω ω
0
oder ( )
f0 f0
x = x 0 + v0 + t − 2 sin ωt, (12.26)
ω ω
wobei x(0) = x0 ist.
Das Computerprogramm
Kurzbeschreibung Wie auch in allen anderen Fällen erstellen wir eine Modellklasse und
leiten von ihr ein Rechenmodell ab. Diese wird die Bewegungsgleichung numerisch lösen.
(Zur Erinnerung: Die Trennung ist nicht zwingend notwendig. Wir können alles innerhalb
einer Modellklasse implementieren. Der Nachteil ist, dass die Klasse zu umfangreich und
dadurch fehleranfälliger und schwieriger zu pflegen wird.) Es folgen die Eigenschaften der
Modellklasse und des Rechenmodells.
Modellklasse
– Eingabeparameter: m, F0 , ω und die Anfangsbedingungen x(0), v(0)
442 12 Eindimensionale Bewegungen
/**
* @brief The X9100 class Teilchen bewegt sich unter dem Einfluss
* einer zeitabhängigen Kraft ( Modellklasse )
*/
class X9100 : public XModel
{
private :
double _f0 , _f0overomega ;
public :
// Eingabeparameter
const double mass;
const double Force0 , omega ;
// Anfangsbedingungen
const double x0 , v0;
public :
/**
* @brief X9100 Konstruktor
* @param m Masse
* @param F0 Kraft
* @param omg Kreisfrequenz
* @param x0 Ort zur Zeit t=0
* @param v0 Geschwindigkeit zur Zeit t=0
*/
inline X9100 ( double m, double F0 , double omg , double x0 , double v0)
: XModel { __func__ }
, mass{ m }
, Force0 { F0 }
, omega { omg }
, x0{ x0 }
, v0{ v0 } {
Check :: all(nmx_msg , { mass > 0, Force0 > 0, omega > 0 });
_f0 = Force0 / (mass);
_f0overomega = _f0 / ( omega );
}
Es folgen der Ort und die Geschwindigkeit sowie die Beschleunigung als Funktion der
Zeit (Gl. 12.26, Gl. 12.25 und Gl. 12.24).
12.3 Teilchen bewegt sich unter dem Einfluss einer zeitabhängigen Kraft 443
/**
* @brief x Ortsfunktion ( analytische Formel )
* @param t Zeit
* @return Ort zum Zeitpunkt t
*/
inline double x( double t) const {
return x0 + (v0 + _f0overomega ) * t - _f0 / pow(omega , 2) *
sin(omega * t);
}
/**
* @brief v Geschwindigkeit als Funktion der Zeit
* ( analytische Formel )
* @param t Zeit
* @return Geschwindigkeit zur Zeit t
*/
inline double v( double t) const { //
return (v0 + _f0overomega ) - _f0overomega * cos(omega * t);
}
/**
* @brief a Beschleunigung als Funktion der Zeit
* ( analytische Formel )
* @param t Zeit
* @return Beschleunigung zur Zeit t
*/
inline double a( double t) const { return _f0 * sin(omega * t); }
}; // X9100
Das Rechenmodell löst die Bewegungsgleichungen numerisch und speichert intern die
Bewegungsdaten in einer Zahlentabelle.
/**
* @brief The CModel class Teilchen bewegt sich unter
* dem Einfluss einer zeitabhängigen Kraft , numerische Lösung
* der Bewegungsgleichungen . ( Rechenmodell )
*/
class C9100 : public X9100
{
public :
using Data = nmx :: Data <7 >;
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
private :
// Zahlentabelle für die Bewegungsdaten
Data _data;
444 12 Eindimensionale Bewegungen
public :
using X9100 :: X9100 ;
mit χ = x und ψ = v.
/**
* @brief odefn Schnittstelle zur gsl
* @param t Zeit
* @param fin Eingabe
* @param fout Ausgabe
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
fout [0] = fin [1];
fout [1] = a(t);
return GSL_SUCCESS ;
}
Es folgt die numerische Lösung der Bewegungsgleichung. Den Daten werden die exakten
Ergebnisse hinzugefügt.
/**
* @brief solve_ode numerische Lösung der Bewegungsgleichung
* @param tmax Zeitintervall [0, tmax]
*/
void exec( double tmax) {
Ode ode{ *this , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { x0 , v0 });
double t = 0, dt = 0.1;
while (t < tmax) {
_data += { t,
ode [0] ,
ode [1] ,
a(t), //
Mechanics :: kinetic_energy (mass , ode [1]) ,
x(t),
v(t) };
t += dt;
ode. solve (t);
}
}
/**
* @brief save_data speichere Daten in Dateien
*/
inline void save_data () {
save(_data , Output :: plot);
save(_data . select_rows (4) , Output :: latex );
}
/**
* @brief run Teilchen bewegt sich unter dem Einfluss
* einer zeitabhängigen Kraft ( Berechnung von Beispieldaten )
*/
inline void run () {
// Bewegungsparameter
const double mass = 2.0 , F0 = 10.0 , omega = 2;
// Anfangsbedingungen
const double x0 = 0.0 , v0 = 10.0;
// Rechenmodell
C9100 cobj{ mass , F0 , omega , x0 , v0 };
cobj.exec (4);
cobj. save_data ();
}
Tab. 12.3: Bewegungsdaten für eine geradlinige Bewegung mit einer zeitabhängigen
Beschleunigung. Die beiden letzten Spalten sind die exakten Werte.
t [s] x [m] v [m/s] a [m/s2 ] K [J] xex [m] vex [m/s]
0.000e+00 0.000e+00 1.000e+01 0.000e+00 1.000e+02 0.000e+00 1.000e+01
3.000e-01 3.044e+00 1.044e+01 2.823e+00 1.089e+02 3.044e+00 1.044e+01
7.000e-01 7.518e+00 1.208e+01 4.927e+00 1.458e+02 7.518e+00 1.208e+01
1.100e+00 1.274e+01 1.397e+01 4.042e+00 1.952e+02 1.274e+01 1.397e+01
1.500e+00 1.857e+01 1.497e+01 7.056e-01 2.243e+02 1.857e+01 1.497e+01
1.900e+00 2.451e+01 1.448e+01 -3.059e+00 2.096e+02 2.451e+01 1.448e+01
2.300e+00 2.999e+01 1.278e+01 -4.968e+00 1.633e+02 2.999e+01 1.278e+01
2.700e+00 3.472e+01 1.091e+01 -3.864e+00 1.191e+02 3.472e+01 1.091e+01
3.100e+00 3.885e+01 1.001e+01 -4.154e-01 1.002e+02 3.885e+01 1.001e+01
3.500e+00 4.293e+01 1.062e+01 3.285e+00 1.127e+02 4.293e+01 1.062e+01
3.900e+00 4.750e+01 1.237e+01 4.993e+00 1.529e+02 4.750e+01 1.237e+01
446 12 Eindimensionale Bewegungen
50
40
14
30
v[m/s]
x[m]
20 12
10
0 10
0 1 2 3 4 0 1 2 3 4
t[s] t[s]
(a) (b)
4
200
2
a[m/s2 ]
K[J]
0
150
−2
−4
100
0 1 2 3 4 0 1 2 3 4
t[s] t[s]
(c) (d)
Abb. 12.4: Bewegung eines Teilchens unter dem Einfluss einer zeitabhängigen Kraft.
(a) x − t-Diagramm, (b) v − t-Diagramm, (c) a − t-Diagramm (d) K − t-Diagramm.
12.4 Übungsaufgaben
1. Zwei Teilchen mit Massen m1 und m2 ruhen auf horizontalem Boden in einem Ab-
stand d voneinander. Zwei konstante Kräfte F⃗1 und F ⃗2 , die jeweils einen Winkel φ1
und φ2 mit der Horizontalen bilden, beginnen auf die zwei Teilchen zu wirken, sodass
diese sich aufeinander zu bewegen. µ ist der Gleitreibungskoeffizient zwischen den
Teilchen und Boden.
a) Stellen Sie die Bewegungsgleichungen für die Teilchen auf. Wann und wo treffen
sich diese und welche Geschwindigkeit haben sie zu diesem Zeitpunkt?
b) Entwerfen Sie eine Modellklasse für das System der beiden Teilchen und ein Re-
chenmodell, welches Bewegungsdaten erstellt. Stellen Sie diese Daten in gemein-
samen Ort-Zeit- und Geschwindigkeit-Zeit-Diagrammen dar. Benutzen sie für die
Rechnungen Werte ihrer Wahl für die Massen, die Kräfte, die Winkel und den
Anfangsabstand d. (Die Modellklasse soll eine Ausnahme auswerfen, falls durch
die Parameterwahl die oben beschriebene Bewegung nicht stattfinden kann).
c) Berechnen Sie auf Basis der erstellten Bewegungsdaten einen Näherungswert für
den Zeitpunkt der Begegnung. Vergleichen Sie das Ergebnis mit dem exakten Wert.
12.4 Übungsaufgaben 447
d) Erstellen Sie zusätzliche Rechenmodelle, die statt mit konstanten Kräften mit
ortsabhängigen bzw. mit zeitabhängigen Kräften arbeiten. Erstellen Sie für diese
Fälle Bewegungsdaten.
13 Bewegung in einem Medium mit
Reibung
Übersicht
13.1 Bewegung mit Reibung nach Stokes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449
13.2 Bewegung mit Reibung nach Newton . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 456
13.3 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 464
y
⃗v1 ⃗2
R
⃗1
R m⃗g m⃗g
⃗ey ⃗ey ⃗v2
⃗ey
(b) (c)
(a)
Abb. 13.1: Kugel bewegt sich im Medium. (a) Ortsvektor (b) Zeitpunkt während der
Aufwärtsbewegung (c) Zeitpunkt während der Abwärtsbewegung
1. Wie lauten die Geschwindigkeit und der Ort als Funktion der Zeit. Wie viel Zeit
benötigt das Teilchen, bis es den höchsten Punkt erreicht?
2. Erstellen Sie Daten für die Bewegung des Teilchens für β = 3 kg/s, β = 6 kg/s und
β = 9 kg/s. Benutzen Sie dafür folgende Werte für Masse, Anfangsgeschwindigkeit
und Anfangshöhe: m = 1 kg, v0 = 10 m/s, y0 = 0 m. Vergleichen Sie die numerisch
berechneten Werte mit den exakten Resultaten.
Lösung
Die Bewegungsgleichung Das zweite Newton’sche Gesetz für die Bewegung der Kugel
lautet
m⃗ y¨ = −m⃗g − β⃗v (13.1)
oder
mÿ⃗ey = −mg⃗ey − βv⃗ey .
oder ( )
mg mg βt
v=− + + v0 e − m . (13.3)
β β
Nach ausreichend langer Zeit fällt die Kugel mit einer konstanten Geschwindigkeit (End-
geschwindigkeit) ( ( ) )
mg mg βt mg
lim − + + v0 e − m = − (13.4)
t→∞ β β β
die wir mit
mg
vT = − (13.5)
β
bezeichnen. Damit wird aus Gl. 13.3:
−β
v = vT + (v0 − vT ) e m t (13.6)
Wann wird die Geschwindigkeit 0? Wir setzen in Gl. 13.6 v = 0 und lösen nach t auf
βt βt
vT + (v0 − vT ) e− m = 0 ⇒ (v0 − vT ) e− m = −vT
( )
βt vT m vT
⇒ e− m = ⇒ t = − ln
vT − v0 β vT − v0
oder ( )
m vT − v0
t= ln . (13.7)
β vT
13.1 Bewegung mit Reibung nach Stokes 451
Der Ort als Funktion der Zeit Eine erneute Integration, diesmal die von Gl. 13.6 nach
der Zeit, liefert den Ort als Funktion der Zeit:
∫ y ∫ t( )
βt′
dy ′ = vT + (v0 − vT ) e− m dt′
0 0
[ m ]
βt′ t
⇒ y = v T t′ − (v0 − vT ) e− m
β 0
m − βt m
⇒ y = vT t − (v0 − vT ) e m + (v0 − vT )
β β
oder ( )
m βt
y = vT t + (v0 − vT ) 1 − e− m (13.8)
β
Das Computerprogramm
Kurzbeschreibung Es folgt eine Auflistung der Eigenschaften der Modellklasse und des
von ihr abgeleiteten Rechenmodells.
Modellklasse:
– Eingabewerte: Masse des Teilchens m, Reibungskoeffizient β und Anfangsbedin-
gungen v0 , y0
– Implementierung der theoretisch hergeleiteten Formeln
Rechenmodell (abgeleitet vom Rechenmodell)
– Numerische Lösung der Bewegungsgleichung
– Ergänzung der Daten mit den exakten Werten für Höhe und Geschwindigkeit
– Speichern der Daten im Diagramm- und Tabellen-Format
/**
* @brief The X2000 class Bewegung im konstanten Gravitationsfeld
* mit Reibung nach Stokes ( Modellklasse )
*/
class X2000 : public XModel
{
private :
double _mOverBeta ; // m/b
double _vTerminal ; // Endgeschwindigkeit
public :
// Eingabeparameter
// Masse , Koeffizient ( Reibung )
const double mass , beta;
// Anfangsbedingungen
const double y0 , v0;
public :
β
Der Konstruktor erzeugt eine Instanz mit gültigen Bewegungsparametern. Es werden m
und die Endgeschwindigkeit vT (Gl. 13.5) berechnet und gespeichert.
/**
* @brief X2000 Konstruktor
* @param m Masse
* @param b Reibungskoeffizient
* @param y0 Höhe zur Zeit t=0
* @param v0 Geschwindigkeit zur Zeit t=0
*/
X2000( double m, double b, double y0 , double v0)
: XModel ( __func__ )
, mass{ m }
, beta{ b }
, y0{ y0 }
, v0{ v0 } {
Check :: all(nmx_msg , { mass > 0, beta >= 0 });
_mOverBeta = (mass) / (beta);
_vTerminal = -_mOverBeta * gravitation :: Earth ::g;
}
/**
* @brief terminalVelocity
* @return Endgeschwindigkeit
*/
inline double terminal_velocity () const { return _vTerminal ; }
/**
* @brief v Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return Geschwindigkeit
*/
inline double v( double t) const {
return _vTerminal + (v0 - _vTerminal ) * exp(-t / _mOverBeta );
}
/**
* @brief y Ort als Funktion der Zeit
* @param t Zeit
* @return Ort zum Zeitpunkt t
*/
inline double y( double t) const {
13.1 Bewegung mit Reibung nach Stokes 453
/**
* @brief time4ymax
* @return Zeit für maximale Höhe
*/
inline double time4ymax () const { //
return _mOverBeta * log (( _vTerminal - v0) / _vTerminal );
}
/**
* @brief ymax
* @return maximale Höhe
*/
inline double ymax () const { return y( time4ymax ()); }
}; // X2000
Es folgt die numerische Lösung der Bewegungsgleichung durch das Rechenmodell. Eine
Tabelle mit acht Spalten wird zur Speicherung der Ergebnisse angelegt.
/**
* @brief The C2000 class Bewegung im konstanten Gravitationsfeld mit
* Reibung nach Stokes ( numerisch berechnete Werte ) ( Rechenmodell )
*/
class C2000 : public X2000
{
public :
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
using Data = Data <8 >;
private :
// Zahlentabelle zur Speicherung der Ergebnisse
Data _data;
public :
// benutze Konstruktoren der Basisklasse
using X2000 :: X2000 ;
Für die numerische Lösung der Bewegungsgleichung Gl. 13.2 muss diese als System von
linearen Differenzialgleichungen erster Ordnung geschrieben werden,
χ̇ = ψ
ψ̇ = −g − β ψ,
m
wobei χ = x und ψ = v.
/**
* @brief odefn Schnittstelle zur gsl
* @param t Zeit
* @param fin Ort Geschwindigkeit
* @param fout Geschwindigkeit Beschleunigung
* @return Status
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [1];
fout [1] = -Earth ::g - (beta / (mass)) * fin [1];
return GSL_SUCCESS ;
}
/**
* @brief solve_ode numerische Lösung der DGL
* numerische Lösung der Bewegungsgleichung
*/
inline void exec () {
Ode myOde { *this , 1e-2, 1e-9, 0 };
myOde. set_init_conditions (0, { y0 , v0 });
double t = 0, dt = 0.01;
do {
_data += { t, myOde [0] , myOde [1] };
t += dt;
myOde . solve (t);
} while ( myOde [0] >= 0); // stop wenn Kugel den Boden erreicht
/**
* @brief save_data speichere Daten im Diagramm -
* und Tabellenformat .
*/
void save_data () {
save(_data , Output :: plot , beta);
std :: ofstream ofs = get_output_stream ( Output :: latex , beta);
/**
* @brief run Bewegung im konstanten Gravitationsfeld mit Reibung
* nach Stokes ( Beispieldaten )
*/
inline void run () {
const double y0 = 0.0 , v0 = 10.0; // Anfangsbedingungen
const double mass = 1.0;
Tab. 13.1: Senkrechte Auf- und Abwärtsbewegung eines Teilchens in einem Medium mit
Reibung und Reibungskoeffizienten β = 3 kg/s. Die beiden letzten Spalten sind die exakt
berechneten Werte.
t [s] y [m] v [m/s] K [J] U [J] E [J] yt [m] vt [m/s]
0.0000 0.0000 10.0000 50.0000 0.0000 50.0000 0.0000 10.0000
0.4400 1.8031 0.2757 0.0380 17.6826 17.7206 1.8031 0.2757
0.8800 1.2307 -2.3220 2.6958 12.0692 14.7650 1.2307 -2.3220
1.3200 0.0237 -3.0159 4.5479 0.2326 4.7805 0.0237 -3.0159
456 13 Bewegung in einem Medium mit Reibung
β=3 10 β=3
β=6 β=6
1.5 β=9 β=9
v[m/s]
1
y[m]
0.5
0
0 0.2 0.4 0.6 0.8 1 1.2 1.4 0 0.2 0.4 0.6 0.8 1 1.2 1.4
t[s] t[s]
(a) (b)
Abb. 13.2: Senkrechte Auf- und Abwärtsbewegung eines Teilchens in einem Medium
mit Reibung. (a) Höhe und (b) Geschwindigkeit als Funktion der Zeit für verschiedene
Reibungskoeffizienten
1. Stellen Sie die Bewegungsgleichungen des Teilchens auf und lösen Sie die Gleichungen.
2. Lösen Sie die Gleichungen numerisch und stellen Sie die Ergebnisse in Diagrammen
und Tabellen dar. Vergleichen Sie die Daten mit den exakten Ergebnissen.
Lösung
Das Koordinatensystem Wie legen den Einheitsvektor ⃗ey entlang der Bewegungsrich-
tung. Die positive Richtung sei die Aufwärtsrichtung.
und damit ( )
γ
v̇ = −g 1 + v|v| . (13.10)
mg
Mit √
mg
vT = (13.11)
γ
13.2 Bewegung mit Reibung nach Newton 457
Wir stellen fest, dass das Ergebnis der Integration vom Vorzeichen der Geschwindigkeit
abhängt.
Hat das Teilchen die maximale Höhe erreicht, ist seine Geschwindigkeit 0. Wir bezeichnen
diesen Zeitpunkt tmax . Mit v = 0 in Gl. 13.13 erhalten wir:
( )
v0
vT arctan = g tmax (13.14)
vT
Mit diesem Ergebnis und Gl. 13.13 erhalten wir für die Geschwindigkeit als Funktion der
Zeit: ( )
v g
arctan =− (t − tmax )
vT vT
( )
g
⇒ v = vT tan (tmax − t) , t ≤ tmax (13.15)
vT
Wir integrieren (Gl. 13.15) nach der Zeit:
∫ y ∫ t ( )
g ( )
dy ′ = vT tan tmax − t′ dt′
0 0 vT
[ ( ) ]t
vT2
g ( ′)
⇒y= ln cos tmax − t
g vT
0
( ( ) ( ))
vT2 g g
⇒y= ln cos (tmax − t) − ln cos tmax
g vT vT
oder ( )
vT2 cos vgT (tmax − t)
y= ln ( ) . (13.16)
g cos vgT tmax
vT2 1
ymax = ln ( ) . (13.17)
g cos arctan vvT0
458 13 Bewegung in einem Medium mit Reibung
1
cos (arctan x) = √ , (13.19)
1 + x2
die sich mit Hilfe eines rechteckigen Dreiecks, in welchem die zwei senkrechten Seiten die
Längen 1 und x haben, beweisen lässt.
Die Abwärtsbewegung Ist der höchste Punkt erreicht, beginnt die Bewegung mit v < 0.
Aus Gl. 13.12 wird ∫ v ∫ t
dv ′
vT 2 ′2
= −g t′ dt′ .
0 vT − v
2
tmax
Setzen wir noch ymax aus Gl. 13.18 ein, erhalten wir:
√ ( )2
v0
2 1 + vT
v
y = T ln ( ) , t ≥ tmax (13.22)
g cosh vT (t − tmax )
g
Das Computerprogramm
Kurzbeschreibung Wir werden mit zwei Klassen, eine Modellklasse und ein von ihr
abgeleitetes Rechenmodell, die Bewegung beschreiben.
13.2 Bewegung mit Reibung nach Newton 459
Modellklasse
– Eingabe: Masse des Teilchens, Reibungskoeffizient und Anfangsbedingungen
– Implementierung der theoretisch hergeleiteten Formeln
Rechenmodell (abgeleitet von der Modellklasse)
– Numerische Lösung der Bewegungsgleichung
– Ergänzung der numerisch berechneten Daten mit exakten Werten
– Ausgabe der Daten in Dateien
/**
* @brief The X2010 class Bewegung mit Newtonscher Reibung
* ( Modellklasse )
*/
class X2010 : public XModel
{
private :
// Endgeschwindigkeit
double vTerminal = 0;
// Steigzeit und Steighöhe
double tMax = 0, yMax = 0;
double gvt = 0, vt2g = 0;
public :
// Eingabeparameter
// Masse , Reibungskoeffizient
const double mass , gamma ;
// Anfangsbedingungen
const double y0 , v0;
public :
Unter Verwendung von Gl. 13.11, Gl. 13.14 und Gl. 13.18 werden vT , tmax und ymax
berechnet und gespeichert.
/**
* @brief X2010 Konstruktor
* @param m Masse
* @param gm gamma
* @param y0in Anfangshöhe
* @param v0in Anfangsgeschwindigkeit
*/
inline X2010 ( double m, double gm , double y0in , double v0in)
: XModel ( __func__ )
, mass{ m }
, gamma { gm }
460 13 Bewegung in einem Medium mit Reibung
, y0{ y0in }
, v0{ v0in } {
// teste Eingabewerte
Check :: all(nmx_msg , { m > 0, gamma > 0, y0 >= 0, v0 > 0 });
// Endgeschwindigkeit
vTerminal = sqrt (( mass) * Earth ::g / (gamma));
Die intern gespeicherten Werte für tmax , ymax und vT können mit den folgenden drei
Elementfunktionen gelesen werden.
/**
* @brief time4ymax
* @return Steigzeit
*/
inline double time4ymax () const { return tMax; }
/**
* @brief ymax
* @return Steighöhe
*/
inline double ymax () const { return yMax; }
/**
* @brief terminalVelocity
* @return Endgeschwindigkeit
*/
inline double terminal_velocity () const { return vTerminal ; }
Es folgen die Ausdrücke für Beschleunigung, Geschwindigkeit und Höhe. In den Element-
funktionen zur Berechnung von Höhe und Geschwindigkeit sind sowohl die Aufwärts- als
auch die Abwärtsbewegung enthalten.
/**
* @brief a Beschleunigung
* @param t Zeit
13.2 Bewegung mit Reibung nach Newton 461
/**
* @brief v Geschwindigkeit
* @param t Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
inline double v( double t) const {
if (t <= tMax) {
return vTerminal * tan(gvt * (tMax - t));
}
return -vTerminal * tanh(gvt * (t - tMax));
}
/**
* @brief y Höhe
* @param t Zeit
* @return Höhe zum Zeitpunkt t
*/
inline double y( double t) const {
if (t <= tMax) {
return vt2g * log(cos(gvt * (tMax - t)) / cos(gvt * tMax));
}
return yMax - vt2g * log(cosh(gvt * (t - tMax)));
}
}; // X2010
Das Rechenmodell implementiert die Bewegungsgleichung Gl. 13.9 als System von linea-
ren Differenzialgleichungen erster Ordnung und löst diese numerisch:
χ̇ = ψ (13.23a)
( )
γ
ψ̇ = −g 1 + mg ψ|ψ| (13.23b)
/**
* @brief The CModel class Bewegung mit Newtonscher Reibung numerische
* Lösung der Bewegungsgleichung ( Rechenmodell )
*/
class C2010 : public X2010
{
462 13 Bewegung in einem Medium mit Reibung
public :
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
using Data = Data <8 >;
private :
Data _data; // Zahlentabelle zur Speicherung der Ergebnisse
/**
* @brief odefn Bewegungsgleichung als System von DGL
* @param t Zeit
* @param fin Eingabe
* @param fout Ausgabe
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [1];
fout [1] = -Earth ::g * (1 + gamma / (mass * Earth ::g) * fin [1] *
abs(fin [1]));
return GSL_SUCCESS ;
}
public :
Zu den numerisch berechneten Daten, werden die kinetische, die potenzielle und die
Gesamtenergie hinzugefügt. Hinzugefügt werden auch die exakten Werte für Ort und
Geschwindigkeit.
/**
* @brief solve_ode numerische Lösung der Bewegungsgleichung
*/
inline void exec () {
Ode myOde { *this , 1e-2, 1e-9, 0 };
myOde. set_init_conditions (0, { y0 , v0 });
// löse Bewegungsgleichung
double t = 0, dt = 0.01;
do {
_data += { t, myOde [0] , myOde [1] };
t += dt;
myOde . solve (t);
} while ( myOde [0] > 0);
/**
* @brief save_data speichert Daten in Dateien
*/
inline void save_data () {
save(_data , Output :: plot , gamma );
// ändere Formatierung der Daten für die LaTeX - Tabelle
std :: ofstream ofs = get_output_stream ( Output :: latex , gamma);
ofs << std :: fixed << std :: setprecision (4);
_data. select_total_rows (5).save(ofs , Output :: latex);
}
}; // C2010
Daten Wir erstellen Daten, indem wir als einzigen Eingabeparameter des Rechenmo-
dells die Reibungskonstante variieren. Die Werte von γ sind zur Demonstration des Kon-
zepts gedacht (Tab. 13.2, Abb. 13.3).
/**
* @brief run Berechnung von Beispieldaten
*/
inline void run () {
const double mass = 1.0;
const double y0 = 0.0;
const double v0 = 30.0;
Von den berechneten Werten stellen wir nur den Fall γ = 1.3 kg/s als Tabelle dar. In den
Diagrammen sind alle Fälle dargestellt.
464 13 Bewegung in einem Medium mit Reibung
Tab. 13.2: Bewegung einer Kugel in einem Medium mit Reibung nach Newton und
Reibungskoeffizienten γ = 1.3 kg/s
t [s] y [m] v [m/s] K [J] U [J] E [J] yex [m] vex [m/s]
0.0000 0.0000 30.0000 450.0000 0.0000 450.0000 0.0000 30.0000
0.2400 1.6825 1.9715 1.9434 16.4996 18.4430 1.6825 1.9715
0.4900 1.8146 -0.7242 0.2622 17.7952 18.0575 1.8146 -0.7242
0.7400 1.4094 -2.2574 2.5480 13.8215 16.3695 1.4094 -2.2574
0.9900 0.7820 -2.6579 3.5323 7.6687 11.2010 0.7820 -2.6579
1.2400 0.1057 -2.7315 3.7305 1.0370 4.7675 0.1057 -2.7315
1.2700 0.0238 -2.7344 3.7384 0.2330 3.9714 0.0238 -2.7344
2
γ = 1.3 30 γ = 1.3
γ = 1.6 γ = 1.6
γ=2 γ=2
1.5
20
v[m/s]
y[m]
1
10
0.5
0
0
0 0.2 0.4 0.6 0.8 1 1.2 0 0.2 0.4 0.6 0.8 1 1.2
t[s] t[s]
(a) (b)
Abb. 13.3: Bewegung einer Kugel in einem Medium mit Reibung nach Newton. (a) Höhe
und (b) Geschwindigkeit als Funktion der Zeit für unterschiedliche Reibungskoeffizienten.
13.3 Übungsaufgaben
1. Wie viel Zeit benötigt das Teilchen aus Abschnitt 13.1, um zum Ausgangspunkt zu-
rückzukehren? Lösen Sie Gl. 13.8 numerisch.
2. Ein Massenpunkt wird mit einer Anfangsgeschwindigkeit v0 unter einem Winkel ϑ0
schräg nach oben geworfen. Es soll in einem horizontalem Abstand d auf den Boden
auftreffen. Erstellen Sie für die Modellklasse aus Abschnitt 10.5.2 ein Rechenmodell,
welche für eine vorgegebene Anfangsgeschwindigkeit und Höhe H den Abwurfwinkel
berechnet. Die Software soll eine Ausnahme auswerfen, falls das Ziel nicht erreicht
werden kann.
3. Ein Massenpunkt wird mit einer Geschwindigkeit v0 unter einem Winkel ϑ0 schräg
nach oben geworfen. Auf den Massenpunkt wirkt außer der Gravitation auch eine
geschwindigkeitsabhängige Reibungskraft der Form F ⃗ = −β⃗v . Wählen Sie Werte für
v0 , ϑ0 , β und
13.3 Übungsaufgaben 465
a) erstellen Sie eine Modellklasse und ein Rechenmodell zur Berechnung der Bahn
des Massenpunktes.
b) Variieren Sie den Wert von β und zeichnen Sie die Bahnkurven in einem gemein-
samen Diagramm.
⃗ = −γv 2 ⃗v .
4. Wiederholen Sie den Vorgang für eine Reibungskraft der Form F |⃗v|
14 Schwingungen
Übersicht
14.1 Harmonische Schwingung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
14.2 Schwingung in vertikaler Richtung eines Systems aus zwei Massen . . . . . . . . . . 475
14.3 Harmonische Schwingungen mit Dämpfung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 482
14.4 Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
x=0 x=0
⃗
x
⃗
F
(a) (b)
Abb. 14.1: Das schwingende Feder-Masse System
Lösung
Die Bewegungsgleichung Wir legen den Einheitsvektor ⃗ex parallel zur Federachse.
Die Feder übt eine Kraft auf die Masse aus, welche proportional und entgegengesetzt zur
Auslenkung ist.
¨ = −k⃗x ⇒ mẍ⃗ex = −kx⃗ex
m⃗x
oder nach Division mit der Masse m
k
ẍ + ω02 x = 0, ω02 = . (14.1)
m
Lösung der Differenzialgleichung Wir setzen eλt in Gl. 14.1 ein und erhalten folgendes
charakteristische Polynom:
λ2 + ω02 = 0 (14.2)
−iω0 t
Für λ = ±iω0 erhalten wir die beiden Lösungen e iω0 t
,e für die Differenzialglei-
chung. Nicht nur diese, sondern auch jede beliebige Linearkombination dieser Funktionen
ist ebenfalls Lösung und somit auch
1 ( iω0 t ) 1 ( iω0 t )
e + e−iω0 t = cos ω0 t und e − e−iω0 t = sin ω0 t.
2 2i
Dies sind zwei linear unabhängige reelle Funktionen. Damit folgt für die reelle allgemeine
Lösung für Gl. 14.1
x(t) = A1 cos ω0 t + A2 sin ω0 t, (14.3)
wobei A1 , A1 reelle Konstanten sind, die durch die Anfangsbedingungen festgelegt wer-
den. Wir können x(t) auch etwas kompakter schreiben:
√ ( )
A1 A2
x(t) = A1 + A2 √ 2
2 2 cos ω0 t + √ 2 sin ω0 t (14.4)
A1 + A22 A1 + A22
Mit √
A1 A2
sin φ0 = √ 2 , cos φ0 = √ 2 ,A = A21 + A22
A1 + A22 A1 + A22
erhalten wir
x(t) = A sin (ω0 t + φ0 ) . (14.5)
Das Computerprogramm
Kurzbeschreibung Wir werden die Schwingung der Masse mithilfe einer Modellklasse
und einem von ihr abgeleitetes Rechenmodell beschreiben.
Modellklasse
– Eingabewerte: Masse m, Federkonstante k und die Anfangsbedingungen x0 und
v0
– Implementierung der theoretisch hergeleiteten Formeln
Rechenmodell
– Numerische Lösung der Bewegungsgleichung
– Ausgabe der berechneten Daten in Dateien
Quelltext Der Konstruktor (Listing 14.2) der Modellklasse initialisiert alle Eingabepa-
rameter und berechnet die Amplitude, Phasenkonstante und Kreisfrequenz. Diese Werte
werden intern gespeichert.
/**
* @brief The X900 class Harmonische Schwingung eines
* Feder -Massen - Systems ( Modellklasse )
*/
class X900 : public XModel
{
private :
double _omega0 , _phi0 , _amplitude ;
public :
// Eingabeparameter
const double mass;
const double k;
// Anfangsbedingungen
const double x0 , v0;
/**
* @brief X900
* @param m Masse
* @param k Federkonstante
* @param x0 Anfangsposition
* @param v0 Anfangsgeschwindigkeit
*/
inline X900( double m, double k, double x0 , double v0)
: XModel ( __func__ )
, mass{ m }
, k{ k }
, x0{ x0 }
, v0{ v0 } {
Check :: all(nmx_msg , { m > 0, k > 0 });
Kreisfrequenz, Phasenkonstante und Amplitude können über die nächsten drei Element-
funktionen gelesen werden.
/**
* @brief omega0
* @return Kreisfrequenz
*/
inline double omega0 () const { return _omega0 ; }
/**
* @brief phi0
* @return Phasenkonstante
*/
inline double phi0 () const { return _phi0; }
/**
* @brief amplitude
* @return Amplitude
*/
inline double amplitude () const { return _amplitude ; }
Es folgen die Phase, die Auslenkung, die Geschwindigkeit und die Beschleunigung als
Funktionen der Zeit.
/**
* @brief phase
* @param t Zeit
* @return Phase zum Zeitpunkt t
*/
inline double phase ( double t) const { return omega0 () * t + _phi0; }
/**
* @brief x Auslenkung
* @param t Zeit
* @return Auslenkung zum Zeitpunkt t
*/
inline double x( double t) const { return _amplitude *
sin(phase(t)); }
14.1 Harmonische Schwingung 471
/**
* @brief v Geschwindigkeit
* @param t Zeit
* @return Geschwindigkeit zum Zeitpunkt t
*/
inline double v( double t) const { //
return _amplitude * omega0 () * cos( phase(t));
}
/**
* @brief a Beschleunigung
* @param t Zeit
* @return Beschleunigung zum Zeitpunkt t
*/
inline double a( double t) const { //
return -_amplitude * pow( omega0 () , 2) * sin( phase(t));
}
Die letzte Elementfunktion implementiert die Kraft als Funktion der Auslenkung.
/**
* @brief force Kraft
* @param x Auslenkung
* @return Kraftgesetz
*/
inline double force ( double x) const { return -k * x; }
}; // X900
Das Rechenmodell löst die Bewegungsgleichung numerisch und speichert die Ergebnisse
in eine Zahlentabelle. Es transformiert Gl. 14.1 in ein System von linearen Differenzial-
gleichungen erster Ordnung (Listing 14.12).
{
χ̇ = ψ (14.9a)
ψ̇ = −kχ (14.9b)
mit x = χ und ψ = ẋ
/**
* @brief The X900 class Harmonische Schwingung eines
* Feder -Massen - Systems ( Rechenmodell )
*/
472 14 Schwingungen
private :
Data _data;
public :
/**
* @brief odefn Bewegungsgleichung als System von
* Differenzialgleichungen erster Ordnung
* @param t Zeit
* @param yin Eingabe
* @param yout Ausgabe
* @return GSL_SUCCESS
*/
int odefn( double t, const double yin [], double yout []) {
(void) t; // compiler warning vermeiden
yout [0] = yin [1];
yout [1] = force (yin [0]) / mass;
return GSL_SUCCESS ;
}
Die nächste Elementfunktion fügt zu den numerischen Lösungen die potenzielle, kineti-
sche und Gesamtenergie des Systems hinzu.
/**
* @brief solve_ode numerische Lösung der Bewegungsgleichungen
*/
inline void exec () {
Ode ode{ *this , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { x0 , v0 });
const double _period = 2 * Math ::PI / omega0 ();
// löse Bewegungsgleichung
double t = 0.0 , dt = 0.01;
while (t <= 1.5 * _period ) {
_data += { t, ode [0] , ode [1] , force(ode [0]) };
t += dt;
ode. solve (t);
}
Die Daten werden im LATEX- und CSV-Format zur Ausgabe in Tabellen und Diagrammen
gespeichert.
/**
* @brief save_data schreibe Daten in Dateien (LaTeX und CSV)
*/
inline void save_data () {
// erzeuge id damit jede Datei einen eindeutigen Namen hat
std :: stringstream id;
id << std :: setprecision (3) << omega0 () << "-" << phi0 ();
// speichere Daten
save(_data , Output :: plot , id.str ());
save(_data . select_total_rows (10) , Output :: latex , id.str ());
}
/**
* @brief run Harmonische Schwingung eines Feder -Massen - Systems
* Berechnung von Beispieldaten
*/
inline void run () {
// Modellparameter
const double mass = 1.0 , k = 100.0;
// Anfangsbedingungen
const double x0 = 1.0 , v0 = 0.0;
// Rechenmodell
C900 cobj{ mass , k, x0 , v0 };
cobj.exec ();
cobj. save_data ();
}
1 10
0.5 5
v[m/s]
x[m]
0 0
−0.5 −5
−1 −10
(a) (b)
100
40
50
0
20
−50
−100 0
(c) (d)
Abb. 14.2: Harmonische Schwingung eines Masse-Feder-Systems m = 1 kg, k =
100 N/m. (a) x − t-Diagramm, (b) v − t-Diagramm (c) a − t-Diagramm (d) Energien
d⃗ ⃗
N
y=0 m2
⃗ey w
⃗2
⃗
y
⃗k
F
m1
⃗′
N w
⃗1
(b)
(a)
Abb. 14.3: Zwei Massen schwingen vertikal am oberen Ende einer Feder. (a) Die Feder
ohne die Massen, mit Massen im Gleichgewicht und zu einem Zeitpunkt während der
Schwingung (b) Kräfte, die auf jede Masse während der Bewegung wirken
1. Zeigen Sie, dass das System der beiden Massen eine harmonische Schwingung ausführt.
Berechnen Sie die Normalkraft, die auf die Masse m2 wirkt. Unter welchen Umständen
verliert m2 den Kontakt zu m1 ?
2. Beschreiben Sie die Bewegung der beiden Massen mit einem Computerprogramm.
Lösung
Die Bewegungsgleichung Wir legen die y-Achse parallel zur Federachse (Abb. 14.3a).
Im Gleichgewicht ist die Summe aller Kräfte, die auf das System wirken 0:
∑
⃗ = 0 ⇒ (m1 + m2 ) g⃗ey − kd⃗ey = 0
F
⇒ (m1 + m2 ) g = kd. (14.10)
Nach dem zweiten Newton’schen Gesetzes gilt für m1 (Abb. 14.3b):
m1 ÿ = m1 g − k (y + d) + N ′ . (14.11)
m2 ÿ = m2 g − N, (14.12)
476 14 Schwingungen
(m1 + m2 ) ÿ = − (m1 + m2 ) g − k (y + d)
⇒ (m1 + m2 ) ÿ = (m1 + m2 ) g − ky − kd. (14.13)
Dieses Ergebnis zusammen mit Gl. 14.10 gibt
oder √
k
ÿ + ω02 y = 0, . ω0 = (14.15)
m1 + m2
Das System der beiden Massen führt eine harmonische Schwingung aus. Die Lösung
dieser Gleichung ist bekannt:
Das Computerprogramm
Modellklasse
– Attribute: Massen m1 , m2 , die Federkonstante k und Anfangsbedingungen
– Elementfunktionen zur Berechnung der Beschleunigung als Funktion der Ortsko-
ordinate und der Normalkraft auf m2 als Funktion der Zeit
Rechenmodell
– Numerische Lösung der Bewegungsgleichung
– Berechnung der Normalkraft auf m2
– Ausgabe der Daten in Dateien
/**
* @brief The X910 class Schwingung in vertikaler Richtung eines
* Systems aus zwei Massen ( Modellklasse )
*/
class X910 : public XModel
{
public :
// Eingabeparameter
const double m1 , m2;
const double k;
const double y0 , v0;
const double omega0 ;
const double period ;
public :
/**
* @brief X910 Konstruktor
* @param m1 Masse 1
* @param m2 Masse 2
* @param k Federkonstante
* @param y0 maximale Auslenkung
* @param v0 Anfangsgeschwindigkeit
*/
inline X910( double m1 , double m2 , double k, double y0 , double v0)
: XModel ( __func__ )
, m1{ m1 }
, m2{ m2 }
, k{ k }
, y0{ y0 }
478 14 Schwingungen
, v0{ v0 }
, omega0 { sqrt(k / (m1 + m2)) }
, period { 2 * Math :: PI / omega0 } {}
Es folgen die Elementfunktionen zur Berechnung der Beschleunigung und der Normal-
kraft (Gl. 14.14 und Gl. 14.19).
/**
* @brief acceleration Beschleunigung
* @param x Auslenkung
* @return Beschleunigung an der Stelle x
*/
inline double acceleration ( double x) const { //
return -pow(omega0 , 2) * x;
}
/**
* @brief normal_force2 Normalkraft
* @param t Zeit
* @return Normalkraft zum Zeitpunkt t
*/
inline double normal_force2 ( double t) const {
return m2 * Earth ::g * (1 + y0 * pow(omega0 , 2) / Earth ::g *
cos( omega0 * t));
}
}; // X910
Das Rechenmodell implementiert alle Routinen zur numerischen Lösung der Bewegungs-
gleichung Gl. 14.15. Diese wird als ein System von Differenzialgleichungen erster Ordnung
formuliert. {
χ̇ = ψ
ψ̇ = −ω02 χ
mit χ = y und ψ = ẏ
/**
* @brief The C910 class Schwingung in vertikaler Richtung eines Systems
* aus zwei Massen ( Rechenmodell )
*/
class C910 : public X910
{
public :
using Ode = gsl :: Odeiv2 <2 >;
friend Ode;
using Data = Data <6 >;
private :
double _normalForce2 ;
// Zahlentabelle zum Speichern der Ergebnisse
Data _data;
/**
* @brief odefn Bewegungsgleichung der beiden Massen
* als System von DGL
* @param t Zeit
* @param yin Eingabe
* @param yout Ausgabe
* @return GSL_SUCCESS
*/
int odefn( double t, const double yin [], double yout []) {
(void) t;
yout [0] = yin [1];
yout [1] = acceleration (yin [0]);
return GSL_SUCCESS ;
}
public :
Es folgt die numerische Lösung der Bewegungsgleichungen, wobei neben der Ortskoor-
dinate und der Geschwindigkeit auch die Werte der Normalkraft aufgezeichnet werden.
Wir berechnen so lange Daten, bis m2 sich von m1 löst. Dies muss aber je nach Wahl
der Anfangsbedingungen nicht immer eintreten, was dann eine Endlosschleife zur Folge
hätte. Wir wissen, dass die Normalkraft auf m2 spätestens dann 0 wird, wenn die maxi-
male Auslenkung erreicht wird, was ein Vorzeichenwechsel der Geschwindigkeit bedeutet.
Diese Tatsache benutzen wir als zusätzliches Abbruchkriterium.
/**
* @brief exec Lösung der Bewegungsgleichung und
* Berechnung der Normalkraft auf m2
*/
void exec () {
// Objekt zur numerischen Lösung der Differenzialgleichung
Ode ode{ *this , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { y0 , v0 });
double t = 0, dt = 0.001;
// Abbruchkriterium
if ( _normalForce2 < 0) {
break ; // m2 hat sich gelöst
}
480 14 Schwingungen
t += dt;
ode. solve (t);
}
}
Die Daten werden im CSV-Format und eine Auswahl der Daten wird im LATEX-
Tabellenformat gespeichert. Wir werden das Programm für unterschiedliche Anfangs-
bedingungen ausführen. Damit die Dateien nicht überschrieben werden, erhält jeder Da-
teiname einen Namenszusatz.
/**
* @brief save_data Speicherung der Daten in Dateien im CSV -
* und LaTeX - Format
*/
inline void save_data () const { //
// erzeuge id (für jede maximale Auslenkung wird eine sepparate
// Datei erzeugt )
std :: stringstream sid;
sid << std :: setprecision (2) << abs(y0);
const auto id = sid.str ();
Daten Für m1 = 1.5 kg, m2 = 0.5 kg, k = 200 N/m und eine Anfangsgeschwindigkeit
v0 = 0 wählen wir zwei Werte für y0 und berechnen Daten für zwei Szenarien: wenn
sich m2 von m1 beim Erreichen der maximalen Auslenkung löst und wenn dies vorher
passiert (Tab. 14.2, Tab. 14.3, Abb. 14.4).
/**
* @brief run Schwingung in vertikaler Richtung eines Systems aus zwei
* Massen ( Berechnung von Beispieldaten )
*/
inline void run () {
// Bewegungsparameter
const double m1 = 1.5 , m2 = .5, k = 200.0;
// Anfangsbedingungen
14.2 Schwingung in vertikaler Richtung eines Systems aus zwei Massen 481
3g 0.15 3g
y0 = 2ω 2 y0 = 2ω 2
0 0
y0 = ωg2 y0 = ωg2
0 0
10 0.1
5 · 10−2
N [N ]
x[m]
5 0
−5 · 10−2
0 −0.1
0 0.1 0.2 0.3 0.4 0.5 0 0.1 0.2 0.3 0.4 0.5
t/T t/T
(a) (b)
Abb. 14.4: Schwingung in vertikaler Richtung eines Systems aus zwei Massen. (a) Nor-
malkraft auf Masse m2 (b) Gemeinsame Auslenkung von m1 und m2 als Funktion des
Quotienten von Zeit und Schwingungsdauer
Tab. 14.2: Schwingung in vertikaler Richtung eines Systems aus zwei Massen. Die Nor-
3g
malkraft wurde sowohl numerisch als auch exakt berechnet (y0 = 2ω 2 ).
0
Tab. 14.3: Schwingung in vertikaler Richtung eines Systems aus zwei Massen. Die Nor-
malkraft wurde sowohl numerisch als auch exakt berechnet (y0 = ωg2 ).
0
x=0 x=0
⃗
x ⃗
x
⃗v
β⃗v
⃗
F
(a) (b)
Abb. 14.5: Schwingung eines Masse-Feder-Systems mit geschwindigkeitsproportionaler
Dämpfung
1. Stellen Sie die Bewegungsgleichung für den Körper auf und geben Sie eine Lösung für
die Anfangsbedingungen x(0) = x0 und ẋ(0) = 0 an.
2. Lösen Sie die Bewegungsgleichung numerisch. Stellen Sie in einer gemeinsamen Tabelle
die analytisch und die numerisch berechneten Werten dar.
3. Zeichnen Sie die Ort-Zeit- und die Geschwindigkeit-Zeit-Diagramme.
Lösung
Die Bewegungsgleichung Die Bewegungsgleichung lässt sich aus dem zweiten New-
ton’schen Gesetz herleiten. Die Kräftebilanz lautet in diesem Fall
¨ = −k⃗x − β ⃗x˙
m⃗x (14.22)
14.3 Harmonische Schwingungen mit Dämpfung 483
oder
mẍ⃗ex = −kx⃗ex − β ẋ⃗ex
β k
⇒ ẍ + ẋ + x = 0.
m m
β
Mit 2γ = m und ω02 = k
m folgt
ẍ + 2γ ẋ + ω02 x = 0. (14.23)
Es handelt sich hier um eine lineare Differenzialgleichung zweiter Ordnung mit konstanten
Koeffizienten, die wir mit folgendem Ansatz lösen wollen:
mit √
ω̃ = γ 2 − ω02 . (14.29)
Der Ausdruck unter der Wurzel in Gl. 14.29 legt die Art der Lösung für die Bewegungs-
gleichung fest.
Starke Dämpfung (γ 2 > ω02 ) ω̃ ist reell und die Lösung von Gl. 14.23 lautet
( )
x(t) = e−γt C1 eω̃t + C2 e−ω̃t , (14.30)
was bedeutet, dass ein exponentielles Abklingen vorliegt. Wird die Masse ausgelenkt und
dann losgelassen, d.h. wenn die Anfangsbedingungen x(0) = x0 ̸= 0 und ẋ(0) = 0 gelten,
ist
x0 = C1 + C2 (14.32)
und aus [ ]
ẋ(t) = e−γt (ω̃ − γ) C1 eω̃t − (ω̃ + γ) C2 e−ω̃t (14.33)
484 14 Schwingungen
Die Anfangsbedingungen x(0) = x0 und ẋ(0) = 0 legen auch hier die Konstanten C1
und C2 fest.
x(0) = x0 ⇒ C1 = x0 (14.39)
und
ẋ(t) = −γe−γt (C1 + C2 t) + e−γt C2
⇒ ẋ(0) = −γC1 + C2 = 0 (14.40)
Wir lösen das Gleichungssystem
C = x C = x
1 0 1 0
⇒ (14.41)
C2 = γC1 C2 = γx0
Schwache Dämpfung (∆ < 0) Diesmal haben wir für Gl. 14.24 ein Paar von konjugiert
√
komplexen Lösungen λ1,2 = −γ ± i ω02 − γ 2 . Mit
√
ω = ω02 − γ 2 (14.43)
Auch hier gilt, dass für große Zeiten limt→∞ x(t) = 0. Gl. 14.44 kann auch folgenderma-
ßen geschrieben werden:
x(t) = Ce−γt sin (ωt + φ0 ) (14.45)
Für die Anfangsbedingungen x(0) = x0 , v(0) = 0 folgt eine erste Gleichung
Hieraus folgt √
γ2
sin φ0 = 1 − 2 (14.50)
ω0
γ
cos φ0 = (14.51)
ω0
und somit ist
x0 ω0 x0
C= = . (14.52)
sin φ0 ω
Das Computerprogramm
Kurzbeschreibung Spätestens an dieser Stelle zeigt der modulare Ansatz seine Vorteile.
Wir werden die gemeinsamen Attribute in einer Modellklasse unterbringen und für jeden
Typ von Schwingung ein spezielles Rechenmodell ableiten. Die numerische Behandlung
des Problems wird eine weitere abgeleitete Klasse übernehmen.
Modellklasse
486 14 Schwingungen
/**
* @brief The Damping enum Mögliche Schwingungstypen
*/
enum class Damping {
over = 0, // starke Dämpfung (0)
critical = 1, // aperiodischer Grenzfall (1)
under = 2 // schwache Dämpfung (2)
};
Die Modellklasse legt alle Bewegungsparameter über den Konstruktor fest. Diese können
dann nicht mehr verändert werden.
/**
* @brief The X1500 class Harmonische Schwingungen mit Dämpfung
* ( Modellklasse )
*/
class X1500 : public XModel
{
public :
using IValues = std :: array <double , 2>;
public :
/**
* @brief X1500 Konstruktor
* @param mass Masse
* @param k Federkonstante
* @param beta Reibungskoeffizient
* @param initc Feld der Länge 2 mit Anfangsbedingungen (x0 ,v0)
*/
inline X1500 ( double mass , double k, double beta , IValues initc)
: XModel { __func__ }
, mass{ mass }
14.3 Harmonische Schwingungen mit Dämpfung 487
, k{ k }
, omega0 { sqrt(k / mass) }
, period0 (2 * Math :: PI / omega0 )
, frequency0 (1 / period0 )
, beta{ beta }
, gamma { 0.5 * beta / mass }
, x0{ initc [0] }
, v0{ initc [1] } {}
Der Faktor e−γt wird benötigt, um die Formeln für x(t) und v(t) zu programmieren. Er
wird über eine Elementfunktion (Listing 14.28) der Klasse zur Verfügung gestellt.
/**
* @brief expfactor Hilfsfunktion
* @param t Zeit
* @return e^(- gamma t)
*/
inline double expfactor ( double t) const { return exp(-gamma * t); }
Die Beschleunigung des Systems (Gl. 14.23) wird als Funktion der Auslenkung und Ge-
schwindigkeit mit dieser Elementfunktion berechnet.
/**
* @brief acceleration
* @param x Ort
* @param v Geschwindigkeit
* @return momentane Beschleunigung
*/
inline double acceleration ( double x, double v) const {
return -2 * gamma * v - pow(omega0 , 2) * x;
}
Zur Berechnung des Schwingungstyps führen wir eine statische Hilfsfunktion ein.
/**
* @brief type Hilfsfunktion zur Ermittlung des Dämpfungstyps
* @param gm gamma
* @param omg0 omega0
* @return Dämpfungstyp
*/
inline static Damping type( double gm , double omg0) {
double gamma2 = pow(gm , 2);
double omega02 = pow(omg0 , 2);
Damping result ;
// entscheide über den Typ der gedämpften Schwingung
if ( gamma2 > omega02 ) {
result = Damping :: over;
} else if (std :: abs( gamma2 - omega02 ) < 1e -6) {
result = Damping :: critical ;
488 14 Schwingungen
} else {
result = Damping :: under ;
}
return result ;
}
}; // X1500
Der Dämpfungstyp einer Instanz der Modellklasse kann mittels dieser Funktion abgefragt
werden. Sie ruft intern die Hilfsfunktion Listing 14.30 auf.
/**
* @brief type Berechnung des Dämpfungstyps
* @return Dämpfungstyp der Instanz
*/
inline Damping type () const { return type(gamma , omega0 ); }
Liegt eine starke Dämpfung vor, so können die Auslenkung und die Geschwindigkeit über
das nächste Rechenmodell als Funktion der Zeit berechnet werden. Es werden Gl. 14.36
x ω2
und ẋ(t) = − 0ω̃ 0 e−γt sinh ω̃t implementiert.
/**
* @brief The Overdamped class starke Dämpfung ( Rechenmodell )
*/
public :
/**
* @brief Overdamped Konstruktor
* @param mass Masse
* @param k Federkonstante
* @param beta Dämpfungskonstante
* @param initc Anfangsbedingungen
*/
inline Overdamped ( double mass , double k, double beta , IValues initc)
: X1500 { mass , k, beta , initc } {
Check :: error_if_not (nmx_msg , type () == Damping :: over);
_omega = sqrt(pow(gamma , 2) - pow(omega0 , 2));
}
/**
* @brief x Ort als Funktion der Zeit
* @param t Zeit
* @return Momentanwert des Ortes
*/
inline double x( double t) const {
const double phs = omega () * t;
const double fac = (x0 / omega ());
const double trm0 = ( omega () * cosh(phs) + gamma * sinh(phs));
const double term = fac * trm0;
return expfactor (t) * term;
}
/**
* @brief v Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return Momentanwert der Geschwindigkeit
*/
inline double v( double t) const {
const double phase = omega () * t;
const double trm0 = (x0 / omega ()) * sinh(phase);
return -pow(omega0 , 2) * expfactor (t) * trm0;
}
/**
* @brief omega Kreisfrequenz
* @return die interngespeicherte Variable
*/
inline double omega () const { return _omega ; }
Es folgt das Rechenmodell für den aperiodischen Grenzfall. Wir implementieren hier Gl.
14.42 und ẋ(t) = −x0 γ 2 te−γt .
/**
* @brief The Critical class Aperiodischer Grenzfall ( Rechenmodell )
*/
class Critical : public X1500
{
public :
/**
* @brief Critical Konstruktor
* @param mass Masse
* @param k Federkonstante
490 14 Schwingungen
/**
* @brief x Ort als Funktion der Zeit
* @param t Zeit
* @return Momentanwert des Ortes
*/
inline double x( double t) const { //
return x0 * expfactor (t) * (1 + gamma * t);
}
/**
* @brief v Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return Momentanwert der Geschwindigkeit
*/
inline double v( double t) const { //
return -x0 * t * expfactor (t) * pow(gamma , 2);
}
Das nächste Rechenmodell ist für die Berechnung von Gl. 14.45, Gl. 14.49 und Gl. 14.52
und der Geschwindigkeit ẋ(t) = Ce−γt (ω cos (ωt + φ0 ) − γ sin (ωt + φ0 )) für den Fall
der schwachen Dämpfung zuständig.
/**
* @brief The Underdamped class Schwache Dämpfung ( Rechenmodell )
*/
class Underdamped : public X1500
{
private :
double _phi0 , _C , _omega ;
public :
/**
* @brief Underdamped Konstruktor
* @param mass Masse
* @param k Federkonstante
14.3 Harmonische Schwingungen mit Dämpfung 491
/**
* @brief x Ort als Funktion der Zeit
* @param t Zeit
* @return Momentanwert des Ortes
*/
inline double x( double t) const { //
return _C * expfactor (t) * sin( omega () * t + _phi0);
}
/**
* @brief v Geschwindigkeit als Funktion der Zeit
* @param t Zeit
* @return Momentanwert der Geschwindigkeit
*/
inline double v( double t) const {
const double phase = omega () * t + _phi0 ;
const double part1 = -gamma * x(t);
const double part2 = _C * expfactor (t) * omega () * cos(phase);
return part1 + part2 ;
}
/**
* @brief omega
* @return
*/
inline double omega () const { return _omega ; }
Das letzte Rechenmodell löst die Bewegungsgleichung numerisch. Sie wird in ein System
von Differenzialgleichungen erster Ordnung transformiert.
{
χ̇ = ψ
ψ̇ = −2γψ − ω02 χ, (14.53)
492 14 Schwingungen
/**
* @brief The X1500 class Harmonische Schwingungen mit Dämpfung
*/
private :
Data _data;
public :
/**
* @brief odefn Transformation in ein System von linearen Dgl
* @param t Zeit
* @param fin Eingangsvektor
* @param fout Ausgangsvektor
* @return GSL_SUCCESS
*/
inline int odefn ( double t, const double fin [], double fout []) {
(void) t;
fout [0] = fin [1];
fout [1] = acceleration (fin [0] , fin [1]);
return GSL_SUCCESS ;
}
/**
* @brief solve_ode numerische Lösung der Bewegungsgleichung
*/
void solve_ode () {
Ode ode{ *this , 1e-2, 1e-9, 0 };
ode. set_init_conditions (0, { x0 , v0 });
double t = 0, tmax = 2 * period0 , dt = tmax / 100;
// Schrittweite
while (t < tmax) {
_data += { t, ode [0] , ode [1] };
t += dt;
ode. solve (t);
}
}
/**
* @brief exec numerische Berechnung der Daten
*/
inline void exec () {
// Lösung der Bewegungsgleichung
solve_ode ();
// Hinzufügen der Gesamtenergie
for (auto &crow : _data .data ()) {
const double ekin = Mechanics :: kinetic_energy (mass ,
crow [2]);
const double epot = 0.5 * k * pow(crow [1], 2);
crow [3] = ekin + epot;
}
// exakte Werte von x und v
auto exactFn = [this ]( auto cobj) {
for (auto &crow : _data .data ()) {
const double t = crow [0];
crow [4] = cobj.x(t);
crow [5] = cobj.v(t);
}
};
// rufe das zuständige Rechenmodell auf
if (type () == Damping :: over) {
exactFn ( Overdamped (mass , k, beta , { x0 , v0 }));
} else if (type () == Damping :: under ) {
exactFn ( Underdamped (mass , k, beta , { x0 , v0 }));
} else if (type () == Damping :: critical ) {
exactFn ( Critical (mass , k, beta , { x0 , v0 }));
}
}
/**
* @brief save_data Ausgabe der Daten im Tabellen und Grafikformat
*/
void save_data () {
save(_data , Output :: plot , static_cast <int >( type ()));
save(_data . select_total_rows (6) , //
Output :: latex ,
static_cast <int >( type ()));
}
}; // C1500
Daten Wir berechnen Beispielwerte für alle drei Dämpfungstypen, indem wir β und k
verändern. Die Anfangsbedingungen und die Masse sind in allen drei Fällen gleich (Tab.
14.4, Tab. 14.4, Abb. 14.4, Abb. 14.5, Abb. 14.6).
/**
* @brief run Berechnung von Beispieldaten
*/
inline void run () {
494 14 Schwingungen
// gemeinsame Programmparameter
const double mass = 1.0;
const double x0 = 30.0 _mm;
const double v0 = 0.0;
// starke Dämpfung
double beta = 15;
double k = 9.0;
C1500 cobj0 { mass , k, beta , { x0 , v0 } };
cobj0.exec ();
cobj0. save_data ();
// schwache Dämpfung
beta = 1;
k = 36.0;
C1500 cobj1 { mass , k, beta , { x0 , v0 } };
cobj1.exec ();
cobj1. save_data ();
// aperiodischer Grenzfall
beta = 6;
k = 9.0;
C1500 cobj2 { mass , k, beta , { x0 , v0 } };
cobj2.exec ();
cobj2. save_data ();
}
·10−2
0.1
v[m/s]
0
x[m]
−0.1
−2
0 0.5 1 1.5 2
0 0.5 1 1.5 2
t[s]
t[s]
(a) (b)
·10−2
·10−2
0
3
−0.5
2
v[m/s]
x[m]
−1
1
−1.5
0
0 1 2 3 4
0 1 2 3 4
t[s]
t[s]
(a)
(b)
Abb. 14.7: Ort-Zeit- und Geschwindigkeit-Zeit-Diagramm, starke Dämpfung
496 14 Schwingungen
·10−2 ·10−2
3 0
2 −1
v[m/s]
x[m]
−2
1
−3
0
0 1 2 3 4 0 1 2 3 4
t[s] t[s]
(a) (b)
Abb. 14.8: Ort-Zeit- und Geschwindigkeit-Zeit-Diagramm, aperiodischer Grenzfall
14.4 Übungsaufgaben
1. Eine Masse m wird an einem Ende einer idealen horizontalen Feder befestigt, die am
anderen Ende an eine Wand befestigt ist. Die Masse wird aus ihrer Ruhelage aus-
gelenkt und losgelassen. Der Gleitreibungskoeffizient zwischen Masse und Boden ist
µ. Erstellen Sie ein Modell für das System, mit dem Sie Bewegungsdaten berechnen
können. Erstellen Sie eine repräsentative Wertetabelle im LATEX-Format sowie Dia-
gramme für den Ort, die Geschwindigkeit und die Beschleunigung als Funktion der
Zeit.
2. Ein Block mit einer Masse m2 wird auf einer schiefen Ebene mit dem Neigungs-
winkel φ am Ende einer Feder befestigt (Abb. 14.9). Eine Kugel mit der Masse m1
bewegt sich mit einer Geschwindigkeit ⃗v parallel zur schiefen Ebene, dringt in den
Block ein und bleibt in ihm stecken. Beschreiben Sie mit einem Computermodell die
Bewegung des Systems aus Klotz und Kugel in Abhängigkeit von der anfänglichen
Kugelgeschwindigkeit.
14.4 Übungsaufgaben 497
k
m2
v
m1
ϕ
Abb. 14.9: Kugel trifft auf Block, der an einer Feder befestigt ist
Literaturverzeichnis
K. Burg, H. Haf, and F. Wille. Höhere Mathematik für Ingenieure: Band I Analysis. Teubner-
Ingenieurmathematik. Vieweg+Teubner Verlag, 2013a. ISBN 9783322940803.
P.D.F. Wille, P.D.H. Haf, and P.D.K. Burg. Höhere Mathematik für Ingenieure: Band II Lineare
Algebra. Vieweg+Teubner Verlag, 2013. ISBN 9783322918888.
K. Burg, H. Haf, and F. Wille. Höhere Mathematik für Ingenieure: Bd. III: Gewöhnliche Diffe-
rentialgleichungen, Distributionen, Integraltransformationen. Teubner-Ingenieurmathematik.
Vieweg+Teubner Verlag, 2013b. ISBN 9783322941268.
A. Gilat. Numerical Methods for Engineers and Scientists, 3rd Edition: Third Edition. Wiley
Global Education, 2013. ISBN 9781118803042.
David Halliday, Robert Resnick, Jearl Walker, and Stephan W. Koch. Halliday Physik -. Wiley,
New York, 1. auflage edition, 2008. ISBN 978-3-527-40920-4.
R.C. Hibbeler. Technische Mechanik: Dynamik / Übers. aus dem Amerikan.: Georgia Mais ;
Frank Langenau. Fachl. Betr. und Erw.:Jörg Wauer ; Wolfgang Seemann. Number Bd. 3 in
Always learning. Pearson, 2012. ISBN 9783868941272.
L. Papula. Mathematik für Ingenieure und Naturwissenschaftler 1: Ein Lehr- und Arbeitsbuch
für das Grundstudium. Mit zahlreichen Beispielen aus Naturwissenschaft und Technik. Mit
307 Übungsaufgaben mit ausführlichen Lösungen. Mathematik für Ingenieure und Naturwis-
senschaftler / Lothar Papula. Vieweg + Teubner, 2009. ISBN 9783834805454.
Willkommen zu den
Springer Alerts
Jetzt
anmelden!
• Unser Neuerscheinungs-Service für Sie:
aktuell *** kostenlos *** passgenau *** flexibel