Programmierpraktikum NPDGL I
|
![]() |
Dies ist eine Sammlung der im Praktikum gesammelten Erkenntnisse zur Programmiersprache C++ im Allgemeinen und zum GNU Compiler g++ im Speziellen.
Für viele Programmieraufgaben gibt es bereits Lösungen im Internet, die in sogenannten Bibliotheken zusammengefasst sind. Diese lassen sich meistens kostenlos herunterladen, manchmal muss man aber auch erst eine Lizenz kaufen.
Es wird zwischen zwei Arten von Bibliotheken unterschieden:
Reine Header Bibliotheken bestehen nur aus Header-Dateien, die mittels #include
befehlen in den eigenen Code eingebunden werden.
Bei vorkompilierten Bibliotheken befindet sich in den Header-Dateien keine Implementierung, sondern nur die Deklaration der Funktionen und Daten. Der eigentliche Code befindet sich in vorkompilierten Archiven, die auf .a oder
.so enden. Über diese Archive muss der Compiler mittels der Option
-lbibliotheksname
informiert werden, damit er ganz am Ende (in der Linke-Phase), die vorkompilierten Codeteile in das Programm einbauen kann. Ein Beispiel für eine solche Bibliothek ist das MyGrid. Die Header-Dateien befinden hier sich im Verzeichnis include/grid.hh
und die Archivdatei lib/libmygrid.a
wird aus der Datei lib/libmygrid.cc
erstellt. Vergleiche auch mit Makefile Referenz.
Wird eine Variablen in C++ bei ihrer Definition mit einem Ampersand (&
) versehen, so wird sie als Referenz gekennzeichnet, d.h. sie referenziert auf eine andere Variable.
Beispiel:
int variable = 3; int &refAufVariable = variable; variable = 4; std::cout << refAufVariable << std::endl;
gibt die Zahl \( 4 \) aus. Ohne das Ampersand Zeichen, wäre eine Kopie der Variablen angelegt worden, und deswegen die Zahl \( 3 \) ausgegeben worden.
Es gibt zwei Hauptverwendungen für Referenzen:
Der erste Punkt sollte relativ klar sein, im zweiten Fall geht es darum einen Funktionsaufruf der Art
op.apply(arg, res);
realisieren zu können, bei dem das Ergebnis der Funktionsauswertung in die Variablen res
geschrieben wird. Dazu muss die Variable res
als Referenz übergeben werden, d.h. die apply
Methode muss von der Art
void apply(const DofVectorType & arg, DofVectorType & res);
sein.
C++ ist eine objekt-orientierte Programmiersprache, d.h. dass Daten und Funktionen in sogenannten Objekten zusammengefasst werden. Eine detaillierte Beschreibung des Konzepts findet sich in oben verlinktem Wikipedia Artikel. Die wichtigsten Begriffe, mit denen man sich vertraut machen muss sind hier kurz erklärt und mit ein wenig C++ Code veranschaulicht:
Kapselung
.In C++ sieht eine Klasse folgendermaßen aus:
class JollyJumperClass // JollyJumper ist der Klassenname : public Pferd // hinter einem Doppelpunkt folgen Klassen, // deren Funktionalität und Methoden geerbt // werden { public: // Zugriffs-Spezifizierer: public = überall sichtbar // private = nur in dieser Klasse sichtbar // protected = nur in dieser und abgeleiteter Klasse sichtbar // Konstruktor JollyJumperClass (Besitzer & besitzer) : besitzer_(besitzer), // hinter einem Doppelpunkt werden die Daten zahnzustand_(10), // siehe: ganz unten besonderheit_("kann sprechen") { // Dieser Code-Block wird bei der Konstruktion des Objekts ausgeführt. } // Weiter Methoden void sprechen() { } private: // Daten Besitzer besitzer_; int zahnzustand_; std::string besonderheit_; // std::stack<Erlebnis> wichtige_erlebnisse; };
JollyJumperClass jollyJumper(luckyLuke);
jollyJumper
zu einer Instanz / einem Objekt der Klasse JollyJumperClass
. Die Daten, die das Objekt jollyJumper
enthät können selbst auch wieder Objekte sein, so hat jollyJumper
beispielsweise eine Instanz Der Klasse Besitzer
. Zur besseren Lesbarkeit des Codes wollen wir Klassennamen mit einem Großbuchstaben beginnen, und Objektnamen mit einem Kleinbuchstaben.
Durch die Möglichkeit der Vererbung, kann eine Hierarchie von Klassen erstellt werden. Beim Design dieser Hierarchie kann man mit Hilfe von Interfaces Gemeinsamkeiten von Klassen definieren, die für eine bestimmte Funktionalität benötigt werden. Möchte man beispielsweise eine Methode
void reiten(Pferd & pferd);
definieren, so müssen alle Daten und Methoden, die zum "reiten" benötigt werden, bereits in der Klasse Pferd
definiert sein. Die Methode kann dann beispielsweise auch mit dem Ojbekt jollyJumper
aufgerufen werden:
reiten(jollyJumper);
Hierzu wird das Objekt jollyJumper
zuerst auf die Klasse Pferd
downgecastet, d.h. seine Funktionalität auf die eines Pferdes eingeschränkt.
Man beachte, dass Interface-Klassen auch abstrakt sein können, was bedeutet, dass einzelne Methoden keine Implementierung haben. In diesem Fall kann kein Objekt dieser Klasse instantiiert werden. Beispiele für solche abstrakte Klassen sind Function und NumericalFluxIf.
Um in C++ ein Interface zu definieren, können sogenannte abstrakte Klassen verwendet werden, welche nicht selbst instanziert werden können. Solche Klassen enthalten Funktionsdeklarationen, die keine Implementierung enthalten, und mit dem Schlüsselwort virtual
versehen sind.
Dies kann beispielsweise so aussehen:
class Gefaehrt { public: virtual double kosten(Ort start, Ort ziel) = 0; virtual double zeit(Ort start, Ort ziel) = 0; };
Eine Klasse, die von einem solchen Interface abgeleitet wird, muss alle mit = 0
markierten Methoden des Interfaces implementieren. Andernfalls ist die abgeleitete Klasse selbst wieder abstrakt. Das virtual
Schlüsselwort sorgt dafür, dass nach einer Umwandlung (cast) in die &Interfaceklasse, weiterhin die Methoden der abgeleiteten Klassen aufgerufen werden. Dies ist praktisch für Funktionen oder Klassen, die nur auf Interface-Methoden zugreifen, wie in diesem Beispiel für eine Funktion, die Reisekosten und Dauer einer Reise in Abhängigkeit des gewählten Gefährts ausgibt:
void reisekosten(Gefaehrt & gefaehrt, Ort start, Ort ziel) { std::cout << "Die Reise kostete " << gefaehrt.kosten(start, ziel) << " und dauerte " << gefaehrt.zeit(start, ziel) << std::endl; }; ... Auto auto; Flugzeug linienMaschine; Flugzeug privatJet; Fahrrad rennrad; Fahrrad klapperkiste; reisekosten(auto, Münster, Berlin); reisekosten(linienMaschine, Münster, Berlin); reisekosten(privatJet, Münster, Berlin); reisekosten(rennrad, Münster, Berlin); reisekosten(klapperkiste, Münster, Berlin);
Eine weitere Methode in C++ Interfaces zu definieren, sind Templates.
durch virtuelle Methoden
Es ist sehr unübersichtlich den ganzen Quellcode für ein Programm in eine einzige Datei zu schreiben, deswegen gibt es den sogenannten Präprozessor. Dieser wird vor dem Kompilieren ausgeführt und ersetzt alle Textstellen mit Befehlen die mit dem Hash-Zeichen (#
) beginnen.
Der wichtigste dieser Präprozessor-Befehle ist sicherlich ( #include <dateiname>
), welcher einfach die Datei mit dem Namen dateiname
an dieser Stelle einfügt. Dadurch ist es möglich den Quellcode auf mehrere Header-Dateien aufzuteilen, und diese an den benötigten Stellen zu "inkludieren".
Damit eine Header-Datei nicht versehentlich mehrfach eingefügt wird, sollte jeder Header von einem sogenannten Guard umgeben werden:
Beispiel: Der Headername ist example.hh
, dann sähe ein Guard ungefähr so aus.
#ifndef EXAMPLE_HH_ #define EXAMPLE_HH_ // here something happens #endif // end of guard EXAMPLE_HH_
Was passiert hier?
#ifndef EXAMPLE_HH_
überprüft ob das sogenannte Makro EXAMPLE_HH_
definiert wurde. Nur wenn dies nicht der Fall ist, wird der Text zwischen #ifndef
und #endif
eingefügt.EXAMPLE_HH_
definiert, welches also verhindert, das der Inhalt der Datei zweimal inkludiert werden kann.cpp
.
In diesem Abschnitt findet sich eine Liste häufiger g++
Fehlermeldungen mit Lösungsvorschlägen.
error: 'sqrt' is not a member of 'std'
error: expected ',' or '...' before '&' token
model.hh: In member function 'double Model::uexact(double, double) const': @n model.hh:51: error: 'sqrt' is not a member of 'std'
<cmath>
eingefügt werden, in welchem die Funktion sqrt
deklariert ist.finite_difference.hh:20: error: expected ',' or '...' before '&' token finite_difference.hh:20: error: ISO C++ forbids declaration of 'Model' with no type
"model.hh"
vergessen, in welchem die Klasse Model
deklariert ist.error: expected `;' before 'dofs
error: expected class-name before '(' token
finite_difference.hh:31: error: expected class-name before '(' token
~Projection()
InitialProjection
. Eine Umbenennung in hat den Fehler behoben.error: no matching function for call to `...`
sont
Qualifizierer oder Pointer und Referenzen die Probleme bereiten. adaptive_solve:
Die Methode will sicherlich das Gitter verändern, und erlaubt daher kein nicht-veränderbares Gitter als Argument.template<class Model> void evaluate(const Model & m, double & arg, double & res) { ... } ... evaluate(model, 0, res);
error: no matching function for call to `evaluate(Model&, int, double&)�
. Und zwar, weil das zweite Argument im Funktionsaufruf, die Null, natürlich eine Konstante ist. Deshalb muss auch die Funktion evaluate
entweder so: void evaluate(const Model & m, const double & arg, double & res)
void evaluate(const Model & m, double arg, double & res)
In diesem Abschnitt findet sich eine Liste häufiger Fehler, die beim Ausführen eines C++ Programms auftreten.
void apply(..., std::vector<double> res)
res
kopiert wird, und in der Funktion mit der Kopie dieser Variablen gerechnet wurde. Die Variable, die wir eigentlich mit den projizierten Dof-Werten füllen wollten blieb unverändert. void apply(..., std::vector<double> & res)
apply
bei ihrem Aufruf die Speicheradresse der Variablen, in der wir unsere Dofs speichern wollen mitgeteilt, statt diese zu kopieren. int
Variablen durcheinander dividiert, so ist das Ergebnis auch wieder ein int
, d.h. dass das Ergebnis nach dem Komma abgschnitten wird. std::cout << 1/2 << std::endl;
std::cout << 1./2 << std::endl;
double
interpretiert, so dass die Division auch eine Fließkommazahl zurückgibt.