Objektorientiertes Programmieren mit C++

Martin Kompf

Inhalt

Objekte

C++ ist nicht nur das bessere C, sondern bietet dem Entwickler die Mglichkeit, objektorientiert zu programmieren. Dies verlangt neben dem Erlernen neuer Sprachelemente auch eine neue "objektorientierte" Denkweise. In Form eines Tutorials soll hier mit dieser Denkweise vertraut gemacht werden.

Herkmmliche Softwareentwicklung bestand oftmals darin, zur Lsung eines vorgegebenen Problems Algorithmen zu entwerfen und diese in Prozeduren zu gieen, die in einer Programmiersprache - wie zum Beispiel C - formuliert sind. Man spricht daher auch von prozeduraler Programmierung.

Betrachtet man jedoch die reale Welt, so stellt man fest, da die Dinge sich hier nicht in einer abstrakten prozeduralen Weise bewegen. Diesen Bruch zwischen realer Welt und Softwareentwicklung versucht der objektorientierte Ansatz zu berwinden. Analysiert man seine materielle Umgebung, so stellt man fest, da diese im wesentlichen aus Objekten besteht, die in verschiedener Art und Weise miteinander agieren. So knnte es in unserer unmittelbaren Umgebung gerade ein Objekt Fahrrad geben.

Objekt Nimmt man das Fahrrad noch weiter unter die Lupe, erkennt man, da dieses Fahrrad bestimmte Eigenschaften bzw. Attribute (wie Farbe, Gre, momentane Geschwindigkeit) besitzt und da es Methoden gibt, diese Eigenschaften zu verndern (z.B. die Methode "Tritt schneller", die zu einer Erhhung der Geschwindigkeit fhren wird oder die Methode "Bremse", die das Gegenteil bewirken sollte).

Transformiert man diese Erkenntnis aus der realen in die Software-Welt, so kann man formulieren:

Ein Software-Objekt ist ein Bndel aus Attributen und darauf bezogenen Methoden.

Klassen

Klassen als Bauplan

Wenn wir verschiedene Fahrrad-Objekte der realen Welt genauer betrachten, so stellen wir fest, da alle diese Fahrrad-Objekte hnlich sind: Alle besitzen Bremsen, alle haben irgendeine Farbe usw. Es mu also einen Bauplan geben, der beschreibt, wie ein Fahrrad grundstzlich auszusehen hat. Alle Fahrrad-Objekte sind nach diesem Bauplan erstellt worden - deswegen ist es uns auch mglich, ein beliebiges Fahrrad zu fahren, wenn wir einmal das Fahrradfahren gelernt haben! Wieder bertragen wir diese Erkenntnis in die Softwarewelt:

Eine Klasse ist ein Bauplan, welcher die Attribute und Methoden definiert, die alle Objekte einer bestimmten Art besitzen.

class StockItem Die Abbildung zeigt eine Klasse StockItem in der sogenannten UML (unified modelling language) Notation. Diese Klasse knnte in einem Programm zur Aktienanalyse verwendet werden. Ein solches Programm mu viele verschiedene Aktien-Objekte verwalten knnen. Damit dies effektiv geschehen kann, sollten alle diese verschiedenen Objekte jedoch nach einem einheitlichen Bauplan - der Klassendefinition - erstellt werden.

Der Name der Klasse steht in der UML-Notation im oberen Drittel des Rechtecks. Im mittleren Drittel stehen die Attribute. Unsere Beispielklasse StockItem definiert die zwei Attribute m_name und m_value, d.h. den Namen der Aktie und den momentanen Kurswert. Das Minuszeichen vor den Attributen bedeutet, das diese private Mitglieder der Klasse sind, d.h. von auen kann nicht direkt auf sie zugegriffen werden.

Im unteren Drittel stehen die Methoden der Klasse StockItem. Das Pluszeichen vor ihnen zeigt, da sie public sind, d.h. die Methoden drfen von anderen Objekten aufgerufen werden. Damit zeigt sich das Prinzip der Kapselung: Anstatt auf das Attribut m_value direkt zuzugreifen, mssen andere Objekte die Zugriffsmethoden setValue() und getValue() benutzen! So hat der Entwickler der Klasse die Mglichkeit, in setValue() noch zustzliche Abfragen, z.B. bezglich der Gltigkeit des Parameters, einzubauen.

