virtuelle Methoden

Die Programmierung mit objekt(typ)-orientierten Techniken sind ein häufig auftretendes Muster in der Programmierung. Die Realisierung von VTables in C ist jedoch aufwendig und so besitzt C++ einige unterstützende Elemente, die dem Programmierer das Aufsetzen von objektorientierten Strukturen durch Automatisierung erleichtert. In C++ ist nicht festgelegt, wie virtuelle Funktionen umgesetzt werden, aber man kann nahezu davon ausgehen, dass C++-Compiler virtuelle Tabellen verwenden.

In C++ muss man die Klassenmethoden und die virtuellen Methoden nicht mehr trennen. Die Syntax erlaubt die Definition direkt innerhalb der Klasse. Die Unterscheidung wird mit dem Schlüsselwort virtual getroffen.

class Tier
{
public: 
  char Name[64];
  int  Fellfarbe;
  int  KrallenLaenge;
  int  AnzahlBeine;
 
  virtual void GibLaut();
  virtual int JageFutter( struct Tier * ); 
  virtual char const * FutterArt(); 
};

Hierfür generiert C++ nun automatisch die virtuelle Tabelle. Der Zugriff auf viruelle Methoden wird damit automatisch über die VTable geleitet. Der Zeiger auf die VTable wird automatisch in die Deklaration eingebunden. Deswegen enthält eine Klasse, die virtuelle Methoden enthält, immer einen zusätzlichen Zeiger, wird also entsprechend 4 Byte (32-Bit-System) oder 8 Byte (64-Bit-System) größer, als die Nutzdaten innerhalb der Klasse.

Virtuelle Funktionen können in abgeleiteten Klassen überladen werden, so dass sich für die abgeleiteten Klassen neue virtuelle Tabellen ergeben.

#include "stdio.h"
#include "stdlib.h"
 
class Tier
{
public:
  char Name[64];
  int  Fellfarbe;
  int  KrallenLaenge;
  int  AnzahlBeine;
 
  virtual void GibLaut()  { printf( "<undefiniertes Geräuch>\n" ); };
  virtual int JageFutter( ) { return 0;/* nix gefangen */ }; 
  virtual char const * FutterArt() { return "Luft und Liebe"; };
};
 
class Hund : public Tier
{
public:
  int  GefangeneKatzen;
 
  Hund() : GefangeneKatzen( 0 ) {}
  virtual void GibLaut() { printf( "Wuff\n" ); }
  virtual int JageFutter() { return ++GefangeneKatzen; }
  virtual char const * FutterArt() { return "Katzen"; }
};
 
class Katze : public Tier
{
public:
  int   GefangeneVoegel;
 
  Katze() : GefangeneVoegel( 0 ) {}
  virtual void GibLaut() { printf( "Miau\n" ); }
  virtual int JageFutter() { return ++GefangeneVoegel; }
  virtual char const * FutterArt() { return "Vögel"; }
};
 
class Amsel : public Tier
{
public:
  int  GefangeneInsekten;
 
  Amsel() : GefangeneInsekten( 0 ) {}
  void GibLaut() { printf( "Piep\n" ); }
  int JageFutter() { return ++GefangeneInsekten; }
  char const * FutterArt() { return "Insekten"; }
};
 
void LautUndZweimalJagen( Tier * tier )
{
  tier->GibLaut();
  printf( "Tier fängt Nahrung: bisher %d %s gefangen\n", tier->JageFutter(), tier->FutterArt() );
  printf( "Tier fängt Nahrung: bisher %d %s gefangen\n", tier->JageFutter(), tier->FutterArt() );
} 
 
int main( void )
{ 
  Tier *tier1, *tier2;
 
  tier1 = new Katze();
  tier2 = new Hund();
 
  LautUndZweimalJagen( tier1 );
  LautUndZweimalJagen( tier2 );
  LautUndZweimalJagen( tier1 );
 
  delete tier1;
  delete tier2;
 
  return EXIT_SUCCESS;  
}

Das Programm ist dank der Unterstützung von C++ einfacher geworden. Das Hauptprogramm selbst hat sich soweit gar nicht verändert. Auch die Initialisierungsroutine wird von C++ automatisch generiert und aufgerufen.

Die Funktionen der Katze und Hund dürfen weiter virtuell überschrieben werden, während den Funktionen der Amsel das virtual-Schlüsselwort fehlt. Dies ist mit Vorsicht zu genießen, denn das bedeutet, dass diese Funktionen nicht mehr virtuell überschrieben werden. Diese Überschreibung funktioniert also nur dann, wenn der abgeleitete Typ bekannt ist.

Hier besteht das Risiko, dass eine Klasse von Amsel unerwartete Reaktionen hervorruft:

class AmselMutant : public Amsel
{
public:
  void GibLaut() { printf( "mutiertes Piep\n" ); }
};
 
int main( void )
{
  AmselMutant * amsel = new AmselMutant;
  Tier * tier = amsel; 
 
  amsel->GibLaut();   // Gibt 'mutiertes Piep' aus
  tier->GibLaut();    // Gibt 'Piep' aus 
 
  delete amsel;
}

Bei amsel->GibLaut() ergibt sich aus der Klassendeklaration, dass die statische Methode gerufen wird, die 'mutiertes Piep' ausgibt. Bei tier->GibLaut() zeigt sich in der Klassendeklaration, dass es sich bei GibLaut() um eine virtuelle Funktion handelt. Also wird in der virtuellen Tabelle nachgeguckt. Hier ist die letzte virtuelle Überschreibung der Funktion GibLaut() von der Amsel gemacht. Also ergibt tier->GibLaut() die Ausgabe 'Piep'.