next up previous contents
Next: Ausblick Up: OOP mit ANSI-C Previous: Die Klasse COMPLEX

Vererbung

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

picture160




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

picture197


      
      /* 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.


next up previous contents
Next: Ausblick Up: OOP mit ANSI-C Previous: Die Klasse COMPLEX

Christian Neher