next up previous contents
Next: Die Klasse STRING Up: KlassenKonstrukturen und Destrukturen: Previous: KlassenKonstrukturen und Destrukturen:

Die Abstraktion - Loslösung von speziellen Klassen

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

picture82


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.


next up previous contents
Next: Die Klasse STRING Up: KlassenKonstrukturen und Destrukturen: Previous: KlassenKonstrukturen und Destrukturen:

Christian Neher