JLI Spieleprogrammierung Foren-Übersicht JLI Spieleprogrammierung

 
 FAQFAQ   SuchenSuchen   MitgliederlisteMitgliederliste   BenutzergruppenBenutzergruppen 
 medals.php?sid=7b307ca1b41bdd5c4619267ab6dd19d0Medaillen   RegistrierenRegistrieren   ProfilProfil   Einloggen, um private Nachrichten zu lesenEinloggen, um private Nachrichten zu lesen   LoginLogin 

Kleine Einführung in die OOP

 
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials
Vorheriges Thema anzeigen :: Nächstes Thema anzeigen  
Autor Nachricht
David
Super JLI'ler


Alter: 38
Anmeldedatum: 13.10.2005
Beiträge: 315

Medaillen: Keine

BeitragVerfasst am: 06.12.2005, 23:34    Titel: Kleine Einführung in die OOP Antworten mit Zitat

Einführung

Am Anfang steht wohl die Frage was Objektorientierte Programmierung überhaupt ist. Dies ist nicht ganz einfach zu erklären und es gibt viele Definitionen die dies versuchen.
Grob gesagt ist Objektorientierte Programmierung (OOP) ein Verfahren zur Strukturierung von Programmen, bei dem Programmlogik zusammen mit Zustandsinformationen (Datenstrukturen) in Einheiten, den Objekten, zusammengefasst werden.
Es ist also Möglich mehrere Objekte des gleichen Typs zu haben. Jedes dieser Objekt hat dann seinen individuellen Zustand.
Dies bietet die Möglichkeit einer besseren Modularisierung der Programme, sowie einer höheren Wartbarkeit des Quellcodes.
Außerdem bietet die OOP Möglichkeiten Verhältnisse zwischen Objekten herzustellen, indem z.B. ein Objekt von einem (oder mehreren) anderen Objekten die Eigenschaften erbt, was nicht zuletzt eine hohe Zeitersparnis beim schreiben des Quellcodes mit sich bringt.

Ein einfaches Beispiel:
In einem Computerspiel sollen mehrere Gegnertypen realisiert werden. Die Anzahl der Gegner ist variabel und jeder Gegnertyp unterscheidet sich in bestimmten Merkmalen von den anderen.
Allerdings haben alle Gegner auch gleiche Merkmale. Jeder Gegner besitzt eine bestimmte Anzahl an Lebenspunkten und hat einen bestimmten Wert von Trefferpunkten, welcher beim Kampf erzielt wird.
Die simple Lösung liegt in der OOP. Es wird eine Basisklasse angelegt, welche die Eigenschaften kapselt, die alle Gegner haben.
Außerdem wird für jeden Gegnertyp eine eigene Klasse angelegt, die alle speziellen Eigenschaften des jeweiligen Gegners kapselt, aber von der Basisklasse abgeleitet ist, also deren Eigenschaften erbt.

Klassen und Objekte

Klassen sind eine erweitertes Konzept von Datenstrukturen denen es erlaubt ist, außer Daten, noch Methoden zu beinhalten.
Ein Objekt ist eine Instanz einer Klasse, welches sich die Eigenschaften der Klasse zunutze machen kann. Es ist möglich mehrere Objekte einer Klasse zu Instanziieren, wobei jedes dieser Objekte zwar die selben Eigenschaften hat, intern aber einen ganz individuellen Zustand haben kann.

Beispiel:
Zwei Instanzen der Klasse „Gegner“ werden erzeugt. Nachdem ein Gegner Schaden erlitten hat sind seine Lebenspunkte auf 50 gesunken. Die des anderen Gegners haben allerdings noch den Wert 100.

Klassen werden normalerweise mit dem Schlüsselwort class deklariert. Außerdem haben sie immer folgendes Format:

class Klassenname
{
Zugriffsspezifizierung 1:
Member 1;
...
Zugriffsspezifizierung 2:
Member 2;
...
...
} Objektname ;

Der Klassenname identifiziert die Klasse, der Objektname ist eine optionale Liste von Namen von Objekten dieser Klasse. Der Körper der Klasse wird durch geschweifte Klammern gekennzeichnet und kann verschiedene Member (Methoden, Membervariablen) beinhalten. Diesen können verschiedene Zugriffsspezifierungen zugewiesen werden.

Zugriffsrechte
public:
Auf Members kann von überall zugegriffen werden von wo das Objekt sichtbar ist
private:
Auf Member kann nur innerhalb anderer Member zugegriffen werden oder aus befreundeten Klassen und Funktionen
protected:
Members sind verfügbar für Member der selben Klasse, befreundeten Funktionen/Klassen und Abgeleiteten Klassen

Beispiel:
CPP:
#include <iostream>

using namespace std;

class Enemy
{
public:
    void SetHealth( int v );
    int  GetHealth( void );
       
private:
    int  health;
};

void Enemy::SetHealth( int v)
{
    health = v;
}

int Enemy::GetHealth( void )
{
    return health;
}

int main(int argc, char *argv[])
{
    Enemy *enemy = new Enemy();

    enemy->SetHealth( 100 );
    cout << "Der Gegner hat " << enemy->GetHealth() << " Lebenspunkte.\n";
    enemy->SetHealth( 50 );
    cout << "Der Gegner hat " << enemy->GetHealth() << " Lebenspunkte.\n";

    delete enemy;

    return 0;
}


Dieses Programm enthält eine einfache Klasse mit zwei Methoden und einer Membervariable.
Die Klasse hat den Namen Enemy:
CPP:
class Enemy


Die Methoden SetHealth und GetHealth sind als public deklariert. Auf sie kann also von überall zugegriffen werden wo ein entsprechendes Objekt sichtbar ist:
CPP:
public:
        void SetHealth( int v );
        int GetHealth( void );





Die Membervariable health ist als privat deklariert. D.h. sie ist nur für anderen Methoden der Klasse Enemy sichtbar. Mit der Ausnahme von befreundeten Klassen. Dazu später mehr.
CPP:
private:
        int     health;


Jetzt müssen die Methoden allerdings noch definiert werden. Dies kann innerhalb des Klassenkörpers geschehen oder aber unter der Klassendeklaration. Im Normalfall trennt man die Deklaration und die Definition voneinander.
CPP:
void Enemy::SetHealth( int v )
{
    health = v;
}

int Enemy::GetHealth( void )
{
    return health;
}


Die Methode SetHealth hat einen Parameter mit dem der Membervariable health ein Wert zugewiesen werden kann. Die Methode GetHealth gibt den Wert der Membervariable zurück.
Die Ausgabe des Programms sieht folgendermaßen aus:
Zitat:
Der Gegner hat 100 Lebenspunkte.
Der Gegner hat 50 Lebenspunkte.


Konstruktor und Destruktor

Während dem erstellen eines Objektes muss es eine Möglichkeit geben Membervariablen zu initialisieren und/oder ggf. Speicher zu reservieren.
Wenn man z.B. beim obigen Beispiel die Methode GetHealth vor dem Aufruf von SetHealth ausgeführt hätte, ohne das die Membervariable health vorher initialisiert wurde, würde die Rückgabe undefinierbare Werte hervorbringen.
Um solche Probleme vorzubeugen gibt es eine spezielle Methode, den Konstruktor, der automatisch nach dem erzeugen einer neuen Instanz aufgerufen wird.
Das Gegenstück des Konstruktors ist der Destruktor. Dieser wird aufgerufen, wenn ein Objekt zerstört wird. Er kann beispielsweise den Code enthalten um reservierten Speicher wieder freizugeben.
Jede Klasse kann mehrere Konstruktoren beinhalten, muss aber mindestens einen Konstruktor haben. Falls explizit kein Konstruktor deklariert wurde, legt der Compiler im Normalfall einen Standardkonstruktor an.
Natürlich kann der Programmierer diesen auch selbst deklarieren. Hierzu wieder ein kleines Beispiel:
CPP:
class Enemy
{
public:
        Enemy( void );
        ~Enemy( void );

