2.3.6 Klassen und Objekte


2.3.6.1 Struktur und Realisierung


Ebenso wie viele andere Sprachelemente ähneln sich auch Klassen und Objekte in C++ und Object Pascal. Sie bilden die Basis für die sehr umfangreichen Klassenbibliotheken Microsoft Foundation Classes (MFC) und Visual Component Library (VCL). Diese wiederum bilden die Grundlagen und Voraussetzungen für die Entwicklungsumgebungen, die ein visuelles Programmieren ermöglichen.


In Klassen und Objekten können Variable, Funktionen und Eigenschaften (Properties) deklariert werden. Variable in Klassen und Objekten werden Datenelemente, Member-Daten und Felder genannt. Klassen-Funktionen werden als Member-Funktionen und Methoden bezeichnet. Im folgenden werden die Bezeichnungen Feld, Methode und Property verwandt. Felder, Methoden und Properties sind Member, die Elemente, der Klassen und Objekte.


Eine Tabelle soll zunächst einen Überblick darüber verschaffen, in welchem Umfang Visual C++ und Object Pascal den Typ Klasse unterstützen:



VC++

Object Pascal

differenzierte Zugriffsrechte

Ja

Ja

Einfachvererbung (single inheritance)

Ja

Ja

Mehrfachvererbung (multiple inheritance)

Ja

Nein

Zugriff auf Vorfahr mit "inherited"

Bedingt

Ja

statische Methoden

Ja

Ja

virtuelle Methoden (Polymorphismus)

Ja

Ja

dynamische Methoden

Nein

Ja

Botschaftsbehandlungs-Methoden

Nein

Ja

Botschaftszuteilung

Nein

Ja

static-Felder

Ja

Nein

static-Methoden = Klassen-Methoden

Ja

Ja

const-Felder und Methoden

Ja

Nein

inline-Methoden

Ja

Nein

Objekt-Variable in Klasse (Vorwärts-Dekl.)

Ja

Ja

verschachtelte Klassen

Ja

Nein

Konstruktor

Ja

Ja

KopieKonstruktor

Ja

Bedingt

virtueller Konstruktor

Nein

Ja

Destruktor

Ja

Ja

Metaklassen

Nein

Ja

Klassen-Schablonen (class Templates)

Ja

Nein

Überladen von Methoden und Operatoren

Ja

Nein

Friends

Ja

Bedingt

Feld-Zeiger für Klassen

Ja

Nein

Methoden-Zeiger für Klassen

Ja

Ja

abstrakte Basisklassen / -methoden

Ja

Ja

Properties (Eigenschaften)

Nein

Ja

Run-Time-Type-Information (RTTI)

Ja

Ja

Feld- / Methoden-Name nachschlagen

Nein

Ja



Eine Klassen-Definition besteht in C++ und Object Pascal aus einem Klassenkopf, zu dem das Schlüsselwort "class" und der Name der Klasse gehören, und einem Klassenrumpf. In C++ kann und in Object Pascal muß die Implementierung der Methoden abgesetzt von der Klassen-Definition erfolgen. C++ erlaubt es, die Methoden-Implementierung auch direkt innerhalb des Klassenrumpfes, im Anschluß an die Methoden-Deklaration, vorzunehmen. Eine Klasse Komplex, welche die in der Mathematik verwendeten komplexen Zahlen auf dem Computer abbilden soll, soll als Beispiel dienen:


VC++

Object Pascal

class Komplex   // KlassenKopf
{               // KlassenRumpf
  public:
    void Zuweisung(double r,
                   double i)
         {Real = r; Imag = i;}

    void Ausgabe()
         {printf("%g + i*%g",
                 Real, Imag);}
  private:
    double Real;
    double Imag;
};

Methoden-Implementierung erfolgt in Object Pascal immer abgesetzt vom Klassenrumpf
class Komplex   // KlassenKopf
{               // KlassenRumpf
  public:
    void Zuweisung(double r,
                   double i);
    void Ausgabe();
  private:
    double Real;
    double Imag;
};

// abgesetzte
// Methodenimplementierung
void Komplex::Zuweisung(
            double r, double i)
{
  Real = r;
  Imag = i;
}

void Komplex::Ausgabe()
{
  printf("%g+i*%g",Real,Imag);
}
 
type
  Komplex = class // KlassenKopf
  public          // KlassenRumpf  
    procedure Zuweisung(r, i:
                        Double);
    procedure Ausgabe;
  private
    Real: Double;
    Imag: Double;
  end;

// abgesetzte
// Methodenimplementierung
procedure Komplex.Zuweisung(
                  r, i: Double);
begin
  Real:= r;
  Imag:= i;
end;

procedure Komplex.Ausgabe;
begin
  writeln(Real, '+i*', Imag);
end;


Auch in C++ werden für gewöhnlich Klassenrumpf und Methodenimplementierung voneinander getrennt. Der Klassenrumpf, die Klassen-Schnittstelle, wird von der Klassen-Implementierung entkoppelt. Die Methoden-Deklarationen stellen dann eine Art Forward-Deklaration dar. Der Klassenrumpf wird in Header-Dateien, die Methodenimplementierung in CPP-Moduldateien definiert. Entsprechend wird in Object Pascal der Klassenrumpf im Interface-Abschnitt und die Methoden-Implementierung im Implementation-Abschnitt einer Unit eingefügt. Über Include- bzw. Uses-Anweisungen können in anderen Modulen bestehende Klassen in beiden Sprachen leicht bekannt gemacht werden.

Klassen belegen keinen Speicher, da sie nur eine spezielle Typ-Deklaration darstellen. Eine Speicherbelegung erfolgt erst beim Instanzieren von Klassen. Eine Klasseninstanz heißt Objekt. C++ gestattet das Anlegen statischer und dynamischer Instanzen.

In Object Pascal sind dagegen, genauso wie z.B. in der "Internet-Sprache" Java der Firma Sun Microsystems, ausschließlich dynamische Objekt-Instanzen zulässig. Der Vorschlag des ANSI-ISO-Komitees für ein Objekt-Modell in Pascal [8, S. 12 / S. 44], genannt Referenzmodell, wurde fast vollständig in Object Pascal realisiert. Eine Variable vom Typ einer Klasse enthält kein Objekt, sondern einen Zeiger. Dieser Zeiger kann zur Laufzeit ein Objekt referenzieren oder den Wert nil enthalten um anzudeuten, daß er gerade kein gültiges Objekt referenziert. Beim Erzeugen eines neuen Objekts wird dem Zeiger die Adresse des Speicherbereiches zugewiesen, der für das Objekt reserviert wurde. D.h., alle Objekte werden auf dem Heap angelegt und belegen immer nur 4 Byte im Stack-Speicher. Der Heap bezeichnet den Speicher, den eine Anwendung belegt, um z.B. dynamische Variable erstellen zu können.

Felder und Methoden dynamisch angelegter Objekte werden normalerweise wie bei gewöhnlichen dynamischen Variablen angesprochen: Zeiger->Feld in C++ und Zeiger^.Feld in Object Pascal. Da nun aber in Object Pascal sowieso alle Objekte dynamisch angelegt werden und Zeiger darstellen, kann der Compiler beim Zugriff auf die Elemente eines Objekts eine Dereferenzierung automatisch durchführen. Es ist weder notwendig noch möglich, den Zeiger explizit zu dereferenzieren. Ein Objekt stellt sich trotz dynamischer Allokation wie eine statische Variable dar. Ein Zugriff erfolgt, ebenso wie in Java, in der Form ObjektZeiger.Feld bzw. ObjektZeiger.Methode. Beim Zugriff auf das gesamte Objekt wird der Zeiger nicht dereferenziert. Statt dessen wird mit dem Wert des Zeigers, also der Adresse der Instanz gearbeitet. Das wirkt sich insbesondere bei Zuweisungen aus.

Das Object Pascal Referenzmodell wird aufgrund seiner Eigenschaft des "automatischen Dereferenzierens" mitunter "zeigerloses Konzept" genannt. Zeigerlos deshalb, weil vor dem Programmierer die Tatsache verborgen wird, daß er in Wahrheit ständig mit Zeigern hantiert. Tatsächlich scheint es für Programmier-Anfänger hilfreich zu sein, zunächst von Zeigern und der Zeiger-Syntax verschont zu bleiben. Das Referenz-Modell wird vielfach als leichter verständlich und einfacher lehrbar empfunden.

Instanzierung, Nutzung und Freigabe des Objekts Komplex kann in beiden Sprachen so erfolgen:


VC++

Object Pascal

statisch
Komplex a;
a.Zuweisung(10, 5);
a.Ausgabe();
keine statische Instanzierung möglich
dynamisch
Komplex* b;

b = new Komplex;
b->Zuweisung(10, 5);
b->Ausgabe();
delete b;

dynamisch
var b: Komplex;

b:= Komplex.Create;
b.Zuweisung(10, 5);
b.Ausgabe;
b.Free;


Statische Objekte werden, wie alle anderen statischen Variablen auch, automatisch freigegeben, sobald ihr Gültigkeitsbereich verlassen wird.

Beachtet werden muß jedoch, daß dynamisch erzeugte Objekte vom Programmierer selbst wieder freigegeben werden müssen, da sonst mit der Zeit Speicher-Löcher (=unbenutzter, nicht freigegebener Speicher) entstehen. Eine Einbettung in einem try/finally-Ressourcenschutzblock des Codes, der auf dynamisch erzeugte Objekte zugreift, ist ratsam, um eine sichere Freigabe des belegten Speichers zu erwirken.


VC++

Object Pascal

Komplex* b;

b = new Komplex;
__try
{
  b->Zuweisung(10, 5);
  b->Ausgabe();
}
__finally
{
  delete b;
}

var b: Komplex;

b:= Komplex.Create;
try
  b.Zuweisung(10, 5);
  b.Ausgabe;
finally
  b.Free;
end;


Die wichtigen Ziele objektorientierter Programmierung, Abstraktion und Verkapselung, werden durch die gezielte Vergabe von Zugriffsrechten an Felder und Methoden in Klassen erzielt. Folgende Zugriffsrechte, auch Sichtbarkeitsbereiche genannt, stehen zur Verfügung:



VC++

Object Pascal

private

Nur klasseneigene Methoden und deren Friends haben Zugriffsrecht.





class Any
{
  private: int x;
};

Any* MyObjekt = new Any;
MyObjekt->x = 15;
 // Fehler, da kein
 // Zugriffsrecht

Unbeschränkte Zugriffsrechte innerhalb des Moduls (z.B. Unit), in dem die Klasse definiert ist.

Außerhalb des Moduls besteht keinerlei Zugriffsrecht.


type 
  Any = class
    private x: Integer;
  end;

