C++ Crash-Kurs

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

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:

0 => false => 0
sonst => true => 1

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 bei struct oder class), während der Strichpunkt bei namespace 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 https://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:

*** Construct objects
Construct BaseClass 1 1
Construct BaseClass
Construct SubClass
*** Call non virtual methods
BaseClass MethodNonVirtual 1 1 0
BaseClass MethodNonVirtual 2 0 3
SubClass MethodNonVirtual 2 0 5

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:

*** Call virtual methods
BaseClass MethodVirtual 1 1 0
SubClass MethodVirtual 2 0 5
SubClass MethodVirtual 2 0 5

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:


Copyright © 2011-2018 Spraylight GmbH.