    // ...     
private:
    int health;
};

Enemy::Enemy( void )
{
    cout << "Konstruktor von 'Enemy'\n";
}

Enemy::~Enemy( void )
{
    cout << "Destruktor von 'Enemy'\n";
}

In der Klasse wurde nun der Standardkonstruktor und ein Destruktor deklariert. Im Konstruktor passiert zwar im Moment, außer einer Ausgabe, noch nichts besonders spannendes allerdings sieht man anhand dieser genau, wann der Konstruktor und wann der Destruktor aufgerufen wird.
Zitat:
Konstruktor von 'Enemy'
Der Gegner hat 100 Lebenspunkte.
Der Gegner hat 50 Lebenspunkte.
Destruktor von 'Enemy'


Nach dem erstellen des Objektes wird der Konstruktor aufgerufen, danach kann beliebig mit dem Objekt hantiert werden. Beim „Zerstörvorgang“ des Objektes wird dann der Destruktor aufgerufen.
Wie erwähnt, ist es auch möglich jede Menge anderer Konstruktoren zu deklarieren.
Es kann z.B. der Fall auftreten, um beim obigen Beispiel zu bleiben, das dem Gegner beim erstellen 100 Lebenspunkte zugewiesen werden sollen. Dies geschieht ganz einfach indem wir, im Konstruktor, der Membervariable health den Wert 100 zuweisen. Falls nun aber optional die Möglichkeit bestehen sollte, dem Objekt einen anderen Wert zuzuweisen kann man die Konstruktor überladen.
CPP:
class Enemy
{
public:
          Enemy( void );
          Enemy( int v );
          ~Enemy( void );
    // ...
       
private:
    int  health;
};

Enemy::Enemy( void )
{
    cout << "Konstruktor 1 von 'Enemy'\n"; 
    health = 100;
}

Enemy::Enemy( int v )
{
    cout << "Konstruktor 2 von 'Enemy'\n";
    health = v;
}

// ...
int main(int argc, char *argv[])
{
    Enemy *enemy1 = new Enemy();
    Enemy *enemy2 = new Enemy( 50 );

    cout << "Enemy 1 hat " << enemy1->GetHealth() << " Lebenspunkte.\n";
    cout << "Enemy 2 hat " << enemy2->GetHealth() << " Lebenspunkte.\n";

    delete enemy1;
    delete enemy2;

    return 0;
}


Hier wurden zwei Konstruktoren deklariert. Zum einen ein Standardkonstruktor zum anderen ein Konstruktor mit einem integer Parameter.
CPP:
Enemy *enemy1 = new Enemy();
Enemy *enemy2 = new Enemy( 50 );


Die erste Zeile erstellt ein Objekt und ruft den Standardkonstruktor auf. Die zweite Zeile ruft den überladenen Konstruktor auf. Beim ersten Objekt wird die Membervariable health auf 100 gesetzt, beim zweiten auf 50.
Die Ausgabe des Programms sieht also folgendermaßen aus:
Zitat:
Konstruktor 1 von 'Enemy'
Konstruktor 2 von 'Enemy'
Enemy 1 hat 100 Lebenspunkte.
Enemy 2 hat 50 Lebenspunkte.
Destruktor von 'Enemy'
Destruktor von 'Enemy'


Zu erwähnen ist allerdings noch, das ein Konstruktor mit einem Parameter auch als Umwandlungsoperator verwendet werden kann. Ein Beispiel dafür ist folgendes:
CPP:
class Integer
{
public:
            Integer( int v );

    int     GetValue( void );

private:
    int     value;
};

Integer::Integer( int v )
{
    value = v;
}

int Integer::GetValue( void )
{
    return value;
}

void Foobar( Integer i )
{
    cout << i.GetValue() << "\n";
}

int main(int argc, char *argv[])
{
    Foobar( 10 );
    return 0;
}


Der Aufruf von Foobar( 10 ) ist gültig, da ein Konstruktor vom mit dem Typ Integer vorhanden ist und der Wert 10 ja ein int Wert ist. Allerdings kann es zu Situationen kommen wo dies nicht erwünscht ist. Daher kann man impliziten Konstruktoren verbieten.

CPP:
class Integer
{
public:
    explicit   Integer( int v );

    // ...
};

int main(int argc, char *argv[])
{
    Foobar( 10 );
    Foobar( Integer( 10 ) );
    return 0;
}


Der Aufruf von Foobar( 10 ) ist in dem Fall nicht mehr möglich. Der Aufruf von
Foobar( Integer( 10 ) ) ist in dem Fall korrekt.

Im Allgemeinen muss der Konstruktor immer den Namen der Klasse haben, der Destruktor hat ebenfalls den Namen der Klasse wird aber durch eine vorangestellte Tilde (~) gekennzeichnet.

Der Kopierkonstruktor

Wird ein Objekt kopiert, so wird der Speicher des Objektes Bit für Bit an die Zielstelle geschrieben. Dies führt allerdings nicht zwangsläufig zum gewünschten Ergebnis, beispielsweise wenn Zeiger im Objekt verwendet wurden.
Der Zeiger würde zwar mitkopiert, nicht aber die Daten auf die der Zeiger zeigt. Ein Problem das hierbei entsteht ist recht deutlich. Wird der Destruktor der Kopie aufgerufen, wird der Speicher schön freigeben (falls dies korrekt implementiert wurde). Allerdings werden die Daten des original Objektes freigegeben, da der Zeiger der Kopie auf die Daten des Originals zeigt.
Um solche Probleme zu vermeiden gibt es den Kopierkonstruktor. Dieser wird immer dann aufgerufen wenn eine Kopie eines Objektes gemacht werden soll.
Sollte kein Kopierkonstruktor implementiert worden sein, so erstellt der Compiler einen default Kopierkonstruktor, welcher den Speicher des Objektes Bit für Bit kopiert, was zu oben genannter Fehleranfälligkeit führt.

Der Kopierkonstruktor hat nur einen Parameter, nämlich eine Referenz auf die eigene Klasse.
Folgendes Beispiel demonstriert die Verwendung des Kopierkonstruktors:
CPP:
class Klasse
{
public:
     Klasse( void );
     Klasse( int v );
        Klasse( const Klasse &other );
     ~Klasse( void );

    int *GetValue( void );

private:
    int *value;
};

Klasse::Klasse( void )
{
    value = new int;
    *value = 0;
}

Klasse::Klasse( int v )
{
    value = new int;
    *value = v;
}

Klasse::Klasse( const Klasse &other )
{
    value = new int;
    *value = *( other.value );
}


Klasse::~Klasse( void )
{
    if ( value )
        delete value;

    value = NULL;
}

int *Klasse::GetValue( void )
{
    return value;
}