MyObjekt := Any.Create;
MyObjekt.x := 15;
 // kein Fehler, da Zugriff
 // im selben Modul ge-
 // stattet ist

protected

Zugriffsrechte wie bei private.

Außerdem haben aber auch abgeleitete Klassen Zugriffsrecht.


Zugriffsrechte wie bei private.

Außerhalb des Moduls, in dem die Klasse definiert ist, haben aber außerdem auch abgeleitete Klassen Zugriffsrecht.


public

Alle haben uneingeschränkte Zugriffsrechte.


Alle haben uneingeschränkte Zugriffsrechte.

published

-

Dient der Veröffentlichung von Properties im Objektinspektor. Uneingeschränkte Zugriffsrechte (wie public)


automated

-

Dient der Veröffentlichung von OLE- Automations - Typ - Informationen. Uneingeschränkte Zugriffsrechte (wie public)




C++ und Object Pascal können innerhalb von Objekten weitere Objekte über Zeigervariable aufnehmen. Um wechselseitig voneinander abhängige Klassen deklarieren zu können, werden Vorwärtsdeklarationen eingesetzt.


VC++

Object Pascal


class B;  // forward-Deklar.

class A{
public:
  B* ObjB;
};

class B{
public:
  A* ObjA;
};

type
  B = class; // forward-Deklar.

  A = class
  public
    ObjB: B;
  end;

  B = class
  public
    ObjA: A;
  end;


Auch in Object Pascal wird also bei der Vorwärtsdeklaration einer Klasse, anders als bei Vorwärtsdeklarationen von Funktionen, die Anweisung "forward" nicht benötigt.


Während Object Pascal verschachtelte Funtkionsdefinitionen, jedoch keine verschachtelten Klassendefinitionen zuläßt, drehen sich die Verhältnisse in C++ paradoxerweise genau um. C++ gestattet die Definition verschachtelter Klassen (nested classes).


class A{                // äußere Klassen-Definition
public:
  class B{              // innere Klassen-Definition
  public:
    void DoIt_B(void);
  private:
    char c;             // A::B::c
  };
  void DoIt_A(void);
private:
  char c;               // A::c
};

void A::B::DoIt_B()
{  
  c = 'b';  
  printf("B:  %c\n", c);
};

void A::DoIt_A()
{
  B ObjB;

  c = 'a';
  ObjB.DoIt_B();
  printf("A:  %c\n", c);
};

void main()
{  
  A ObjA;
  A::B ObjB;

  ObjA.DoIt_A();
  ObjB.DoIt_B();
}


Ausgabe:

B: b

A: a

B: b

Die innere Klasse (hier B) ist für die äußere, sie umschließende Klasse (hier A), lokal. Verschachtelte Klassen werden manchmal auch Member-Klassen genannt.

Das ANSI-ISO-Komitee hat verschachtelte Klassen in OO-Pascal abgelehnt, weil "keine guten Gründe gefunden werden konnten, Verschachtelung zuzulassen und die Sprachdefinition, -implementation und Nutzung unnötig verkompliziert wird". [8, S. 41/42]


Klassen lassen sich in C++ auch lokal innerhalb von Funktionen definieren.


void MyProc(void)  // Prozedur
{
  class B{         // lokale Klasse
    int i;
    ···
  };  

  B Obj;
   ···
};


Visual C++ gestattet keine Definition von Methoden in lokalen Klassen. Lokale Klassen dürfen außerdem nicht von anderen Klassen erben. Aufgrund dieser starken Einschränkungen sind sie praktisch nicht interessant. Christian merkt dazu in [3] an: "Mit den verschachtelten und den lokalen Klassen hat man versucht, eine Nische zu füllen. Allerdings hat man sich damit weit von der eigentlichen Philosophie von C++ und der objektorientierten Programmierung entfernt." Die beide Spracheigenschaften verschachtelte und lokale Klassen stellen Beispiele für das unnötige Überfrachten einer Sprache dar.


2.3.6.2 Vererbung (Inheritance)


Bei der Definition einer neuen Klasse kann eine bereits bestehende Klasse als Vorfahr (ancestor) angegeben werden. Die neue Klasse, auch Nachkomme genannt, kann dadurch automatisch über sämtliche Felder, Methoden und Properties des Vorfahren verfügen. Sie erbt diese Elemente von ihrer Basisklasse (base class). Die neue Klasse ist von ihrem Vorfahr abgeleitet (derived).

In Object Pascal ist die Klasse TObject der Urahn aller Klassentypen. Alle Klassen, die nicht explizit von einer anderen Klasse abgeleitet werden, werden automatisch von TObject abgeleitet. In Visual C++ haben nicht-abgeleitete Klassen dagegen keinen generellen Vorfahren. Programme, die die MFC benutzen, leiten ihre Klassen oft von CObject oder von einem Nachkommen CObject's ab.


Klassenbeziehung


VC++

Object Pascal

class A
{
  public:	
    int Feld1;
    int Methode1();
};


class B : public A
{
  // erbt alles von A
}

int A::Methode1()
{
  printf("A: Methode 1\n");
  return 1;
};
···
A* ObjA;
B* ObjB;

ObjA = new A;
ObjB = new B;

ObjA->Methode1();
ObjB->Methode1();

delete ObjA;
delete ObjB;

Ausgabe:

A: Methode 1

A: Methode 1

type
  A = class
  public
    Feld1: Integer;
    function Methode1: Integer;
    property Property1: Integer
        read Feld1 write Feld1;
  end;

  B = class(A)
    // erbt alles von A
  end;

 function A.Methode1: Integer;
 begin
   writeln('A: Methode 1');
   Result:= 1;
 end;
···
var ObjA: A;
    ObjB: B;

ObjA:= A.Create;
ObjB:= B.Create;

ObjA.Methode1;
ObjB.Methode1;

ObjA.Free;
ObjB.Free;

Ausgabe:

A: Methode 1

A: Methode 1

Zusätzlich können neue Elemente deklariert oder vom Vorfahren geerbte Elemente überschrieben werden. Überschreiben bedeutet, es wird für ein Element der neuen Klasse ein Bezeichner gewählt, der bereits im Vorfahren verwendet wird. Dadurch wird die überschriebene Komponente innerhalb der neuen Klasse verdeckt.



VC++

Object Pascal

class A
{
  public:	
    int Feld1;
    int Methode1();
};



class B : public A
{
  public:	
  int Feld1;  
  // überschriebenes Feld1
  int Methode1(); 
  // überschrieben
};

 
int A::Methode1()
{
  printf("A: Methode 1\n");
  return 1;
};

int B::Methode1()
{
  printf("B: Methode 1\n");
  return 1;
};

···

A* ObjA;
B* ObjB;

ObjA = new A;
ObjB = new B;

ObjA->Methode1();
ObjB->Methode1();

delete ObjA;
delete ObjB;

Ausgabe:

A: Methode 1

B: Methode 1

type
  A = class
  public
    Feld1: Integer;
    function Methode1: Integer;
    property Property1: Integer
        read Feld1 write Feld1;
  end;

  B = class(A)
  public
    Feld1: Integer;
    // alle überschrieben
    function Methode1: Integer;
    property Property1: Integer
        read Feld1 write Feld1;
  end;


function A.Methode1: Integer;
begin
  writeln('A: Methode 1');
  Result:= 1;
end;

function B.Methode1: Integer;
begin
  writeln('B: Methode 1');
  Result:= 1;
end;

···

var ObjA: A;
    ObjB: B;

ObjA:= A.Create;
ObjB:= B.Create;

ObjA.Methode1;
ObjB.Methode1;

ObjA.Free;
ObjB.Free;

Ausgabe:

A: Methode 1

B: Methode 1


Das Überschreiben hat aber keinen Einfluß auf den Vorfahren selbst. Man beachte, daß Objekt B zwei Felder und zwei Methoden (und in Pascal zwei Properties) besitzt. Da aber jeweils die alten und die neuen Elemente denselben Namen besitzen, werden die alten Elemente verborgen bzw. überschrieben, jedoch nicht ersetzt. Der explizite Zugriff auf Elemente vorhergehender Klassen erfolgt in C++ mit Hilfe des Scope-Operators über die genaue Angabe der (Vorfahr-)Klasse und in Object Pascal mit Hilfe des Schlüsselwortes inherited. Mit inherited wird immer auf die vorhergehende Klasse zugegriffen, ohne daß man deren genauen Namen angeben muß.

Methode 1 in Klasse B und das Hauptprogramm werden im Beispiel abgeändert:


VC++

Object Pascal

int B::Methode1()
{
  A::Methode1();
  printf("B: Methode 1\n");
  return 1;
};
···

B* ObjB;

ObjB = new B;
ObjB->Methode1();
delete ObjB;

Ausgabe:

A: Methode 1

B: Methode 1

function B.Methode1: Integer;
begin
  inherited Methode1;  
  writeln('B: Methode 1');
  Result:= 1;
 end;
···

var ObjB: B;

ObjB:= B.Create;
ObjB.Methode1;
ObjB.Free;

Ausgabe:

A: Methode 1

B: Methode 1


Object Pascals Inherited darf nur in der Methodenimplementierung benutzt werden, während C++ die explizite Klassenangabe auch außerhalb der Methodenimplementierung (zum Beispiel im Hauptprogramm) gestattet, wenn die Klassen-Ableitung wie im Beispiel öffentlich (public) erfolgte:


B* ObjB;

ObjB = new B;
ObjB->Methode1();
ObjB->A::Methode1();
ObjB->A::Feld1 = 5;
ObjB->Feld1 = 6;
delete ObjB;


Durch die explizite und exakte Angabe der (Vorfahr-)Klasse in C++ werden, anders als in Object Pascal, statische Klassenbindungen erwirkt und polymorphes Verhalten dadurch unterlaufen. Einen zu Object Pascal kompatiblen Zugriff auf die Eltern-Klasse kann aber nach [6] mit relativ wenig Aufwand auch in C++ realisiert werden, indem in jeder Klasse durch eine typedef-Deklaration unter dem Bezeichner "inherited" die jeweilige Eltern-Klasse bekannt gemacht wird:


class A
{
  ···
};

class B : public A
{ 
  typedef A inherited;
  void virtual print();
  ···  
};

class C : public B
{
  typedef B inherited;
  void virtual print();
  ···
};


void C::print()
{
  inherited::print();
  ···  
};


Die jeweilige typedef-Deklaration stellt eine lokale Typ-Deklaration dar. Der Gültigkeitsbereich eines klassen-lokalen Typs ist auf die Klasse und deren Klassen-Methoden beschränkt, in dem er deklariert wurde. Durch den begrenzten Gültigkeitsbereich ist sichergestellt, daß inherited für jede Klasse einmalig ist und keine Konflikte mit Basis- oder abgeleiteten Klassen auftreten können, die einen eigenen Typ unter dem selben Namen deklariert haben.

