C++ Kurs

Abgeleitete Klassen

Die Themen:

Einleitung
Beispiel einer Ableitung
Zugriffsrechte
Ableitung
Konstruktor und Destruktor
Zugriff auf Basisklassen-Member
Erweitern von Klassen durch Ableitung
Beispiel und Übung

Einleitung

Mit Hilfe der Ableitung (Vererbung, Inheritance) von Klassen lassen sich prinzipiell zwei Aufgaben durchführen:

  1. Eigenschaften und Memberfunktionen, die in mehreren Klassen benötigt werden, können in einer eigenen Klasse zusammengefasst werden. Diese zusammengefassten Eigenschaften und Memberfunktionen können dann in weiteren Klassen 'eingebaut' werden. Der Vorteil für den Entwickler nun besteht darin, dass er den Code für diese Klasse mit den gemeinsamen Eigenschaften und Memberfunktion nur einmal zu schreiben und zu testen(!) braucht.
  2. Dem Anwender einer Klasse bietet die Ableitung die Möglichkeit, eine vorgegebene Klasse in ihren Eigenschaften und Memberfunktionen erweitern zu können, ohne dass er dazu den Quellcode der Ausgangsklasse modifizieren muss.

Zu 1:

Stellen Sie sich einmal vor, Sie sollen eine Klassenbibliothek zur Darstellung von grafischen Elementen entwickeln. Da jedes Grafikelement eine Position und Ausdehnung besitzt, können diese Eigenschaften und die für die Manipulation der Eigenschaften notwendigen Memberfunktionen in einer gesonderten Klasse zusammengefasst werden (was wir nachher auch gleich tun).

Zu 2:

Kaufen Sie jetzt als Anwender diese Grafikbibliothek, so stehen Ihnen die vom Entwickler der Klassenbibliothek implementierten Grafikelemente zur Verfügung. Nehmen wir einmal an, dass diese Bibliothek eine Klasse zur Darstellung eines ausgefüllten Rechtecks enthält. Diese Klasse lässt aber nur vollflächig ausgefüllte Rechtecke zu. Sie benötigen aber ein Rechteck, das mit einem Muster ausgefüllt werden kann. Dann können Sie als Anwender jetzt die vorgegebene Klasse für das ausgefüllte Rechteck einfach als Basis verwenden und sie um die Eigenschaft 'Muster' erweitern. Die restlichen Eigenschaften und Memberfunktionen wie z.B. die Position übernehmen Sie unverändert. Sie brauchen für diese Erweiterung nicht einmal den Quellcode der Ausgangsklasse besitzen!

Weiter vorne im Kurs (siehe hier)haben Sie ebenfalls eine Möglichkeit kennen gelernt, eine Klasse in ihrer Funktionalität zu erweitern, nämlich durch Einschließen von Objekten in einer Klasse. Und damit stellt sich nun die Frage, wann ein Objekte einer Klasse in eine andere eingeschlossen wird und wann eine Klasse abgeleitet wird. Als Faustregel können Sie sich folgenden Satz merken: ein Objekt einer Klasse wird in einer anderen Klasse eingeschlossen wenn zwischen ihnen eine hat-eine (has-a) Beziehung besteht und abgeleitet wird, wenn eine ist-eine (is-a) Beziehung besteht.

Beispiel: Gegeben sei eine Klasse Color mit Farbinformationen und eine Klasse Win zur Darstellung eines Fensters. Da ein Fenster eine Farbe hat (und keine Farbe ist), wird die Klasse Color in Win eingeschlossen. Nun soll für eine abweichende Fensterdarstellung eine neue Fensterklasse SpecWin geschrieben werden. In diesem Fall wird SpecWin von Win abgeleitet, da SpecWin ein spezielles Fenster ist (und kein Fenster 'hat'). Eigentlich nicht schwierig, oder?

Beispiel einer Ableitung

Sehen wir uns zunächst anhand eines Beispiels das prinzipielle Auslagern von gemeinsamen Member (Eigenschaften/Memberfunktionen) in eine eigene Klasse an.

Ausgangsbeispiel:

Nachfolgend sehen Sie zwei Klassen für die Darstellung von einfachen Grafikelementen. Die Klasse Frame soll zur Darstellung eines Rahmens dienen und die Klasse Bar zur Darstellung eines ausgefüllten Rechtecks. Beide Klassen besitzen nun gewisse Gemeinsamkeiten, die rot hervorgehoben sind. So haben beide Klassen die Eigenschaft, dass Sie eine Position und eine Größe besitzen. Außerdem stehen in beiden Klassen Memberfunktionen zur Verfügung, um diese Eigenschaften verändern zu können.


class Frame
{
    short xPos, yPos;
    short width, height;
    ....
  public:
    Frame();
    void Draw(...);
    void SetPosition(...);
    void SetSize(...);
};
class Bar
{
    short xPos, yPos;
    short width, height;
    short fillColor;
    ....
  public:
    Bar(...);
    void Draw();
    void SetPosition(...);
    void SetSize(...);
};

Warum die Memberfunktion Draw(...) hier nicht zu den Gemeinsamkeiten zählt, das erfahren Sie gleich noch.

Basisklasse definieren

Laut vorheriger Aussage lassen sich nun diese gemeinsamen Eigenschaften und Memberfunktionen in eine eigene Klasse auslagern. Im nachfolgenden Beispiel sind die Gemeinsamkeiten nun in die Klasse GBase ausgelagert. Um diese ausgelagerten Eigenschaften und Memberfunktionen wieder zu den Klassen Frame und Bar hinzuzufügen, werden die beiden Klassen Frame und Bar von der Klasse GBase abgeleitet. Wie dies genau geht, das erfahren Sie gleich noch.