int main( int argc, char **argv )
{
    Klasse test1( 50 );
    Klasse test2 = test1;

    cout << "Adresse von test1::value: " << test1.GetValue() << "\n";
    cout << "Adresse von test2::value: " << test2.GetValue() << "\n";
    cout << "Wert von test1::value:    " << *test1.GetValue() << "\n";
    cout << "Wert von test2::value:    " << *test2.GetValue() << "\n";

    return 0;
}


Klammert man den Kopierkonstruktor im Beispiel aus, so wird man einen Fehler erhalten. Da das Programm versucht eine Speicherstelle zweimal freizugeben.


Die Initialisierungsliste


Die Initialisierungsliste gibt die Möglichkeit einer Klasse Werte zuzuweisen, noch bevor der Konstruktorkörper ausgeführt wird.
Dies ist dann sinnvoll, wenn eine Klasse Referenzen oder konstante Member hat. Diese müssen sogar bei der Initialisation einen Wert zugewiesen bekommen.

Zur Verdeutlichung ein kleines Beispiel:
CPP:
class Integer
{
public:
                        Integer( int v );

private:
        int     &       ref;
        const int       cnst;
};

Integer::Integer( int v )
        : ref( v ),
          cnst( v )
{}

int main(int argc, char *argv[])
{
        Integer obj( 10 );

        return 0;
}
 

Dieser Code funktioniert, würde man allerdings die Initialisierungsliste entfernen würde der Compiler Fehler ausspucken. Der Versuch die Werte im Konstruktor-Körper zuzuweisen würde auch fehlschlagen, da ref und cnst zu dem Zeitpunkt schon Werte zugewiesen sein müssen.

Durch die initialisierung, der Membervariablen/Objekte, in der Initialisierungsliste spart man sich außerdem den Aufruf des Defaultkonstruktors.
Dies kann einen großen Geschwindigkeitsvorteil bringen, da dieser sämtliche Membervariablen mit Defaultwerten füllt, welche im Endeffekt meist sowieso nicht benötigt werden.
In der Initialisierungsliste kann auch Speicher reserviert werden, falls ein Zeiger nicht zwangsläufig auf einen gültigen Speicherbereich zeigen muss, sollte dieser mit null initialisert werden. Dies macht Sinn, da das löschen eines Null-Zeigers sicher ist, das löschen eines uninitialisierten Zeigers aber schwere Fehler verursacht.
Im Allgemeinen sollte die Initialisierungsliste immer verwendet werden. Es sei denn der gewünschte Code ist in der Liste nicht erlaubt. Nur für solche Fälle sollte auf den Konstruktorenkörper zurückgegriffen werden.

Der this-Pointer

Um den this-Pointer zu verstehen ist es sinnvoll zu wissen, wo sich Member und Methoden einer Klasse im Speicher befinden.
Jedes Objekt hat seine eigene Gruppe von Membervariablen. Die Methoden gibt es allerdings nur einmal im Speicher, d.h. alle Objekte teilen sich die selben Methoden. Woher weis nun aber jede Methode wo genau die Variablen stehen welche zu ändern sind? Die Antwort auf diese Frage ist der this-Pointer. Dieser Zeiger zeigt auf den Speicherbereich an dem sich das entsprechende Objekt befindet.

CPP:
class Enemy
{
public:
            Enemy( void );
            ~Enemy( void );

    void    SetHealth( int v );
    int     GetHealth( void );

private:
    int     health;
};

Enemy::Enemy( void )
    : health( 100 )
{}

Enemy::~Enemy( void )
{}

void Enemy::SetHealth( int v )
{
    health = v;
}

int Enemy::GetHealth( void )
{
    return health;
}

Die Methoden SetHealth und GetHealth hätten auch wie folgend geschrieben werden können.

void Enemy::SetHealth( int v )
{
    this->health = v;
}

int Enemy::GetHealth( void )
{
    return this->health;
}


Die erste Variante funktioniert, da der Compiler den fehlenden this-Pointer hinzufügt.
Will man direkt auf das Objekt zugreifen, muss ein Asterisk vor das this geschrieben werden. Folgende Variante wäre also auch denkbar.

CPP:
void Enemy::SetHealth( int v )
{
    ( *this ).health = v;
}

int Enemy::GetHealth( void )
{
    return ( *this ).health;
}


Statische Methoden und Membervariablen

In einer Klasse ist es möglich statische Methoden zu deklarieren. Dies macht immer dann Sinn, wenn man auf die Methoden Zugriff haben will ohne vorher eine Instanz der Klasse zu erzeugen.
Diese Methoden besitzen einfach keinen this-Pointer, d.h. sie haben auch keinen Zugriff auf Membervariablen oder nicht statische Methoden.

CPP:
class Enemy
{
public:
                    Enemy( void );
                    ~Enemy( void );

        void        Hello( void );

        static void Talk( void );
};

// ...
void Enemy::Hello( void )
{
        cout << "Enemy: Hello, how are you?\n";
}

void Enemy::Talk( void )
{
        Hello();
}


Dieser Code würde nicht funktionieren, da die Methode Hello unstatisch ist und nicht von der statischen Methoden Talk aufgerufen werden kann.

CPP:
int main(int argc, char *argv[])
{
        Enemy enemy;
        enemy.Talk();

        return 0;
}


Gleichermaßen ist dieser Code fehlerhaft. Da die statische Methode Talk nicht von einer bestehenden Instanz der Klasse Enemy aufgerufen werden kann.

Zu beachten ist aber, das manche Compiler diesen Fehler ausbügeln und nicht als Fehler anzeigen.

Der korrekte Aufruf der Methode Talk wäre also folgender:
CPP:
int main(int argc, char *argv[])
{
        Enemy::Talk();

        return 0;
}



Allerdings können nicht nur Methoden sondern auch Membervariablen statisch sein. Diese befinden sich dann, genau wie Methoden, nur einmal im Speicher und werden von allen Instanzen einer Klasse geteilt.

CPP:
class Enemy
{
public:
                        Enemy( void );
                        ~Enemy( void );

        static void     SetValue( int v );
        static int      GetValue( void );

private:
        static int      value;
};

int Enemy::value = 0;

Enemy::Enemy( void )
{}

Enemy::~Enemy( void )
{}

void Enemy::SetValue( int v )
{
        value = v;
}

int Enemy::GetValue( void )
{
        return value;
}

int main(int argc, char *argv[])
{
        cout << "value von Enemy: " << Enemy::GetValue() << "\n";
        Enemy::SetValue( 10 );
        cout << "value von Enemy: " << Enemy::GetValue() << "\n";

        return 0;
}


Die Ausgabe des Programm sieht folgendermaßen aus:

Zitat:
value von Enemy: 0
value von Enemy: 10


Was zu erwähnen ist das statische Membervariablen immer extra definiert werden müssen. Dies geschieht mit der Zeile direkt unter der Klassendekleration.

CPP:
int Enemy::value = 0;


Auch ist zu erwähnen das unstatische Methoden statische Membervariablen verändern können.
Ein Beispiel wäre z.B. eine Klasse welche die Anzahl der Instanzen speichert, die sich aktuell im Speicher befinden.

CPP:
class Enemy
{
public:
                        Enemy( void );
                        ~Enemy( void );

        static int      GetCount( void );
private:
        static int      count;
};

int Enemy::count = 0;

Enemy::Enemy( void )        { count++; }
Enemy::~Enemy( void )       { count--; }
int Enemy::GetCount( void ) { return count; }