Aufmerksamen Lesern wird bei Betrachtung der Abbildung eine Erweiterung von C++ gegenber C nicht entgangen sein: Funktionen werden in C++ nicht nur - wie in C - durch den Namen, sondern durch Namen und Parameteranzahl und -typ unterschieden! So kann es die Methode setValue() zweimal geben: Einmal mit einem Parameter vom Typ double und mit einem vom Typ const char*. Der Compiler erkennt beim Aufruf dieser Methode anhand des Parametertyps selbstttig, welche Variante er verwenden mu.

Klassendefinition

Es ist angebracht, C++-Klassendefinitionen generell in Headerdateien (das sind die mit der Extension .h) vorzunehmen. Guter Programmierstil ist dabei, jeweils eine Headerdatei je Klasse vorzusehen und diese auch wie die Klasse zu benennen.

Die Klassendefinition fr StockItem wird also in der Headerdatei mit dem Namen StockItem.h vorgenommen:

/*  1 */  #ifndef StockItem_h
/*  2 */  #define StockItem_h 1
/*  3 */
/*  4 */  class StockItem {
/*  5 */
/*  6 */  public:
/*  7 */      // ctor
/*  8 */      StockItem( const char* name = "", double val = 0.0);
/*  9 */
/* 10 */      // copy ctor
/* 11 */      StockItem( const StockItem &right);
/* 12 */      // dtor
/* 13 */      virtual ~StockItem();
/* 14 */      // assignment operator
/* 15 */      const StockItem& operator=(const StockItem &right);
/* 16 */
/* 17 */      // public member functions
/* 18 */      virtual const char* getName() const;
/* 19 */      virtual void setValue( double val);
/* 20 */      virtual void setValue( const char* val);
/* 21 */      virtual double getValue() const;
/* 22 */
/* 23 */  private:
/* 24 */      // private member variables
/* 25 */      char *m_name;
/* 26 */      double m_value;
/* 27 */  };
/* 28 */
/* 29 */  #endif 

Die Klassendefinition beginnt mit dem Schlsselwort class gefolgt vom Namen der Klasse (Zeile 4). Die eigentliche Definition wird von geschweiften Klammern { } umschlossen. Die Schlsselwrter public und private trennen die im letzten Abschnitt besprochenen ffentlichen (Zeilen 7 bis 21) und privaten (Zeilen 24 bis 26) Bereiche der Definition ab. Es gibt auerdem noch die Mglichkeit, mittels protected Variablen und Methoden zu deklarieren, die ausschlielich abgeleiteten Klassen zur Verfgung stehen sollen. Dazu mehr spter mehr beim Thema Vererbung.

Die Deklaration der ffentlichen Methoden (Zeilen 18 bis 19) und der privaten Variablen (Zeilen 25 und 26) erfolgt genauso wie eine normale Deklaration von Funktionen bzw. Variablen in C. Bei den Funktionen fllt die Verwendung der Spezifikation virtual auf. Dies ist wiederum fr das Thema Vererbung wichtig und wird spter besprochen. Man macht jedoch nicht viel verkehrt, wenn man direkt jeder Funktion von vorneherein diese Spezifikation verpat.

Auf den Zeilen 7 bis 15 werden Constructor, Copy-Constructor, Destructor und Assignment Operator deklariert. Um den Sinn dieser Elemente verstehen zu knnen, mssen wir zunchst auf den Objektlebenszyklus eingehen

PS: Noch ein kleiner Tip: Der Compiler produziert die wunderlichsten Fehlermeldungen, wenn der Programmierer das Semikolon am Ende der Klassendefinition (Zeile 27) vergit...

Objektlebenszyklus

Der Lebenszyklus eines C++-Objektes besteht aus den Abschnitten

  1. Erzeugung
  2. Benutzung
  3. Zerstrung

Erzeugen eines Objektes

Das Erzeugen eines Objektes erfogt durch Aufruf des Konstruktors (kurz ctor) des Objektes. Dies kann sowohl statisch durch Variablendeklaration als auch dynamisch mittels des new Operators geschehen:

StockItem bay;
// default constructor, statisch

StockItem dte( "Deutsche Telekom AG", 50.34);
// spezieller ctor, statisch

StockItem *bas = new StockItem( "BASF", 120.34);
// spezieller ctor, dynamisch 
// der für bas nötige Speicherplatz wird auf dem Heap alloziert

Benutzen eines Objektes

