Das wohl mächtigste Instrument der OOP ist die Möglichkeit,
Code wiederzuverwenden, ohne den alten Code ändern oder neu
entwerfen zu müssen.
Im vorigen Kapitel haben wir mit der Einführung der polymorphen
Funktionen bereits eine solche Möglichkeit kennengelernt.
Nun wollen wir an dieser Stelle fortsetzen und die Mittel
der Vererbung von Eigenschaften benutzen, um den Code zu
vereinfachen.
Dabei bleiben class.d, new.h, new.c und str.h völlig unverändert. Die Anwendung der Vererbung hat nur Einfluß auf die Klassendefinition von STRING und ihre Implementation.
Als Beispiel für die Vererbung wollen wir aufbauend auf der Klasse STRING eine Klasse INITSTRING entwerfen, die als zusätzliche Komponente einen Zeiger auf einen Initialisierungstext hat, der den Text enthält, mit dem das Objekt ursprünglich initialisiert wurde, sowie eine Funktion, um den eigentlichen Wert des Objekts auf seinen Initialisierungswert zurückzusetzen. Das hat nicht unbedingt gerade einen sehr großen praktischen Nutzen, zeigt aber sehr schön die Technik der Vererbung.
/* (initstr.h) */ #ifndef INITSTR_H #define INITSTR_H #include "str.h" extern const void *INITSTRING; void initstring( void * _self ); /* Funktion, um den Wert des Strings zurueckzusetzen */ #endif ________________________________________________ /* (main_istr.c) */ #include <stdio.h> #include "new.h" #include "str.h" #include "initstr.h" int main() { void *s; s = new( INITSTRING ,1, "Hallo"); printf ("%s =s\n", getvalue(s)); setvalue(s,"neuer Text"); printf ("%s =neues s\n", getvalue(s)); initstring(s); printf ("%s =s\n", getvalue(s)); delete(s); return 0; }
Ein Testlauf:
cn@cicero:/home/cn/seminar/aktuell/kap3 > ls class.d initstr.c main_istr.c new.c str.d error.c initstr.h main_str.c new.h str.h error.h kap3.tar makefile str.c typescript cn@cicero:/home/cn/seminar/aktuell/kap3 > make all gcc -c main_str.c gcc -c str.c gcc -c new.c gcc -c error.c gcc -o main_str main_str.o str.o new.o error.o gcc -c main_istr.c gcc -c initstr.c gcc -o main_istr main_istr.o str.o error.o initstr.o new.o cn@cicero:/home/cn/seminar/aktuell/kap3 > main_istr Hallo =s neuer Text =neues s Hallo =s cn@cicero:/home/cn/seminar/aktuell/kap3 >
Damit INITSTRING die Klasse STRING beerben kann, müssen die Definitionen von STRING der Klasse INITSTRING zur Verfügung gestellt werden. Wir lagern diese Definitionen, die vorher in str.c waren, aus in eine neue Datei str.d.
#ifndef STR_D #define STR_D #include "class.d" struct Class_Str { const struct CLASS _; /* m u s s die erste Komponente der Klasse sein */ /* ab hier stehen die nun die Methoden der Klasse selbst*/ void (*setvalue) (void * self, void * chars); char * (*getvalue) (void * self ); void (*setindex) (void * self, int index, const char c); char (*getindex) (void * self, int index); }; struct String { const void * class; char * text; }; void String_cout ( const void * self ); void String_setvalue ( void * self, char * chars); char * String_getvalue (void * self); #endif
Die Definitionen von STRING sind identisch wie vorher, mit
einer Ausnahme: statt die Komponenten der abstrakten Klasse
CLASS explizit aufzuführen, beerben wir diese Klasse jetzt!
Das Beerben von CLASS funktioniert folgendermaßen:
auf den ersten Blick sieht es zwar aus, als ob struct CLASS
nichts anderes als eine aggregierte Komponente ist; tatsächlich
ist das jedoch nicht so. Wir beerben eine Klasse, indem wir
die Basisklasse als den ersten Teil der Struktur auffassen und
vor den Eigenschaften der abgeleiteten Klasse einfügen:
=1.0pt
Die Funktionen String_cout, String_setvalue und
String_getvalue werden hier deklariert, damit die
abgeleiteten Klassen sie erben können.
Betrachten wir nun str.c (nur die Veränderungen) :
#include ... /* Hier steht die Definition der abstrakten Klasse */ #include "class.d" /* Die Implementierung der Klasse STRING */ /* Deklarationen der Elementfunktionen von STRING */ /* die in 'class.d' definierte Klasse ist abstrakt, d.h. ihre Funktionen sind dynamisch gebunden. Also muessen wir sie implementieren: */ static void * String_constructor (void * self, const int argnr, va_list * app); static void * String_destructor (void * self); static void * String_copy (const void * self); /* cout, getvalue und setvalue sollen von abgeleiteten Klassen verwendet werden duerfen, koennen also nicht statisch definiert werden */ void String_cout (const void * self); void String_setvalue (void * self , char * chars); char * String_getvalue (void * self); static void String_setindex (void * self, int index, const char c); static char String_getindex (void * self, int index); /* Die Definition der Objektstruktur: ein Objekt zeigt auf seine Klasse und enthaelt seine eigenen Daten ausgelagert, da Struktur von abgeleiteten Klassen benoetigt wird */ #include "str.d" /* Die Erzeugung der Klasse STRING : eine Klasse implementieren wir, indem wir genau ein statisches Element von Class_Str erzeugen, das zur gesamten Programmlaufzeit erhalten bleibt. Initialisiert werden muessen hier vor allem size und constructor! */ static const struct Class_Str _String = { sizeof(struct String), String_constructor, String_destructor, String_copy, String_cout, String_setvalue, String_getvalue, String_setindex, String_getindex }; /* Die Schnittstelle der Klasse STRING: Die Adresse des statischen Elements von Class_Str ist unser Zugriff auf die Klasse */ const void * STRING = & _String;
Hier hat sich nicht viel geändert. Die Klassendefinition wird jetzt über #include eingefügt. Die Funktionen String_setvalue, String_getvalue und String_cout sind nicht mehr statisch implementiert; die abgeleiteten Klassen dürfen auf sie zugreifen.
Und nun die abgeleitete Klasse:
#include <stdio.h> #include <stdarg.h> #include "str.h" #include "new.h" #include "error.h" #include "initstr.h" #include "str.d" static void * InitString_constructor (void * self, \ const int argnr, va_list * app); static void * InitString_destructor (void * self); struct InitString { const struct String _; char *inittext; };
Ähnlich wie bei der Vererbung der Klassenmethoden/Funktionen
(STRING erbt von CLASS, siehe oben) fügen wir
bei der abgeleiteten Klasse am Anfang ein Objekt der Basisklasse
ein und können dadurch durch einen cast (struct String *) ein
Objekt der Klasse INITSTRING die Methoden der Klasse
STRING verwenden lassen:
=1.0pt
/* Die Erzeugung der Klasse INITSTRING : eine Klasse implementieren wir, indem wir genau ein statisches Element von Class_Str erzeugen, das zur gesamten Programmlaufzeit erhalten bleibt. Initialisiert werden muessen hier vor allem size und constructor! */ static const struct Class_Str _InitString = { sizeof(struct String), InitString_constructor, InitString_destructor, 0, String_cout, String_setvalue, String_getvalue }; /* Die Schnittstelle der Klasse INITSTRING: Die Adresse des statischen Elements von Class_InitStr ist unser Zugriff auf die Klasse */ const void * INITSTRING = & _InitString;
Die Erzeugung der abgeleiteten Klasse läuft jetzt analog zur Erzeugung der Basisklasse.
static void * InitString_constructor (void * _self,\ const int argnr, va_list * app) { struct InitString * self ; struct String * self2; if ( _self == 0) fatalerror(NULLPTR,1); self = ((struct CLASS * ) STRING) -> constructor\ (_self,argnr,app); ((struct String *) self) -> class = STRING; self2 = (struct String *) self; /* hier kann nat"urlich nur der Basisklassenkonstruktor vern"unftige Parameterverarbeitung machen */ self -> inittext = (char *) calloc \ (strlen(self2 -> text)+1,sizeof(char)); if (self == 0) fatalerror(ALLOCMEM,1); strcpy(self -> inittext, self2 -> text); return self; }
Im Konstruktor wird der von new besorgte Speicherplatz einmal als Objekt INITSTRING (das passiert bei self) und zum anderen als Objekt STRING aufgefaßt (bei self2). Die Struktur im Speicher ist immer dieselbe, nur die Zeiger sind unterschiedlich gecastet.
Als nächstes wird der Konstruktor der Basisklasse aufgerufen, erst
dann darf der Konstruktor der abgeleiteten Klasse seine Arbeit
verrichten (wichtig!). Der Cast beim Konstruktor ist natürlich
(struct CLASS *), da STRING selbst eine abgeleitete
Klasse ist und keine eigene Methode constructor besitzt.
Leider sind wir jetzt nicht mehr in der Lage, eine absolut eindeutige
Klassenidentifizierung durchzuführen. Damit das Objekt vom
Typ INITSTRING die Methoden der Basisklasse benutzen kann,
muß seine Komponente .class auf STRING zeigen.
Das ist natürlich eine Fehlerquelle, denn sollte eine Methode
von INITSTRING mit einem Objekt der Basisklasse STRING
aufgerufen werden, kann das nicht erkannt werden.
Wir nehmen ferner an, daß nur der Konstruktor der Basisklasse
die variable Argumentliste benutzen kann, der abgeleiteten Klasse
also keine Argumente übergeben werden können. (Es wäre aber
durchaus möglich, das zu realisieren, wenngleich mit etwas Aufwand
verbunden.)
static void * InitString_destructor (void * _self) { struct InitString * self = _self; if (_self == 0) return; free( self -> inittext ); ((struct CLASS *) STRING) -> destructor( _self ); return _self; }
Beim Destruktor ist die Reihenfolge genau umgekehrt: zunächst führt der Destruktor der abgeleiteten Klasse seine Speicherfreigabe durch, danach wird erst der Destruktor der Basisklasse aufgerufen.
void initstring( void * _self) { struct InitString * self = _self; struct String * self2 = _self; if ( _self == 0 ) fatalerror(NULLPTR,1); if ( self2 -> class != STRING) fatalerror(TYPE,1); free( self2 -> text ); self2 -> text = (char *) calloc (strlen\ (self -> inittext)+1,sizeof(char)); if ( self2 -> text == 0) fatalerror(ALLOCMEM,1); strcpy( self2 -> text, self -> inittext); }
initstring muß wieder aufpassen, ob es auf die Komponenten der abgeleiteten oder der Basisklasse zugreift. Deshalb benötigen wir hier wieder die zwei verschiedenen Zeiger, die auf das gleiche Objekt zeigen.