int main(int argc, char *argv[])
{
        Enemy *enemy1 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        Enemy *enemy2 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        Enemy *enemy3 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        delete enemy1; delete enemy2;
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        delete enemy3;
        return 0;
}


Hier wird count einfach immer inkrementiert wenn ein neues Objekt erzeugt wird und dekrementiert wenn das Objekt zerstört wird.


Die Ausgabe des Programms sieht wie folgend aus:

Zitat:
Anzahl Instanzen von Enemy: 1
Anzahl Instanzen von Enemy: 2
Anzahl Instanzen von Enemy: 3
Anzahl Instanzen von Enemy: 1


Konstante Methoden

Das die Membervariblen einer Klasse konstant sein können, wurde schon bei dem Beispiel der Initialisierungsliste gezeigt.
Allerdings können nicht nur Membervariablen, sondern auch Methoden konstant sein. Bei diesen ist der this-Pointer konstant, d.h. sie dürfen keine Membervariablen verändern und keine nichtkonstante Methoden aufrufen, da diese Membervariablen ändern könnten.

CPP:
class Integer
{
public:
                Integer( int v );
                ~Integer( void );

        int     GetValue( void ) const;
        void    SetValue( int v );

private:
        int     value;
};

Integer::Integer( int v )
        : value( v )
{}

Integer::~Integer( void )
{}

int Integer::GetValue( void ) const
{
        return value;
}

void Integer::SetValue( int v )
{
        value = v;
}

int main(int argc, char *argv[])
{
        Integer obj( 20 );

        cout << "obj: " << obj.GetValue() << "\n";
        obj.SetValue( 50 );
        cout << "obj: " << obj.GetValue() << "\n";

        return 0;
}


Dieses Programm ist lauffähig, da die Methode GetValue keine Membervariable verändert. SetValue darf nicht konstant sein, da sie den Wert value ändert.

Folgende Aufrufe sind falsch:

CPP:
int Integer::GetValue( void ) const
{
        value = 40;
        return value;
}
int Integer::GetValue( void ) const
{
        SetValue( 40 );
        return value;
}


Beim ersten Beispiel wird versucht direkt auf die Membervariable value zuzugreifen und ihr einen Wert zuzuweisen, beim zweiten Beispiel wird das selbe über die Methode SetValue versucht.
Da sowohl value als auch SetValue unstatisch sind, wird der Zugriff verweigert.

Weiterhin ist zu bemerken, das ein konstantes Objekt nur Zugriff auf konstante Methoden hat.
Obiges Beispiel etwas umgeschrieben würde also Fehler verursachen:

CPP:
int main(int argc, char *argv[])
{
        const Integer obj( 20 );

        cout << "obj: " << obj.GetValue() << "\n";
        obj.SetValue( 50 );
        cout << "obj: " << obj.GetValue() << "\n";

        return 0;
}


Der Fehler liegt in der Zeile obj.SetValue( 50 );. Hier wird versucht einer Membervariable einen Wert zuzuweisen was bei einem konstanten Objekt nicht funktioniert.
Würde man die Methode GetValue als nichtkonstant deklarieren so würde schon der Aufruf von obj.GetValue(), obwohl keine Membervariable geändert wird, zu einer Fehlerausgabe führen.
Daher sollte man alle Methoden, die keine Veränderungen an Membervariablen vornehmen, als konstant deklarieren. Diese Vorgehensweise nennt man „const correctness1“.


Mutable Membervariablen

Im letzten Kapitel wurden Konstante Methoden besprochen. Es wurde u.A. erwähnt, das konstante Methoden keine Membervariablen ändern dürfen.
Das stimmt, allerdings gibt es eine kleine Ausnahme, die mutablen Membervariablen.
Es kann durchaus die Situation eintreten, das eine Methode als konstant deklariert wurde, da sie keine Membervariablen, die den Status des Objektes beeinflussen. Allerdings ist es möglich, das in der Klasse Membervariablen vorkommen die den Status des Objektes nicht beeinflussen. Diese können theoretisch von konstanten Methoden geändert werden, allerdings auch nur theoretisch da der Compiler ja nicht weis, das es sich um solche Variablen handelt.
Um ihm das mitzuteilen wird eine solche Membervariable als mutable deklariert.

Ein kleines Beispiel hierzu:
Eine Klasse besitzt eine Methode die einen Wert eines bestimmten Members ausgibt. Der Programmierer will dem Programm allerdings mitteilen, wie oft diese Methode insgesamt aufgerufen wurde. Da die Methode GetValue als konstant deklariert wurde, muss der Programmierer auf mutable Member zurückgreifen.

CPP:
class Integer
{
public:
                        Integer( int v );
                        ~Integer( void );

        int             GetValue( void ) const;
        void            SetValue( int v );
        int             GetMutVal( void ) const;

private:
        int         value;
        mutable int mutval;
};

Integer::Integer( int v )
        : value( v ),   
          mutval( 0 )
{}

Integer::~Integer( void )
{}

int Integer::GetValue( void ) const
{
        mutval++;
        return value;
}



void Integer::SetValue( int v )
{
        value = v;
}

int Integer::GetMutVal( void ) const
{
        return mutval;
}

int main(int argc, char *argv[])
{
        Integer obj( 0 );

        for ( int i = 0; i < 10; i++ )
        {
                obj.SetValue( i );

                if ( !( i % 2 ) )
                        cout << "obj: " << obj.GetValue() << "\n";
        }

        cout << "GetValue() wurde " << obj.GetMutVal() << " mal aufgerufen.\n";

        return 0;
}


Die Ausgabe des Programm sieht folgendermaßen aus:
Zitat:
obj: 0
obj: 2
obj: 4
obj: 6
obj: 8

GetValue() wurde 5 mal aufgerufen.

Sollte ein Objekt konstant sein, darf dieses nur konstante Methoden aufrufen. Mutable Member können aber auch hier geändert werden.

Vererbung von Klassen

Dieses Thema ist ein sehr wichtiges der Objektorientierten Programmierung. Leider wird die OOP oft nur auf dieses eine Thema beschränkt, wenn es um die Erklärung derselben geht.
Wenn man seine Klassenstruktur gut durchplant, kann man mit Vererbung eine Menge Programmieraufwand einsparen.
Nimmt man Beispielsweise einen Vogel und einen Fisch so wird man relativ schnell erkennen: beides sind Tiere. Und beide Tierarten haben sowohl ganz unterschiedliche als auch gleiche Eigenschaften.
Beide Tierarten haben z.B. ein gewisses Alter, allerdings unterscheiden sich die Art der Fortbewegung ganz grundlegend.
Man könnte nun beide Klassen unabhängig voneinander implementieren. Dies wäre allerdings unnötiger Programmieraufwand und außerdem sehr langweilig! Smile

CPP:
class Tier
{
public:
                Tier();
                ~Tier();
       
        void    SetAlter( int v );
        int     GetAlter( void ) const;

private:
        int     alter;
};

class Vogel : public Tier
{
public:
        char*   Fortbewegung( void ) const;
};

class Fisch : public Tier
{
public:
        char*   Fortbewegung( void ) const;
};

Tier::Tier()
        : alter( 0 )
{}

Tier::~Tier()
{}

void Tier::SetAlter( int v )
{
        alter = v;
}




int Tier::GetAlter( void ) const
{
        return alter;
}
char* Vogel::Fortbewegung( void ) const
{
        return "fliegt meist.";
}

