Im vorigen Kapitel haben wir eine erste Implementierung der Klasse STRING realisiert. Das Ergebnis befriedigt jedoch noch nicht; sämtlicher Code läßt sich momentan nur für die Klasse STRING verwenden. Dies wollen wir nun verbessern.
So ist es z.B. nicht notwendig, für jedes Objekt eigene Funktionen zu schreiben, die sich um die Speicherbeschaffung und -freigabe kümmern, d.h. die Funktionen new und delete können für beliebige Objekte verwendet werden, müssen aber tatsächlich nur einmal definiert und implementiert werden. Freilich wissen diese Funktionen nicht mehr, wie die beschafften Datenbereiche zu initialisieren bzw. zu löschen sind. Jedes Objekt muß folglich selbst wissen, wie es sich bei der Erzeugung zu initialisieren und bei der Zerstörung zu löschen hat; new und delete übernehmen nur noch die Speicherverwaltung der Objekte und rufen die Initialisierungs- bzw. Zerstörungsfunktionen der Objekte auf. Man nennt solche Funktionen Konstruktoren und Destruktoren einer Klasse; sie sind eine grundlegende Eigenschaft und jede Klasse muss sie besitzen.
Weiter gehen wir davon aus ( das ist nicht unbedingt notwendig ),
daß jede Klasse eine Funktion zum Kopieren eines Objektes sowie
eine Ausgabefunktion besitzt.
Das führt zu der folgenden abstrakten Klassendefinition:
/* (class.d) */ #ifndef CLASS_D #define CLASS_D #include <stdarg.h> #include <stdio.h> struct CLASS { size_t size; void * (*constructor) (void * _self , const int argnr,\ va_list * _ap); void * (*destructor) (void * _self); void * (*copy) (const void * _self); void (*cout) (const void * _self); };
CLASS ist die für alle folgenden Definitionen grundlegende Klassendefinition. Ihre Komponenten sind Funktionen, über die jede Klasse verfügen muß, d.h. jede Klasse, die wir später definieren, muss exakt diese Komponenten besitzen, mit exakt diesen Namen!
Tatsächlich ist CLASS für uns eine abstrakte Klasse, d.h. es existiert nur ihre Definition, und es können keine Objekte vom Typ CLASS erzeugt werden. Dazu benötigen wir dann die Definitionen der eigentlichen Klassen. ( Rein theoretisch wäre es natürlich möglich, Objekte vom Typ CLASS zu erzeugen. Das würde jedoch kaum Sinn machen, da CLASS nur Funktionen als Komponenten hat und keine wirklichen Daten.)
Warum gehen wir diesen auf den ersten Blick komplizierten Weg?
Nun, unser Ziel ist es, Funktionen new , delete,
usw. zu entwickeln, die völlig unabhängig von einer
speziellen Klassendefinition sind.
Das bedeutet, daß wir die tatsächlichen Objekte, identifiziert
durch void * - Zeiger, so behandeln müssen, als wären
sie vom Typ CLASS, was einen expliziten Cast erfordert
(siehe später).
Eine Klasse definieren wir dann folgendermaßen:
struct Class_xyz { /* hier stehen die Komponenten von struct CLASS danach die Funktionen der Klasse Class_xyz */ }; struct CLASS_xyz { void * class; /* unbedingt zuerst */ .... (Daten) };
Class_xyz definiert die Klasse, von der später Objekte
erzeugt werden können.
CLASS_xyz beschreibt die eigentliche Struktur der Objekte,
.class zeigt dann auf die Klasse, in diesem Fall wäre
das Class_xyz. Wie das im einzelnen funktioniert, werden
wir nachher bei der Klasse STRING sehen, entscheidend
ist momentan nur, daß jedes Objekt eine Komponente .class
besitzt, auf die wir durch Dereferenzierung des Objekts
zugreifen können.
=1.0pt
Die Schnittstellendatei dieser grundlegenden Funktionen:
/* (new.h) */ #ifndef NEW_H #define NEW_H #include <stdio.h> /* Die Definition der grundlegenden Funktionen zur Erzeugung der Instanzen Die Funktionen sind unabhaengig von speziellen Klassen; die Klassen muessen nur die in 'class.d' definierten Elementfunktionen besitzen. */ void * new (const void * _class,const int argnr, ...); void delete (void * _self); void * copy (const void * _object) ; void cout (const void * _self); #endif
Die hier definierten Funktionen sind völlig unabhängig von der Zugehörigkeit der Objekte zu einer speziellen Klasse, solange sich die Klassendefinitionen nur an der abstrakten Klasse CLASS orientieren. Man nennt solche Funktionen auch polymorph, da sie ,,mehrgestaltig`` sind, d.h. mit Objekten verschiedenen Typs arbeiten können.
Als einziges ist hier wohl die Funktion new von besonderem
Interesse: sie besitzt eine variable Parameterliste.
Notwendige Argumente sind zunächst natürlich die Klasse;
weiter verlangen wir die Anzahl der Argumente, die an new
übergeben werden, um wenigstens eine gewisse Überprüfung
durchführen zu können. (Wäre die optionale Argumentliste
nämlich leer und wir würden trotzdem darauf zugreifen, so
wäre das Resultat entweder eine ziemlich blödsinnige Ausgabe
des Programms oder ein core dump.)
Allein mit den bislang vorhandenen Informationen können wir schon
new.c implementieren:
/* (new.c) */ #include <stdarg.h> #include "class.d" #include "error.h" /* new: Erzeugen einer neuen Instanz einer Klasse */ void * new( const void * _class, const int argnr, ...) /* _class : die Klasse des Objekts argnr : Anahl der Initialisierungsargumente (werden an den Konstruktor uebergeben) */ { const struct CLASS * class = _class; /* wir interpretieren die Klasse _class als abstrakte Klasse, wie in 'class.d' definiert, um die Funktion new fuer beliebige (aehnliche) Klassen verwenden zu koennen. */ void * p; if ( class==0 ) fatalerror(NULLPTR,1); p = (void * )calloc(1, class -> size ); /* Speicher fuer Objekt besorgen */ if (p == 0) fatalerror(ALLOCMEM,1); * (const struct CLASS **) p = class; /* das Objekt gehoert zur Klasse _class ! (ein etwas diffiziler cast, laesst sich aber nicht vermeiden) */ if ( class -> constructor ) { if (argnr>0) /* Die variable Argumentliste fuer den Konstruktor erzeugen */ { va_list ap; va_start(ap, argnr); p = class -> constructor (p,argnr, &ap); va_end(ap); } else /* Der Konstruktor bekommt keine Argumente fuer Initialisierung */ { p = class -> constructor (p, 0, 0); } } else /* jede Klasse muss ueber einen Default-Konstruktor verfuegen! */ { fatalerror("no constructor found\n",1); } return p; /* das Objekt wird repraesentiert durch seine Adresse */ }
new besorgt zunächst Speicher für das Objekt. Dabei tut es so, als ob das zu erzeugende Objekt der Klasse CLASS angehörte. Der explizite cast ist unbedingt notwendig, da beim folgenden Aufruf des Konstruktors der void * - Zeiger dereferenziert wird. Je nachdem, ob die optionale Argumentliste von new leer ist oder nicht (das können wir leider nur über den Parameter argnr überprüfen), wird der Konstruktor mit den Initialsierungsargumenten aufgerufen bzw. mit dem Nullzeiger als Argument. Ein Objekt einer Klasse wird identifiziert über seine Adresse, also geben wir den Zeiger p zurück.
/* delete: eine Instanz einer Klasse zerstoeren */ void delete(void * _self) /* _self : das zu zerstoerende Objekt */ { const struct CLASS ** str = _self; if ( str == 0 ) return; if ( (*str) -> destructor ) _self = (*str) -> destructor( _self ); free (_self); }
delete zerstört ein Objekt, d.h. es wird zunächst der Destruktor der Klasse aufgerufen und danach der Speicher freigegeben. Dabei wird die tatsächliche Klasse des Objekts gar nicht festgestellt - über den Zeigercast struct CLASS ** und die Dereferenzierung (*str) wird automatisch bereits der richtige Destruktor aufgerufen. Die Dereferenzierung (*str) ist möglich, da .class die erste Komponente der Objektstruktur ist, also der Zeiger auf die Klasse!
/* copy: eine identische Kopie eines Objekts erzeugen */ void * copy (const void * _self) { const struct CLASS * const * self = _self; if (self==0) fatalerror(NULLPTR,1); if (!( (*self) -> copy ) ) fatalerror("no copy constructor found\n",1); return (*self)->copy(_self); } void cout(const void * _self) { const struct CLASS * const * self = _self; if (self==0) fatalerror(NULLPTR,1); if ( ! (*self)->cout ) return; (*self) -> cout (_self); }
Unsere polymorphen Funktionen sind nun fertig, wir können jetzt die Klasse STRING definieren und das Hauptprogramm betrachten.