Tutorials - C++ Tutorial - Kleine Einführung in die OOP

Sprachenübersicht/Programmierung/C / C++/ C#

C++ Tutorial - Kleine Einführung in die OOP

Diese Seite wurde 47286 mal aufgerufen.

Dieser Artikel wurde in einem Wikiweb System geschrieben, das heißt, Sie können die Artikel jederzeit editieren, wenn Sie einen Fehler gefunden haben, oder etwas hinzufügen wollen.

Editieren Versionen Linkpartnerschaft Bottom Printversion

Keywords: Objekt Orientierte Programmierung

Inhaltsverzeichnis



Einführung Top



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 Objekte 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 Top



Klassen sind ein 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:

Code:


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:

Code:


#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:

Code:


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:

Code:


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.

Code:


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.

Code:


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:

Quote:


Der Gegner hat 100 Lebenspunkte.
Der Gegner hat 50 Lebenspunkte.



Konstruktor und Destruktor Top



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:

Code:


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.

Quote:


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.

Code:


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.

Code:


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:

Quote:


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:

Code:


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.

Code:


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 Top



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:

Code:


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 Top



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:

Code:


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 Top



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.

Code:


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, um den Pointer zu dereferenzieren. Folgende Variante wäre also auch denkbar.

Code:


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

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



Statische Methoden und Membervariablen Top



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 nicht statische Membervariablen oder nicht statische Methoden.

Code:


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.

Code:


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:

Code:


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.

Code:


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:

Quote:


value von Enemy: 0
value von Enemy: 10



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

Code:


int Enemy::value = 0;



Auch ist zu erwähnen dass 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.

Code:


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 ein Objekt zerstört wird.

Die Ausgabe des Programms sieht wie folgend aus:

Quote:


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



Konstante Methoden Top



Dass 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.

Code:


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:

Code:


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 nicht konstant 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:

Code:


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 eine nicht konstante Methode aufzurufen 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 correctness“.

Mutable Membervariablen Top



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.

Code:


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:

Quote:


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 Top



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! laugh

Code:


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:

Quote:


Tier 1 ist 10 Jahre alt und fliegt meist.
Tier 2 ist 20 Jahre alt und schwimmt.



Virtuelle Methoden Top



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:

Code:


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:

Quote:


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?

Code:


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.

Code:


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 weiß 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, dass ein Zeiger eines bestimmten Typs auf Instanzen eines anderen Typs zeigen darf, solang dieser eine Ableitung von ersten Typ 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.

Code:


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.

Code:


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:

Code:


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[ 2 ];

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

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

        return 0;
}



Ausgabe:

Quote:


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 weiß das andere Klassen von ihr abgeleitet sind/werden, als virtuell zu deklarieren.

Code:


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 Top



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

Code:


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:

Code:


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;
}
[/co
CPP:de]

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:

[code]
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.

Code:


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 Top



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:

Code:


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:

Quote:


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:

Code:


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 -=.

Code:


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 Top



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:

Code:


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:

Code:


friend class KlasseB;



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

Code:


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 Top



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

Code:


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 Top



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:

Code:


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 >:ush( const T &obj )
{
    assert( num < size );
    data[ num++ ] = obj;
}

template< class T, DWORD size >
T &StaticStack< T, size >:op( 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.

Code:


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 Top



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!

Anhang:
Das ganze nochmals als PDF: hier.

Gibt es noch irgendwelche Fragen, oder wollen Sie über den Artikel diskutieren?

Editieren Versionen Linkpartnerschaft Top Printversion

Haben Sie einen Fehler gefunden? Dann klicken Sie doch auf Editieren, und beheben den Fehler, keine Angst, Sie können nichts zerstören, das Tutorial kann wiederhergestellt werden

Sprachenübersicht/Programmierung/C / C++/ C#/C++ Tutorial - Kleine Einführung in die OOP