char* Fisch::Fortbewegung( void ) const
{
        return "schwimmt.";
}
int main(int argc, char *argv[])
{
        Vogel tier1;
        Fisch tier2;

        tier1.SetAlter( 10 );
        tier2.SetAlter( 20 );

        cout << "Tier 1 ist " << tier1.GetAlter() << " Jahre alt und ";
        cout << tier1.Fortbewegung() << "\n";
        cout << "Tier 2 ist " << tier2.GetAlter() << " Jahre alt und ";
        cout << tier2.Fortbewegung() << "\n";
        return 0;
}


Beide Klassen, Fisch und Vogel, erben die Methoden SetAlter und GetAlter sowie die Membervariable alter von der Klasse Tier.
Diesem Umstand verdanken wir es, das sowohl der Vogel als auch der Fisch ein Alter haben darf und wir die völlig identischen Methoden nicht zweimal implementieren mussten.

Die Ausgabe des Programms wäre dann folgende:
Zitat:
Tier 1 ist 10 Jahre alt und fliegt meist.
Tier 2 ist 20 Jahre alt und schwimmt.


Virtuelle Methoden

Virtuelle Methoden sind sehr nützliche Konstrukte. Sie können bei von abgeleiteten Klassen überschrieben werden, d.h. Sollte Klasse „B“ von Klasse „A“ abgeleitet sein und beide Klassen haben eine Methode MethodeX(), so kann ein Zeiger vom Typ „A“ auf ein Objekt der Klasse „B“ Zeigen. Wird nun die Funktion A->MethodeX() aufgerufen, welche Methode würde dann wirklich ausgeführt? Die von „A“ oder von „B“?
Um dies zu erklären ein kleines Beispiel:
CPP:
class KlasseA
{
public:
        void Message( void ) const;
};

class KlasseB : public KlasseA
{
public:
        void Message( void ) const;
};

void KlasseA::Message( void ) const
{
        cout << "KlasseA::Message()\n";
}

void KlasseB::Message( void ) const
{
        cout << "KlasseB::Message()\n";
}

int main(int argc, char *argv[])
{
        KlasseA a;
        KlasseB b;

        a.Message();
        b.Message();

        return 0;
}


Die Ausgabe des Programms ist relativ klar:
Zitat:
KlasseA::Message()
KlasseB::Message()


Die Instanzen von KlasseA und KlasseB rufen die entsprechenden Methoden auf. Dieses Beispiel sollte bei dem bisher Gelerntem relativ klar sein.
Was passiert aber, wenn die Methode KlasseA::Message() als virtual deklariert wird?
CPP:
class KlasseA
{
public:
        virtual void Message( void ) const;
};


Die Ausgabe bleibt die Selbe. Wozu also gibt es virtuelle Methoden nun eigentlich genau. Ganz einfach, virtuelle Methoden haben nicht direkt mit den entsprechenden Objekten zu tun, sondern mit den Zeigern auf die Objekte.

CPP:
int main(int argc, char *argv[])
{
        KlasseA *a = new KlasseA();
        KlasseB *b = new KlasseB();

        a->Message();
        b->Message();

        delete KlasseA();
        delete KlasseB();

        return 0;
}


Schreibt man das Programm wie im letzten Listing um, so erhält man immer noch die selbe Programmausgabe.
Das ist so, da der Compiler genau weis auf was für einen Typ von Instanz der Zeiger zeigt. Es gibt aber einen Fall wo dies nicht ersichtlich ist.
C++ bietet die Möglichkeit, das ein Zeiger eines bestimmten Typs auf Instanzen eines anderen Typs zeigen darf, solang dieser eine Ableitung von Typ eins ist.
Würde man das Schlüsselwort virtual aus der Methodendekleration von KlasseA::Message entfernen und das Hauptprogramm folgendermaßen umschreiben, so wäre die Ausgabe eine grundlegend andere.

CPP:
class KlasseA
{
public:
        void Message( void ) const;
};
// ...
int main(int argc, char *argv[])
{
        KlasseA *a;
        a = new KlasseA(); a->Message(); delete a;
        a = new KlasseB(); a->Message(); delete a;
        return 0;
}


Die Ausgabe wäre falsch, beide male würde die Methode Message von KlasseA ausgeführt werden.
Zitat:
KlasseA::Message()
KlasseA::Message()


Und genau hier kommen die Virtuellen Methoden zum Zug. Wenn die Methode Message von KlasseA wieder als virtual deklariert wird, so ist die Ausgabe wieder korrekt.

Ein weiteres Beispiel ist folgendes Programm:

CPP:
class KlasseA
{
public:
                        KlasseA();
                        ~KlasseA();

        virtual void    Message( void ) const;
};

class KlasseB : public KlasseA
{
public:
                KlasseB();
                ~KlasseB();

        void    Message( void ) const;
};
KlasseA::KlasseA()
{
        cout << "Konstruktor von KlasseA\n";
}

KlasseA::~ KlasseA()
{
        cout << "Destruktor von KlasseA\n";
}
void KlasseA::Message( void ) const
{
        cout << "KlasseA::Message()\n";
}
KlasseB::KlasseB()
{
        cout << "Konstruktor von KlasseB\n";
}


KlasseB::~KlasseB()
{
        cout << "Destruktor von KlasseB\n";
}

void KlasseA::Message( void ) const
{
        cout << "KlasseA::Message()\n";
}

void KlasseB::Message( void ) const
{
        cout << "KlasseB::Message()\n";
}
// ...
int main(int argc, char *argv[])
{
        KlasseA *a[ 1 ];

        a[ 0 ] = new KlasseA();
        a[ 1 ] = new KlasseB();

        a[ 0 ]->Message();
        a[ 1 ]->Message();
               
        delete a[ 0 ];
        delete a[ 1 ];

        return 0;
}

Ausgabe:
Zitat:
Konstruktor von KlasseA
Konstruktor von KlasseA
Konstruktor von KlasseB
KlasseA::Message()
KlasseB::Message()
Destruktor von KlasseA
Destruktor von KlasseA


Wenn man sich die Ausgabe des Programm betrachtet wird man feststellen, das beim Zerstören von a[1] nur der Destruktor von KlasseA aufgerufen wurde.
Das ist schlecht, da im Destruktor von KlasseB evtl. Speicher freigegeben werden soll. Daher ist es immer ratsam einen Destruktor, einer Klasse von der man weis das andere Klassen von ihr abgeleitet sind/werden, als virtuell zu deklarieren.
CPP:
class KlasseA
{
public:
        virtual         ~KlasseA();
        //...
};


Zum Schluss noch ein Wort zur Vererbung. Wenn eine Basisklasse virtuelle Methoden beinhaltet und die abgeleitete Klasse wiederum als Basisklasse zu einer dritten fungiert, so ist die virtuelle Methode von Klasse 1 automatisch auch in Klasse 2 virtuell und kann somit von Klasse 3 ohne Problem überschrieben werden.

Abstrakte Klassen

Abstrakte Klassen, sind Klassen die sich nicht instanziieren lassen. Eine abstrakte Klasse zeichnet sich dadurch aus, das sie mindestens eine rein virtuelle Methode beinhaltet.

CPP:
class Shape
{
public:
                        Shape( void );
        virtual         ~Shape( void );

        void            SetWidth( int w );
        void            SetHeight( int h );

        virtual int     Area( void ) const = 0;

protected:
        int             width;
        int             height;
};