Object Pascal gestattet keine lokalen Typ-Deklarationen.


Bei der Vererbung kann in C++ optional angegeben werden, ob die Vererbung private oder public erfolgen soll (voreingestellt ist private).


class B : public A
{
   ...
};
class B : private A
{
   ...
};


Protected- und public- Elemente der Basisklasse werden bei einer private-Ableitung zu private-Elementen. Ableitungen in Object Pascal entsprechen der public Ableitung in C++: die Zugriffsrechte der Basisklasse bleiben in der abgeleiteten Klasse erhalten.


Neben der Einfachvererbung (single inheritance) gestattet C++ zusätzlich Mehrfachvererbung (multiple inheritance), bei der eine abgeleitete Klasse aus mehreren Basisklassen gebildet wird. In Anlehnung an das vierte Bild in Kapitel 2.1 kann Klasse "Amphibienfahrzeug" durch Mehrfachvererbung aus Land- und Wasserfahrzeug gebildet werden:


class AmphibienFahrzeug : public LandFahrzeug,
                          private WasserFahrzeug
{
  ...
};
 

Probleme können Mehrdeutigkeiten bereiten, wenn Elemente mit gleichem Namen (aber unterschiedlicher Funktionalität) in mehreren Basisklassen existieren.

Bei der Entwicklung der Klassenbibliothek von Visual C++ wurde auf die Verwendung von Mehrfachvererbungen verzichtet. Die Online-Dokumentation merkt dazu an: "Wir haben herausgefunden, daß die Mehrfachvererbung weder bei der Entwicklung einer Klassenbibliothek, noch beim Schreiben ernsthafter Anwendungen benötigt wird." [13]


2.3.6.3 Zuweisungskompatibilität


In C++ können nur Klassen, die public abgeleitet wurden, vom Compiler implizit in ihre Basisklassen konvertiert werden. Solche C++ Klassen und alle Klassen in Object Pascal sind zuweisungskompatibel mit sich selbst und mit ihren Vorfahren.


VC++

Object Pascal


class Figur{
  ···
};


class Rechteck : public Figur{
  ···
};

class RundRechteck : public 
                     Rechteck{
  ···
};

class Ellipse : public Figur{
  ···
};

type
  Figur = class 
   ··· 
  end;
  
  Rechteck = class(Figur)
   ··· 
  end;

  RundRechteck = class(Rechteck)
   
    ···
  end;
  
  Ellipse = class(Figur)
   ···
  end;

Figur* F;
Rechteck* R;
RundRechteck* RR;
Ellipse* E;
  ···
F = F;
F = R; 
F = RR;
F = E;

R = RR;
R = F; 
// Fehler, inkompatible Typen
R = E; 
// Fehler, inkompatible Typen

RR = R; 
// Fehler, inkompatible Typen

var F:  Figur;
    R:  Rechteck;
    RR: RundRechteck;
    E:  Ellipse;
  ···
F := F;
F := R;
F := RR;
F := E;

R := RR;
R := F;  
// Fehler, inkompatible Typen
R := E;  
// Fehler, inkompatible Typen

RR := R; 
// Fehler, inkompatible Typen


Wie man sieht, gilt die Zuweisungskompatibilität umgekehrt nicht. Eine Klasse ist mit keinem ihrer Nachfahren direkt zuweisungskompatibel (R := F). Zwischen gänzlich unverwandten Klassen, die völlig verschiedenen Ästen der Objekthierarchie entstammen, besteht ebenfalls keinerlei Zuweisungskompatibilität (R := E).

Durch statische und dynamische Typkonvertierung sind aber trotzdem auch solche Zuweisungen möglich:


VC++

Object Pascal

dynamische
Objekttyp-Konvertierung:

RR = new RundRechteck;
F = RR;
 ···
R = dynamic_cast<Rechteck*>(F);

dynamische
Objekttyp-Konvertierung:

RR := RundRechteck.Create;
F := RR;
 ···
R := F as Rechteck;

statische
Objekttyp-Konvertierung:

F = new Figur;
 ···
R = static_cast<Rechteck*>(F);

statische
Objekttyp-Konvertierung:

F := Figur.Create;
 ···
R := Rechteck(F);


Solche Typwandlungen funktionieren nur dann, wenn beide Objekttypen Zeiger sind (ist in Object Pascal immer der Fall).

Bei dynamischer Typ-Konvertierung verläuft die Zuweisung zur Laufzeit nur dann erfolgreich, wenn der zuzuweisende und zu konvertierende Objektzeiger auf ein Objekt zeigt, das zuweisungskompatibel zur Ziel-Variablen ist. Diese Bedingung ist im Beispiel oben erfüllt: Zunächst wurde F ein gültiges Objekt vom Typ RundRechteck zugewiesen. Durch Typkonvertierung wird das Objekt, auf das F zeigt (ein RundRechteck), in ein Rechteck konvertiert. RundRechtecke sind, wie weiter oben gesehen, zu Rechtecken zuweisungskompatibel, weil RundRechteck ein Nachfahre von Rechteck ist (also ist R = RR erlaubt).

Eine Besonderheit bei der dynamischen Typkonvertierung stellt in Object Pascal die Tatsache dar, daß bei der Verwendung des "as" - Operators in dem Ausdruck


MyObject as MyClass


"MyClass" nicht unmittelbar den Namen einer Klasse angeben muß, sondern auch eine Variable vom Typ einer "Metaklasse" sein kann. Die tatsächliche Klasse dieser Variablen wird dann erst zur Laufzeit entnommen; d.h. der Compiler weiß in diesem Fall zur Übersetzungszeit weder, welcher Objekttyp zu konvertieren ist, noch in welchen Objekttyp diese Unbekannte zu konvertieren ist. Alle Entscheidungen werden auf die Laufzeit verschoben.

Beim Fehlschlag einer dynamischen Konvertierung wird in Visual C++ eine Exception der Klasse "bad_cast" und in Object Pascal eine Exception der Klasse "EInvalidCast" ausgelöst. Dynamische Typ-Konvertierungen beruhen auf den Run-Time-Typ-Informationen (RTTI) von Klassen.


Bei statischer Typ-Konvertierung existiert keine Einschränkung der Art, daß der zu konvertierende Objektzeiger auf ein Objekt zeigen muß, das zuweisungskompatibel zum Zieloperanden ist. Dafür gehen aber bei der Nutzung der statischen Konvertierung polymorphe Eigenschaften des zu konvertierenden Objekts verloren. Die Methodenadressen virtueller Methoden werden nicht zur Laufzeit aus der Methodentabelle (VMT), sondern bei der Übersetzung statisch ermittelt.



2.3.6.4 Methoden und spezielle Felder


Methoden sind Prozeduren oder Funktionen, deren Funktionalität fest mit einer Klasse verknüpft ist. Methoden werden üblicherweise in Verbindung mit einer Instanz der Klasse aufgerufen. Bei Klassenmethoden ist auch ein Aufruf in Verbindung mit einer Klasse möglich.


Alle Methoden, die in einer Klasse deklariert werden, sind standardmäßig statisch (nicht zu verwechseln mit static!). Die Adresse der aufzurufenden Methode steht bereits zur Compilierzeit fest und kann vom Linker fest in das Programm eingetragen werden. Statische Methoden entsprechen in dieser Hinsicht gewöhnlichen Funktionen und Prozeduren außerhalb von Klassen. In C++ sind alle Konstruktoren statische Methoden.


Virtuelle Methoden werden in beiden Sprachen mit Hilfe des Wortes "virtual" deklariert. Die Adresse des Funktions-Aufrufes steht bei ihnen nicht bereits zur Zeit der Compilierung fest. Statt dessen wird die exakte Methoden-Adresse erst zur Laufzeit des Programms ermittelt.

Wird in C++ eine virtuelle Funktion in einer abgeleiteten Klasse neu definiert, dann ist auch diese Version der Funktion automatisch virtuell. Eine weitere Angabe von virtual ist erlaubt, aber nicht unbedingt nötig. In Object Pascal wird dagegen nur bei der Erstdeklaration einer virtuellen Funktion der Methodenzusatz virtual angegeben. Wird eine virtuelle Funktion in einer abgeleiteten Klasse neu definiert, dann muß hier durch Angabe des Wortes "override" kenntlich gemacht werden, daß diese Funktion eine neue Implementierung der virtuellen Basisfunktion darstellt. Andernfalls wird die neue Funktion als statische Funktion interpretiert und überschreibt (versteckt) die virtuelle Funktion. Wird in einer abgeleiteten Klassen eine bisher existierende, virtuelle Funktion erneut deklariert und mit dem Bezeichner virtual versehen, so geht in Object Pascal die Beziehung zur bisherigen virtuellen Funktionen verloren. Es wird vielmehr eine neue virtuelle Funktion erzeugt, welche die bisherige überschreibt (versteckt).

Die einmal festgelegte Liste formaler Parameter muß beim Überschreiben einer virtuellen Methode beibehalten bleiben. In C++ können zwar alle Methoden überladen werden, also mit gleichem Namen, aber veränderten Parametern neu definiert werden. Allerdings geht beim Überladen virtueller Methoden die polymorphe Beziehung zu möglichen Vorfahr-Methoden verloren. Es können also beim Überladen höchstens neue virtuelle Funktionen erstellt werden.


VC++

Object Pascal

class Figur{
public:
  void virtual print();
};

class Rechteck : public Figur{
  void virtual print();  
};

class RundRechteck : public Rechteck{
  void virtual print();
};

void Figur::print()
{
  printf("Ich bin die Figur");
};

