Virtuelle Constructoren

Für alles, was in den übrigen Lazarusthemen keinen Platz, aber mit Lazarus zutun hat.
Antworten
Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Virtuelle Constructoren

Beitrag von Lorca »

Hallo zusammen,

wie und wann ist es sinnvoll, einen Contructor als Virtual zu kennzeichnen?
Ich arbeite mit der Lazarusversion: 2.2.4

Ich habe mit mehreren, aufeinander aufbauenden Klassen gearbeitet.
Zunächst habe ich den Constructor der UR Vater Klasse, als Virtual gekennzeichnet. Danach gab es Probleme mit Override wenn der Constructor der Child Klasse andere Parameter hatte. Also habe ich diesen dann als Overload gekennzeichnet.
Die Konsequenz war, das er in den Overload Constructor nicht hinein gesprungen ist, sondern in den Vorgänger des Overload Constructor. Dies führte jedoch (verständlicher weise ) laufend zu Querschläger Fehler.

Also habe ich die Kennzeichnung für Virtual, Override und Overload bei den Constructoren entfernt.
Nun habe ich jedoch immer noch noch das Problem, das bei dem Aufruf von "INHERITED Create( Parameter )" zunächst in eine
"x-beliebige" Methode einer anderen Klasse gesprungen wird, welche nicht einmal in der USES Liste der Unit eingebunden ist, in welcher die zu erstellenden Klasse liegen. Erst danach, also im nächsten Step (F7), wird dann in die gewünschte INHERITED Methode (Create) gesprungen.

Irgendwo verschluckt bzw. vertauscht Lazarus die Einsprung Adressen in diese Methoden und ich kann nicht nachvollziehen was ich falsch gemacht habe.

Hat jemand von euch auch schon mal so ein Problem gehabt?
Kann es sein das es eine Mengenbegrenzung der eingebundenen Units gibt?
Kann jemand helfen?

Viele Grüße
Lorca

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von af0815 »

Bei so sonderbaren Problemen immer zuerst einmal alles Clean compilieren, bzw. den lib Ordner des Projektes löschen, damit ist einmal sichergestellt da alles neu und sauber kompiliert wird. Wenn es so aussieht, als würde er sich verschlucken, so ist es immer gut, wenn man mal alles sauber rekompiliert BEVOR man was herumbastelt.

Das mit dem virtual, overload und inherited hat bei dir schon grundlegend gestimmt. Da sehe ich keinen Fehler. Aber mit dem Debugger kommt man relativ rasch ans Ziel oder mit den LazLogger.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Benutzeravatar
fliegermichl
Lazarusforum e. V.
Beiträge: 1430
Registriert: Do 9. Jun 2011, 09:42
OS, Lazarus, FPC: Lazarus Fixes FPC Stable
CPU-Target: 32/64Bit
Wohnort: Echzell

Re: Virtuelle Constructoren

Beitrag von fliegermichl »

Bei virtuellen Methoden müssen die Parameter immer identisch sein. Man kann das aber durch reintroduce überschreiben.

Code: Alles auswählen

type
 TBase = class
  constructor Create(Param1 : integer); virtual;
 end;
 
 TDerived = class ( TBase )
  constructor Create(Param1 : integer; Param2 : string); reintroduce; virtual;
 end;
Damit kannst du auch in dem constructor TDerived.Create inherited Create(Param1) aufrufen.

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von m.fuchs »

Es wäre auch gut, wenn du ein bisschen schildern könntest was eigentlich dein Zweck ist. In den meisten Fällen müssen Konstruktoren nicht virtual sein.
Wenn du overload benutzt ist das ein Zeichen dafür, dass der Konstruktor nicht virtual sein sollte.
Wie rufst du denn das .Create deiner abgeleiteten Klasse auf? Kannst du Code mitliefern für dein Problem?
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Re: Virtuelle Constructoren

Beitrag von Lorca »