class GBase
{
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
};
class Frame   // Ableitung von GBase einfügen
{
    ....
  public:
    Frame();
    void Draw(...);

};
class Bar    // Ableitung von GBase einfügen
{
    short fillColor;
    ....
  public:
    Bar(...);
    void Draw();
};

Wenn Sie sich die beiden Grafikklassen Frame und Bar nochmals ansehen werden Sie feststellen, dass die Memberfunktion Draw(...) ebenfalls in beiden Klassen vorhanden ist. Diese Memberfunktion eignet sich nicht zur Auslagerung in die Klasse GBase, da sie die Objekte zeichnen soll. Und ein Rahmen wird sicherlich anders gezeichnet werden wie ein ausgefülltes Rechteck.

Erweitern von bestehenden Klassen

Sehen wir uns die Ableitung nun aus Anwendersicht an, d.h. dem Erweitern einer vorgegebenen Klasse. Angenommen Sie haben die vorherige Klassenbibliothek mit den Grafikelementen käuflich erworben. Diese Bibliothek enthält u.a. die Klasse Bar für ein ausgefülltes Rechteck. Sie benötigen aber ein Rechteck, das mit einem von Ihnen definiertem Muster (Pattern) ausgefüllt werden kann. In diesem Fall leiten Sie einfach eine neue Klasse PBar von der vorgegebenen Klasse Bar ab und fügen ihr dann die Eigenschaft Muster (Pattern) sowie die entsprechenden Memberfunktionen hinzu und schon haben Sie eine neue Klasse für ein Rechteck, das mit einem Muster ausgefüllt werden kann. Und das sogar ohne den Quellcode der Klasse Bar besitzen zu müssen!


class PBar   // Ableitung von CBar einfügen
{
    ....
    short pattern;
  public:
    PBar(...);
    void Draw();
    void SetPattern(...);
};

Im Folgenden tauchen im Zusammenhang mit Ableitungen zwei neue Begriffe immer wieder auf:

  1. Basisklasse oder Superklasse
  2. Abgeleitete Klasse oder Subklasse

Die Basisklasse ist diejenige Klasse, die ihre Eigenschaften und Memberfunktionen an die abgeleitete Klasse weitergibt (vererbt). Die abgeleitete Klasse erbt (prinzipiell) alle Eigenschaften und Memberfunktion der Basisklasse, d.h. die abgeleitete Klasse verhält sich so, als wären die geerbten Eigenschaften und Memberfunktionen innerhalb der abgeleiteten Klasse selbst enthalten.

Wie Sie dem Bild entnehmen können, kann eine Klasse sowohl Basisklasse als auch abgeleitete Klasse gleichzeitig sein. So ist im Beispiel die Klasse CBar von CGBase abgeleitete und gleichzeitig Basisklasse für die Klasse CPBar.

Zugriffsrechte

Beim Ableiten spielen auch die Zugriffsrechte (public, private) eine wichtige Rolle. Eine abgeleitete Klasse hat nur Zugriff auf die public (und die gleich noch aufgeführten protected) Member der Basisklasse, nicht aber auf ihre private Member. Im nachfolgenden Beispiel kann die Klasse Frame z.B. nur auf die Memberfunktionen SetPosition(...) und SetSize(...) der Basisklasse zugreifen, aber nicht auf deren Eigenschaften, da diese private sind.


class GBase
{
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
};
class Frame  // Ableitung von GBase einfügen
{
    ....
  public:
    Frame(...);
    void Draw()
};

Eine abgeleitete Klasse erbt aber nicht nur die Eigenschaften und 'normalen' Memberfunktionen ihrer Basisklasse, sondern auch deren überladene Operatoren, mit einer einzigen Ausnahme die Sie nie vergessen sollten:

Überlädt eine Basisklasse den Zuweisungsoperator '=', so wird dieser überladene Operator niemals an die abgeleitete Klasse vererbt!

Alle anderen, nicht-private überladenen Operatoren der Basisklasse stehen in der abgeleiteten Klasse ebenfalls zur Verfügung.

Resultierende abgeleitete Klasse

Wie stellt sich nun eine von einer Basisklasse abgeleitete Klasse (Subklasse) prinzipiell nach außen hin, d.h. für den Anwender, dar? Da die Subklasse alle Member ihrer Basisklasse erbt, sieht es für den Anwender (und auch für weitere von der Subklasse abgeleitete Klassen) so aus, als ob die nicht-private Member der Basisklasse innerhalb der Subklasse deklariert/definiert wären. private Member der Basisklasse sind niemals nach außen hin sichtbar. Dieses Verhalten ist unten durch die fiktive Klasse FFrame einmal dargestellt.


class GBase
{
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
};
class Frame  // Ableitung von GBase einfügen
{
    ....
  public:
    Frame(...);
    void Draw()
};
Die aus der Ableitung resultierende fiktive Klasse FFrame (Anwendersicht)

class FFrame
{
    ....   // Eigenschaften von Frame
  public:
    FFrame(...);
    void Draw();            // Frame Member
    void SetPosition(...);  // geerbtes GBase Member
    void SetSize(...);      // geerbtes GBase Member
};

Zugriffsrecht protected

Bevor gleich näher auf das eigentliche Ableiten eingegangen wird, gehen wir an dieser Stelle auf das bis jetzt noch fehlende Zugriffsrecht protected ein. Das Zugriffsrecht protected ist eine Mischung aus den beiden bereits bekannten Zugriffsrechten public und private und kann überall dort stehen, wo Zugriffsrechte erlaubt sind. Auf die protected Member einer Klasse kann nur von abgeleiteten Klassen aus direkt zugegriffen werden, d.h. sie verhalten aus der Sicht der abgeleiteten Klasse aus wie public Member. Der Zugriff auf protected Member aus anderen Klassen oder Funktionen heraus ist jedoch nicht möglich. Hier verhalten sich die protected Member also wie private Member. Im Beispiel können Memberfunktionen der Klasse Frame direkt auf die protected Member der Klasse GBase zugreifen während alle anderen Klassen oder Funktionen keinen Zugriff darauf haben.