Ein Objekt wird benutzt, indem eine seiner Methoden aufgerufen wird:

dte.setValue( 52.80);
// ruft Methode StockItem::setValue() des Objektes dte auf

a = bas->getValue();
// ruft Methode StockItem::getValue() des Objektes bas auf 

Auerdem kann das komplette Objekt als Parameter an eine Funktion bergeben oder von dieser per return zurckgegeben werden. Erfolgt die Wertbergabe dabei per Value, so wird implizit der Copy Konstruktor des Objektes aufgerufen:

StockItem cnv( StockItem x) {
    // ...

    return x2;
    // return per value: ruft den Copy Constructor von x2 auf
}

StockItem bas( "BASF", 120.34);
cnv( bas);
// parameter per value: ruft den Copy Constructor von bas auf

StockItem bas2 = bas;
// explizite Kopie: ruft den Copy Constructor von bas2 auf 

Ebenso kann ein Objekt komplett einem anderen bereits existierenden Objekt zugewiesen werden, wenn dieses den gleichen Typ oder den Typ einer Basisklasse hat. Dann wird der Assignment Operator des Objektes aufgerufen:

StockItem x;
StockItem y( "ABC", 22);

x = y;
// ruft den Assignment Operator von x auf

Zerstren eines Objektes

Wird ein Objekt nicht mehr bentigt, so sollte es zerstrt werden, damit es keinen Speicherplatz mehr verbraucht. Bei der Zerstrung eines Objektes wird automatisch sein Destruktor (kurz dtor) aufgerufen. Im Falle eines dynamisch per new erzeugten Objektes wird dies mit dem Operator delete bewerkstelligt. Ein statisch erzeugtes Objekt wird automatisch zerstrt, wenn bei der Programmausfhrung der Gltigkeitsbereich der statischen Deklaration verlassen wird:

delete bas;
// der Destructor des Objektes bas wird aufgerufen und anschließend
// der durch bas belegte Speicherplatz freigegeben
if (x) {
    StockItem bmw;
    // ...
} // der dtor des statisch angelegten Objektes bmw wird hier am Blockende
// wegen des Verlassens des Gültigkeitsbereiches der Deklaration aufgerufen

Beachte:Der Destruktor eines Objektes wird in der Regel niemals direkt aufgerufen, sondern sein Aufruf erfolgt wie oben dargestellt implizit durch Verwendung von delete oder beim Verlassen des Gltigkeitsbereiches!

Objektimplementierung

Was uns nun noch zum ersten vollstndigen C++-Programm fehlt, ist die Implementierung der einzelnen Methoden des Objektes StockItem.

Die Implementierung wird vorzugsweise in einer Datei mit Namen StockItem.cpp vorgenommen. Zu Beginn wird die Headerdatei StockItem.h inkludiert:

/*  1 */  #include "StockItem.h"
/*  2 */  #include <cstdlib>
/*  3 */  #include <cstring>
/*  4 */ 
/*  5 */  StockItem::StockItem( const char* name /* = "" */, double val /* = 0.0 */)
/*  6 */  {
/*  7 */      m_name = new char[strlen(name)+1];
/*  8 */      strcpy( m_name, name);
/*  9 */      m_value = val;
/* 10 */  }
/* 11 */
/* 12 */  StockItem::StockItem( const StockItem &right)
/* 13 */
/* 14 */  {
/* 15 */
/* 16 */      m_name = new char[strlen( right.m_name)+1];
/* 17 */      strcpy( m_name, right.m_name);
/* 18 */      m_value = right.m_value;
/* 19 */  }
/* 20 */
/* 21 */  StockItem::~StockItem()
/* 22 */  {
/* 23 */      delete[] m_name;
/* 24 */  }
/* 25 */
/* 26 */  const StockItem& StockItem::operator=(const StockItem &right)
/* 27 */  {
/* 28 */      // handle self assignment
/* 29 */      if (this != &right) {
/* 30 */          delete[] m_name;
/* 31 */          m_name = new char[strlen( right.m_name)+1];
/* 32 */          strcpy( m_name, right.m_name);
/* 33 */          m_value = right.m_value;
/* 34 */      }
/* 35 */
/* 36 */      return *this;
/* 37 */  }
/* 38 */
/* 39 */  const char* StockItem::getName() const
/* 40 */  {
/* 41 */      return m_name;
/* 42 */  }
/* 43 */
/* 44 */  void StockItem::setValue( double val)
/* 45 */  {
/* 46 */      m_value = val;
/* 47 */  }
/* 48 */
/* 49 */  void StockItem::setValue( const char* val)
/* 50 */  {
/* 51 */      m_value = atof( val);
/* 52 */  }
/* 53 */
/* 54 */  double StockItem::getValue() const
/* 55 */  {
/* 56 */      return m_value;
/* 57 */  } 