Hallo af0815,

zunächst Danke für Deine Antwort :)

Nun, das mit dem "alles Clean compilieren, " kann ich nicht machen, da ich nicht weiß wo ich diesen Befehl ausführen kann.
Ich habe bereits mehrfach alles mit Start/Aufräumen und Compilieren und zuvor die Option: Löschen betätigt und danach mittels dem Schalter "Aufräumen und Compilieren" alles neu erstellt. Der Fehler bleibt jedoch.
Dann bin ich hergegangen und habe zuerst den LIB Ordner des Projektes mittels Windows Explorer gelöscht und dann erst "Start/Aufräumen und Compilieren" ausgeführt.
Das Problem bleibt nach wie vor.
Erst wenn ich das Projekt ganz neu anlege und alle Units und Klassen neu mache klappt es wieder.
Bis dann irgendwann wieder ein solcher Fehler in einer anderen Klasse auftaucht.

Ich habe bereits mehr als 10 mal dieses Projekt neu aufgebaut. Irgendwann tritt dann immer dieser Fehler auf.
Da ich nicht weiß, wo genau mein Fehler liegt, bzw. was schief läuft, bleibt mir keine andere Wahl. Doch dies geht mir jetzt echt auf den Keks.
Ich kann nicht einmal sage ob es ein Lazarus Fehler ist, oder mein eigener.

So langsam verliere ich echt die Lust, da ich mit dem Projekt einfach nicht weiter komme.


Viele Grüße
Lorca

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von af0815 »

Lorca hat geschrieben:
Di 14. Feb 2023, 12:18
Ich habe bereits mehr als 10 mal dieses Projekt neu aufgebaut. Irgendwann tritt dann immer dieser Fehler auf.
Da ich nicht weiß, wo genau mein Fehler liegt, bzw. was schief läuft, bleibt mir keine andere Wahl. Doch dies geht mir jetzt echt auf den Keks.
Ich kann nicht einmal sage ob es ein Lazarus Fehler ist, oder mein eigener.

So langsam verliere ich echt die Lust, da ich mit dem Projekt einfach nicht weiter komme.
Wenn das so ist, so machst du immer dasselbe falsch.

* ) Namensgleichheit im Projekt unbedingt vermeiden
* ) Unitnamen und Objekte müssen sich unterscheiden
* ) Erzeugst du das richtige Objekt, oder machst du irgendwo einen TypeCast ?

Bau das Projekt man neu auf und schau dir genau an, wann dieser Fehler auftritt. Oder stell das Projekt zur Verfügung und erkläre was deiner Meinung nach schiefläuft.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Re: Virtuelle Constructoren

Beitrag von Lorca »

Hallo zusammen :),

das mit reintroduce habe ich noch nie betrachtet. Das war wohl ein Fehler. Denn diesen Befehl kenne ich nicht.
Werde ich jedoch ausprobieren.

Für dieses Beispiel ist anzumerken, das ich für jede Klasse eine eigene Unit habe welche ich dann auch über die USES Liste einbinde.

Viele Grüße
Lorca


Ein code Beispiel;
TCL_Core = CLASS( TObject )
PUBLIC
CONSTRUCTOR Create; Virtual; (in der aktuellen Version keine Kennzeichnung als Virtual )
END;

TCL_Base = CLASS( TCL_Core )
PUBLIC
CONSTRUCTOR Create( iv_Value : Boolean ); OverLoad; (in der aktuellen Version keine Kennzeichnung als Overload )
END;

IMPLEMENTATION

CONSTRUCTOR TCL_Core.Create;
BEGIN
INHERITED Create;
END;


CONSTRUCTOR TCL_Base.Create( iv_Value : Boolean );
BEGIN
INHERITED Create;
END;

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von m.fuchs »

Dein Code sagt mir noch nicht, wie du dein Create aufrufst.
Ich habe mal ein Beispiel entworfen, dass die Probleme bei virtuellen Konstruktoren aufzeigt:

Code: Alles auswählen

program Project1;
{$MODE ObjFpc}
{$H+}

uses
  Classes;

type
  TFirst = class(TObject)
    public
      constructor Create; virtual;
  end;

  TFirstClass = class of TFirst;

  TSecond = class(TFirst)
    public
      constructor Create; override;
  end;

  TThird = class(TSecond)
    public
      constructor Create(AInt: Integer); overload;
  end;

  TThirdAlt = class(TSecond)
    public
      constructor Create(AInt: Integer); reintroduce;
  end;



constructor TFirst.Create;
begin
  WriteLn('1');
end;

constructor TSecond.Create;
begin
  WriteLn('2');
  inherited Create;
end;

constructor TThird.Create(AInt: Integer);
begin
  WriteLn('3');
  inherited Create;
end;


constructor TThirdAlt.Create(AInt: Integer);
begin
  WriteLn('3');
  inherited Create;
end;

var
  co: TFirstClass;
  o: TFirst;

begin
  co := TThird;
  o := co.Create;
  o.Free;
  WriteLn;
  co := TThirdAlt;
  o := co.Create;
  o.Free;
  WriteLn;
  o := TThirdAlt.Create(0);
  o.Free;
end.
Beim Aufruf bekommst du dann diese Ausgabe:

Code: Alles auswählen

2
1

2
1

3
2
1
Zeigt: Wenn du virtuelle Konstruktoren benutzt, dann darfst du die Parameter nicht ändern. Sonst geht die die Virtualität verloren. Ist ja auch logisch, der Compiler muss ja mit der Klassendefinition von TFirst arbeiten - welche abgeleitete Klasse später benutzt wird weiß er ja nicht.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Re: Virtuelle Constructoren

Beitrag von Lorca »

Hi :)

Unitnamen fangen bei mir immer mit U gefolgt vom Projekt Kürzel an.
Klassennamen fangen bei mir immer mit TCL_ an.
Dadurch ist bei mir gewährleistet, das es keine Namensgleichheit gibt.

In meinen Programmen erzeuge ich IMMER ein echtes Objekt. (xx := TCL_nnn.Create)
Ein TypeCast findet auf existierende Objekte statt. z.B. TCL_xx( Sender ).yyy oder
SENDER as TCL_xx.

Gruß
Lorca

Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Re: Virtuelle Constructoren

Beitrag von Lorca »

Hi,

meine Klasse rufe ich dann folgendermaßen auf:

VAR go_Base : TCL_Base;

BEGIN
go_base := TCL_Base.Create( True );
...
...
FreeAndNil( go_base );
END.

Gruß
Lorca

Benutzeravatar
af0815
Lazarusforum e. V.
Beiträge: 6198
Registriert: So 7. Jan 2007, 10:20
OS, Lazarus, FPC: FPC fixes Lazarus fixes per fpcupdeluxe (win,linux,raspi)
CPU-Target: 32Bit (64Bit)
Wohnort: Burgenland
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von af0815 »

Der Konstruktor muss auch wieder virtual sein (oder gar keinen modifier), da er eine andere Signatur als der geerbte hat, das ist für den Compiler ein neuer Konstruktor. Die Klasse TCL_Base hat jetzt 2 Konstruktoren, den von dir hier definierten und den geerbten.
Genaugenommen wäre es besser, wenn der mit der anderen Signatur überhaupt einen anderen Namen (zB CreateIV) bekommt

Code: Alles auswählen

TCL_Base = CLASS( TCL_Core )
PUBLIC
CONSTRUCTOR Create( iv_Value : Boolean ); 
END;


....
// Beispiel
CONSTRUCTOR Create( iv_Value : Boolean ); 
begin
  iv:= iv_Value;
  create;
end;
// oder
CONSTRUCTOR Create( iv_Value : Boolean ); 
begin
  create;
  iv:= iv_Value;
