Effektiv C++ für Spieleprogrammierer

Aus Spieleprogrammierer-Wiki
Wechseln zu: Navigation, Suche

Bitte beachte, dass dieser Artikel noch unvollständig ist! Hilf mit, ihn fertigzustellen.
Näheres dazu findest du ggf. auf der Diskussionsseite. Wenn du der Meinung bist, dass der Artikel vollständig ist, kannst du diesen Hinweis entfernen.

Einführung

Oft fällt auf, dass sich viele C++ Spiele-Programmierer das Leben schwerer machen sie es sich eigentlich machen müssen. Das liegt oft daran, dass einfach gewisse Grundsätze nicht eingehalten werden. Einige sehr hilfreiche werden hier vorgestellt und mit Beispielen belegt.

Nutzt Konstruktor!

Oft scheuen sich viele den Konstruktor zu nutzen und schreiben Init() oder Create() Methoden. Aber das widerspricht einem ganz wichtigem Grundprinzip von C++, dem so genannten RAII (Resource Acquisition Is Initialization). Das bedeutet kurz zusammengefasst, das Resourcen anfordern gleich initialisieren bedeutet. Ein einfaches Beispiel:

Es soll eine Klasse Field ein Spielfeld verwalten. Dieses Spielfeld kann unterschiedlich groß sein und wird deswegen in einem Array auf dem Heap angelegt.

class Field
{
private:
 
    unsigned char *mField;
    int mFieldWidth;
    int mFieldHeight;
 
public:
 
    Field()
    {
        mField = 0;
        mFieldWidth = mFieldHeight;
    }
    ~Field()
    {
    }
 
    void Init(unsigned char *data,int width,int height)
    {
        mField = new unsigned char[width * height];
        memcpy(mField,data,width*height*sizeof(char));
        mFieldWidth = width;
        mFieldHeight = height;
    }
 
    void Destroy()
    {
        delete[] mField;
    }
 
    unsigned char Get(int x,int y) const
    {
        return mField[y * mFieldWidth + x];
    }
};

Soweit sieht das ja nach einer Klasse aus. Verwenden würde man Sie so:

unsigned char anydata[] = { /*hier stehen daten....*/ };
 
Field field;
field.Init(anydata,10,10);
 
//funktionen nutzen, karteninhalt an 2;1 bekommen und ausgeben
std::cout << field.Get(2,1);
 
//am ende freigeben
field.Destroy();

Diese Klasse hat aber einige Nachteile. Man muss zwei Funktionen aufrufen, damit das Objekt korrekt funktioniert. Vergisst man das Init(), so erzeugt field.Get() eine Access Violation. Außerdem existert eine extra Funktion zum Freigeben der Daten auf dem Heap. Das ganze ist extrem umschön und unelegant. Nutzt man RAII so vereinfacht sich die Klasse:

class Field
{
private:
 
    unsigned char *mField;
    int mFieldWidth;
    int mFieldHeight;
 
public:
 
    Field(unsigned char *data,int width,int height):
        mField(0),
        mFieldWidth(width),
        mFieldWidth(height)
    {
        mField = new unsigned char[mFieldWidth * mFieldWidth];
        memcpy(mField,data,width*height*sizeof(char));
    }
    ~Field()
    {
        delete[] mField;
    }
 
    unsigned char Get(int x,int y) const
    {
        return mField[y * mFieldWidth + x];
    }
};

Dazu passend der Anwendungscode:

unsigned char anydata[] = { /*hier stehen daten....*/ };
 
Field field(anydata,10,10);
 
//funktionen nutzen, karteninhalt an 2;1 bekommen und ausgeben
std::cout << field.Get(2,1);

Funktionalität aus der Init()-Methode wurde in den Konstruktor und Funktionalität aus Destroy() in den Destruktor ausgelagert. Damit werden zwei öffentliche Methoden gespart und somit bei der Verwendung des Objektes zwei Funktionsaufrufe. Nach dem Aufruf des Konstruktors kann man sicher sein, dass alles ordnungsgemäß erzeugt wurde (abgesehen von ungültigen Parametern, das wird hier nicht abgefangen, dazu später).

Und was ist mit dem Freigeben? Nun, wenn einfach alle Daten, die beim Freigeben des Objektes freigegeben werden sollen - wie das char-Array - so wird dies einfach im Destruktor der Klasse erledigt. Viele nutzen das Argument, dass man dann den genauen Aufräumzeitpunkt nicht bestimmen kann, aber das ist nur die halbe Wahrheit. Erzeugt man das Objekt auf dem Heap mit new, so kann man durch delete den Aufruf des Destruktors bestimmen, weil Dieser beim Aufruf von delete automatisch aufgerufen wird. Der Vorteil, dass man sicher sein kann, dass der Destruktor eines Objektes immer aufgerufen wird, ist viel sicherer als der Aufruf einer eigenen Methode.

Meine Werkzeuge
Namensräume
Varianten
Aktionen
Navigation
Werkzeuge