class GBase
{
  protected:
    short xPos, yPos;
    short width, height;
    ....
};
// Definition der abgeleiteten Klasse
class Frame // Ableitung GBase einfügen
{
    ....
};

Ableitung

Doch sehen wir uns jetzt endlich an, wie eine Klasse von einer anderen Klasse abgeleitet wird.

Das Ableiten eine Subklasse von einer Basisklasse erfolgt nach folgender Syntax:

class CSub: ACCESS CSuper
{
    ....
};

CSub ist der Name der abgeleiteten Klasse und CSuper der Name ihrer Basisklasse. ACCESS gibt an, mit welchem Zugriffrecht aus der Sicht des Anwenders oder weiterer abgeleiteten Klassen die Member der Basisklasse in die abgeleitete Klasse übernommen werden. Für ACCESS können folgende Schlüsselwörter stehen:

public, protected, private

Wie gesagt, das Zugriffsrecht ACCESS spielt nur aus der Sicht des Anwenders oder weiteren abgeleiteten Klassen eine Rolle. Die abgeleitete Klasse selbst hat immer Zugriff auf alle public und protected Elemente ihrer Basisklasse.

Beispiel für eine Ableitung:


// Definition der Basisklasse
class GBase
{
    ....
};
// Definition der abgeleiteten Klasse
class Frame: public GBase
{
    ....
};

Wie sich diese Zugriffsrechte im Einzelnen auswirken soll jetzt anhand eines Beispiels demonstriert werden.

Die public Ableitung

Vorgegeben sind die drei Klassen GBase, Frame und MyFrame. GBase enthält die drei Member pub, prot und priv mit den entsprechenden Zugriffsrechten. Frame ist nun public von GBase abgeleitet.  Bei einer public Ableitung werden die Zugriffsrechte aus der Basisklasse 1:1 in die abgeleitete Klasse übernommen. Damit kann dann auch über ein Objekt der abgeleiteten Klasse Frame auf das public Member pub der Basisklasse zugegriffen werden.


// Definition der Basisklasse
class GBase
{
  public:    short pub;
  protected: short prot;
  private:   short priv;
};
// Von GBase abgeleitete Klasse
class Frame: public GBase
{
    ....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
   ....
   void AnyMethode()
   {
       pub  = ....;   // ist erlaubt
       prot = ....;   // ist erlaubt
       priv = ....;   // ist nicht erlaubt
   }
}
// Objekt der Klassen Frame definieren
Frame   frameObj;
// Zugriffe
frameObj.pub  = ...;  // ist erlaubt
frameObj.prot = ...;  // nicht erlaubt
frameObj.priv = ...;  // nicht erlaubt

Die protected Ableitung

Leiten wir jetzt die Klasse Frame protected von GBase ab. Die Ableitung der Klasse MyFrame lassen wir gegenüber vorhin unverändert auf public. Durch die protected Ableitung werden alle public-Member der Basisklasse zu protected-Member der abgeleiteten Klasse. Der Unterschied zum vorherigen Beispiel liegt nur im Zugriffsrecht auf die public Member der Basisklasse. Damit können zwar die Memberfunktionen der Klassen Frame und MyFrame immer noch auf das Member pub zugreifen, aber der direkte Zugriff darauf aus der Anwendung heraus über Objekte von Typ Frame und MyFrame ist gesperrt.

Selbstverständlich könnten Sie über ein Objekt der Basisklasse GBase immer noch auch das public Member pub zugreifen.


// Definition der Basisklasse
class GBase
{
  public:    short pub;
  protected: short prot;
  private:   short priv;
};
// Von GBase abgeleitete Klasse
class Frame: protected GBase
{
    ....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
   ....
   void AnyMethode()
   {
       pub  = ....;   // ist erlaubt
       prot = ....;   // ist erlaubt
       priv = ....;   // ist nicht erlaubt
   }
}
// Objekt der Klassen Frame definieren
Frame   frameObj;
// Zugriffe
frameObj.pub  = ...;  // nicht erlaubt
frameObj.prot = ...;  // nicht erlaubt
frameObj.priv = ...;  // nicht erlaubt

Die private Ableitung

Und nun zum letzten Fall, der private Ableitung. Jetzt werden alle vererbten Member der Basisklasse zur private Member der abgeleiteten Klasse. Damit besitzt nur noch die Klasse Frame Zugriff auf die public und protected Member der Basisklasse GBase. Selbst die Memberfunktionen der von Frame abgeleiteten Klasse MyFrame haben jetzt keinen Zugriff mehr auf die Member der Klasse GBase. Dies ist die restriktivste Form der Ableitung. Objekte vom Typ Frame haben auch hier ebenfalls keinen Zugriff auf die Member der Basisklasse.


// Definition der Basisklasse
class GBase
{
  public:    short pub;
  protected: short prot;
  private:   short priv;
};
// Von GBase abgeleitete Klasse
class Frame: private GBase
{
    ....
};
// Weitere Klasse von Frame ableiten
class MyFrame: public Frame
{
   ....
   void AnyMethode()
   {
       pub  = ....;   // ist nicht erlaubt
       prot = ....;   // ist nicht erlaubt
       priv = ....;   // ist nicht erlaubt
   }
}
// Objekt der Klassen Frame definieren
Frame   frameObj;
// Zugriffe
frameObj.pub  = ...;  // nicht erlaubt
frameObj.prot = ...;  // nicht erlaubt
frameObj.priv = ...;  // nicht erlaubt