end;
Schau dir zum Beispiel an wie es in den Vererbungen von TForm gehandhabt wird (kann man leicht bis zu TObject rückverfolgen). Da gibt es auch einige zusätzliche Construktoren zu beobachten.
Blöd kann man ruhig sein, nur zu Helfen muss man sich wissen (oder nachsehen in LazInfos/LazSnippets).

Benutzeravatar
m.fuchs
Lazarusforum e. V.
Beiträge: 2636
Registriert: Fr 22. Sep 2006, 19:32
OS, Lazarus, FPC: Winux (Lazarus 2.0.10, FPC 3.2.0)
CPU-Target: x86, x64, arm
Wohnort: Berlin
Kontaktdaten:

Re: Virtuelle Constructoren

Beitrag von m.fuchs »

Lorca hat geschrieben:
Di 14. Feb 2023, 12:48
meine Klasse rufe ich dann folgendermaßen auf:

Code: Alles auswählen

VAR go_Base : TCL_Base;

BEGIN
  go_base :=  TCL_Base.Create( True );
...
...
  FreeAndNil( go_base );
END.
Schmeiß mal bitte überall virtual raus bei deinen Konstruktoren. Das brauchst du doch gar nicht.
Software, Bibliotheken, Vorträge und mehr: https://www.ypa-software.de

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Virtuelle Constructoren

Beitrag von Warf »

Kurz zur erklärung, was bedeutet Virtual.
Wenn eine Methode als Virtual gekennzeichnet ist bedeutet das das diese nicht statisch durch den Compiler ermittelt wird, sondern dynamisch über die so genannte V-Table des objekts.

Am besten kann man das erklären wenn man OOP einfach mal mit records nachbaut. Nehmen wir diese simple klasse:

Code: Alles auswählen

TTest = class
public
  FMyInt: Integer;
  procedure PrintInt;
end;

procedure TTest.PrintInt;
begin
  WriteLn(Self.FMyInt);
end;

...
var test: TTest;
...
  test.PrintInt;
Im grunde ist das nix anderes als das folgende mit Records:

Code: Alles auswählen

TTest = record
  FMyInt: Integer;
end;

procedure TTest__PrintInt(var self: TTest);
begin
  WriteLn(Self.FMyInt);
end;

...
var test: PTest;
...
  TTest__PrintInt(test^);
In diesem Fall ist die klasse nix anderes als syntaktischer Zucker der aus variable.method den funktionsbezeichner TYPE(variable)__method macht. Welche funktion aufgerufen wird hängt also von dem typen der variable ab.

Eine Virtuelle Methode hingegen ist im Grunde nichts anderes als ein Funktionspointer der im Objekt selbst lebt:

Code: Alles auswählen

TTest = class
public
  FMyInt: Integer;
  procedure PrintInt; virtual;
  constructor Create(AInt: Integer);
end;
Ist also so ungefähr so etwas:

Code: Alles auswählen

TTest = record
public
  FMyInt: Integer;
  PrintInt = procedure;
end;

procedure TTest__PrintInt(var self: TTest);
begin
  WriteLn(self.FMyInt);
end;

procedure TTest__Create(out self: TTest); // Konstruktor equivalent
begin
  // Automatisch generierter Konstruktor teil:
  // Baue V-Table auf
  self.PrintInt := @TTest__PrintInt;
  // Rest des konstruktors
end;
Die analogie ist nicht ganz perfekt, da die V-Table normalerweise nicht im Objekt selbst liegt, sondern separat, wobei das objekt nur einen Pointer auf die V-Table hat, aber im grunde funktioniert es genau so. Bei einer virtuellen methode liegt ein Funktionspointer in dem Objekt selbst. Dann entscheidet der Compiler nicht welche funktion aufgerufen werden soll an hand des typen der variable zur kompilezeit, sondern zur Laufzeit wird der aktuelle Funktionspointer aus der V-Table geladen und ausgeführt.
Das hat z.B. auch den Zusatzeffekt das man die V-Table eines Objektes im nachhinein verändern kann. Man kann also ein Objekt erstellen, sodass PrintInt die standard funktionalität ausführt, und dann später die VTable ändern sodass dann eine andere Funktion statdessen ausgeführt wird.