Die Implementierung der ffentlichen Methoden fr das Setzen und Auslesen der Werte der Attribute in den Zeilen 39 bis 57 enthlt nichts besonderes, der Code drfte fr sich sprechen.

Interessanter ist der Konstruktor (Zeilen 5 bis 10): Da wir den Namen des StockItem in einer C-Zeichenkette vom Typ char* speichern wollen, mu zuerst der hierfr notwendige Speicherplatz allokiert werden (Zeile 7). Da wir in C++ programmieren, verwenden wir nicht das aus der C-Welt vertraute malloc, sondern den C++-Operator new[].

hnlich verhlt sich die Sache beim Copy Konstruktor (Zeilen 12 bis 19): Hier ist das einzige Argument immer eine Referenz auf ein Objekt des gleichen Typs. Dessen Membervariablen mssen in die eigenen Variablen kopiert werden, damit eine 1:1 Kopie des Objektes entsteht. Wir erinnern uns: Der Copy Konstruktor wird bei bergabe von Objekten per Value an oder aus Funktionen oder beim expliziten Kopieren verwendet.

Noch interessanter gestaltet sich die Sache beim Assignment Operator (Zeilen 26 bis 37): Hier existiert ja schon ein Objekt x, in das ein zweites Objekt y hineinkopiert wird! Also mu zuerst der von x belegte Speicherplatz freigegeben werden (Zeile 30), bevor die Variablen kopiert werden knnen. In der C++-Sprache bezeichnet man brigens unser Objekt x als this. Der Bezug auf das eigene Objekt this ist so wichtig, da this sogar ein reserviertes Wort in C++ ist und immer innerhalb von Objektfunktionen zur Verfgung steht. Der Assignment Operator gibt einen Zeiger auf das Objekt selbst (also auf this) zurck, schlielich wird er ja in Zuweisungen wie

StockItem x;
StockItem y( "ABC", 22);

x = y;

verwendet.

brigens hindert niemand den Programmierer, statt x = y die Zuweisung x = x hinzuschreiben! Dann haben wir den Fall des Self Assignments vorliegen: Ein Objekt wird sich selbst zugewiesen. Dann darf der Assignment Operator auch nichts weiter tun. Die Abfrage, ob Self Assignment vorliegt, findet in Zeile 29 statt.

Der mittels new[] vom Betriebssystem whrend der Objektkonstruktion explizit angeforderte Speicherplatz mu beim Zerstren des Objektes auch wieder ausdrcklich freigegeben werden. Wir erinnern uns: Beim Zerstren des Objektes wird automatisch sein Destruktor ausgefhrt. Deswegen wird im Destruktor (Zeilen 21 bis 24) der fr m_name angeforderte Speicherbereich mittels delete[] freigegeben.

Vererbung

Konzept

Vererbung erlaubt die Definition neuer Klassen auf der Basis von bestehenden Klassen. Dies ist ein grundlegendes Konzept objektorientierten Designs. Vererbung Weiter oben wurde der Begriff der Klasse als eine Art Bauplan fr Objekte erklrt. Anhand des dort verwendeten Beispiels "Fahrrad" lassen sich weitere Parallelen zur realen Welt ziehen: Es fllt auf, da es hier verschiedene Arten von Fahrrdern gibt: Rennrder, Montainbikes, Treckingrder und das gute alte Hollandrad. Warum sind alle diese verschiedenen Rder fr uns als Fhrrder erkennbar? Weil sie gewisse gemeinsame Eigenschaften haben: Alle haben zwei Rder, einen Lenker und lassen sich durch Tritt auf die Pedale fortbewegen. Zustzlich zu diesen Gemeinsamkeiten bringen sie aber auch neue Eigenschaften ein: Montainbikes und Rennrder haben jeweils eine Gangschaltung, unterscheiden sich aber durch die Art der Bereifung.

In objektorientierter Sprache knnte man also sagen: Die Klassen Montainbikes, Rennrder und Hollandrder erben von der Klasse Fahrrder gemeinsame Eigenschaften und fgen zustzliche hinzu. Allgemein gilt:

Es ist wichtig zu verstehen, da Vererbung nur in eine Richtung luft: Ein Rennrad ist zwar immer auch ein Fahrrad, aber nicht jedes Fahrrad ist automatisch ein Rennrad. Wenn in C++ ein Objekt a der Klasse A definiert wird und die Klasse A von B abgeleitet ist, dann kann a jederzeit per Cast in ein Objekt vom Typ B umgewandelt werden. Die Umkehrung gilt nicht: Ein Objekt b der Klasse B kann in diesem Fall nicht in den Typ A umgewandelt werden!

Vererbung in C++

Die von uns bereits definierte und benutzte Klasse StockItem erlaubt die Speicherung eines Namens und eines dazugehrenden Wertes und ist zur Darstellung von Aktienkursen gedacht. Wer sich mit dieser Materie schon beschftigt hat, der wei, da zu einer Aktie noch viele Informationen mehr gespeichert werden knnen. So gibt es neben dem (Tages- oder Wochen-)Schlusskurs (Close) noch Erffnungs- (Open), Hchst- (High) und Tiefstkurs (Low). Wir wollen daher nun StockItem um die Mglichkeit erweitern, zustzlich auch den Erffnungskurs abfragen und setzen zu knnen.

Diese Erweiterung soll jedoch nicht durch Verndern der existierenden Klasse StockItem erfolgen - diese Klasse wird bereits in vielen Softwareprojekten benutzt und eine nderung ihrer Funktionalitt knnte unter Umstnden bse Auswirkungen haben. Auch wollen wir das Rad nicht vllig neu erfinden - vorhandener Code soll so weit wie mglich wiederverwendet werden. Dies alles erreichen wir dadurch, da wir eine neue Klasse StockItemOC definieren, die von der vorhandenen Klasse StockItem abgeleitet ist:

/*  1 */  #ifndef StockItemOC_h
/*  2 */  #define StockItemOC_h 1
/*  3 */
/*  4 */  class StockItemOC : public StockItem {
/*  5 */
/*  6 */  public:
/*  7 */      // ctor
/*  8 */      StockItemOC( const char* name = "", double open = 0.0, double close = 0.0);
/*  9 */
/* 10 */      // copy ctor
/* 11 */      StockItemOC( const StockItemOC &right);
/* 12 */      // dtor
/* 13 */      virtual ~StockItemOC();
/* 14 */      // assignment operator
/* 15 */      const StockItemOC& operator=(const StockItemOC &right);
/* 16 */
/* 17 */      // public member functions
/* 18 */      virtual void setValue( double val);
/* 19 */      virtual double getValue() const;
/* 20 */      virtual void setOpen( double val);
/* 21 */      virtual double getOpen() const;
/* 22 */      virtual void setClose( double val);
/* 23 */      virtual double getClose() const;
/* 24 */
/* 25 */  private:
/* 26 */      // private member variables
/* 27 */      double m_open;
/* 28 */      double m_close;
/* 29 */  };
/* 30 */
/* 31 */  #endif

Die einzige Erweiterung gegenber der uns bekannten Klassendefinition ist, da zu Beginn in Zeile 4 die Basisklasse nach dem Doppelpunkt und dem Schlsselwort public angegeben wird.

Welche Auswirkung hat diese Vererbungsbeziehung nun auf das Verhalten unserer neuen Klasse StockItemOC? Schauen wir uns dazu das UML-Diagramm an: class StockItemOC

Implementierung der abgeleiteten Klasse

Zunchst wollen wir einen Blick auf die Implementierung der abgeleiteten Klasse in StockItemOC.cpp werfen:

/*  1 */  #include <cstdlib>
/*  2 */  #include "StockItem.h"
/*  3 */  #include "StockItemOC.h"
/*  4 */
/*  5 */  StockItemOC::StockItemOC( const char* name, double open, double close)
/*  6 */   : StockItem(name), m_open(open), m_close(close)
/*  7 */  { }
/*  8 */
/*  9 */  StockItemOC::StockItemOC( const StockItemOC &right)
/* 10 */   : StockItem(right), m_open(right.m_open), m_close(right.m_close)
/* 11 */  { }
/* 12 */
/* 13 */  StockItemOC::~StockItemOC()
/* 14 */  { }
/* 15 */
/* 16 */  const StockItemOC& StockItemOC::operator=(const StockItemOC &right)
/* 17 */  {
/* 18 */      // handle self assignment
/* 19 */      if (this != &right) {
/* 20 */          StockItem::operator=(right);
/* 21 */          m_open = right.m_open;
/* 22 */          m_close = right.m_close;
/* 23 */      }
/* 24 */
/* 25 */      return *this;
/* 26 */  }
/* 27 */
/* 28 */  void StockItemOC::setValue( double val)
/* 29 */  {
/* 30 */      m_close = val;
/* 31 */  }
/* 32 */
/* 33 */  double StockItemOC::getValue() const
/* 34 */  {
/* 35 */      return m_close;
/* 36 */  }
/* 37 */
/* 38 */  void StockItemOC::setOpen( double val)
/* 39 */  {
/* 40 */      m_open = val;
/* 41 */  }
/* 42 */
/* 43 */  double StockItemOC::getOpen() const
/* 44 */  {
/* 45 */      return m_open;
/* 46 */  }
/* 47 */
/* 48 */  void StockItemOC::setClose( double val)
/* 49 */  {
/* 50 */      m_close = val;
/* 51 */  }
/* 52 */
/* 53 */  double StockItemOC::getClose() const
/* 54 */  {
/* 55 */      return m_close;
/* 56 */  }

Interessant ist hier zunchst wieder der Konstruktor (Zeilen 5 bis 7): Im Gegensatz zum Konstruktor von StockItem weiter oben erfolgt die Initialisierung der Variablen (m_open und m_close) hier nicht im Funktionsrumpf, sondern in einer Initialisierungsliste. Dies schafft vor allem Performancevorteile bei der Konstruktion der Objekte. Ferner mssen wir bedenken, da die in der Basisklasse als privat deklarierten Variablen (wie m_name) hier nicht direkt angesprochen werden knnen. Um diese trotzdem korrekt zu initialisieren, wird der Konstruktor der Basisklasse in der Initialisierungsliste aufgerufen (Zeile 6).

Gleiches gilt fr den Copy Konstruktor (Zeilen 9 bis 11), der den Copy Konstruktor der Basisklasse in seiner Initialisierungsliste auffhrt. Und auch der Assignment Operator muss den der Basisklasse explizit aufrufen (Zeile 20). Lediglich beim Destruktor (Zeilen 13 und 14) ist das Verhalten anders. Hier stellt die C++ Laufzeitumgebung sicher, da alle Destruktoren voneinander abgeleiteter Klassen in der richtigen Reihenfolge aufgerufen werden. Da in StockItemOC kein dynamisch allozierter Speicher verwendet wird, braucht der Destruktor auch nichts weiter zu tun.

Die restlichen Methoden von StockItemOC dienen dem Setzen und Auslesen der privaten Variablen und bieten nichts Neues.

Verwendung abgeleiteter Klassen

Die Verwendung von abgeleiteten Klassen soll nun anhand der Klasse StockItemOC in einem kleinen Testprogramm demonstriert werden.

Am Anfang der Programmdatei mssen die bentigten Headerfiles inkludiert werden:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include "StockItem.h"
#include "StockItemOC.h"

using namespace std;