Zusammenfassung

Die nachfolgende Tabelle zeigt nochmals in einer Übersicht, wie die Zugriffsrechte von Basisklassen-Member beim Ableiten umgewandelt werden

Konstruktor und Destruktor

Damit haben wir das Prinzip des Ableitens abgehandelt und wir können zum nächsten Schritt übergehen, der Abarbeitung der Konstruktore und Destruktore beim Ableiten von Klassen.

Als Ausgangspunkt dient wieder die Klasse GBase sowie die von ihr abgeleitete Klasse Frame. Beide Klassen enthalten nun entsprechende Konstruktore. Bei der Definition eines Objekts der Klasse Frame ist die Position und Größe des Rahmens sowie die Linienfarbe des Rahmens anzugeben. Da Frame selbst aber nur die Linienfarbe enthält, müssen die anderen Daten irgendwie an die Basisklasse GBase weitergegeben werden. GBase wiederum enthält einen entsprechenden Konstruktor, der die erforderlichen Daten über Parameter erhält. Wir müssen jetzt also beim Definieren eines Frame Objekts 'nur' noch den Konstruktor von GBase aufrufen.


class GBase
{
    ....
  public:
     GBase (short x, short y, short w, short h);
    ....
};
class Frame: public GBase
{
    ....
    short lineColor;
  public:
    Frame(short x, short y, short w, short h, short lCol);
};
// Definition eines Frame Objekts
Frame   myFrame(10,20,640,480,0xC0C0CO);

Konstruktor der abgeleiteten Klasse

Der Aufruf des Konstruktors der Basisklasse erfolgt nun bei der Definition des Konstruktors der abgeleiteten Klasse mit folgender Syntax:

CSub::CSub(P1, P2,...): CSuper(Pa, Pb,...)
{
    ....
}

CSub ist der Name der abgeleiteten Klasse und CSuper der Name der Basisklasse. D.h. nach der Parameterklammer des Konstruktors der abgeleiteten Klasse folgt zunächst ein Doppelpunkt und danach der Aufruf des Konstruktors der Basisklasse. An dessen Konstruktor können dann die erforderlichen Daten als Parameter übergeben werden, wobei es keine Rolle spielt, ob die zu übergebenden Daten Parameter des Konstruktors der abgeleiteten Klasse sind (P1, P2,...) oder anderweitig bestimmt werden (zum Beispiel als Konstanten).

Damit kann der Konstruktor der vorherigen Klasse Frame wie unten angegeben definiert werden. Wie Sie sehen, werden die Parameter x, y, w und h einfach an den Konstruktor der Basisklasse GBase weitergegeben. Der Konstruktor von Frame 'merkt' sich nur die Eigenschaften seiner Klasse, nämlich die Linienfarbe.


class GBase
{
    ....
  public:
     GBase (short x, short y, short w, short h);
    ....
};
class Frame: public GBase
{
    ....
    short lineColor;
  public:
    Frame(short x, short y, short w, short h, short lCol);
};
// Definition des Konstruktors
Frame::Frame (short x, short y, short w, short h, short lCol):
       GBase(x, y, w, h)
{
    lineColor = lCol;
}

Beachten Sie, dass der Aufruf des Konstruktors der Basisklasse nur bei der Definition des Konstruktors der abgeleiteten Klasse steht. An der Deklaration des Konstruktors ändert sich nichts. Und übersehen ja nicht den unscheinbaren Doppelpunkt nach der Parameterklammer des Konstruktors der abgeleiteten Klasse!

Konstruktor bei mehrstufigen Ableitungen

Doch wie sieht im Fall einer mehrstufigen Ableitung (GBase -> Frame -> MyFrame) die Definition des Konstruktors? Muss der Konstruktor vom MyFrame sowohl den Konstruktor von Frame wie auch den Konstruktor von GBase aufrufen?

Nein, so schlimm wird's dann doch nicht! Dies würde sonst sehr schnell zu recht unübersichtlichen Konstruktoren in abgeleiteten Klassen führen. Der Konstruktor von MyFrame muss (und darf) nur den Konstruktor seiner Basisklasse aufrufen, also den von Frame. Und der Konstruktor von Frame ruft dann wiederum den seiner Basisklasse, also GBase, auf. Im Beispiel sehen Sie die prinzipiellen Definitionen aller drei Konstruktore. Beim Aufruf des Konstruktors der Klasse Frame wird im Beispiel für den letzten Parameter eine Konstante übergeben.


// Konstruktor der Klasse GBase
GBase::GBase(short x, short y, short w, short h)
{
    ....
};
// Konstruktor der Klasse Frame
Frame::Frame(short x, short y, short w, short h, short lCol):
      GBase(x, y, w, h)
{
    ....
}
// Konstruktor der Klasse MyFrame
MyFrame::MyFrame(short x, short y, short w, short h, short any):
     Frame(x, y, w, h, 0xFF)
{
    ....
}

Dieser Aufrufmechanismus funktioniert sogar auch dann, wenn eine Basisklasse selbst keinen Konstruktor oder 'nur'  den Standard-Konstruktor besitzt. In diesem Fall braucht der Konstruktor der abgeleiteten Klasse gar nichts tun. Es wird immer automatisch der 'nächst höhere' Konstruktor aufgerufen.

Aufruf-Reihenfolge der Konstruktore

