Programmierkurs: Delphi: Pascal: Konstruktoren und Destruktoren

Konstruktoren und Destruktoren

Bearbeiten

Allgemeines

Bearbeiten

Wie bereits im Kapitel über Klassen beschrieben, muss der Speicher für die Datenstruktur einer Klasse angefordert werden, bevor man mit ihr arbeiten kann. Da eine Klasse mehr als nur eine Datenansammlung ist - nämlich ein sich selbst verwaltendes Objekt - müssen gegebenenfalls weitere Initialisierungen durchgeführt werden. Genauso kann nach der Arbeit mit einer Klasse noch Speicher belegt sein, der von Delphis Speicherverwaltung nicht erfasst wird.

Für zusätzliche anfängliche Initialisierungen stehen die Konstruktoren („Errichter“) zur Verfügung, wogegen die Destruktoren („Zerstörer“) die abschließende Aufräumarbeit übernehmen. Diese sind spezielle Methoden einer Klasse, die nur zu diesem Zweck existieren.

Konstruktoren

Bearbeiten

Für die Arbeit mit einfachen Klassen, die keine weitere Initialisierung benötigen, braucht kein spezieller Konstruktor verwendet zu werden. In Delphi stammt jede Klasse automatisch von der Basisklasse TObject ab und erbt von dieser den Konstruktor Create. Folgender Aufruf ist daher gültig, obwohl keine Methode Create definiert wurde:

type
  TErsteKlasse = class
  end;

var
  Versuch: TErsteKlasse;

begin
  Versuch := TErsteKlasse.Create;
end.

Wie zu sehen ist, erfolgt der Aufruf des Konstruktors über den Klassentyp. Eine Anweisung wie Variable.Create führt zu einer Fehlermeldung.

Das „einfache“ Create erzeugt ein Objekt des Typs TErsteKlasse und gibt einen Zeiger darauf zurück.

Verwendet man Klassen, welche die Anfangswerte ihrer Felder einstellen oder z.B. weiteren Speicher anfordern müssen, so verdeckt man die geerbte Methode mit seiner eigenen:

type
  TErsteKlasse = class
    constructor Create;
  end;

constructor TErsteKlasse.Create;
begin
  inherited;
  { Eigene Anweisungen }
end;

Das Schlüsselwort inherited ruft dabei die verdeckte Methode TObject.Create auf und sorgt dafür, dass alle notwendigen Initialisierungen durchgeführt werden. Daher muss dieses Schlüsselwort immer zu Beginn eines Konstruktors stehen.

Solange ein gleichnamiger Vorfahre aufgerufen wird, reicht das Schlüsselwort inherited, ohne weitere Angaben. Unterscheidet sich der Bezeichner vom Vorfahr im Namen oder Parametern, sollte nach inherited der Vorfahre genannt werden. Besteht der Konstruktor also nur aus dem Bezeichner Create, reicht inherited, bekommt er weitere Parameter, z. B. Create(Index: Word), kann der Vorfahre nicht mehr erkannt werden. In solchen Fällen sollte der Vorfahre genannt werden: inherited Create.

Beispiel: Sie wollen eine dynamische Adressliste schreiben, bei der der Name Ihrer Freundin immer als erster Eintrag erscheint.

type
  TAdresse = record
    Vorname, Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;

  TAdressListe = class
    FListe: array of TAdresse;
    constructor Create;
  end;

In diesem Falle erstellen Sie den Konstruktor wie folgt:

constructor TAdressListe.Create;
begin
  inherited;
  SetLength(FListe, 1);  // Speicher für 1. Eintrag anfordern
  FListe[0].Vorname   := 'Barbie';
  FListe[0].Nachname  := 'Löckchen';
  FListe[0].Anschrift := 'Puppenstube 1, Kinderzimmer';
  FListe[0].TelNr     := '0800-BARBIE';
end;

Wenn Sie jetzt eine Variable mit diesem Konstruktor erstellen, enthält diese automatisch immer den Namen der Freundin:

var
  Adressen: TAdressListe;

begin
  Adressen := TAdressListe.Create;
  with Adressen.FListe[0] do
    {Ausgabe: "Barbie Löckchen wohnt in Puppenstube 1, Kinderzimmer und ist erreichbar unter 0800-BARBIE."}
    Writeln(Vorname+' '+Nachname+' wohnt in '+Anschrift+' und ist erreichbar unter '+TelNr+'.')
end.

Destruktoren

Bearbeiten

Destruktoren dienen, wie schon oben beschrieben dazu, den von einer Klasse verwendeten Speicher wieder freizugeben. Auch hierfür stellt die Basisklasse TObjekt bereits den Destruktor Destroy bereit. Dieser gibt grundsätzlich zwar den Speicher einer Klasse wieder frei, aber nur den des Grundgerüstes. Wenn eine Klasse zusätzlichen Speicher anfordert, z.B. weil sie mit Zeigern arbeitet, wird nur der Speicher für den Zeiger freigegeben, nicht aber der Speicherplatz, den die Daten belegen.