void Rechteck::print()
{
  printf("Ich bin ein
          Rechteck");
};

void RundRechteck::print()
{
  printf("Ich bin ein
          RundRechteck");
};

type
  Figur = class
    procedure print; virtual;
  end;

  Rechteck = class(Figur)
    procedure print; override;
  end;

  RundRechteck = class(Rechteck)
    procedure print; override;
  end;


procedure Figur.Print;
begin
  writeln('Ich bin die Figur');
end;

procedure Rechteck.Print;
begin
  writeln('Ich bin ein
           Rechteck');
end;

procedure RundRechteck.Print;
begin
  writeln('Ich bin ein
           RundRechteck');
end;

Figur* F;
RundRechteck* RR;

RR = new RundRechteck;
F = RR;
F->print();
// print() von Figur
// aufgerufen!
delete RR;

Ausgabe:

Ich bin ein RundRechteck

var F:  Figur;
    RR: RundRechteck;

RR:= RundRechteck.Create;
F:= RR;
F.Print; 
// Print von Figur 
// aufgerufen!
RR.Free;

Ausgabe:

Ich bin ein RundRechteck


Wenn die Methoden print nicht virtuell deklariert worden wären, hätte man statt dessen die Ausgabe "Ich bin die Figur" erhalten.


Dynamische Methoden sind nur in Object Pascal vertreten und werden durch die Angabe von "dynamic" anstelle von "virtual" deklariert. Sie verhalten sich exakt gleich wie virtuelle Funktionen, unterscheiden sich jedoch in der (internen) Implementierung. Um den Sinn dynamischer Methoden zu verstehen, muß man zunächst untersuchen, wie virtuelle Methoden durch die Compiler praktisch realisiert werden.

In identischer Weise legen Visual C++ und Object Pascal eine sogenannte virtuelle Methodentabelle, abgekürzt VMT, für jede Klasse (nicht für jedes Objekt!) an. Die VMT wird in C++ manchmal auch V-Tabelle (abgekürzt vtbl) genannt. Sie stellt ein Array von 32 bit langen Pointern dar, die auf die virtuellen Methoden zeigen. Jede Methode, die einmal virtuell deklariert wurde, erhält hierin einen Eintrag:


Virtuelle Methoden-Tabelle


Die Anordnung der Einträge erfolgt in derselben Reihenfolge wie bei der Deklarierung. Der Compiler kodiert einen Methodenaufruf einer virtuellen Funktion als indirekten Aufruf über dieses Array anhand des Indexes der virtuellen Methode. Dieser Index ist zur Zeit der Compilierung bekannt und für alle Methoden in einer virtual/override-Sequenz einer Klassenhierarchie gleich. Jede Klasse bekommt eine vollständige Kopie der VMT der Elternklasse, die nach Bedarf vergrößert wird, wenn die Klasse neue virtuelle Methoden deklariert. Bei virtuellen Methoden, die in Nachfahren neu definiert werden (override), ersetzt der Compiler einfach die Adresse im entsprechenden Eintrag der VMT durch die der neuen Methode.

Den Aufruf einer virtuellen Methode per Methoden-Index kann man sich zur Laufzeit grob so vorstellen:


  1. Aktuelles Objekt und Klassentyp dieses Objekts ermitteln.
  2. VMT-Array für diesen Klassentyp aufsuchen.
  3. Element VMT_ARRAY[MethodenIndex] auslesen. Man erhält die Einsprung-Adresse, an der die Funktion beginnt, die für das aktuelle Objekt aufgerufen werden soll.
  4. Zu dieser Adresse springen und Abarbeitung beginnen. Als Parameter wird der this- / Self - Parameter (implizit) an die Methode übergeben.


Der Aufruf virtueller Methoden läuft, trotz des großen Aufwands, recht schnell ab, benötigt aber natürlich minimal mehr Zeit als der Aufruf statischer Methoden.

Zur Realisierung dynamischer Methoden legt Object Pascal neben der VMT noch eine zweite, dynamische Methodentabelle (DMT) für jede Klasse an. Auch diese Tabelle stellt ein Array von Pointern auf Methoden dar, ist jedoch anders aufgebaut als die VMT. Zu jedem Pointer (=Value) existiert ein numerischer Schlüssel (=Key). Beim Aufruf einer dynamic- Methode wird nicht einfach ein Index zum Zugriff verwendet, sondern das Array anhand des Keys (der die Rolle des Indexes übernimmt) durchsucht. Anders als bei den VMTs hat jede Klasse nur die Methoden-Einträge in der DMT, die mit dynamic neu deklariert bzw. die mit override als polymorphe Nachfahren einer dynamischen Methode deklariert wurden. Dafür besitzen DMTs einen Zeiger, der auf die DMT der Elternklasse verweist. Wenn die Suche nach dem Key in der DMT der Klasse erfolglos blieb, werden die DMTs der Elternklasse(n) durchsucht, notfalls bis zurück zur DMT der Klasse TObject.

Dynamische Methoden bieten nur dann Vorteile, wenn eine Basisklasse sehr viele virtuelle Methoden und eine Anwendung zusätzlich sehr viele Nachkommen-Klassen deklariert, und zwar überwiegend ohne override. Der Mechanismus der dynamischen Methoden ist hauptsächlich für die Implementierung von Botschaftsbehandlungsmethoden gedacht. Gegenüber "normalen" virtuellen Methoden ist der Zugriff langsamer, dafür wird durch eine DMT aber weniger Speicherplatz verbraucht als für eine entsprechende VMT. Eine virtuelle Methodentabelle für eine bestimmte Klasse ist immer mindestens so groß, wie die VMT der Elternklasse.


Botschaftsbehandlungs-Methoden sind ebenfalls nur in Object Pascal realisiert. Sie dienen dazu, benutzerdefinierte Antworten auf dynamisch zugeteilte Botschaften zu implementieren. Sie sind intern als dynamische Methoden realisiert.

Windows-Programme werden oft durch einen Strom von Ereignissen aktiviert, die durch äußere Einflüsse hervorgerufen wurden. Anwendungen, die eine Benutzerschnittstelle aufweisen, werden durch ereignis-auslösende Eingabegeräte wie Tastatur und Maus bedient. Jedes Drücken einer Taste, jeder Klick mit der Maus erzeugt Botschaften, die die Anwendung empfängt. Es ist die Aufgabe der Anwendung, auf diese Botschaften sinnvoll im Sinne der Anwendung zu reagieren. Botschaftsbehandlungs-Methoden eignen sich hervorragend, um benutzerdefiniert auf Windows-Botschaften reagieren zu können. Folgerichtig setzt Delphis Klassenbibliothek Botschaftsbehandlungs-Methoden dazu ein, um die Behandlung von Windows-Botschaften zu implementieren. Andererseits sind aber die Botschaftsbehandlungs-Methoden vollkommen unabhängig vom Nachrichtensystem Windows.

Eine Methode wird zu einer botschaftsverarbeitenden Methode, indem an ihre Deklaration das Schlüsselwort "message", gefolgt von einer ganzzahligen, positiven Konstanten angehängt wird. Diese Konstante ist der Botschaftsbezeichner.


type
  TMyButton = class(TButton)
    procedure WMButton1Down(var Message); message WM_LBUTTONDOWN;
    ...
  end;


Zusätzlich muß die Methode eine Prozedurmethode sein, die über genau einen var-Parameter beliebigen Typs verfügt. Auch ein untypisierter var-Parameter ist erlaubt, so daß die Übergabe beliebiger Typen möglich wird. Wichtig für das Funktionieren des Botschaftsmechanismus ist nur, daß die ersten vier Byte des var-Parameters die Nummer der Botschaft enthalten. Die darauf folgenden Daten können vom Programmierer frei bestimmt werden.


type
  MeineMsg = record
    Msg: Cardinal;   // die ersten 4 Byte für Botschafts-Nummer
    Text: String;    // Beispiel für einen Botschafts-Inhalt
  end;

  
A = class
  public
    procedure Botschaftbehandlung1(var M: MeineMsg); message 1;
  end;

procedure A.Botschaftbehandlung1(var M: MeineMsg);
begin
  writeln('A: Botschaft ', M.Text, ' erhalten.');
end;

var
  ObjA: A;
  Nachricht: MeineMsg;
begin
  ObjA:= A.Create;

  Nachricht.Msg:= 1;            // NachrichtenNummer eintragen
  Nachricht.Text:= 'Guten Tag'; // NachrichtenInhalt eintragen
  ObjA.Dispatch(Nachricht);     // Nachricht verschicken

  ObjA.Free;
end.


Programm-Ausgabe: A: Botschaft Guten Tag erhalten.


In diesem Beispiel schickt Objekt A eine Botschaft an sich selbst. Das ist nicht besonders sinnvoll, demonstriert aber den Mechanismus des Versendens und Empfangens von Botschaften. Durch den Aufruf von Dispatch wird eine Botschaft verschickt. Der Empfänger der Botschaft ist klar bestimmt. Es ist aber für den Sender unerheblich, von welchem Klassen-Typ der Empfänger ist; der Empfänger kann irgendein beliebiges Objekt sein.

Die Prozedur Dispatch, die in allen Objekten bekannt ist, erledigt auch das Auffinden der zur Botschaft passenden Methode und ruft diese dann auf. Dazu sucht Dispatch zuerst in der DMT der tatsächlichen Klasse der Instanz nach einer Methode mit der in der Botschaft enthaltenen Nummer. Ist dort keine solche Methode enthalten, wird, wie bei allen dynamischen Methoden, die Tabelle des Vorfahren der Klasse durchsucht, dann die von dessen Vorfahren, und so weiter. In welchem Schutzabschnitt einer Klasse (private, protected, public) eine Botschaftsbehandlungs-Methode definiert wurde, ist für den Empfang von Botschaften unerheblich.

Es ist ein entscheidendes Merkmal der Botschaftsbehandlungs-Methoden, daß für den Aufruf einer botschaftsverarbeitenden Methode über Dispatch nur deren Nummer entscheidend ist. Der Name der Methode ist völlig irrelevant (im Beispiel lautet er Botschaftbehandlung1). Es ist insbesondere möglich, eine botschaftsverarbeitende Methode durch eine Methode völlig anderen Namens zu überschreiben, solange nur die Nummern übereinstimmen.

Enthält beim Aufruf von Dispatch der gesamte in Frage kommende Zweig der Klassenhierarchie keine Methode mit der angegebenen Nummer, dann wird eine virtuelle Methode DefaultHandler aufgerufen, die in der Basisklasse TObject wie folgt deklariert ist:


procedure DefaultHandler(var Message); virtual;


Ein Aufruf dieser Methode bewirkt nichts, weil der Funktionsrumpf der Methode leer ist. Es ist aber möglich, DefaultHandler zu überschreiben, um ein anderes Standardverhalten bei unbehandelten Nachrichten zu implementieren, zum Beispiel die Ausgabe einer Fehlermeldung. Innerhalb der Klassenbibliothek VCL ruft die Methode DefaultHandler für Fenster-Klassen die Windows-API-Funktion DefWindowProc auf, die der Standardbehandlung von Windows-Botschaften dient.

Man kann für das Versenden von Botschaften eine alternative Routine schreiben, die sich mehr an die Syntax der Windows-Funktion SendMessage anlehnt:


function SendMsg(ZielObject: TObject; var Message): Boolean;
begin
  if ZielObject <> nil then begin
    ZielObject.Dispatch(Message);
    SendMsg:= True; 
  end else
    SendMsg:= False;
end;


Im obigen Beispiel-Programm könnte dann das Versenden einer Botschaft statt durch

   ObjA.Dispatch(Nachricht);

durch einen Aufruf

   SendMsg(ObjA, Nachricht);

erfolgen.


Auch innerhalb von Botschaftsbehandlungs-Methoden kann die ererbte Botschaftsbehandlungs-Methode der Vorfahr-Klasse mit Hilfe einer inherited-Anweisung aufgerufen werden. Der var-Parameter wird beim inherited-Aufruf automatisch als Parameter übergeben, ohne daß er explizit angegeben werden müßte. Botschaftsbehandlungs-Methoden unterstützen auf diese Weise noch besser dynamische Konzepte. Ein Objekt sendet eine Botschaft und der Empfänger entscheidet selbst, was er mit der eingehenden Nachricht macht: ob er sie überhaupt auswertet, ob er die Nachricht anderen mitteilt, ob er dem Sender darauf antworten will usw. Ob eine Botschaft behandelt werden soll (vom Objekt "verstanden wird"), wird allein dadurch bestimmt, ob eine Botschaftsbehandlungs-Methode mit der Botschaftsnummer definiert wurde. In jedem Fall steht immer eine ererbte Implementierung zur Verfügung.

Die durch Dispatch aufgerufene Methode ähnelt im weitesten Sinne einer Software-Interrupt-Behandlungsroutine. Der "Interrupt" wird durch das Verschicken einer Nachricht mittels Dispatch ausgelöst. Es findet also insbesondere kein ressource-fressendes Polling statt, bei dem ein Objekt in einer Endlosschleife prüfen würde, ob irgendwelche Botschaften eingegangen sind, um dann in einer bestimmten Weise darauf zu reagieren.


Das Schlüsselwort "static" hat in C++ auch im Zusammenhang mit Klassen-Feldern und Methoden besondere Bedeutung. Ein Feld, das in einer Klasse mit static deklariert wurde, wird von allen Variablen dieser Klasse (von allen Objekten) gemeinsam genutzt. Es wird nur einmal gespeichert und ist deswegen nicht von einer bestimmten Instanz abhängig. Static-Felder müssen explizit deklariert und im globalen (dateiweiten) Block definiert werden. Sie wirken im Kontext der zugeordneten Klasse wie globale Variable.




class A{
public:
  static int x;
  ···
};

int A::x = 8;               // explizite Definition nötig

void main()
{  
  A ObjA1;
  A ObjA2;

  ObjA1.x = 4;
  printf("%d\n", A::x);     // Ausgabe 4
  printf("%d\n", ObjA2.x);  // Ausgabe 4
}


Object Pascal kennt keine static-Felder. Die einzige (nicht sehr elegante) Möglichkeit, static-Felder nachzuahmen besteht darin, diese durch globale Variable zu ersetzen. Nach Möglichkeit sollten sie im Implementation-Abschnitt einer Unit definiert werden, so daß sie nicht im gesamten Programm sichtbar sind.


Beide Sprachen können Methoden definieren, die bereits vor einer Klassen-Instanzierung aufgerufen werden können. C++ nennt diese static-Methoden und Object Pascal bezeichnet sie als Klassenmethoden (gekennzeichnet durch das Wort class). Innerhalb solcher Methoden kann nicht auf Felder, Properties und normale Methoden zugegriffen werden.


VC++

Object Pascal



class A{
public:
  static void Print(void);
};

void A::Print(void)
{
  printf("Print!\n");
};

void main()
{  
  A::Print();
  // Aufruf als Klassenmethode

  A* ObjA = new A;
  ObjA->Print();
  // Aufruf als Objektmeth.
}
type
  A = class
  public
    class procedure Print;
  end;

class procedure A.Print;
begin
  writeln('Print!');
end;

var ObjA: A;
begin
  A.Print; 
  // Aufruf als Klassenmethode

  ObjA:= A.Create;
  ObjA.Print; 
  // Aufruf als Objektmeth.
end.


Syntaktischer Unterschied: Einer static-Methode darf in C++ bei der Methodenimplementierung nicht ein zweites Mal das Schlüsselwort static vorangestellt werden. In Object Pascal dagegen muß das Schlüsselwort class auch vor der Methodenimplementierung ein zweites Mal erscheinen. Static-/Klassenmethoden werden auch in den Klassenbibliotheken eingesetzt, um z.B. neue Objektinstanzen anzulegen:


VC++

Object Pascal

Im Makro DECLARE_DYNCREATE:

static CObject* CreateObject();

In der Klassendefinition zu TObject:

class function NewInstance:
             TObject; virtual;



Neben static-Feldern und Methoden kennt C++ noch const-Felder und Methoden. Bei const-Methoden wird das Schlüsselwort const zwischen Parameterliste und Funktionsrumpf plaziert und gibt damit an, daß durch diese Methode keine Felderinhalte des Objekts modifiziert werden.


class A{
public:
  int i;
  void Print(void) const;
};

void A::Print(void) const
{
  printf("Print!\n");
  i = 8;    
  // Fehler, da Felder nicht verändert werden dürfen
};


Der Compiler prüft allerdings nicht wirklich, ob Felder unverändert bleiben. Er meldet beim Übersetzen einen Fehler, wenn er einen Ausdruck erkennt, der die Möglichkeit zur Veränderung eines Feldes bietet. So dürfen Felder z.B. generell nicht als left-value Ausdrücke auftreten. Der Compiler bemängelt deswegen eine Zuweisung der Art

i = i;

die eigentlich gar keine Feldänderung zur Folge hat.


Const-Felder stellen Felder in Objekten dar, die bei der Objekt-Initialisierung einen festen Wert zugewiesen bekommen und dann nicht mehr verändert werden dürfen. Die Initialisierung muß in einer kommaseparierten Konstruktor-Initialisierungsliste erfolgen. Auch Referenzen müssen über eine Konstruktor-Initialisierungsliste initialisiert werden. Referenz steht hier wieder im Zusammenhang mit dem C++ Operator, durch den ein Alias für eine Variable erzeugt wird. Diese C++ Referenz hat nichts direkt mit dem "Referenz-Modell" von Object Pascal zu tun.


Initialisierung eines const-Feldes in einer C++ Klasse

Initialisierung eines Referenz-Feldes in einer C++ Klasse

class A{
public:
  const int i;
  A(void);    // Konstruktor
  void Print(void);
};



A::A(void) : i(3)  // i = 3
{
};

void A::Print(void)
{
  printf("%d", i);
};

void main()
{  
  A ObjA;
  ObjA.Print();
}


Ausgabe: 3

class A{
public:
  int i;
  int& k;   // Referenz
  A(void);  // Konstruktor
  void Print(void);
};

A::A(void) : k(i)   // k = i
{
  i = 6;
};

void A::Print(void)
{
  printf("%d", k);
};

void main()
{  
  A ObjA;
  ObjA.Print();
}

Ausgabe: 6


Einfache Funktionen können in C++ mit dem Zusatz inline deklariert werden (vgl. auch Kapitel 2.3.5). Das gilt auch für statische und virtuelle Methoden in Klassen. Steht der genaue Typ eines Objekts bereits zur Compilierzeit fest, so wird ein virtueller Funktionsaufruf nicht über die VMT codiert, sondern vom Compiler als inline-Funktion expandiert.


class A{
public:
  virtual void Print(void);
};

class B : public A{
public:
  virtual void Print(void);
};

inline void A::Print(void)
{
  printf("Print von A");
};

inline void B::Print(void)
{
  printf("Print von B");
};

void main()
{
  B Obj;
  Obj.Print();
}


Der tatsächliche Objekttyp steht aber, zumindest aus der Sicht des Compilers, bei dynamisch instanzierten Objekten niemals genau fest. Virtuelle inline-Methoden müssen deswegen meist doch über die VMT codiert werden, so daß ein Geschwindigkeitsvorteil nur bei Aufrufen (kleiner) statischer inline-Funktionen als sicher gelten kann.


Abstrakte Methoden werden in Basisklassen als virtuelle oder dynamische Methoden definiert. In C++ werden sie auch "rein virtuelle Funktionen" (pure virtual functions) genannt. Abstrakte Methoden werden erst in abgeleiteten Klassen implementiert, sind aber bereits in der Basisklasse bekannt und können so von anderen Methoden aufgerufen werden. Für abstrakte Methoden wird keine Adresse in die VMT eingetragen; der VMT-Eintrag erhält den Wert NULL bzw. nil.


VC++

Object Pascal


class A{
public:  
  // abstrakte Methode
  virtual void Print(void) = 0;
};



class B : public A{
public:  
  virtual void Print(void);
};

void B::Print(void)
{
  printf("Born in the USA");
};
type 
  A = class
  public
    // abstrakte Methode
    procedure Print;
              virtual; abstract;
  end;

  B = class(A)
  public
    procedure Print; override;
  end;

procedure B.Print;
begin
  writeln('Born in the USA');
end;


Eine Klasse, die mindestens eine abstrakte Methode besitzt, wird abstrakte Basisklasse genannt. In Visual C++ kann eine Klasse erst dann instanziert werden, wenn alle abstrakten Methoden überschrieben und implementiert wurden. Object Pascal läßt die Instanzierung von Klassen zu, in denen abstrakte Methoden noch nicht implementiert wurden, gibt aber beim Übersetzen eine entsprechende Warnmeldung aus. Wird eine abstrakte Methode eines Objekts aufgerufen, bei dem die Methode nicht überschreiben wurde, so wird zur Laufzeit eine Exception ausgelöst.


Auf Felder und Methoden einer Klasse kann auch mittels eines Zeigers verwiesen werden (Klassenelement-Zeiger). Zeiger auf Felder (Datenelement-Zeiger) wurden bereits in der Tabelle der Operatoren eingeführt und Zeiger auf Methoden im Abschnitt "Prozedurale Datentypen" besprochen.


2.3.6.5 Konstruktoren und Destruktoren


Konstruktoren sind spezielle Methoden, die das Erzeugen und Initialisieren von Objekten steuern. In C++ müssen Konstruktoren denselben Namen tragen, den die Klasse besitzt. In Object Pascal dagegen können die Namen für Konstruktoren beliebig gewählt werden. Daß eine Methode ein Konstruktor ist, wird hier durch das Schlüsselwort "constructor" kenntlich gemacht, der den Vorsatz "procedure" ersetzt.

Destruktoren bilden das Gegenstück zu den Konstruktoren und definieren die Aktionen, die mit dem Entfernen eines Objekts verbunden sind. In C++ muß ein Destruktor denselben Namen wie die Klasse, jedoch mit vorgesetzter Tilde ~ , tragen. Destruktoren in Object Pascal werden durch das Schlüsselwort "destructor" kenntlich gemacht und unterliegen keinen Namensbeschränkungen. Pro Klasse ist in C++ die Definition eines Destruktors, in Object Pascal die Definition beliebig vieler Destruktoren gestattet. Destruktor-Methodenaufrufe können in beiden Sprachen keinen Wert als Ergebnis zurückliefern; Funktions-Parameter sind in Object Pascal zulässig und in C++ nicht zulässig. Anders als in C++ muß in Object Pascal bei vererbten Klassen der Destruktor der Vorgänger-Klasse explizit aufgerufen werden (inherited), sofern ein solcher Aufruf gewünscht wird.


VC++

Object Pascal

class A{
private:
  int i;
public:  
  A(int x);  // Konstruktor
  ~A();      // Destruktor
  void Print(void);
};

A::A(int x)  // Konstruktor
{
  i = x; 
  // möglich wäre auch 
  // this->i = x;
};

A::~A(void)  // Destruktor
{
  printf("Tschuess\n");
};

void A::Print(void)
{
  printf("%d\n", i);
};

void main()
{  
  A* ObjA;  
  ObjA = new A(5);
  ObjA->Print();
  delete ObjA;
} 
type A = class
private
  i: Integer;
public
  constructor Start(x: Integer); 
  destructor Fertig; 
  procedure Print;
end;

constructor A.Start(x:Integer); 
begin
  i:= x;
  // möglich wäre auch
  // Self.i:= x;
end;

destructor A.Fertig; 
begin
  writeln('Tschuess');
end;

procedure A.Print;
begin
  writeln(i);
end;

var 
  ObjA: A;
begin
  ObjA:= A.Start(5);
  ObjA.Print;
  ObjA.Fertig;
end.


Bei einer statischen Objekt-Variablen werden in C++ Konstruktor und Destruktor automatisch aufgerufen, wenn der Gültigkeitsbereich der Variablen betreten bzw. verlassen wird. Am Beispiel oben wird ein Unterschied bei der dynamischen Objektinstanzierung zwischen beiden Sprachen deutlich. Ein Objekt wird in C++ durch die Operatoren new und delete erzeugt, ohne daß die Konstruktor- / Destruktor-Methoden explizit aufgerufen werden. In Object Pascal dagegen werden Konstruktor und Destruktor namentlich aufgerufen. Der Konstruktor führt hier als erstes die Speicherreservierung und der Destruktor nach Abarbeitung der benutzerdefinierten Aktionen die abschließende Speicherfreigabe durch. Als Ergebnis eines Konstruktor-Aufrufs wird in Object Pascal (im Erfolgsfall) eine Zeiger-Referenz auf das neu angelegte Objekt zurückgeliefert.

Bei einem Konstruktor-Aufruf werden in Object Pascal nach der Speicherreservierung für das Objekt alle Felder mit 0 und alle Zeiger mit nil initialisiert. C++ führt keine automatische Felderinitialisierung durch. Nur Felder, die explizit in der Initialisierungsliste des Konstruktors aufgeführt werden, bekommen den dort angegebenen Wert zugewiesen (wie im Beispiel mit const-Objektfeld weiter oben).

Wird in C++ bei einer Klassendefinition kein Konstruktor angegeben, so legt der Compiler implizit einen Default-Konstruktor an. In Object Pascal stehen allen Klassen der statische Default-Konstruktor Create und der virtuelle Default-Destruktor Destroy (bzw. eine statische Methode Free, die ihrerseits Destroy aufruft) zur Verfügung.

Konstruktoren können in C++, wie das bei allen normalen Funktionen und Methoden möglich ist, überladen werden, d.h., es können innerhalb eines Objekts weitere Konstruktoren mit veränderter Parameterliste definiert werden.

Ein besonderer Konstruktor ist jener, dessen Parameter-Typ eine Referenz auf ein Objekt der eigenen Klasse darstellt. Er wird Kopier-Konstruktor (Copy-Constructor) genannt und in folgenden Fällen aufgerufen:


  1. Bei der Initialisierung eines Klassenobjekts durch ein anderes Objekt
  2. Beim Aufruf einer Funktion und der Übergabe eines Objekts als aktuellem Parameter
  3. Bei der Ergebnisrückgabe einer Funktion (Objekt ist Rückgabewert)


Wenn kein Kopie-Konstruktor definiert wird, so wird in den genannten drei Fällen in der Objekt-Kopie jedem Feld der Wert des entsprechenden Quell-Objekt-Feldes zugewiesen (memberweises Kopieren/Initialisieren). Probleme können aber dann auftreten, wenn einige Objekt-Felder Zeiger sind. In diesem Fall wird nur der Zeiger (=die Adresse) selbst kopiert, nicht jedoch der Inhalt, auf den er verweist. Das Problem soll an einem Beispiel verdeutlicht werden. Ohne Kopie-Konstruktor (linke Spalte) verweist die Zeigervariable Text im kopierten Objekt auf den (dynamischen) Speicherbereich des Quellobjekts. Durch den Kopie-Konstruktor (rechte Spalte) wird für das kopierte Objekt ein neuer dynamischer Speicherbereich alloziert. Anschließend wird durch einen StringCopy-Aufruf explizit der Text aus dem dynamischen Speicherbereich des Quell-Objekts in den neuen dynamischen Speicher des Ziel-Objekts kopiert.


C++: Ohne Kopie-Konstruktor

C++: Mit Kopie-Konstruktor

class A{
public:
  // Standard-Konstruktor
  A();  


  void Print(void);
  char* Text;
};


// Standard-Konstruktor
A::A()
{
  printf("Std.-Konstruktor");  
  Text = new char[50];
  strcpy(Text, "Hallo Welt");
};

      











void A::Print(void)
{
  printf("%s\n", Text);  
};

void main()
{  
  A Obj1;
  Obj1.Print();
  
  A Obj2 = Obj1;      // !!
  Obj1.Text[3] = '\0';

  Obj2.Print();
} 

Ausgabe:

Std.-Konstruktor

Hallo Welt

Hal

class A{
public:
  // Standard-Konstruktor
  A();
  // Kopie-Konstruktor
  A(A& QuellObj); 
  void Print(void);
  char* Text;
};

// Standard-Konstruktor
A::A()          
{
  printf("Std.-Konstruktor");  
  Text = new char[50];
  strcpy(Text, "Hallo Welt");
};

// Kopie-Konstruktor
A::A(A& QuellObj) 
{
  printf("Kopie-Konstruktor");
  Text = new char[50];
  strcpy(Text, QuellObj.Text);
};

void A::Print(void)
{
  printf("%s\n", Text);  
};

void main()
{  
  A Obj1;
  Obj1.Print();
  
  A Obj2 = Obj1;        // !!
  Obj1.Text[3] = '\0';

  Obj2.Print();
} 

Ausgabe:

Std.-Konstruktor

Hallo Welt

Kopie-Konstruktor

Hallo Welt


Object Pascal kennt keinen Kopie-Konstruktor. Als Ersatz wurde in Delphis Klassenbibliothek VCL eine Methode "Assign" deklariert, die durchgängig in fast allen Klassen existiert:


procedure Assign(Source: TPersistent); virtual;

Sie weist ein Objekt einem anderen zu. Der allgemeine Aufruf lautet:


Destination.Assign(Source);

Dem Objekt Destination wird dadurch der Inhalt von Source zugewiesen. Das klassenspezifische Überschreiben der Methode Assign (bzw. der mit Assign verbundenen Methode AssignTo) entspricht somit der Definition eines klassenspezifischen Kopie-Konstruktors in C++. Allerdings wird eine Assign-Methode, anders als ein Kopie-Konstruktor, niemals automatisch aufgerufen.


Destruktoren können in C++ und Object Pascal virtuell sein. Ein polymorphes Überschreiben wird notwendig, wenn klassenspezifische Freigabeaktionen ausgeführt werden müssen. Virtuelle Konstruktoren sind nur in Object Pascal zulässig und werden im Zusammenhang mit Metaklassen verwendet.


2.3.6.6 Metaklassen


Metaklassen sind in C++ nicht definiert; neben Object Pascal unterstützen z.B. die Sprachen Smalltalk und CLOS (Common Lisp Object System) Metaklassen. Eine Metaklasse ist ein Datentyp, dessen Werte Klassen sind. Während eine Klasse einen Typ definiert, der Objektreferenzen aufnehmen kann, kann eine Variable vom Typ einer Metaklasse Referenzen auf Klassen enthalten. Deshalb werden Metaklassen auch "Klassenreferenztypen" genannt.


VC++

Object Pascal


Klasse => Objekt


zu lesen als:

Instanzen von Klassen sind Objekte


Metaklasse => Klasse => Objekt


zu lesen als:

Instanzen von Metaklassen sind Klassen

Instanzen von Klassen sind Objekte


Der Wertebereich einer Metaklasse umfaßt die Klasse, für die sie deklariert wurde und alle deren Nachfahren, insbesondere auch jene, die erst zu einem späteren Zeitpunkt deklariert werden. Metaklassen können überall dort im Programm eingesetzt werden, wo auch direkt mit Klassen operiert wird:

In allen Fällen muß die tatsächliche Klasse nicht bereits zur Übersetzungszeit feststehen, sondern es wird zur Laufzeit die Klasse benutzt, die als Wert der Variablen vorgefunden wird.


type
  TFigur = class
    constructor Create; virtual; // virtueller Konstruktor
     ···
  end;

  TRechteck = class(TFigur)
    constructor Create; override;
     ···
  end;

  TKreis = class(TFigur)
    constructor Create; override;
    ···
  end;

  TFigurClass = class of Tfigur; // MetaKlasse

var
  // Klassen-Referenz Variable
  ClassRef: TFigurClass;  
  // Objekt-Instanz Variable
  Figur: TFigur;          
begin
  // Klassen-Referenz zeigt auf Klasse Kreis
  ClassRef:= TKreis;      
  // polymorphes Erstellen eines Objekts vom Typ Kreis
  Figur:= ClassRef.Create; 
    ···
  // Objekt wieder freigeben
  Figur.Free;               

  // Klassen-Referenz zeigt jetzt auf  Klasse Rechteck
  ClassRef:= TRechteck;     
  // polymorphes Erstellen eines Rechteck - Objekts
  Figur:= ClassRef.Create;
    ···
  // Objekt wieder freigeben
  Figur.Free;               
end.

Man muß beim polymorphen Konstruieren von Objekten beachten, daß die Konstruktoren der zu benutzenden Objekte als virtuell gekennzeichnet sind. Wenn das nicht der Fall ist, findet die Bindung bereits zur Übersetzungszeit statt. Es wird dann stets eine Instanz der Klasse erzeugt, zu der die Metaklasse der Variablen deklariert wurde (im Beispiel wäre das TFigur).

Stroustrup bewertet in [6] Metaklassen als "sicher recht leistungsfähig", möchte sie aber andererseits nicht in C++ implementieren: "Ich verwehrte mich damals nur gegen den Gedanken, jeden C++ Programmierer mit diesen Mechanismen zu belasten zu müssen ...".


2.3.6.7 Friends


Durch "Friend"-Deklarationen lassen sich in C++ Zugriffseinschränkungen einer Klasse für ausgewählte Funktionen oder Klassen deaktivieren. In Object Pascal existiert ein implizites Friend-Modell, bei dem automatisch alle Klassen "Freunde" sind, die innerhalb einer Unit implementiert sind.

Durch die expliziten und impliziten Freundschaften wird die Klassen-Kapselung aufgehoben und somit bewußt ein elementarer Grundsatz objektorientierter Entwicklung unterlaufen. Andererseits ermöglichen es Freundschaften oftmals, daß Klassen klein und einfach gehalten werden können.




VC++

Object Pascal

class A{
friend class B;    
// Klasse B ist
// Freund von A
private:
  int TopSecret;
};

class B{
public:
  void WeiseZu(A* ObjA);
};

void B::WeiseZu(A* ObjA)
{
   ObjA->TopSecret = 7;
   // B hat Zugriff auf 
   // private Daten von A
};
  
void main()
{  
  A* Obj1 = new A;
  B* Obj2 = new B; 
  
  Obj2->WeiseZu(Obj1);

  delete Obj1;
  delete Obj2;
}

type
A = class  
      
private
  TopSecret: Integer;
end;


B = class
public
  procedure WeiseZu(ObjA: A);
end;

procedure B.WeiseZu(ObjA: A);
begin
  ObjA.TopSecret := 7;
   // B hat Zugriff auf 
   // private Daten von A
end;



var Obj1: A; Obj2: B;
begin
  Obj1:= A.Create;
  Obj2:= B.Create;

  Obj2.WeiseZu(Obj1);

  Obj1.Free;
  Obj2.Free;
end.

class A{
friend void GlobalProc(A* ObjA);
private:
  int TopSecret;
};

// globale Funktion
void GlobalProc(A* ObjA) 
{
  ObjA->TopSecret = 5;
};
  
void main()
{  
  A Obj; 
  GlobalProc(&Obj);
} 
keine Freundschaften
für einzelne 
Funktionen 
und Methoden


Im letzten Beispiel erhält eine globale Funktion GlobalProc die vollständige Zugriffsberechtigung auf alle geschützten Elemente in der Klasse A (hier auf die private Variable TopSecret).

Um zu verhindern, daß unbeabsichtigt auf geschützte Klassenelemente anderer Klassen zugegriffen wird, müssen in Object Pascal einzelne Klassen in separaten Units untergebracht werden.


2.3.6.8 Überladen von Operatoren


Ein Spezialfall des Überladens von Funktionen in C++ stellt das Überladen von Operatoren (operator overloading) dar. Vordefinierte Operatoren erlangen, angewandt auf Klassen, per Definition eine neue Bedeutung. Unter Verwendung des Schlüsselworts "operator" lautet die generelle Syntax:


Ergebnistyp operator Operator-Zeichen ( ... , ... );

Priorität und Assoziativität des überladenen Operators bleiben erhalten. Die zu Beginn des Kapitels eingeführte Klasse Komplex wird im Beispiel um den Operator "+" erweitert:


class Komplex
{
  public:
    void Zuweisung(double r,
                   double i);
    void Ausgabe();
    Komplex operator+(Komplex z);
  private:
    double Real;
    double Imag;
};

  ...

Komplex Komplex::operator+(Komplex z)
{
  z.Zuweisung(Real + z.Real, Imag + z.Imag);
  return z;
};
  
void main()
{ 
  Komplex a, b, c;

  a.Zuweisung(3, 2);
  a.Ausgabe();       // 3+i*2
  b.Zuweisung(10, 5);
  b.Ausgabe();       // 10+i*5
  
  c = a + b;         // überladener Operator + wird benutzt
  c.Ausgabe();       // 13+i*7
}


Durch das Konzept der überladenen Operatoren wird einem Programmierer die Möglichkeit gegeben, eine gewohnte, konventionelle Notation zur Manipulation von Klassenobjekten anzubieten.

Da Object Pascal diese Technik nicht beherrscht, müssen neu zu definierende Funktionen die Aufgabe der überladenen Operatoren übernehmen:


Komplex = class
public
   ...
  procedure Addiere(z: Komplex);
  procedure Assign(z: Komplex);
   ...
end;
...

begin
  ...
  a.Addiere(b);
  c.Assign(a);
end;


2.3.6.9 Klassen-Schablonen


Wie bereits im Kapitel "2.3.5 Programmstruktur" erwähnt, stellen Schablonen eine wesentliche Erweiterung der Sprache C++ dar. In Object Pascal existiert kein entsprechendes Gegenstück zu dieser Spracherweiterung.

Analog zu Funktions-Klassen werden Klassen-Schablonen zur Beschreibung von Schablonen für Klassen erstellt. Mit Hilfe des Schlüsselworts "template" wird eine Familie von Klassen definiert, die mit verschiedenen Typen arbeiten kann. Im Beispiel wird eine Template-Klasse definiert, die ein gewöhnliches Array mit 20 Elementen kapselt und Grenzüberprüfung beim Schreiben der Elemente durchführt. Der exakte Typ, den die 20 Elemente des Arrays annehmen sollen, wird bei der Definition der Template-Klasse jedoch offen gelassen. Er wird nicht näher spezifiziert und allgemein "Typ" genannt.


template <class Typ>
class TemplateKlasse{
public:
  void SetArray(Typ a, int b);
private:
  Typ EinArray[20];
};

template <class Typ> 
void TemplateKlasse<Typ>::SetArray(Typ a, int b)
{
  if(( b >= 0 ) && (b <= 19))  
    EinArray[b] = a;
}


void main()
{ 
  TemplateKlasse<double>* MeinArray1;
  MeinArray1 = new TemplateKlasse<double>;
  // erstes Element belegen
  MeinArray1->SetArray(14.783 , 0);         
  // zweites Element belegen
  MeinArray1->SetArray( -3.66 , 1);         
  delete MeinArray1;

  TemplateKlasse<char*>* MeinArray2;
  MeinArray2 = new TemplateKlasse<char*>;
  // erstes Element belegen
  MeinArray2->SetArray("Hallo Welt" , 0); 
  // zweites Element belegen
  MeinArray2->SetArray("Aha" , 1);          
  delete MeinArray2;
}


Zunächst wird ein Objekt MeinArray1 angelegt, dessen 20 Elemente vom Typ Double sind. Im Anschluß daran wird ein Objekt MeinArray2 angelegt, dessen 20 Elemente Zeiger auf Strings sind (Typ = char*). Bei der Definition und der Instanzierung eines Objekts, das durch eine Klassen-Schablone beschrieben wird, wird dazu explizit der konkrete Typ in spitzen Klammern angegeben. Erst wenn der Compiler auf die Bezeichnung einer Klassen-Schablone in Verbindung mit einem genauen Typ trifft, legt er die entsprechende Klassendefinition im globalen Gültigkeitsbereich des Programms an. Dabei wird der Parameter-Typ textuell durch den konkreten Typ-Namen (hier double bzw. char*) ersetzt.

Außer Typ-Parametern können bei der Schablonen-Definition auch Argumente angegeben werden, die keinen Typ repräsentieren:


template <class Typ, int i>


Im Beispiel oben könnte man dadurch z.B. die starre Fixierung auf 20 Elemente aufheben. Die exakte Array-Größe würde dann erst bei der Schablonen-Instanzierung durch den Parameter i festgelegt werden.

Schablonen eignen sich besonders zur Erstellung leistungsfähiger und kompakter Containerklassen. Durch den Einsatz von Schablonen kann oftmals auf die Nutzung undurchschaubarer und fehleranfälliger Präprozessor -"#define"- Sequenzen verzichtet werden. Die Klassenbibliothek von Visual C++ (MFC) nutzt intensiv sowohl Funktions- als auch Klassen-Schablonen.


2.3.6.10 Eigenschaften - Properties


Eigenschaften, im folgenden Properties genannt, stellen benannte Attribute eines Objekts dar. Properties sind nur in Object Pascal verfügbar.


Wirkung nach außen:


Properties sehen für den Endanwender eines Objekts wie Felder aus, da sie über einen Typ verfügen, wie Felder gelesen werden können und ihr Wert in Form einer Zuweisung verändert werden kann. Properties erweitern aber gewöhnliche Felder, da sie intern Methoden verkapseln, die den Wert des Feldes lesen oder schreiben. Properties können behilflich sein, die Komplexität eines Objekts vor dem Endanwender zu verbergen. Sie gestatten es, den Objektstatus durch Seiteneffekte zu verändern.

Um die Wirkungsweise und Nutzung zu verdeutlichen, soll ein Beispiel angeführt werden. Jedes Fenster-Objekt in Delphis Klassenbibliothek (VCL) besitzt ein Property mit dem Namen "Top". Dies ist ein Lese- und Schreib-Property, mit dem einerseits die aktuelle y-Koordinate der Fensterposition ermittelt werden kann, mit dem andererseits beim Zuweisen eines Wertes an das Property das Fenster an die angegebene y-Koordinate bewegt wird. Ein Objekt-Anwender muß zum Verschieben des Fensters keine Funktionen aufrufen.


// Lesen und Ausgabe des Propertys
ShowMessage(IntToStr(MeinFenster.Top));

// Schreiben des Propertys
MeinFenster.Top := 20;
// das Fenster wird zur Position 20 verschoben


Ein weiteres Beispiel stellt das Property "Caption" dar. Durch das Lesen der Property erhält man den aktuellen Fenster-Text (Überschrift) und durch das Zuweisen an das Property wird der Fenster-Text gesetzt.


ShowMessage(MeinFenster.Caption);

MeinFenster.Caption := 'Hallo Welt';


Der Programmierer muß sich weder damit beschäftigen, wie die Fensterposition / der Fenster-Text ermittelt werden kann, noch muß er wissen, wie man in Windows ein Fenster verschiebt oder wie die Überschrift eines Fensters gesetzt wird.


Realisierung im Inneren:


Es ist möglich, sowohl den lesenden als auch den schreibenden Zugriff auf ein Property einzeln zu gestatten bzw. zu unterbinden. Drei verschiedene Properties erhalten im Beispiel unterschiedliche Zugriffsrechte:


MeineKlasse = class
  FText: String; // normales Feld in d. Klasse
  
  // Lese- und Schreib-Zugriff
  property Text: String read FText write FText;
  // nur Lese-Zugriff
  property Lies_Text: String read FText;
  // nur Schreib-Zugriff
  property Schreib_Text: String write FText;
end;

  ...

 // Property Text gestattet Schreib- und
 Text := 'Hallo';
 // Lese-Zugriff
 ShowMessage(Text);
 
 Schreib_Text := 'Welt';
 ShowMessage(Lies_Text);
 // Fehler, da nur Lese-Zugriffe gestattet sind
 Lies_Text := 'Guten Tag';
 // Fehler, da nur Schreib-Zugriffe gestattet sind
 ShowMessage(Schreib_Text);

Allen drei Properties liegt ein Feld (FText) in der Klasse zugrunde, aus dem gelesen bzw. in das geschrieben wird. Wenn einem Property zum Lesen und Schreiben dasselbe Feld zugrunde liegt (wie bei Text), dann ist dieses Property eigentlich überflüssig, weil es nur den Direktzugriff auf das Feld in der Klasse abfängt.

Sinnvollere Properties erhält man, wenn einer oder beiden Zugriffsarten (read / write) eine Methode zugrunde liegt. In dem Fall wird beim Zugriff auf das Property automatisch die angegebene Methode aufgerufen, so daß durch das Auslesen bzw. das Zuweisen beliebig komplexe Operationen in Gang gesetzt werden können.


MeineKlasse = class
  // normales Feld in d. Klasse
  FText: String;
  // Methode
  procedure SetText(Value: String);
  property Text: String read FText write SetText;
end;

procedure MeineKlasse.SetText(Value: String);
begin
  if Value <> FText then begin
    // Wandlung in Großbuchstaben
    FText:= AnsiUpperCase(Value);
    // Fenstertext mit Windows-API - Funktion setzen
    SetWindowText(MyWin.Handle, PChar(FText));
  end;
end;
  ...

var MeinObj: MeineKlasse;
begin
  MeinObj := MeineKlasse.Create;
  MeinObj.Text := 'Hallo Welt';
end;

Als Ergebnis der Zuweisung an Property Text wird implizit die Methode SetText aufgerufen, die ihrerseits dem Feld FText den Wert "HALLO WELT" zuweist. Derselbe Text erscheint zudem auch in der Überschrift-Zeile eines Fensters.

Die einem Property zugrundeliegenden Methoden müssen immer dieselbe Syntax aufweisen:


Lese-Methode

function MethodenName: PropertyTyp;

Schreib-Methode

procedure MethodenName(Value: PropertyTyp);


Borland empfiehlt für Lese- und Schreib-Methoden eine einheitliche Namensgebung zu gebrauchen. Der Name einer Lese-Methode sollte demnach stets mit dem Vorsatz "Get" beginnen, dem der Property-Name folgt (im Beispiel wäre das GetText). Der Name einer Schreib-Methode sollte stets mit dem Vorsatz "Set" beginnen, dem der Property-Name folgt (wie im Beispiel zu sehen: SetText). Das Feld, auf dem ein Property beruht, sollte immer mit dem Buchstaben "F" beginnen, dem der Property-Name folgt (im Beispiel: FText).


Beim Übersetzen ersetzt der Compiler jede Auslese- und jede Zuweisungs-Anweisung eines Propertys durch das zugeordnete Feld bzw. durch einen Aufruf der zugeordneten Methode. Bei Methodenaufrufen, die Schreibzugriffe ersetzen, setzt er dabei den Wert des Propertys als aktuellen Parameter der Set-Methode ein.

Die Implementierungs-Methoden von Properties können virtuell sein. In abgeleiteten Objekten können die Methoden polymorph überschrieben werden. Das Lesen und Zuweisen der gleichen Properties kann dann im Basis-Objekt ganz andere Aktionen als im abgeleiteten Objekt zur Folge haben.

Neben einfachen Properties existieren noch indizierte Properties, die sich dem Benutzer wie ein Array darstellen. Schreib- und Lese-Methoden für indizierte Arrays benötigen einen weiteren Parameter, der den Index für den Zugriff festlegt.


Für alle Properties, die im published-Abschnitt einer Klasse deklariert sind, erzeugt der Compiler spezielle Informationen, die für die Zusammenarbeit mit Delphis "Objektinspektor" von Bedeutung sind. Diese veröffentlichten Properties bilden die Basis für das "visuelle Programmieren". Beim visuellen Programmieren und Gestalten von Programmen können Objekte in einer Design-Phase auf sogenannten Formularen, den Fenstern der Anwendung, plaziert werden. Alle veröffentlichten Properties werden zur Design-Zeit von Delphi ausgewertet und im Objektinspektor zur Ansicht gebracht. Die Werte der Properties können im Objektinspektor manuell verändert werden, wobei Änderungen zur Design-Zeit genau dieselben automatischen Methodenaufrufe wie im endgültig übersetzten Programm auslösen. Durch dieses Verhalten kann der Programmierer sofort, noch bevor er das Programm neu übersetzt und gestartet hat erkennen, wie sich Eigenschaftsänderungen auswirken. Die Gestaltung der Programmoberfläche kann so aufgrund der Properties regelbasiert nach dem beim Menschen sehr beliebten Schema "Trial and Error" (Versuch und Irrtum) erfolgen. Der Entwickler setzt die Hintergrundfarbe eines Fensters auf rot, indem Property Color im Objektinspektor entsprechend geändert wird. Der implizite Aufruf der Funktion SetColor im betreffenden Fenster-Objekt stößt daraufhin selbständig ein Neuzeichnen des eigenen Fensterbereichs an; das Fenster wird rot dargestellt. Der Entwickler erkennt, daß ein roter Fensterhintergrund doch weniger gut anzusehen ist und kann sofort wieder Property Color auf Grau zurücksetzen.

Die Bezeichnung "visuelles Programmieren" geht auf diese visuellen Gestaltungsabläufe zurück. Durch einfache Maus-Klicks und das Ändern einiger Property-Werte können so erstaunlich schnell komplexe Anwendungen entstehen. Selbst das Erstellen von Datenbankprogrammen mit relationalen Daten-Beziehungen, Abfragen (Queries), Filtern usw. wird auf diese Weise zum Kinderspiel.

Allerdings muß beim Erstellen eigener Objekte, hier auch Komponenten genannt, beachtet werden, daß auf die Änderung aller veröffentlichten Property-Werte richtig bzw. überhaupt reagiert wird. Das kann beim Neu-Erstellen von Komponenten zunächst einigen Mehraufwand bedeuten, der sich aber schnell bezahlt macht. Jeder Endanwender von Objekten erwartet wie selbstverständlich, daß vorgenommene Änderungen sofort entsprechende, sichtbare Auswirkungen zur Folge haben.

Um auf komfortable Weise auch die Werte sehr komplexer, strukturierter Properties im Objektinspektor verändern zu können gestattet es Delphi, für solche Typen eigene "Property-Editoren" zu schreiben. Sie müssen bei der Programmierumgebung (IDE) registriert und angemeldet werden. Die Property-Editoren stellen teilweise komplette kleine Anwendungen, manchmal mit mehreren Dialogfenstern, dar. Sie werden vom Objektinspektor aufgerufen und dienen einzig und allein dem "visuellen Programmieren" und werden (bzw. sollten) nicht mit in das endgültig erstellte Programm aufgenommen (werden).

Die zunächst weniger spektakulär anmutende Spracherweiterung der Properties in Object Pascal kann die hinter diesem Konzept stehende Leistungsfähigkeit erst im Zusammenspiel mit der geschickt gestalteten Programmierumgebung Delphis zum Tragen bringen. Es stellt eine bedeutende Neuerung dar, daß bei der Objekt- und Komponenten-Entwicklung ein Großteil des erstellten Codes mehr oder weniger ausschließlich dafür geschrieben wird, die Gestaltung und Entwicklung von Programmen zu vereinfachen und zu beschleunigen. Properties bilden also nicht nur die Grundlage für "visuelles Programmieren" sondern sind auch der ausschlaggebende Grund für deutlich kürzere Entwicklungszeiten in Delphi gegenüber Visual C++ bei der Programmierung fensterbasierender, GUI-orientierter Anwendungen. GUI = Graphical User Interface (graphische Benutzeroberfläche)


2.3.6.11 Laufzeit-Typinformationen


Durch die Laufzeit-Typinformation (Run-Time Typ Information = RTTI) kann man zur Laufzeit eines Programms die Zugehörigkeit eines Objekts zu einer bestimmten Klasse sicher ermitteln. Damit die Klassen-Zugehörigkeit bestimmt werden kann, muß der Compiler zusätzlichen Code im zu erstellenden Programm einfügen. In Visual C++ werden RTTI erst dann eingefügt, wenn das in den Projektoptionen explizit angegeben wurde. In Object Pascal stehen RTTI immer für alle Objekte zur Verfügung, da bereits die Basisklasse "TObject", von der alle anderen Objekte abgeleitet sind, Typinformationen zur Verfügung stellt.

In Visual C++ und Object Pascal stehen Operatoren und Funktionen zur Ermittlung und Auswertung der RTTI zur Verfügung. In der Tabelle der Operatoren wurden bereits der Objekttyp-Informationsoperator, der dynamische Konvertierungsoperator und die Möglichkeiten zur Objekttyp-Prüfung aufgeführt. C++ Programme müssen die Datei TYPEINFO.H einbinden, um die RTTI ermitteln und auswerten zu können. Der typeid()-Operator liefert als Ergebnis ein Objekt, das die Informationen über ein anderes Objekt enthält. Dieses Informations-Objekt ist in der genannten Header-Datei definiert. Im wesentlichen liefert das Informations-Objekt den Klassennamen und eine Methode "before", die das Informations-Objekt eines benachbarten Objekts liefert. Es besteht aber keine Beziehung zwischen dem so ermittelten Nachbar-Informations-Objekt und Vererbungsbeziehungen zwischen den Klassen.

Derartige Informationen kann Object Pascal liefern. Durch eine Methode "InheritsFrom" kann geprüft werden, ob ein Objekt von einer bestimmten Klasse abstammt. Die Ermittlung solcher Informationen wird ermöglicht, da Object Pascal die RTTI in einer Tabelle verwaltet. Diese RTTI-Tabelle enthält Informationen über die Klasse, deren Vorfahr-Klasse und alle veröffentlichten Felder und Methoden. Durch die Klassen-Methode "MethodAddress" und die Methode "FieldAddress" kann man prüfen, ob eine bestimmte Methode bzw. ein bestimmtes Feld in einem Objekt vorhanden ist. Man kann also zur Laufzeit Feld- und Methoden-Name nachschlagen (field name lookup / method name lookup). Die Lookup-Methoden liefern die Adresse der gefundenen Methode bzw. des gefundenen Felds zurück. Falls keine Methode / kein Feld unter dem genannten Namen existiert, liefern die Lookup-Methoden "nil" zurück.

Dynamische Typkonvertierungen und Metaklassen basieren auf den Run-Time Typ Informationen.


Zurück

Zurück zum Inhaltsverzeichnis

Weiter

Weiter in Kapitel 3