Wird nun ein Objekt einer abgeleiteten Klasse definiert, so läuft folgender Vorgang ab. Zuerst wird immer der Konstruktor der Basisklasse ausgeführt um sicher zu stellen, dass beim Eintritt in den Konstruktor der abgeleiteten Klasse alle Eigenschaften der Basisklasse bereits initialisiert sind.

Ist die Basisklasse selbst von einer anderen Klasse abgeleitet, so wird zuerst der Konstruktor deren Basisklasse ausgeführt. Im Beispiel sehen Sie die Klassen GBase, Frame und MyFrame mit den bereits bekannten Ableitungen. Da GBase die 'oberste' Basisklasse ist, wird auch deren Konstruktor als erstes ausgeführt. Danach wird der Konstruktor von Frame ausgeführt und ganz zum Schluss der Konstruktor der 'untersten' Klasse MyFrame.


class GBase
{
    ....
};
class Frame: public GBase
{
    ....
};
class MyFrame: public Frame
{
    ....
};

Aufruf-Reihenfolge der Konstruktoren:

1) Konstruktor von GBase
2) Konstruktor von Frame
3) Konstruktor von MyFrame

Aufruf-Reihenfolge der Destruktoren

Und was für die Konstruktore gilt, gilt auch für die Destruktoren. Nur ist hier die ganze Sache etwas einfacher, da ein Destruktor keine Parameter besitzt und auch (fast) niemals direkt aufgerufen wird. Beim Schreiben eines Destruktors müssen Sie also (zunächst einmal) keine Rücksicht darauf nehmen, ob die Klasse von einer anderen Klasse abgeleitet ist oder nicht. Wird ein Objekt, das in der Ableitungshierarchie ganz unten steht, entfernt, so wird zuerst dessen Destruktor ausgeführt. Anschließend wird der Destruktor seiner Basisklasse ausgeführt und dann der Destruktor der nächst höheren Basisklasse. Die Aufruf-Reihenfolge ist hier also genau umgekehrt wie bei den Konstruktoren.


class GBase
{
    ....
};
class Frame: public GBase
{
    ....
};
class MyFrame: public Frame
{
    ....
};

Aufruf-Reihenfolge der Konstruktoren:

1) Destruktor von MyFrame
2) Destruktor von Frame
3) Destruktor von GBase

Zugriff auf Basisklassen-Member

Doch nun genug abgeleitet, sehen wir uns jetzt an wie auf Member innerhalb von abgeleiteten Klassen zugegriffen wird.

Beginnen wir mit dem Zugriff auf die Member einer Basisklasse aus den Memberfunktionen einer abgeleiteten Klasse heraus. Wie Sie schon wissen, können abgeleitete Klassen auf alle nicht-private Member der Basisklasse zugreifen. Hierzu reicht es im Normalfall aus, wenn der entsprechende Membername angegeben wird. Nicht-private Member von Basisklassen verhalten sich für abgeleitete Klassen ja prinzipiell wie eigene Member. Im Beispiel wird aus der Memberfunktion DoAnything(...) der abgeleiteten Klasse auf die Eigenschaft xPos der Basisklasse zugegriffen und deren Memberfunktion SetSize(...) aufgerufen.


class GBase
{
  protected:
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
};
class Frame: public GBase
{
    ....
  public:
    Frame(...);
    void DoAnything()
};
// Zugriff auf Member der Basisklasse
void Frame::DoAnything()
{
    xPos -= 10;       // Dies sind Member der
    SetSize(100,100); // der Basisklasse!
    ....
}

In wie weiter der direkte Zugriff auf Basisklassen-Eigenschaften einer sauberen Programmierung entspricht wollen wir einmal offen lassen. Das Beispiel dient nur zur Demonstration, wie auf die Member einer Basisklasse zugegriffen werden kann.

Gleiche Member in Basis- und Sub-Klasse

Dieser direkte Zugriff funktioniert aber nur so lange, wie die abgeleitete Klasse keine Member mit gleichem Namen besitzt wie die Basisklasse. Besitzt die abgeleitete Klasse Member mit gleichen Namen wie Member in der Basisklasse, so wird standardmäßig immer das Member der eigenen (abgeleiteten) Klasse angesprochen. Um das Member der Basisklasse anzusprechen, ist vor dem Membernamen noch der Name der Basisklasse anzugeben, gefolgt vom Zugriffsoperator ::.


class GBase
{
  protected:
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
}; 
class Frame: public GBase
{
    ....
    short width;
  public:
    Frame(...);
    void DoAnything()
};
// Zugriff auf Member der Basisklasse
void Frame::DoAnything()
{
    width++;         // eigenes Member
    GBase::width++;  // Basisklassen-Member
    ....
}

Zugriff auf Basisklassen-Member über Objekte

Auch beim Zugriff über Objekte einer abgeleiteten Klassen auf die Basisklassen-Member reicht im Regelfall die Angabe des Membernamens aus. Der erste Zugriff unten über ein Objekt der Klasse Frame ruft die Memberfunktion SetPosition(...) der Klasse GBase auf. Auch wird die Memberfunktion SetSize(...) der Klasse GBase aufgerufen, wenn der Aufruf über ein Objekt der Klasse MyFrame erfolgt. Dieser Zugriffsmechanismus funktioniert also auch über mehrere Ableitungsebenen hinweg.


class GBase
{
  protected:
    short xPos, yPos;
    short width, height;
  public:
    void SetPosition(...);
    void SetSize(...);
};
class Frame: public GBase
{
    ....
};
class MyFrame: public Frame
{
    ....
};
// Objekt der abgeleiteten Klassen
Frame   frameObj;
MyFrame myFrameObj; 
// Zugriff auf public Member von GBase
frameObj.SetPosition(...);
myFrameObj.SetSize(...);

Verdeckte Basisklassen-Member