Eine Klasse sollte daher immer dafür sorgen, dass keine Datenreste im Speicher zurückbleiben. Dies erfolgt durch Überschreiben des Destruktors TObject.Destroy mit einem eigenen:

type
  TZweiteKlasse = class
    destructor Destroy; override;
  end;

destructor TZweiteKlasse.Destroy;
begin
  { Anweisungen }
  inherited;
end;

var
  Versuch: TZweiteKlasse;

begin
  Versuch := TZweiteKlasse.Create;
  Versuch.Destroy;
end.

Eine eigene Implementation von Destroy muss immer mit dem Schlüsselword override versehen werden, da Destroy eine virtuelle Methode ist (mehr dazu unter Virtuelle Methoden). Auch hier kommt wieder das Schlüsselwort inherited zum Einsatz, welches den verdeckten Destruktor TObject.Destroy aufruft. Da dieser Speicher frei gibt, muss inherited innerhalb eines Destruktors immer zuletzt aufgerufen werden.

Wenn wir unser Beispiel der Adressliste erweitern und auf das Wesentliche reduzieren, ergibt sich folgendes Programm:

type
  TAdresse = record
    Vorname, Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;

  TAdressListe = class
    FListe: array of TAdresse;
    destructor Destroy; override;
  end;

destructor TAdressListe.Destroy;
begin
  SetLength(FListe, 0);  // Speicher der dynamischen Liste freigeben
  inherited;             // Objekt auflösen
end;

var
  Adressen: TAdressListe;

begin
  Adressen := TAdressListe.Create;
  { Anweisungen }
  Adressen.Destroy;
end.

Die Methode Free

Bearbeiten

Free dient ebenso wie Destroy dazu, den Speicher einer Klasse freizugeben. Dabei funktioniert Free jedoch etwas anders.

Destroy gibt ausschließlich den Speicher des Objektes frei, von dem aus es aufgerufen wurde. Bei allen Objekten, die von TComponent abstammen, gibt Free zusätzlich den Speicher aller Unterkomponenten frei. TComponent wird in der Programmierung grafischer Oberflächen verwendet.

Weiterhin können Sie Free auch bei Objekten anwenden, deren Wert nil ist, die also noch nicht mittels Create erstellt wurden. Destroy löst in diesem Falle eine Exception aus. Hierbei ist jedoch zu beachten, dass nur globalen Objektvariablen und Klassenfeldern automatisch nil zugewiesen wird. Lokale Variablen von Routinen bzw. Methoden enthalten anfangs meist Datenmüll, also nicht nil. Free versucht daher einen ungültigen Speicherbereich freizugeben, was zu einer Exception führt. Objektvariablen, die nicht mit Sicherheit initialisiert werden, sollten Sie daher am Anfang einer Routine oder Methode den Wert nil manuell zuweisen.

Aufgrund dieser Vorteile sollten Sie immer Free bevorzugen, um ein Objekt aufzulösen.

Richtige Verwendung von Free und Destroy

Bearbeiten

Wenn Sie in einer Nachfahren-Klasse die Funktion des Destruktors erweitern wollen, überschreiben Sie immer Destroy direkt und rufen dort die benötigten Anweisungen auf. Zum tatsächlichen Freigeben eines Objektes verwenden Sie jedoch Free! Diese Methode ruft automatisch den Destruktor auf.

type
  TMyClass = class
  public
    destructor Destroy; override;
  end;

destructor TMyClass.Destroy;
begin
  { eigene Freigaben }
  inherited;
end;

var
  MyClass: TMyClass;

begin
  MyClass := TMyClass.Create;
  MyClass.Free;
end;

Überladen

Bearbeiten

Das Überladen von Konstruktoren und Destruktoren ist genauso möglich wie bei anderen Methoden auch. Für das Beispiel unserer Adressliste könnten zum Beispiel zwei Konstruktoren bestehen: einer, der eine leere Liste erzeugt, und einer, der eine Liste aus einer Datei lädt. Genauso könnte ein Destruktor die Liste einfach verwerfen, während ein anderer die Liste vorher in einer Datei speichert.

Sie können die Konstruktoren und Destruktoren jeweils gegenseitig aufrufen. Sie sollten hierbei jedoch darauf achten, dass nur genau einer das Schlüsselwort inherited aufruft.

type
  TAdressListe = class
    constructor Create; overload;
    constructor Create(Dateiname: string); overload;
    procedure DateiLaden(Dateiname: string);
  end;

procedure TAdressListe.DateiLaden(Dateiname: string);
begin
  {...}
end;

constructor TAdressListe.Create;
begin
  inherited;
  {...}
end;

constructor TAdressListe.Create(Dateiname: string);
begin
  Create;  // ruft TAdressListe.Create auf
  Dateiladen(Dateiname);
end;

Weiteres zum Thema Überladen in den Kapiteln „Prozeduren und Funktionen“ sowie „Klassen“.


  Pascal: Klassen Inhaltsverzeichnis Pascal: Eigenschaften