Die Methode Area ist rein virtuell. Dies ist an dem Schlüsselwort virtual und dem „= 0“ zu erkennen.
Rein virtuelle Methoden müssen von abgeleiteten Klassen überschrieben werden, daher fungieren abstrakte Klassen auch als Interfaces.
Eine Instanz kann nur mit einer abgeleiteten Klasse des Interfaces erzeugt werden. Wie in folgendem Beispiel gezeigt wird:

CPP:
class Rectangle : public Shape
{
public:
        int     Area( void ) const;
};
class Triangle : public Shape
{
public:
        int Area( void ) const;
};
int Rectangle::Area( void ) const
{
        return width * height;
}



int Triangle::Area( void ) const
{
        return ( width * height / 2 );
}
int main(int argc, char *argv[])
{
        Shape *rect = new Rectangle();
        Shape *tri  = new Triangle();

        rect->SetHeight( 100 );
        rect->SetWidth( 50 );
        tri->SetHeight( 100 );
        tri->SetWidth( 50 );

        cout << "Die Flaeche des Rechtecks betraegt: " << rect->Area() << "\n";
        cout << "Die Flaeche des Dreiecks betraegt: " << tri->Area() << "\n";

        delete rect;
        delete tri;

        return 0;
}


Shape hat also nur eine Schnittstellenfunktion zwischen Triangle und Rectangle.
Wenn eine Methode als rein virtuell deklariert wurde, so heißt das allerdings nicht, das sie keinen Methodenkörper haben darf.
Dieser muss nur extra definiert werden:

CPP:
int Shape::Area( void ) const
{
        cout << "Shape::Area()\n";
        return 0;
}
int Triangle::Area( void ) const
{
        Shape::Area();
        return ( width * height / 2 );
}


Die Methode kann dann aus einer anderen Methode, mit Klassenname::Methodenname(), aufgerufen werden. Dies funktioniert allerdings nicht außerhalb von abgeleiteten Objekten, auch wenn der Aufruf aussieht wie der einer statischen Methode.
Es kann den Fall geben, das eine Klasse abstrakt gemacht werden soll, ohne das virtuelle Methoden angeboten werden sollen. In diesem Fall kann einfach der Destruktor rein virtuell gemacht werden.



CPP:
class Shape
{
public:
        virtual ~Shape( void ) = 0;
        // ...
};


Shape ist hier eine abstrakte Klasse. Wenn andere Klassen von Shape abgeleitet werden sollen, so muss für diese nun immer ein Destruktor implementiert werden. Da der Compiler diese Arbeit in diesem Fall nicht mehr übernimmt.


Überladene Operatoren

In C++ gibt es die Möglichkeit, außer Methoden, auch Operatoren zu überladen. Dies hat den Sinn, Objekte wie ganz normale Variablen zu behandeln.
Nehmen wir an, wir haben eine Klasse die 2 Dimensionale Vektoren verarbeiten soll. Jetzt soll es eine Möglichkeit geben Vektoren zu addieren und zu subtrahieren. Eine Lösung wäre folgende:

CPP:
class Vec2d
{
public:
        float x, y;

public:
              Vec2d( void );
              Vec2d( float _x, float _y );
              ~Vec2d( void );
};

Vec2d::Vec2d( void )
        : x( 0 ), y( 0 )
{}

Vec2d::Vec2d( float _x, float _y )
        : x( _x ), y( _y )
{}

Vec2d::~Vec2d( void )
{}
int main(int argc, char *argv[])
{
        Vec2d v1( 10, 20 );
        Vec2d v2( 40, 23 );

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        v1.x += v2.x;
        v1.y += v2.y;

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        return 0;
}


Die Ausgabe des Programms wäre folgende:
Zitat:
X: 10 Y: 20
X: 50 Y: 43


Diese Lösung ist zwar Möglich, man muss sich aber nur die Tipparbeit vorstellen, die anfällt, wenn man mehrere Vektoroperationen ausführen will, oder wenn noch andere hinzukommen würden.
Ein Verbesserungsvorschlag wäre, für jeden Rechenoperator eine Methode zu implementieren. Ähnlich wie in folgendem Beispiel:

CPP:
class Vec2d
{
public:
        float x, y;

public:
               Vec2d( void );
               Vec2d( float _x, float _y );
               ~Vec2d( void );

        Vec2d & Add( const Vec2d &v );
        Vec2d & Sub( const Vec2d &v );
};
Vec2d &Vec2d::Add( const Vec2d &v )
{
        x += v.x;
        y += v.y;

        return *this;
}

Vec2d &Vec2d::Sub( const Vec2d &v )
{
        x -= v.x;
        y -= v.y;
       
        return *this;
}
int main(int argc, char *argv[])
{
        Vec2d v1( 10, 20 );
        Vec2d v2( 40, 23 );

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        v1 = v1.Add( v2 );

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        return 0;
}


Nun ist nur noch eine Zeile für die Addition zweier Vektoren zuständig. Allerdings wäre es doch wirklich cool etwas wie v1 += v2 zu realisieren.
Und das geht tatsächlich, eben mit überladenen Operatoren. Hierbei ist allerdings zu erwähnen, das keine neuen Operatoren erzeugt -, sondern wirklich nur vorhandene überladen werden können.


Folgendes Beispiel ist das oben gezeigte, erweitert durch Überladung der Operatoren += und -=.

CPP:
class Vec2d
{
public:
        float x, y;

public:
                Vec2d( void );
                Vec2d( float _x, float _y );
                ~Vec2d( void );

        Vec2d & operator+=( const Vec2d &v );
        Vec2d & operator-=( const Vec2d &v );
};
Vec2d &Vec2d::operator+=( const Vec2d &v )
{
        x += v.x;
        y += v.y;
       
        return *this;
}

Vec2d &Vec2d::operator-=( const Vec2d &v )
{
        x -= v.x;
        y -= v.y;

        return *this;
}
int main(int argc, char *argv[])
{
        Vec2d v1( 10, 20 );
        Vec2d v2( 40, 23 );

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        v1 += v2;

        cout << "X: " << v1.x << " Y: " << v1.y << "\n";

        return 0;
}


Einfacher geht es wirklich nicht. Zumal tatsächlich fast alle Operatoren überladen werden können.
Allerdings sind einige Dinge zu beachten. Überladene Operatoren sollten immer das tun, was von ihnen erwartet wird. So sollte der „+“ Operator nicht auf einmal eine Subtraktion durchführen.
Außerdem gibt es einige Operatoren die nicht überladen werden können, diese sind: „->*“ und „,“.


Befreundete Klassen

Es ist möglich, Klassen Freunde zuzuweisen. Diese können zum einen andere Klassen, zum anderen Funktionen sein.
Wenn eine Freundesbeziehung zwischen einer zwei Klassen und/oder einer Funktion besteht, so ist diese sehr eng. D.h. der befreundeten Funktion/Klasse ist es möglich auf private Member der Freundesklasse zuzugreifen.
Um dies zu verdeutlichen ein kleines Beispiel:

CPP:
class KlasseA
{
        friend void Call( const KlasseA& );

private:
        void Secret( void ) const;     
};

void KlasseA::Secret( void ) const
{
        cout << "KlasseA::Secret()\n";
}

void Call( const KlasseA &c )
{
        c.Secret();
}

int main(int argc, char *argv[])
{
        KlasseA obj;
        Call( obj );

        return 0;
}