Im Zusammenhang mit Aufrufen von Basisklassen-Memberfunktionen soll noch auf eine kleine Stolperfalle hingewiesen werden. Sehen Sie zunächst einmal die nachfolgend dargestellten Klassen an. Sowohl Frame wie auch MyFrame besitzen jeweils eine Memberfunktion Move(...). Die Memberfunktion der Klasse Frame besitzt jedoch einen Parameter und die der Klasse MyFrame zwei Parameter.


class Frame: public GBase
{
  public:
    void Move(short x);
    ....
};
class MyFrame: public Frame
{
  public:
    void Move(short x, short y);
    ....
};

Aufbauend auf diesen Klassendefinitionen sehen wir uns einige Aufrufe der Memberfunktionen Move(...) an. Die Interpretation der ersten beiden Aufrufe im nachfolgenden Beispiel  sollten Ihnen jetzt keine mehr Schwierigkeiten bereiten. Hier wird einfach die zum Objekt gehörige Memberfunktion aufgerufen. 

Einen Fehler verursacht dagegen der dritte Aufruf. Was hier versucht wird, ist die Memberfunktion Move(...) der Klasse Frame über ein MyFrame Objekt aufzurufen, was aber fehlschlägt. Eine Memberfunktion der abgeleiteten Klasse verdeckt immer gleichnamige Memberfunktionen (und Eigenschaften) ihrer Basisklasse(n). Wollen Sie trotzdem auf die Memberfunktion der Move(...) der Klasse Frame über ein MyFrame Objekt zugreifen, so müssen Sie, wie unten im vierten Aufruf angegeben, nach dem Punktoperator zunächst den Klassennamen, dann den Zugriffsoperator :: und erst danach die Memberfunktion angeben.


// Objekts der abgeleiteten Klassen
Frame   frameObj;
MyFrame myFrameObj; 
// Aufrufe der Memberfunktion Move(...)
frameObj.Move(10);            // Move() der Klasse Frame
myFrameObj.Move(10,20);       // Move() der Klasse MyFrame
myFrameObj.Move(10);          // Fehler!
myFrameObj.Frame::Move(10);   // Move() der Klasse Frame!

Basisklassenzeiger

So, jetzt wissen schon fast alles über 'einfache' Ableitungen. Im Zusammenhang mit abgeleiteten Klassen sei an dieser Stelle vorab noch auf folgenden wichtigen Sachverhalt hingewiesen:

In einem Zeiger vom Typ der Basisklasse kann auch Zeiger vom Typ einer abgeleiteten Klasse abgelegt werden.

Diese Eigenschaft ist unten dargestellt. Sie spielt später in der Lektion über virtuelle Memberfunktionen noch eine entscheidende Rolle. Die selbstverständliche Entfernung der erzeugten Objekte ist im Beispiel nicht explizit aufgeführt!


class GBase
{
    ....
};
class Frame: public GBase
{
    ....
};
class MyFrame: public Frame
{
    ....
}
int main()
{
    GBase *pBase;
    pBase = new Frame(...);     // Adresse Frame Objekt zuweisen
    ....
    pBase = new MyFrame(...);   // Adresse MyFrame Objekt zuweisen!
}

Damit wäre diese doch recht umfangreiche Lektion fast beendet. Zum Schluss sehen wir uns nochmals genauer an, wie Sie eine vorgegebene Klasse mittels Ableitung anpassen können.

Erweitern von Klassen durch Ableitung

Wenn Sie eine Klassenbibliothek käuflich erwerben, erhalten Sie nur im Ausnahmefall den Quellcode der Bibliothek. In der Regel erhaltenen Sie den übersetzen Quellcode in Form einer Objektdatei xxx.obj oder Bibliotheksdatei xxx.lib sowie die dazugehörige Header-Datei xxx.h. Wenn Sie nun eine Klasse aus der Klassenbibliothek einsetzen, müssen Sie zum  einen die Header-Datei  xxx.h in Ihren Quellcode einbinden und zum anderen den Objektcode xxx.obj bzw. xxx.lib zu Ihrem Programm dazu linken (hier nicht dargestellt).


// Modul myprog.cpp welches die Klasse OClass verwendet
#include "oclass.h"       // Einbinden der Headerdatei
....
int main()
{
    OClass theObject;     // OClass Objekt definieren
    ...
}

Um jetzt eine vorgegebene Klasse anzupassen (hier OClass), gehen Sie am Besten wie folgt vor:

1. Erzeugen Sie eine neue Header-Datei für die Klassendefinition Ihrer neuen Klasse (hier MyClass.h).


#ifndef MYCLASS_H
#define MYCLASS_H
....
#endif

2. Binden Sie in diese neue Header-Datei die Header-Datei der zu erweiternden Klasse ein (hier oclass.h).


#ifndef MYCLASS_H
#define MYCLASS_H
#include "oclass.h"
....
#endif

3. Leiten Sie dann in der Header-Datei eine neue Klasse von der zu erweiternden Klasse ab und erweitern Sie die neue Klasse entsprechend ihren Anforderungen.


#ifndef MYCLASS_H
#define MYCLASS_H
#include "oclass.h"
class MyClass: public OClass
{
    ....
};
#endif

4. Erstellen Sie eine Quellcode-Datei, in der Sie die Memberfunktionen Ihrer neuen Klasse definieren. Am Besten geben Sie der Quellcode-Datei den gleichen Namen wie der Header-Datei, jetzt natürlich aber mit der Extension cpp. Binden Sie die Header-Datei mit ihrer neuen Klassendefinition ein.


#include "myclass.h"
.... // hier die Memberfunktionen der
.... // neuen Klasse definieren