int main( int argc, char** argv)
{

Als erste bung werden wir zwei StockItem Objekte und ein StockItemOC Objekt statisch anlegen und diese dann zur Ausgabe einiger Werte benutzen:

    StockItem a( "BAY", 34.9);
    StockItem b( "BAS");
    StockItemOC c( "DTE", 57.0, 59.4);

    b.setValue( 24.2);

    cout << a.getName() << ": " << a.getValue() << endl;
    cout << b.getName() << ": " << b.getValue() << endl;
    cout << c.getName() << ": " << c.getValue() << " ("
         << c.getOpen() << " -> " << c.getClose() << ")\n";

Dies bietet auf den ersten Blick nichts neues. Beim zweiten Hinsehen erkennen wir, da in der letzten Zeile die Methode getName() des Objektes c, welches vom Typ StockItemOC ist, aufgerufen wird. Die Klasse StockItemOC hat aber gar keine Methode getName() definiert! Mit dem neu erworbenen Wissen ber Vererbung ist jedoch klar, was passiert: Es wird einfach die von der Klasse StockItem geerbte Methode verwendet!

Interessant ist auch die Verwendung der Methode getValue(). Diese gibt es sowohl in StockItem als auch in StockItemOC. In diesem Beispiel ist jedoch relativ einfach zu verstehen, was passiert - sowohl fr uns als auch fr den C++-Compiler: In der letzten Zeile wird StockItemOC::getValue() aufgerufen, in den beiden Zeilen davor StockItem::getValue(). Das ist deshalb klar, weil der Typ der Objekte a, b und c bereits zur bersetzungszeit bekannt ist und der Compiler entscheiden kann, welchen Methodenaufruf er verwenden mu. Dies wird auch als statische oder frhe Bindung bezeichnet.

Was passiert jedoch, wenn der Compiler beim bersetzen den genauen Typ des Objektes noch nicht kennt? Sehen wir uns die Fortsetzung des Programmes an:

    StockItem *astocks[3];
    astocks[0] = &a;
    astocks[1] = &b;
    astocks[2] = &c;      // cast from StockItemOC* to StockItem*
    
    for (int i = 0; i < 3; ++i) {
        cout << astocks[i]->getName() << ": " << astocks[i]->getValue() << endl;
    }

Beim Aufruf astocks[i]->getValue() mu je nachdem, ob der in astocks stehende Pointer auf ein Objekt vom Typ StockItem (bei Index 0 und 1) oder vom Typ StockItemOC (Index 2) zeigt, eine andere Methode aufgerufen werden. Dies kann erst zur Laufzeit entschieden werden, man spricht dann von dynamischer oder spter Bindung. Wie kann nun aber das Laufzeitsystem entscheiden, welchen Typ das Objekt hat? Der Schlssel hierzu ist die sogenannte vtable, in der die spezifischen Funktionssignaturen fr jedes Objekt abgelegt sind. Eine Funktionssignatur wird jedoch nur dann richtig in die vtable eingetragen, wenn die Funktionen in der Klassendefinitionen als virtual deklariert sind. Dies haben wir zum Glck in unseren Definitionen schon getan, so da das Programm wie erwartet arbeitet. Folgende Regel sollte man sich beim Umgang mit C++ jedoch verinnerlichen:

Deklariere alle Memberfunktionen - inklusive des Destruktors - als virtual, wenn Du die Absicht hast, Vererbungsmechanismen zu verwenden!

Zum Schlu noch eine fortgeschrittene Anwendung unserer Objekte. Statt in ein Array werden sie in einen STL-Vektor gepackt. Anschlieend werden die Objekte nach ihrem Wert (getValue()) sortiert und ausgegeben. Das alles funktioniert auch dann, wenn der Vektor sowohl Objekte vom Typ StockItem* als auch StockItemOC* enthlt, weil wieder dynamische Bindung zum Einsatz kommt:

    // get the stock items odered by price
    vector<StockItem*> stocks;

    stocks.push_back(&a);
    stocks.push_back(&b);
    stocks.push_back(&c); // cast from StockItemOC* to StockItem*

    sort(stocks.begin(), stocks.end(), CompareStockPrice());

    cout << endl << "stock items odered by price\n";
    reverse_copy( stocks.begin(), stocks.end(), ostream_iterator<StockItem*>(cout));
}

Fr die richtige Sortierung und die Ausgabe sind noch zwei Hilfsfunktionen notwendig, die vor main() in die Programmdatei eingefgt werden knnen: CompareStockPrice ist ein sogenannter functor. Er wird als Parameter an den Sortieralgorithmus sort bergeben und dient zum Vergleich zweier Objekte vom Typ StockItem. Die zweite Funktion ist der Ausgabeoperator fr ein StockItem Objekt. Dieser wird im Alorithmus reverse_copy ber den ostream_iterator verwendet.

// compare two stock prices
class CompareStockPrice {
public:
    int operator()( const StockItem* s1, const StockItem* s2)
    { return s1->getValue() < s2->getValue(); }
};

// operator<< for class StockItem
// write name and value to output stream ost
ostream& operator<<( ostream& ost, const StockItem* item) {
    ost << item->getName() << ": " << item->getValue() << endl;
    return ost;
}

Wie man sieht, reicht es vllig aus, die Hilfsfunktionen fr die Klasse StockItem zu implementieren. Es ist beim Aufruf der Funktion dann egal, ob der aktuelle Parameter auf ein Objekt vom Typ StockItem oder StockItemOC zeigt. Dies ist ein Resultat (und Vorteil) der von uns implementierten Vererbungshierarchie.