Beispiel:

Code: Alles auswählen

TFoo = class
  public procedure Print;
end;

TBar = class(TFoo)
  public procedure Print;
end;

procedure TFoo.Print;
begin
  WriteLn('Foo');
end;

procedure TBar.Print;
begin
  WriteLn('Bar');
end;

var
  f: TFoo;
begin
  f := TBar.Create;
  f.Print; // "Foo"
  TBar(f).Print; // "Bar"
end;
In diesem fall wird immer die Print Methode ausgeführt von dem typen der vor dem . steht, also bei "f" dann TFoo.Print weil f vom typen TFoo ist, und bei TBar(f) dann TBar, da f hier zu TBar gecastet wird.

Wäre print jetzt virtual (und override in TBar), dann würde in der V-Table von f ein Funktionspointer stecken der entweder auf TFoo__Print zeigt, falls TFor.Create aufgerufen wurde, oder auf TBar__Print falls TBar.Create verwendet wurde um das Objekt zu erstellen, da der Konstruktor diese V-Table setzt.

Damit das alles funktionieren kann, muss der funktionspointer immer von dem gleichen typen sein. Das folgende geht offensichtlich nicht:

Code: Alles auswählen

procedure Test1(A: Integer);
...

procedure Test2(A: String; B: Integer);
...

var
  test: procedure(A: Integer);
begin
  test := @Test2;
Da test ein Funktionspointer auf eine Funktion ist die einen Parameter vom typen Integer erhält kann man keine Funktion referenzieren die 2 parameter von unterschiedlichen Typen bekommt. Es ist einfach der falsche typ.
Genau das gleiche ist bei der V-Table, ein funktionspointer kann nur in die V-Table geschrieben werden wenn sie den selben typen hat wie in der V-Table definiert wurde. Virtual definiert den typen in der V-Table, und override schreibt eine andere funktion in einen bestehenden eintrag in die V-Table. Und damit das geht, müssen beide vom selben typen sein, genau wie im beispiel oben.

Von daher wenn du eine Methode virtual definierst, können alle Methoden die diese überschreiben, und damit die Variable in der V-Table überschreiben, den selben Typen, also die selbe Funktionssignatur haben.

Jetzt zu deiner Ursprünglichen Frage, warum einen Konstruktor virtuell machen? In der typischen Logik die oben erklärt wurde macht das keinen Sinn, wenn die VTable teil des Objekts ist, das vom Konstruktor erstellt wird, dann macht es ja keinen Sinn den konstruktor in diese Tabelle zu schreiben nachdem er bereits verwendet wurde.

Der Grund hierfür ist das Delphi (und damit auch ObjectPascal) aus der JavaEsquen OOP nieche kommen, in der Klassen selbst Objekte von Metaklassen sind. Diese können selbst methoden und felder haben:

Code: Alles auswählen

  TTest = class
  public
    class var MyInt: Integer;
    class procedure PrintInt;
  end;

class procedure TTest.PrintInt;
begin
  WriteLn(Self.MyInt);
end;

begin
  TTest.PrintInt;   
Hier ist TTest selbst ein objekt von der Metaklasse "class of TTest":

Code: Alles auswählen

  TTest = class
  public
    class var MyInt: Integer;
    class procedure PrintInt;
  end;

  TTestClass = class of TTest;
...
var
  t: TTestClass;
begin
  t := TTest;
  t.PrintInt; 
Und diese Metaklassen spiegeln auch Vererbung wieder:

Code: Alles auswählen

  TFoo = class
    public class procedure Print;
  end;

  TFooClass = class of TFoo;

  TBar = class(TFoo)
    public class procedure Print;
  end;

  TBarClass = class of TBar;

class procedure TFoo.Print;
begin
  WriteLn('Foo');
end;  

class procedure TBar.Print;
begin
  WriteLn('Bar');
end;

var
  f: TFooClass;
begin
  f := TBar;
  f.Print; // "Foo"
  TBarClass(f).Print; // "Bar"
Dies ist das exakt gleiche beispiel wie oben, nur mit Klassen statt objekten und Metaklassen statt klassen. Aber im grunde ist es nix anderes.
Und genau wie bei normalen Klassen kann hier auch virtual und override verwendet werden:

Code: Alles auswählen

  TFoo = class
    public class procedure Print; virtual;
  end;

  TFooClass = class of TFoo;

  TBar = class(TFoo)
    public class procedure Print; override;
  end;

  TBarClass = class of TBar;

class procedure TFoo.Print;
begin
  WriteLn('Foo');
end;  

class procedure TBar.Print;
begin
  WriteLn('Bar');
end;

var
  f: TFooClass;
begin
  f := TBar;
  f.Print; // "Bar" weil virtual den Funktionspointer zu TBarClass__Print überschrieben hat
  TBarClass(f).Print; // "Bar"  
Und der Konstruktor ist nichts anderes als eine Klassenmethode die ein paar zusatzfunktionalitäten hat. D.h. Virtuelle Konstruktoren erlauben so etwas:

Code: Alles auswählen

  TFoo = class
  public
    Name: String;
    constructor Create; virtual;
  end;

  TFooClass = class of TFoo;

  TBar = class(TFoo)
  public
    constructor Create; override;
  end;

constructor TBar.Create;
begin
  inherited Create;
  Name := 'Bar';
end;

constructor TFoo.Create;
begin
  Name := 'Foo';
end;

function CreateObject(objClass: TFooClass): TFoo;
begin
  Result := objClass.Create;
end;
    
var
  f: TFoo;
begin
  f := CreateObject(TFoo);
  WriteLn(f.Name); // Foo
  f := CreateObject(TBar);
  WriteLn(f.Name); // Bar
Es erlaubt also das dynamische Klassenerstellen zur Laufzeit indem man die Klassen selbst als variablen benutzt.

Und genau wie bei normalen Virtuellen Methoden muss auch hier der Typ, also die Signatur zwischen virtual und override gleich sein.

Die letzt Frage ist also nur noch, brauchst du virtuelle Konstruktoren. Die kurze antwort ist, da du nachfragen musstest was sie machen, benutzt du keine Metaklassen in deinem code. Also kannst du getrost auf Virtuelle Konstruktoren verzichten.

Generell muss man sich virtuelle Methoden immer überlegen. Virtuelle Methoden benötigt man nur, wenn man OOP polymorphismus machen möchte. Aber Virtuelle Methoden haben einen nicht zu unterschätzende Auswirkung auf deinen Code. Zum einen musst du wenn du die Signatur ändern willst dich immer mit Reintroduce rumkämpfen um Meldungen zu unterdrücken, zum anderen Kann der Kompiler weniger optimieren (da z.B. inlining nicht möglich ist mit virtuellen Methoden).

Von daher die Faustregel: Benutz virtual nur wenn dus auch wirklich brauchst. Im nachinein hinzufügen kann man es immer noch. (Ein bisschen anders verhält es sich bei Bibliotheken, ich hab schon mehr als einmal geflucht weil z.B. TThread.Start nicht virtuell ist und damit nicht überladen werden kann)

Lorca
Beiträge: 196
Registriert: Di 3. Nov 2020, 12:25

Re: Virtuelle Constructoren

Beitrag von Lorca »

Hallo zusammen,

ein ganz herzlichen Dank für eure Mühen und Ausführungen. :)

Viele Grüße
Lorca

Antworten