5. Binden Sie dann anstelle der ursprünglichen Header-Datei Ihre neue Header-Datei im Programm ein und fügen Sie die Quellcode-Datei Ihrer neuen Klasse (myclass.cpp)  zum Projekt hinzu (hier nicht dargestellt).


#include "myclass.h"
....
int main()
{
    MyClass myObj;
    ....
}

So einfach geht's, wenn man es weiß.

Beispiel und Übung

Beispiel:

Als Basisklasse in diesem Beispiel fungiert die Klasse Window. Sie enthält die Grundeigenschaften eines Fensters. Über Memberfunktionen der Klasse kann ein Fenster verschoben (MoveWindow(...)) und dargestellt werden (Draw(....)). Zusätzlich wird der Zuweisungsoperator private überladen damit Fensterobjekte nicht einander zugewiesen werden können.

Diese Grundeigenschaften der Klasse Window werden durch die Klasse FrameWin für die Darstellung eines Fensters mit einem farbigen Rahmen erweitert. Für die Farbauswahl des Rahmens wird ein enum-Datentyp verwendet. Beachten Sie bitte, dass der enum-Datentyp public sein muss, aber die dazugehörige enum-Variable als private definiert ist. Dies ist notwendig, da bei der Definition des Rahmenfensters die Rahmenfarbe als enum-Parameter mit angegeben wird und damit der Zugriff auf die enum-Konstanten erforderlich ist. Der Konstruktor von CFrameWin ruft, wie in der Lektion beschrieben, den der Klasse Window auf. Hierbei werden die Parameter für die Fenstergröße als Konstanten übergeben, d.h. ein Rahmenfenster besitzt im Beispiel immer die feste Größe 640x480. Sehen Sie sich auch die Memberfunktion Draw(...) des Rahmenfensters genauer an. Dort wird die Memberfunktion Draw(...) der Klasse Window aufgerufen. Sie müssen bei diesem Aufruf unbedingt die Klasse mit angeben, da sich ansonsten die Memberfunktion Draw(...) der Klasse CFrameWin selbst wieder aufrufen würde (Stichwort: Rekursion).

Im Programm werden dann Objekte von jedem Fenstertyp definiert und 'dargestellt'. Anschließend wird das Rahmenfenster mittels MoveWindow(...) verschoben was zum Aufruf der entsprechenden Memberfunktion in der Basisklasse Window führt.

Pos (10,10), Grösse (800,600)
Titel: Normales Fenster
Pos (40,40), Grösse (640,480)
Titel: Rahmenfenster
Rahmenfarbe: blau

Pos (0,0), Grösse (640,480)
Titel: Rahmenfenster
Rahmenfarbe: blau


// Beispiel zu abgeleiteten Klassen

// Zuerst Dateien einbinden
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

// Definition der allg. Fensterklasse
class Window
{
    short xPos, yPos;         // Fensterposition
    short width, height;      // Fenstergrösse
    string title;             // Fenstertitel
    // Zuweisungsoperator als private definieren damit
    // Objekte nicht einander zugewiesen werden können
    Window& operator=(const Window&)
    {return *this;};
public:
    // ctor
    Window(short x, short y, short w, short h,
           const char* const pT);
    // Memberfunktionen zum Verschieben und Zeichnen des Fensters
    void MoveWindow(short x, short y);
    void Draw() const;
};
// Defintion der Memberfunktionen
// Konstruktor
Window::Window(short x, short y, short w, short h,
               const char* const pT):
        xPos(x), yPos(y), width(w),
        height(h), title(pT)
{}
// Verschiebt Fenster
inline void Window::MoveWindow(short x, short y)
{
    xPos = x; yPos = y;
}
// Zeichnet Fenster
void Window::Draw() const
{
    cout << "Pos (" << xPos << ',' << yPos << "), Grösse (" <<
            width << ',' << height << ")\n";
    cout << "Titel: " << title << endl;
}

