Kurze fassung: Konstruktor Virtual macht keinen sinn, da der Konstruktor nur zum zeitpunkt der Creation aufgerufen wird, und der destruktor von TObject ist mit virtual markiert, und jede andere klasse muss ihn als override markieren (nicht als virtual).
Hier mal eine lange erklärung, indem wir OOP selbst bauen, mit Records:
Beginnen wir mit einer basis klasse für Formen:
Mit zwei Methoden MoveBy, ToString und einen Konstruktor:
Code: Alles auswählen
procedure moveBy(var self: TShape; const offset: TPoint);
begin
Self.Position.X += offset.X;
Self.Position.Y += offset.Y;
end;
function toString(var self: TShape): String;
begin
Result := 'Shape: (' + Self.Position.X + ', ' + Self.Position.Y+ ')';
end;
function Shape(const InitialPosition: TPoint): TShape;
begin
Result.Position := InitialPosition;
end;
Jetzt erzeugen wir einen abkömmling:
Inklusive einem Konstruktor:
Code: Alles auswählen
fucntion Circle(const InitialPosition: TPoint; Radius: TPoint);
begin
Result.super := Shape(InitialPosition); // das gleiche wie inherited Create; in OOP
Result.Radius := Radius;
end;
Das selbe machen wir noch für ein Quadrat:
Code: Alles auswählen
TSquare = record
super: TShape;
SideLength: Double;
end;
Inklusive einem Konstruktor:
Code: Alles auswählen
fucntion Square(const InitialPosition: TPoint; SideLength: TPoint);
begin
Result.super := Shape(InitialPosition); // das gleiche wie inherited Create; in OOP
Result.SideLength := SideLength;
end;
Außerdem überladen wir die toString funktion für beide:
Code: Alles auswählen
function toString(var self: TCircle): String;
begin
Result := 'Circle: (' + Self.Super.Position.X + ', ' + Self.Super.Position.Y+ ', ' + Self.Radius + ')';
end;
function toString(var self: Square): String;
begin
Result := 'Square: (' + Self.Super.Position.X + ', ' + Self.Super.Position.Y+ ', ' + Self.SideLength + ')';
end;
Da die superclass das erste element im record ist kann man die ableitungshierachie ziemlich einfach runtergehen, wenn man pointer hat:
Code: Alles auswählen
var circ: PCircle;
sp: PShape;
circ^ := Circle; // angenommen der speicher exsistiert, z.b. via new/GetMem geholt
sp := PShape(circ); // weil super das erste element ist gilt sp = circ^.super, somit wird die hierachie runter gegangen
MoveBy(sp^, offset); // selbe wie MoveBy(circ^.super, offset)M
circ := PCircle(sp); // somit kann wieder die hierachieleiter hochgegangen werden
Aber jetzt beobachten wir das folgende:
Code: Alles auswählen
circ^ := Circle;
sp := PShape(circ);
toString(circ^); // Circle (...)
toString(sp^); // Shape (...)
Obwohl beide die selbe funktion bereitstellen, ist das ergebnis anders, je nachdem was man für einen typen übergibt. Das nennt sich shadowing, da die funktion toString von circ toString von shape überschattet, da sie den selben namen hat.
Das ist das selbe verhalten wenn du klassen überlädts und keine annotationen wie virtual hinzufügst, dann wird wenn du das Objekt in die Unterklasse castest eine andere funktion aufgerufen als wenn du in die überklasse castest.
Jetzt willst du das aber nicht, sondern willst viel mehr das egal was für ein objekt aus der Hierachie verwendet wird, immer die toString funktion des obersten elements der hierachie verwendet wird. Dafür ist virtual und override da. Intern wird das über funktionspointer umgesetzt:
Code: Alles auswählen
TShape = record
Position: TPoint;
ToString: function(var Self: Pointer); // same as virtual keyword in OOP
end;
Welche dann im Konstruktor gesetzt wird (sagen wir mal die toString methoden werden umbenannt zu xxx_toString):
Code: Alles auswählen
function Shape(const InitialPosition: TPoint): TShape;
begin
Result.Position := InitialPosition;
Result.ToString := @shape_ToString; // virtual keyword
end;
fucntion Circle(const InitialPosition: TPoint; Radius: TPoint);
begin
Result.super := Shape(InitialPosition);
Result.super.toString := @circle_ToString; // override keyword
Result.Radius := Radius;
end;
fucntion Square(const InitialPosition: TPoint; SideLength: TPoint);
begin
Result.super := Shape(InitialPosition);
Result.super.toString := @square_ToString; // override keyword
Result.SideLength := SideLength;
end;
Das wichtige an dieser stelle ist das die Signatur der toString Methode sich daher nicht mehr ändern darf, da sonst der funktionspointer nicht mehr passt. Daher heißt das ganze auch override, da es tatsächlich einfach überschrieben wird.
Wenn man jetzt das folgende schreibt:
Code: Alles auswählen
var circ: PCircle;
sq: PSquare;
sp: PShape;
sp := PShape(circ);
sp^.toString(sq); // Circle (...)
sp := PShape(sq);
sp^.toString(sq); // Square (...)
OOP macht intern nix anderes, nur werden alle virtuellen funktionen zusammen in eine so genannte vTable geschrieben (somit hat man einen pointer pro objekt statt einem pointer pro methode, das spart speicher), und macht ne ganze menge syntaktischen zucker drum herum, das du nicht die ganze zeit x.super schreiben musst oder hin und her casten musst
Wenn du eine funktion die als virtual in einer Basis klasse markiert ist nochmal erzeugst in einer oberklasse, (egal ob mit virtual oder ohne) wird der Funktionspointer auch geshadowed. In deinem beispiel shadowst du also das TObject Destroy mit deinem eigenen Destroy
Fassen wir also zusammen: Virtual macht aus einer normalen funktion im sourcecode einen funktionspointer im Objekt. Override überschreibt diesen funktionspointer mit einer neuen funktion. Wenn du eine normale Methode aufrufst ist das wie ein normaler funktionsaufruf, wenn du eine virtuelle/override Methode aufrufst wird der funktionspointer aus der vTable aufgerufen. Wichtig: Funktionspointer chasen (aka vtable benutzen) kostet laufzeit, normale funktionen (also ohne virtual) können geinlined werden, und sind damit im allgemeinen schneller. Also benutze virtual nur wenn dus musst, ansonsten benutz normale.
Damit können wir auch die Frage mit Create und Destroy beantworten:
Create virtual zu machen bringt nix. Create wird nur ein einziges mal aufgerufen, und niemals aus einer niedrigeren stufe der Ableitungshierachie aufgerufen. Daher macht ein eintrag in dier VTable für den Konstruktor keinen Sinn. Der destruktor hingegen muss overriden, damit egal in welcher stufe der Ableitungshierachie du dich befindest, immer der oberste konstruktor ausgeführt wird, sonst kann es sein das nur der super teil aufgeräumt wird, und dein konstruktor nie aufgerufen wird, weil .Free auf einem objekt der Hierachie aufgerufen wurde, von dem du den Destruktor geshadowed hast