Prolog
Dieser Crash-Kurs ist für Umsteiger gedacht, die bereits Programmiererfahrung mit objektorientierten Programmiersprachen wie Java oder C# haben und soll einen schnellen Einstieg in die Programmierung mit der Murl Engine ermöglichen. Programmieranfänger sollten sich eher an ein C++ Einsteigerbuch halten. Eine Liste mit weiterführender Literatur rund um das Thema C++ findet sich am Ende dieses Kapitels.
Inhalt
- Header
- Namensbereiche
- Grundlegende Datentypen
- Ablaufsteuerung
- Klassen und Objekte
- Referenzen & Zeiger
- Copy-Konstruktor, Kopierzuweisungsoperator
- Cast-Operatoren
- Abgeleitete Klassen
- Exceptions
- Dynamisch erzeugte Elemente
- String, Container
- Weiterführende Literatur
Header
Bei C++ unterscheidet man zwischen Header-Dateien (.h
) und CPP-Dateien (.cpp
). Die Header Dateien beinhalten die nach außen sichtbaren Deklarationen, und die CPP-Dateien die Implementierung. Um in einer CPP-Datei (file_a.cpp
) Elemente einer anderen CPP-Datei (file_b.cpp
) verwenden zu können, genügt es also die Header-Datei (file_b.h
) mit der #include
Anweisung in (file_a.cpp
) zu inkludieren.
#include "file_b.h"
Jede Header-Datei ist von den Präprozessor Direktiven #ifndef __HEADER_NAME__
#define __HEADER_NAME__
und #endif
. umschlossen. Diese Direktiven stellen einen Include-Wächter dar und verhindern ein mehrfaches Auswerten der Header-Datei.
#ifndef __MY_CLASS_H__ #define __MY_CLASS_H__ // declarations … #endif
Weitere Infos dazu gibt es z.B. hier: www.cplusplus.com/forum/articles/10627/
Namensbereiche
In großen Projekten kann es bei globalen Namen leicht zu Konflikten kommen. Mit Namespaces kann der globale Namensbereich unterteilt und diese Konflikte somit vermieden werden. Ein Namensbereich wird mit der Anweisung namespace
erstellt und ist durch geschwungene Klammern abgegrenzt:
namespace MyNamespace { UInt32 myVar; void MyFunction(); }
Ein Namensbereich kann aus mehreren, nicht zusammenhängenden Blöcken bestehen und mehrere Namensbereiche dürfen ineinander verschachtelt werden.
Außerhalb des Namespace-Blocks können die Elemente mit dem Bereichsoperator bzw. Scopeoperator (::
) angesprochen werden:
MyNamespace::myVar = 42;
Mit der Anweisung using namespace …
lassen sich alle Elemente eines Namensbereichs importieren, sodass diese direkt (ohne Bereichsoperator) verwendet werden können. Diese Anweisung sollte nur mit Bedacht verwendet werden, da sie das Konzept der Namespaces eigentlich wieder aushebelt.
using namespace MYNameSpace; myVar = 42;
Die Murl Engine Klassen sind alle im Namespace Murl definiert. Eine vollständige Liste der Namensbereiche findet sich hier: Murl Engine Namespace List.
- Zu beachten
- Achtung! In Header-Dateien sollte niemals
using namespace …
verwendet werden!
Grundlegende Datentypen
Basisdatentypen
C++ macht keine Größenangaben für die Basisdatentypen (short
, int
, float
etc.). Diese werden daher auf unterschiedlichen Systemen möglicherweise unterschiedlich repräsentiert. Daher sollten immer die im Framework definierten plattformunabhängigen Datentypen verwendet werden (siehe auch Primitive Datentypen; Großschreibung beachten z.B. Char
statt char
):
Die Initialisierung kann auf zwei verschiedene Möglichkeiten erfolgen:
UInt32 myFirstVariable = 42; UInt32 mySecondVarable(42);
Bool
Eine Bool
Variable kann nur die zwei Zustände true
und false
annehmen. Bei der Umwandlung zwischen Bool- und Integer-Variablen gilt:
Beispiel:
Bool myBool = false; // myBool is false myBool = 0; // myBool is false myBool = Bool(-42); // myBool is true SInt32 myInt = myBool; // myInt is 1
enum
Mit dem Schlüsselwort enum
kann eine Aufzählung definiert werden. Jedem Namen in einer Enumeration wird in aufsteigender Reihenfolge von 0 beginnend ein ganzzahliger Wert zugewiesen. Der Wert kann aber auch explizit festlegt werden.
Beispiel:
enum MyState { IDLE, RUNNING, SLEEPING, }; ... MyState currentState = IDLE; ... UInt32 intValue = currentState; intValue = (intValue + 1) % 3; currentState = static_cast<MyState>(intValue); ... switch(currentState) { case IDLE: Debug::Trace("IDLE"); break; case RUNNING: Debug::Trace("RUNNING"); break; case SLEEPING: Debug::Trace("SLEEPING"); break; }
- Zu beachten
- Die Definition
enum { ... };
muss mit einem Strichpunkt abgeschlossen werden (wie auch beistruct
oderclass
), während der Strichpunkt beinamespace
optional ist.
const
Mit dem Schlüsselwort const
können Elemente definiert werden, die Ihren Wert nicht mehr verändern können.
UInt32 const MY_INT(400); // Alternativ: UInt32 const MY_INT = 400;
Nicht nur Variablen können const
sein, sondern auch Übergabeparameter, Rückgabeparameter, Membermethoden, Pointer etc. Dabei gilt const
immer für das Element, das links von const
steht. Ausgenommen links steht nichts mehr, dann gehört es zu dem was rechts steht.
UInt32 myInt1; UInt32 myInt2; UInt32 const * p1 = &myInt1; // Zeiger auf konstanten UInt32 p1 = &myInt2; // geht *p1 = myInt2; // geht nicht, UInt32 konstant UInt32 * const p2 = &myInt1; // konstanter Zeiger auf UInt32 p2 = &myInt2; // geht nicht, Zeiger konstant *p2 = myInt2; // geht
Wird einer Methode ein Parameter als const
übergeben, darf diese Methode konsequenterweise auch nur const
Methoden auf diesen Parameter anwenden. Daher ist es wichtig, const
konsequent und durchgängig zu verwenden - siehe auch isocpp.org/wiki/faq/const-correctness.
Ablaufsteuerung
if Anweisung
Beispiel einer if
/ else
Verzweigung:
if (varA >= varB) { i = varA; } else { i = varB; }
Beispiel einer äquivalenten Anweisung mit Bedingungsoperator (?:
):
i = (varA >= varB) ? varA : varB;
switch Anweisung
Beispiel switch
Anweisung:
switch(currentState) { case IDLE: Debug::Trace("IDLE"); break; case RUNNING: Debug::Trace("RUNNING"); break; case SLEEPING: Debug::Trace("SLEEPING"); break; default: Debug::Trace("Unknown state"); }
for Schleife
Beispiel for
Schleife:
for (UInt32 size = myArray.GetCount(), i = 0; i < size; i++) { Debug::Trace("Element %d has value %d\n", i, myArray[i]); }
while Schleife
Beispiel while
Schleife:
while (!finished) { // do something }
Beispiel do
while
Schleife:
do { // do something } while (!finished)
break und continue
Der Befehl break
beendet die aktuelle Schleife. Mit continue
kann unmittelbar zum nächsten Schleifendurchlauf gesprungen werden.
Klassen und Objekte
Klasse definieren
Beispiel einer Punktklasse Point2d
im Namespace Murl::MyApp
:
#ifndef __POINT_2D_H__ #define __POINT_2D_H__ #include "murl_app_types.h" namespace Murl { namespace MyApp { class Point2d { public: Point2d(); // Default Konstruktor Point2d(Double x, Double y); // Konstruktor ~Point2d(); // Destruktor Double GetX() const; // Methodendeklarationen Double GetY() const; void SetXY(Double x, Double y); private: Double mX, mY; // Membervariablen }; } } #endif
#include "point_2d.h" using namespace Murl; // Default Konstruktor MyApp::Point2d::Point2d() : mX(0) //Initialisierung von mX mit 0 , mY(0) //Initialisierung von mY mit 0 {} // Konstruktor MyApp::Point2d::Point2d(Double x, Double y) : mX(x) //Initialisierung von mX mit x , mY(y) //Initialisierung von mY mit y {} // Destruktor MyApp::Point2d::~Point2d() { } Double MyApp::Point2d::GetX() const { return mX; } Double MyApp::Point2d::GetY() const { return mY; } void MyApp::Point2d::SetXY(Double x, Double y) { mX = x; mY = y; }
Der Konstruktor wird automatisch aufgerufen, wenn eine Instanz der Klasse erzeugt wird und dient der Initialisierung des neuen Objekts. Die Initialisierung der Membervariablen wird vorzugsweise mit einer Initialisierungsliste implementiert. Eine Initialisierungsliste wird bei der Konstruktordefinition durch einen Doppelpunkt eingeleitet und die zu initialisierenden Variablen und Objekte werden danach mit Beistrich getrennt aufgelistet.
Der Destruktor wird automatisch aufgerufen, wenn ein bestehendes Objekt zerstört (freigegeben) wird und kann für das Freigeben von gehaltenen Speicherressourcen verwendet werden.
Sowohl der Konstruktor als auch der Destruktor können auch weggelassen werden. In diesem Fall wird implizit ein leerer Konstruktor bzw. Destruktor erzeugt.
- Zu beachten
- Basisklassen (Klassen von denen abgeleitet wird) sollten immer einen Destruktor haben, der
virtual
deklariert ist. (siehe auch http://stackoverflow.com/questions/461203/when-to-use-virtual-destructors)
Über die Zugriffsattribute kann gesteuert werden, auf welche Elemente von außen zugegriffen werden darf:
public: // Zugriff erlaubt. private: // Zugriff verboten. protected: // Zugriff für abgeleitete Klassen erlaubt, sonst verboten.
Objekte Instanzieren
Instanzierung eines Objektes:
MyApp::Point2d point1; //Instanzierung mit parameterlosen Konstruktor MyApp::Point2d point2(42.3,0); //Instanzierung mit Parameter-Konstruktor
- Zu beachten
- Achtung! Es gibt kein new und keine Klammer beim parameterlosen Konstruktor!
MyApp::Point2d point2(); // Falsch! Eine parameterlose Methode mit dem Namen point2 wird deklariert! MyApp::Point2d point3 = new Point2d(); // Falsch! Ungültiger Syntax.
Alternative gleichwertige Schreibweisen:
MyApp::Point2d point1; // keine Klammer! MyApp::Point2d point2 = MyApp::Point2d(); MyApp::Point2d point3(42.3, 0); MyApp::Point2d point4 = MyApp::Point2d(42.3, 0);
Der Speicher für ein so instanziertes Objekt wird automatisch freigegeben, sobald das Objekt "out of scope" fällt. Man muss sich also nicht um die Speicherfreigabe kümmern (automatic storage duration).
Alternativ kann ein Objekt auch mit new
instanziert werden. Ein solches Objekt muss aber auch mit delete
wieder freigeben werden (dynamic storage duration). Siehe auch Dynamisch erzeugte Elemente.
Verwendung von Methoden eines Objekts:
Double x2 = point2.GetX(); point1.SetXY(x2, 0);
Referenzen & Zeiger
Referenzen
Referenzen in C++ sind interne Zeiger auf Elemente. Sie verweisen also auf das Element mit dem sie initialisiert wurden. Eine Referenz wird mit dem &
Operater deklariert und muss beim Anlegen initialisiert werden. Danach kann die Referenz wie eine normale Variablen verwendet werden.
UInt32 myInt = 41; UInt32 & myIntRef = myInt; // Referenz zu myInt myIntRef++; // myInt == 42 MyApp::Point2d myPoint; MyApp::Point2d & myPointRef = myPoint; myPointRef.SetXY(1, 1); // myPoint == (1/1)
Zeiger
Zeiger (engl. pointer) sind Variablen, die auf ein Element an einer bestimmten Speicheradresse zeigen. Sie haben also einen Typ und speichern eine Speicheradresse.
Der Dereferenzierungsoperator (*
) wird verwendet, um direkt auf die Variable der gespeicherten Speicheradresse zuzugreifen. Ohne Dereferenzierungsoperator wird auf die Speicheradresse zugegriffen.
Mit dem Adress-of Operator (&
) kann die Speicheradresse einer Variable ermittelt werden.
UInt32 myInt = 41; UInt32* myIntPointer; // Zeiger auf UInt32 myIntPointer = &myInt; // Adresse von myInt im Zeiger speichern *myIntPointer += 1; // myInt wird um 1 erhöht (myInt == 42) myIntPointer += 1; // Achtung! Ohne Dereferenzierung wird die Adresse um 1 (eine Elementgröße) erhöht! // Der Pointer zeigt jetzt auf einen unbekannten Speicherbereich!
Bei Zeigern, die auf ein Objekt zeigen, kann auf die Elemente des Objekts auch mit dem Pfeiloperator (->
) zugegriffen werden:
MyApp::Point2d myPoint; MyApp::Point2d* myPointPointer = &myPoint; myPointPointer->SetXY(1.0, 1.0); (*myPointPointer).SetXY(1.0, 1.0); // optionale gleichwertige Schreibweise
Wann sollten Zeiger und wann Referenzen verwendet werden? Als Faustregel gilt: Verwende Referenzen wo immer es möglich ist und Zeiger nur dann, wenn es nicht anders geht. Der Grund dafür ist, dass die Verwendung von Zeigern üblicherweise fehleranfälliger und schwieriger zu handhaben ist.
- Zu beachten
- Das
&
Zeichen wird, wie auch Zeichen für andere Operatoren, je nach Verwendung unterschiedlich interpretiert, z.B. als:- Deklarationsoperator von Referenzen
- Adress-of Operator
- Bitweiser AND Operator
Call By Reference / Call By Value
Bei Funktionsaufrufen könne die Parameter als Wert (Call By Value) oder als Referenz übergeben werden (Call By Reference).
Bei Objekten ist es in den meisten Fällen sinnvoll das Objekt als Referenz zu übergeben. Soll sichergestellt werden, dass die übergebene Variable nicht verändert wird, muss der Parameter als const
deklariert werden.
void f1(const MyApp::Point2d& p) // Call by Reference { Double x = p.GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f1(myPoint1); ...
Alternativ kann das Objekt auch als Pointer übergeben werden.
void f2(const MyApp::Point2d* pPointer) // Call by Pointer-Reference { Double x = p->GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f2(&myPoint1); ...
Wird ein Objekt als Wert übergeben, wird eine Kopie des Objekts erstellt und diese Kopie übergeben.
void f3(MyApp::Point2d p) // Call by Value { Double x = p.GetX(); Debug.Trace("Value of x: " + Util::DoubleToString(x)); } ... MyApp::Point2d myPoint1; f3(myPoint1); ...
Copy-Konstruktor, Kopierzuweisungsoperator
Jedesmal wenn eine Kopie eines Objekts erzeugt werden soll, wird für die Initialisierung der Copy-Konstruktor aufgerufen. Implizit wird immer ein Copy-Konstruktor definiert, der die Inhalte aller Membervariablen kopiert. Alternativ kann stattdessen auch ein eigener Copy-Konstruktor definiert werden:
Point2d(const Point2d& object) : mX(object.mX) , mY(object.mY) {}
Ähnliches gilt für den Kopierzuweisungsoperator. Dieser wird aufgerufen, wenn ein Objekt einem anderen zugewiesen wird. Auch hier wird implizit ein Kopierzuweisungsoperator definiert, der die Inhalte aller Membervariablen kopiert. Alternativ kann auch ein eigener Kopierzuweisungsoperator definiert werden:
Point2d& operator=(const Point2d& object) { mX = object.mX; mY = object.mY; return *this; }
In den meisten Fällen ist kein eigener Copy-Konstruktor oder Kopierzuweisungsoperator notwendig. Falls doch, sollte die Dreierregel beachtet werden. Diese besagt, dass wenn eine der drei Elemente, nämlich Destruktor, Copy-Konstruktor oder Kopierzuweisungsoperator, benötigt wird, dann werden ziemlich sicher auch die beiden anderen Element benötigt.
Cast-Operatoren
C++ bietet vier Cast-Operatoren für die Umwandlung von Datentypen.
static_cast - zum Umwandeln von Datentypen anhand einer existierenden Konvertierungsregel:
Double d = 42.1; UInt32 i = static_cast<UInt32>(d);
dynamic_cast - zum Umwandeln von abgeleiteten Klassen-Objekten:
class A{ virtual ~A(){} }; class B: public A{}; // abgeleitet von A B& b; A& a = b; B& new_b = dynamic_cast<B&>(a);
const_cast - um const Elemente in nicht const Elemente zu casten:
UInt32 myInt = 42; const UInt32& a = myInt; UInt32& b = const_cast<UInt32&>(a);
reinterpret_cast - zum Umwandeln von Datentypen ohne Konvertierungsfunktion (der Wert wird bitweise neu interpretiert):
Double value=42.0; UInt64 address_of_value = reinterpret_cast<UInt64>(&value);
Abgeleitete Klassen
- Zu beachten
- C++ erlaubt im Gegensatz zu Java oder C# auch Mehrfachvererbung, d.h. das eine Klasse von mehreren Basisklassen abgeleitet werden kann.
Vererbung
Klassen können von einer Basisklasse (oft auch als Superklasse bezeichnet) abgeleitet werden und erben dann alle Methoden und Variablen der Basisklasse. Dabei können Variablen und Methoden auch von der abgeleiteten Klasse überschrieben werden. Die überschriebenen Elemente werden aber nur überdeckt und sind dann doppelt vorhanden.
Beispiel Basisklasse:
#ifndef __BASE_CLASS_H__ #define __BASE_CLASS_H__ #include "murl_app_types.h" #include "murl_util_string.h" #include "murl_debug_trace.h" namespace Murl { namespace MyApp { class BaseClass { public: BaseClass(); // Constructor BaseClass(UInt32 x, UInt32 y); // Constructor virtual ~BaseClass(); // Destructor void MethodNonVirtual(); virtual void MethodVirtual(); UInt32 mX, mY; UInt32 mMyVar; }; } } #endif
#include "base_class.h" using namespace Murl; MyApp::BaseClass::BaseClass() : mX(0) , mY(0) , mMyVar(0) { Debug::Trace("Construct BaseClass"); } MyApp::BaseClass::BaseClass(UInt32 x, UInt32 y) : mX(x) , mY(y) , mMyVar(0) { Debug::Trace("Construct BaseClass %d %d",x,y); } MyApp::BaseClass::~BaseClass() { } void MyApp::BaseClass::MethodNonVirtual() { Debug::Trace("BaseClass MethodNonVirtual %d %d %d", mX, mY, mMyVar); } void MyApp::BaseClass::MethodVirtual() { Debug::Trace("BaseClass MethodVirtual %d %d %d", mX, mY, mMyVar); }
Abgeleitete Subklasse:
#ifndef __SUB_CLASS_H__ #define __SUB_CLASS_H__ #include "base_class.h" namespace Murl { namespace MyApp { class SubClass : public BaseClass // Ableitung von BaseClass { public: SubClass(); // Constructor SubClass(UInt32 x, UInt32 y); // Constructor virtual ~SubClass(); // Destructor void MethodNonVirtual(); virtual void MethodVirtual(); UInt32 mMyVar; }; } } #endif
#include "sub_class.h" using namespace Murl; MyApp::SubClass::SubClass() : mMyVar(42) { Debug::Trace("Construct SubClass"); } MyApp::SubClass::SubClass(UInt32 x, UInt32 y) : BaseClass(x, y) // Konstruktor der Basisklasse , mMyVar(42) { Debug::Trace("Construct SubClass %d %d",x,y); } MyApp::SubClass::~SubClass() { } void MyApp::SubClass::MethodNonVirtual() { Debug::Trace("SubClass MethodNonVirtual %d %d %d", mX, mY, mMyVar); } void MyApp::SubClass::MethodVirtual() { Debug::Trace("SubClass MethodVirtual %d %d %d", mX, mY, mMyVar); }
Auf überschriebene (überdeckte) Variablen und Methoden kann durch explizite Angabe der Basisklasse zugegriffen werden.
Debug::Trace("*** Construct objects"); MyApp::BaseClass baseClass(1,1); MyApp::SubClass subClass; MyApp::BaseClass &b1 = baseClass; MyApp::BaseClass &b2 = subClass; MyApp::SubClass &b3 = subClass; b3.mX = 2; b3.mMyVar = 5; //mMyVar of SubClass b3.BaseClass::mMyVar = 3; //mMyVar of BaseClass Debug::Trace("*** Call non virtual methods"); b1.MethodNonVirtual(); b2.MethodNonVirtual(); b3.MethodNonVirtual();
Als Ergebnis erhalten wir:
Das Ergebnis bestätigt die zuvor gemachten Aussagen:
- Die Elemente der Basisklasse sind auch in der abgeleiteten Klasse vorhanden.
- Offensichtlich existiert die überschriebene Variable
mMyVar
tatsächlich zweimal, wie auch alle anderen überschriebenen Elemente.
Polymorphie, virtuelle Methoden
Bei der Referenz b2
wird auf die Elemente der BaseClass
zugegriffen, obwohl diese ein SubClass
Objekt referenziert. Damit automatisch die "richtige" Methode verwendet wird, auch wenn der Zeiger oder die Referenz vom Typ der Basisklasse ist, muss die Methode als virtual
deklariert werden.
Debug::Trace("*** Call virtual methods"); b1.MethodVirtual(); b2.MethodVirtual(); b3.MethodVirtual();
Als Ergebnis erhalten wir:
Man spricht bei der Verwendung von virtuellen Methoden auch von "später Bindung" (engl. "late binding" oder "dynamic binding"), da erst zur Laufzeit entschieden wird, welche Methode aufgerufen wird. Siehe auch www.tutorialspoint.com/cplusplus/cpp_polymorphism.htm
Interfaces, rein virtuelle Methoden
In C++ gibt es keine klare Trennung zwischen den Begriffen Interfaces und abstrakte Klassen. Eine Interface-Methode wird in der Basisklasse als rein virtuell (engl. pure virtual) deklariert, d.h. die Methode wird als virtual
gekennzeichnet und der Methode wird 0 zugewiesen. Die abgeleitete Klasse überschreibt und implementiert dann diese pure virtual Methode.
... virtual void InterfaceMethod() = 0; ...
Exceptions
Die Murl Engine verwendet aus verschiedenen Gründen keine Exceptions. (siehe auch stackoverflow.com/..arguments-for-exceptions-over-return-codes). Anwendern ist es natürlich freigestellt, ob sie Exceptions im User Code verwenden oder nicht.
Der allgemeine Syntax dafür sieht so aus:
try { if (value == 0) throw 42; } catch (SInt32 ex) { Debug::Trace("An exception occurred. Exception Nr. " + Util::SInt32ToString(ex)); }
- Zu beachten
- In C++ können sowohl Objekte als auch primitive Basisdatentypen mit
throw
geworfen werden.
Dynamisch erzeugte Elemente
C++ bietet die Möglichkeit Elemente dynamisch mit dem new
Operator zu erzeugen. Der new
Operator gibt einen Zeiger auf das neue Objekt zurück. Für diese dynamisch erzeugten Elemente wird ein Speicher im Heap allokiert. Dieser Speicher muss mit dem delete
Operator (bzw. besser mit Murl::Util::Release
) auch wieder freigegeben werden.
MyApp::Point2d* point4 = new MyApp::Point2d(); // Ein Objekt wird im Heap angelegt! MyApp::Point2d* point5 = new MyApp::Point2d(42.3,0); // Ein Objekt wird im Heap angelegt! Double x = point4->GetX(); // ... if (point4 != 0) { delete point4; // Speicher wird wieder freigeben! point4 = 0; } Util::Release(point5); // Speicher wird wieder freigeben
String, Container
Die Murl Engine verzichtet auf die Verwendung von STL Containern und verwendet stattdessen eine adaptierte Variante der NTL Container (siehe www.ultimatepp.org/NTLvsSTL). Eine ausführliche Beschreibung über die Verwendung der Container gibt es hier: Tutorial #09: Container & Basics
Weiterführende Literatur
Dieser Crash-Kurs ist bewusst sehr knapp gehalten und kann daher ein gutes C++ Buch nicht ersetzten. Weitere Informationen rund um das Thema C++ gibt es hier:
- Bücher
- Online
- Deutsch