Da die Funktion Call() als befreundete Funktion von KlasseA deklariert wurde, funktioniert dieses Beispiel problemlos.
Genauso funktioniert das ganze mit Freundschaften zwischen zwei Klassen:

CPP:
friend class KlasseB;


Das ist alles was zu tun ist um KlasseB einen vollen Zugriff auf Freundesklasse zu gewähren.


CPP:
class KlasseA
{
        friend class KlasseB;

private:
        void Secret( void ) const;     
};

class KlasseB
{
public:
        void Call( const KlasseA &obj ) const;
};

void KlasseA::Secret( void ) const
{
        cout << "KlasseA::Secret()\n";
}

void KlasseB::Call( const KlasseA &obj ) const
{
        obj.Secret();
}

int main(int argc, char *argv[])
{
        KlasseA objA;
        KlasseB objB;

        objB.Call( objA );
        return 0;
}


Es ist recht schnell ersichtlich, das dies eine ziemlich heikle Angelegenheit ist. Daher sollte man mit Freundschaften zwischen Klassen und Funktionen sehr vorsichtig umgehen.
Ein paar abschließende Worte:
Arrow Freundschaften werden im Normalfall immer über den public, protected, private Teilen einer Klasse deklariert.
Arrow Freundschaften werden nicht weiter vererbt.


Methodenzeiger

Zeiger können auch auf Methoden zeigen, das funktioniert auch mit Methoden einer Klasse. Folgendes Beispiel demonstriert die Verwendung von Methodenzeigern:

CPP:
class Klasse1
{
public:
        void Funktion1( void ) const;
        void Funktion2( void ) const;
};

void Klasse1::Funktion1( void ) const
{
        cout << "Klasse1::Funktion1()\n";
}

void Klasse1::Funktion2( void ) const
{
        cout << "Klasse1::Funktion2()\n";
}

void Call( Klasse1 *obj, void ( Klasse1::*method )( void ) const )
{
        ( obj->*method )();
}

int main(int argc, char *argv[])
{
        Klasse1 *obj = new Klasse1;

        Call( obj, &Klasse1::Funktion1 );
        Call( obj, &Klasse1::Funktion2 );

        delete obj;

        return 0;
}


Die Funktionszeiger funktionieren im Grunde genauso wie „normale“ Funktionszeiger, allerdings wird hier noch eine Instanz der Klasse benötigt, von welcher die Methode aufgerufen werden soll.


Template Klassen

Eine wichtige Funktion bietet C++, im Zusammenhang mit Klassen. Es können sog. Templates erstellt werden.
Templates erlauben es, Klassen unabhängig von erwarteten Typen zu deklarieren und machen es somit möglich Klassen als Schablonen zu verwenden.
Ein Beispiel:

CPP:
typedef unsigned long DWORD;

template< class T, DWORD size=32 >
class StaticStack
{
public:
      StaticStack( void );
      ~StaticStack( void );
       
    void   Push( const T &obj );
    T &     Pop( void );
    void   Clear( void );
    bool   IsEmpty( void ) const;
    int   GetSize( void ) const;

private:
    T      data[ size ];
    DWORD   num;
};

template< class T, DWORD size >
StaticStack< T, size >::StaticStack( void )
   : num( 0 )
{}

template< class T, DWORD size >
StaticStack< T, size >::~StaticStack( void )
{}

template< class T, DWORD size >
void StaticStack< T, size >::Push( const T &obj )
{
    assert( num < size );
    data[ num++ ] = obj;
}

template< class T, DWORD size >
T &StaticStack< T, size >::Pop( void )
{
    assert( num > 0 );
    return data[ --num ];
}
template< class T, DWORD size >
void StaticStack< T, size >::Clear( void )
{
    num = 0;
}


template< class T, DWORD size >
bool StaticStack< T, size >::IsEmpty( void ) const
{
    return num == 0;
}

template< class T, DWORD size >
int StaticStack< T, size >::GetSize( void ) const
{
    return num;
}

int main( int argc, char **argv )
{
    StaticStack< int > stack;

    for ( int i = 0; i < 10; i++ )
    {
        stack.Push( i );
    }

    while ( !stack.IsEmpty() )
    {
        cout << stack.Pop() << "\n";
    }   

    return 0;
}


Die Klasse StaticStack bietet eine Möglichkeit einen Stack mit fester Größe zu verwalten. Falls nun der Fall eintreten sollte, das ein Stack für verschiedene Datentypen verfügbar sein sollte, hätte man die Klasse einfach zweimal Implementieren können.
Einmal beispielsweise für den Datentyp int und einmal für den Datentyp double.
Eine viel bessere Lösung ist aber eine Template-Klasse zu erstellen, die unabhängig vom Datentyp ist. Somit spart man sich sehr viel Programmieraufwand.
Mit solch einem Template ist die Realisierung eines Stacks für den Datentyp double keine große Arbeit mehr.

CPP:
int main( int argc, char **argv )
{
    StaticStack< double > stack;

    for ( int i = 0; i < 10; i++ )
    {
        stack.Push(static_cast< double >( i ) );
    }

    while ( !stack.IsEmpty() )
    {
        cout << stack.Pop() << "\n";
    }   

    return 0;
}


Schlusswort

Ich hoffe nun, das ich dem Leser das Thema Objektorientierte Programmierung etwas näher bringen konnte.
Bei gefundenen Fehlern bitte eine kleine Beschreibung an reinigdavid@hotmail.com senden. Rechtschreibfehler dürfen behalten werden! Wink

Anhang:
Das ganze nochmals als PDF: hier


Zuletzt bearbeitet von David am 13.01.2006, 14:30, insgesamt 4-mal bearbeitet
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden
Patrick
Dark JLI Master



Anmeldedatum: 25.10.2004
Beiträge: 1895
Wohnort: Düren
Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 07:58    Titel: Antworten mit Zitat

Gutes Tutorial, aber hab denoch was Kritik.

1. 17.4.3.1.2 Global names
1 Certain sets of names and function signatures are always reserved to the implementation:

— Each name that contains a double underscore (_ _) or begins with an underscore followed by an uppercase
letter (2.11) is reserved to the implementation for any use.

Each name that begins with an underscore is reserved to the implementation for use as a name in the global namespace.165)

2. Was mir direkt zu Anfang nicht gefallen hat war das "GetHealth" und "SetHealth". Ich weiß nicht wer damals auf diesen Unsinn gekommen ist, aber so wird das OOP-Konzept elegant über den Haufen geworfen.

3.
CPP:
int main(int argc, char *argv[])
{
        Enemy *enemy1 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        Enemy *enemy2 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        Enemy *enemy3 = new Enemy();
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        delete enemy1; delete enemy2;
        cout << "Anzahl Instanzen von Enemy: " << Enemy::GetCount() << "\n";
        return 0;
}
Kann sein das ichs hier irgendwo auf den Augen habe, aber Win95 würd dir hier schon einen Bluescreen zeigen, da der Speicher von 3 zum Schluss nicht mehr freigegeben wird.

4. Logische Werte: Hast Du schonmal einen Stack gesehen mit Minuswerten? Oder eine Negative Breite Höhe? Ich jedoch nicht. Daher: unsigned! Wo wir grad noch dabei sind: Benutz long, hab mich schon oft darüber ausgelassen das int benutzt wird.

5. Wo ist der Copyconstructor?