// Abgeleitete Klasse für Rahmenfenster
class FrameWin: public Window
{
public:
    // enums für Rahmenfarbe, müssen public sein da
    // sie im ctor benötigt werden
    enum eFColor{ROT=0xFF0000,GRUEN=0x00FF00,BLAU=0x0000FF};
private:
    eFColor frameColor;        // Rahmenfarbe
public:
    // ctor
    FrameWin(short x, short y,
             const char* const pT, eFColor eFC);
    // Memberfunktion zum Zeichnen des Fensters
    void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
FrameWin::FrameWin(short x, short y,
                   const char* const pT, eFColor eFC)
    :Window(x, y, 640, 480, pT), frameColor(eFC)
{}
// Zeichnet Fenster
void FrameWin::Draw() const
{
    // 'Normales' Fenster zeichnen
    Window::Draw();
    // Rahmenfarbe darstellen
    cout << "Rahmenfarbe: ";
    switch (frameColor)
    {
    case ROT:
        cout << "rot";
        break;
    case BLAU:
        cout << "blau";
        break;
    case GRUEN:
        cout << "grün";
    }
    cout << endl << endl;
}

// main() Funktion
int main()
{
    // Ein normales Fenster und ein Rahmenfenster erstellen
    Window myWin(10,10,800,600,"Normales Fenster");
    // Beachten Sie die Angabe des enum-Parameter (letzter Parameter)!
    FrameWin myFrameWin(40,40,"Rahmenfenster",FrameWin::BLAU);

    // Beide Fenster darstellen
    myWin.Draw();
    myFrameWin.Draw();
    // Rahmenfenster verschieben
    // MoveWindow(...) ist Memberfunktion von Window!
    myFrameWin.MoveWindow(0,0);
    // Rahmenfenster erneut darstellen
    myFrameWin.Draw();
}

Übung:

In dieser Übung sollen Sie die folgende Klasse Address erweitern:


// Ausgangslisting für Übung zu abgeleiteten Klassen

// Zuerst Dateien einbinden
#include <iostream>
#include <fstream>
#include <string>

using std::cout;
using std::endl;
using std::string;

// Definition der Adressenklasse
// Enthält der Einfachheit halber nur einen Namen
class Address
{
    string name;
public:
    Address();
    friend std::ostream& operator << (std::ostream& OS, const Address& obj);
    friend std::istream& operator >> (std::istream& IS, Address& obj);
};
// Definition der Memberfunktionen
// Standard-ctor, wird für Objektfeld immer benötigt
Address::Address()
{}
// Gibt Adresse aus
std::ostream& operator << (std::ostream& os, const Address& obj)
{
    os << "Name: ";
    // Falls kein Name vorhanden, irgendwas ausgeben
    if (obj.name.length() == NULL)
        os << "unbekannt!\n";
    // sonst Namen ausgeben
    else
        os << obj.name << endl;
    return os;
}
// Liest Adresse ein
std::istream& operator >> (std::istream& is, Address& obj)
{
    // Ganze Zeile einlesen
    getline(is,obj.name);
    return is;
}

// Konstanten für Dateiname und Grösse des Objektfeldes
const char* const pFILENAME = "PERS.DAT";
const int ARRAYSIZE=3;

// main() Funktion
int main()
{
    int index;             // Schleifenindex
    Address *pAddr;        // Zeiger für Objektfeld

    // Objektfeld erstellen
    pAddr = new Address[ARRAYSIZE];

    // Nun die Namen von der Tastatur einlesen
    cout << "Bitte " << ARRAYSIZE << " Namen eingeben:\n";
    for (index=0; index<ARRAYSIZE; index++)
    {
        cout << index+1 << ". Name:";
        // Adresse einlesen
        std::cin >> pAddr[index];
    }

    // Zur Kontrolle die eingelesenen Daten ausgeben
    cout << "\nGespeicherte Daten:\n";
    for (index=0; index<ARRAYSIZE; index++)
        // Adresse ausgeben
        cout << pAddr[index];

    // Nun Daten in Datei ablegen
    cout << "Speicher Daten jetzt in Datei " << pFILENAME << endl;
    // Datei öffnen
    std::ofstream outFile;
    outFile.open(pFILENAME);
    // Falls Datei erfolgreich geöffnet
    if (outFile)
    {
        // Daten ablegen
        for (index=0; index<ARRAYSIZE; index++)
            outFile << pAddr[index];
        outFile.close();
    }
    else
        cout << "Fehler beim Öffnen der Datei!\n";

    // Objektfeld löschen
    delete [] pAddr;
}

Der Einfachheit halber enthält die Klasse im Beispiel nur einen Namen als Adresse. Zum Einlesen und Ausgeben der Adressdaten stehen die beiden überladenen Operatoren << und >> zur Verfügung.

In main() wird dann ein Objektfeld für drei Adresseinträge dynamisch angelegt. Anschließend werden die Adressen (hier nur der Name) von der Tastatur mittels des überladenen Operators >> eingelesen. Die eingelesenen Daten werden dann über den Operator << auf die Standardausgabe ausgegeben. Danach werden die Adressdaten noch einer Datei abgelegt. Auch hierfür wird wieder der überladene Operator << aufgerufen, wobei als Stream nun anstelle von cout der Dateistream verwendet wird. Zum Schluss muss das Objektfeld mit den Adressdaten dann noch entfernt werden.

Ihre Aufgabe ist es nun eine weitere Klasse Student zu schreiben. Diese Klasse soll die Ausgangklasse Address um eine Eigenschaft zur Aufnahme eines von einem Studenten belegten Kurses erweitern. Der Kurs ist ebenfalls als string abzulegen. Und auch für diese Klasse ist für die Ein- und Ausgabe der überladene Operator >> bzw. << zu verwenden.

Sie müssen innerhalb der überladenen Operatoren << und >> der Klasse Student die überladenen Operatoren für die Basisklasse Address aufrufen. Wie Sie einen überladenen Operator direkt aufrufen, das können Sie sich hier nochmals ansehen.

Schreiben Sie dann main() so um, dass anstelle eines Address Objektfeldes ein Student Objektfeld dynamisch erstellt wird. Der Rest des Programms soll in seiner Funktionalität nicht verändert werden, d.h. Sie sollen zunächst drei Studentendaten von der Tastatur einlesen, diese dann auf der Standardausgabe ausgeben und zum Schluss noch in eine Datei ablegen.

In der nachfolgenden Programmausgabe sind die Eingaben farblich hervorgehoben.

Der Inhalt dieser Übung steht zwar etwas im Widerspruch zur obigen Aussagen, dass eine Klasse in der Regel von einer anderen Klasse nur dann abgeleitet wird, wenn eine is-a Beziehung zwischen ihnen besteht. So 'enthält' ein Student eine Adresse und ist keine Adresse, jedoch sollen Sie zu Übungszwecken ausnahmsweise die Klasse Student von der vorgegeben Klasse Address ableiten.

Bitte Studenten-Daten eingeben.
Zuerst den Namen und dann den Kurs.
Beide Angaben bitte in getrennten Zeilen!
1. Name:Gustav Gans
Mathe
2. Name:Donald Duck
Wirtschaft
3. Name:Daisy Duck
Informatik

Gespeicherte Daten:
Name: Gustav Gans
Kurs: Mathe
Name: Donald Duck
Kurs: Wirtschaft
Name: Daisy Duck
Kurs: Informatik
Speichere Daten jetzt in Datei PERS.DAT

Lösung ansehen!