6. Also wenn man schon meine Stackklasse kopiert, dann sollte man da es auch schon richtig machen! Ruf ich 1x bei Dir Push auf und 2x Pop haste den Salat.

Dazu wieder fehlt das unsigned!

Was ich auch sehr interessant finde ist das hier:
CPP:
return num == 0;
Also ich weiß nicht, wenn num ein int ist, da fragst du echt nach 0?? Generell sollte man dafür < 1 nehmen.

Dazu fehlt wieder Copyconstructor und Destructor. Wir wissen doch alle das man sich oft auf den Compiler nicht verlassen kann in dieser Hinsicht.

Dazu frag ich mich welcher Notation du folgst? Jedenfalls ist es nicht der in C++ üblichen. Keine suffixe, am Anfang geschriebene große Funktionsnamen/Klassennamen usw.

Vorallem was mir sehr in die Augen gegangen ist ist die manchmal schlechte Einrückung oder sowas:
CPP:
( void )
sorry, find ich ugly. Ich hab nie verstanden wieso sowas gemacht wird. Schreibt ihr in Deutsch oder Mathe sowas?

Code:

Der (also der Typ) hat mich gehauen.

bzw.

5/(2+3)

Hab noch nie gesehen das in Schriftstücken, Gleichungen usw. so was gemacht wird.

Aber im großen und ganzen gutes tut.
_________________
'Wer der Beste sein will muss nach Perfektion streben und jede Gelegenheit nutzen sich zu verbessern.' - KIA
[ German Game Dev | Boardsuche hilft sehr oft | Google rockt | Wie man Fragen richtig stellt | ICQ#: 143040199 ]
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen
David
Super JLI'ler


Alter: 38
Anmeldedatum: 13.10.2005
Beiträge: 315

Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 08:32    Titel: Antworten mit Zitat

Hi!

Zu 1:
Dieser Vorschlag muss allerdings nicht angenommen werden, oder? Ich kenne einige Programmierer die für jeden Parameter ein Underscore setzen.

Zu 2:
Stimmt, da hast du recht. Hier sollte allerdings nur das Prinzip von Methoden demonstriert werden. Ist vllt kein besonderst gutes Beispiel, allerdings ist mir nichts besseres eingefallen. Wink

Zu 3:
Stimmt. Da fehlt tatsächlich eine freigabe von enemy3. Danke für den Hinweis.

Zu 4:
Das ist auch richtig. Allerdings wird "int" sehr oft von Anfängern (an die es gerichtet ist) verwendet (weis auch nicht warum). Da diesem Tutorial keine Erklährungen von Datentypen vorrausgehen (und es auch keine Datentypen lehren soll), hab ich der Einfachheit halber ein int verwendet.
Für eine tatsächliche Implementation wäre sich ein unsigned long natürlich logischer.

Zu 5:
Ich wusste doch, das ich einen wichtigen Punkt übersehen habe! Wink

Zu 6:
Das ist mitnichten deine Stackklasse. Ich hab so ein ähnliches Teil in meinem Framework, allerdings mit der hier fehlenden Fehlerbehandlung. Die ist deswegen rausgelassen worden weil sie nichts mit dem Thema zu tuen hat.
Der Quellcode soll ein Beispiel darstellen und nicht einen Quellcode der 1:1 kopiert werden soll.
Hätte ich aber evtl mit "// ..." verdeutlichen sollen.

Zu 7:
Das ist in der Tat auch richtig. Allerdings ist die Abfrage ja trotzallem korrekt. (Wenn die Fehlerbehandung funktionieren würde und keine Minuswerte zulassen würde). Allerdings wäre dein Vorschlag bei einem unsigned Wert nichtmehr richtig.

Jep, Kopiercon- und De-struktor fehlen hier. Danke für den Hinweis. Smile

Nachdem ich Jahrelang der ungarischen Notation "gefolgt" bin, hat mich das so demaßen angenervt irgendwann, das ich nun garkeiner mehr folge! Wink
Das mag für manche einen unschöneren Quellcode ergeben, mir gefällts so allerdings besser.

Das (void) ist nicht verboten und es ist Geschmackssache, würde ich sagen, ob man es macht oder nicht. Ich finde eine "()" leere Klammer sieht hässlich aus im Quellcode...
Daher find ich dein Beispiel hierzu etwas weit hergeholt.

Danke für die Kritik, werd' schauen das ich die hier genannten Punkte möglichst ausbügel.

grüße
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden
Patrick
Dark JLI Master



Anmeldedatum: 25.10.2004
Beiträge: 1895
Wohnort: Düren
Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 08:42    Titel: Antworten mit Zitat

David
Zu 1.: Das ist eine feste Regelung im Standard. An diese sollte man sich umbedingt halten. Wenn die anderen gerne Rumbomben, bombst Du mit?

Zu 2.: erledigt

Zu 3.: erledigt

Zu 4.: erledigt

Zu 5.: erledigt

Zu 6.: erledigt

Zu 7?.: ich mein ja auch nicht (), da dies aussieht wie ein Funktionsaufruf sondern (void) und nicht ( void )
_________________
'Wer der Beste sein will muss nach Perfektion streben und jede Gelegenheit nutzen sich zu verbessern.' - KIA
[ German Game Dev | Boardsuche hilft sehr oft | Google rockt | Wie man Fragen richtig stellt | ICQ#: 143040199 ]
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen
David
Super JLI'ler


Alter: 38
Anmeldedatum: 13.10.2005
Beiträge: 315

Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 09:09    Titel: Antworten mit Zitat

Hi!

Hm, nungut, mit 1 geb ich dir recht.
Aber zum letzten Punkt... Das ist trotzdem Geschmackssache. Und tatsächlich habe ich diese Leerzeichen schon des öfteren gesehen. Mir gefällts, wem's nich gefällt der solls anders machen! Wink
Tatsächlich finde ich Konstrukte ohne Leerzeichen oft hässlicher als mit.

Hoffe das ich diesmal verstanden hab um was es dir geht! Wink

grüße
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden
abc_d
JLI Master Trainee


Alter: 34
Anmeldedatum: 27.01.2003
Beiträge: 615

Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 12:40    Titel: Antworten mit Zitat

Hallo, mir gefällt das Tutorial gut, darf ich es auf Online-tutorials.net, http://www.online-tutorials.net/c-c++-c/tutorials-1.html veröffentlichen?
_________________
http://mitglied.lycos.de/sarti/linuxisevil.gif Linux is evil - get the fact.

Never touch a running System - der Systemling
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden E-Mail senden
David
Super JLI'ler


Alter: 38
Anmeldedatum: 13.10.2005
Beiträge: 315

Medaillen: Keine

BeitragVerfasst am: 07.12.2005, 13:22    Titel: Antworten mit Zitat

Hi!

Wenn ich die Verbesserungen vorgenommen habe, dann gern.

grüße
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden
Beiträge der letzten Zeit anzeigen:   
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials Alle Zeiten sind GMT
Seite 1 von 1

 
Gehe zu:  
Du kannst keine Beiträge in dieses Forum schreiben.
Du kannst auf Beiträge in diesem Forum nicht antworten.
Du kannst deine Beiträge in diesem Forum nicht bearbeiten.
Du kannst deine Beiträge in diesem Forum nicht löschen.
Du kannst an Umfragen in diesem Forum nicht mitmachen.


Powered by phpBB © 2001, 2005 phpBB Group
Deutsche Übersetzung von phpBB.de

Impressum