Während wir DELPHI zur Entwicklung von Software verwenden, sind wir wie eine Gruppe glücklicher Kühe und Schafe auf dem Grasland und genießen unbeschwert den Sonnenschein, den uns die Object Pascal-Sprache und die reichen Wasserpflanzen bieten, die uns verschiedene VCL-Steuerungen bieten. Wer würde beim Blick nach oben in den grenzenlosen blauen Himmel und beim Blick nach unten auf das üppige grüne Gras auf der Erde darüber nachdenken, wie groß das Universum ist und welche Dinge kleiner als Moleküle und Atome sind? Das ist Sache der Philosophen. Zu dieser Zeit saß der Philosoph auf dem Gipfel eines hohen Berges, blickte zu den Veränderungen in den Nebeln des Universums hinauf, starrte auf die kriechenden Insekten auf dem Boden, drehte sich plötzlich um, nickte und lächelte unserer Gruppe Weidender zu Rinder und Schafe. Er nahm ein Stück Gras, hielt es sanft in den Mund, schloss die Augen und probierte es vorsichtig. Ich frage mich, wie dieses Stück Gras im Mund des Philosophen schmeckte. Allerdings hatte er stets ein zufriedenes Lächeln im Gesicht.
Wenn wir die mikroskopische atomare Welt von DELPHI kennen und verstehen, können wir die makroskopische Anwendungsstruktur von DELPHI gründlich verstehen und so unsere Software in einem breiteren ideologischen Raum entwickeln. Es ist, als hätte Newton die Bewegung makroskopischer Objekte entdeckt, war aber beunruhigt, weil er nicht herausfinden konnte, warum sich die Objekte auf diese Weise bewegten. Im Gegenteil, Einstein erlebte das glückliche Leben der Relativität zwischen den Gesetzen der Grundteilchen und der Bewegung makroskopischer Objekte !
Abschnitt 1 TObject Atom
Was ist TObject?
Es ist der grundlegende Kern der Object Pascal-Spracharchitektur und der Ursprung verschiedener VCL-Steuerelemente. Wir können uns TObject als eines der Atome vorstellen, aus denen eine DELPHI-Anwendung besteht. Natürlich bestehen sie aus subtileren Partikeln wie grundlegenden Elementen der Pascal-Syntax.
Es wird gesagt, dass TObject das Atom des DELPHI-Programms ist, da TObject intern vom DELPHI-Compiler unterstützt wird. Alle Objektklassen werden von TObject abgeleitet, auch wenn Sie TObject nicht als Vorgängerklasse angeben. TObject wird in der Systemeinheit definiert, die Teil des Systems ist. Am Anfang der System.pas-Einheit steht dieser Kommentartext:
{Vordefinierte Konstanten, Typen, Prozeduren,}
{ und Funktionen (z. B. True, Integer oder }
{Writeln) haben keine tatsächlichen Deklarationen.}
{ Stattdessen sind sie in den Compiler integriert }
{ und werden so behandelt, als wären sie deklariert }
{ am Anfang der Systemeinheit }
Dies bedeutet, dass diese Einheit vordefinierte Konstanten, Typen, Prozeduren und Funktionen enthält (z. B. True, Integer oder Writeln). Sie werden nicht tatsächlich deklariert, sondern vom Compiler integriert und zu Beginn der Kompilierung verwendet eine festgelegte Definition sein. Sie können Ihrer Projektdatei andere Quellprogrammdateien wie Classes.pas oder Windows.pas hinzufügen, um den Quellcode zu kompilieren und zu debuggen. Sie können jedoch auf keinen Fall die System.pas-Quellprogrammdatei zur Kompilierung zu Ihrer Projektdatei hinzufügen! DELPHI meldet Kompilierungsfehler für doppelte Definitionen von System!
Daher ist TObject eine intern vom Compiler bereitgestellte Definition. Für diejenigen von uns, die DELPHI zum Entwickeln von Programmen verwenden, ist TObject eine atomare Sache.
Die Definition von TObject in der Systemeinheit lautet wie folgt:
TObject = Klasse
Konstruktor Erstellen;
Verfahren kostenlos;
Klassenfunktion InitInstance(Instance: Pointer): TObject;
Prozedur CleanupInstance;
Funktion ClassType: TClass;
Klassenfunktion ClassName: ShortString;
Klassenfunktion ClassNameIs(const Name: string): Boolean;
Klassenfunktion ClassParent: TClass;
Klassenfunktion ClassInfo: Pointer;
Klassenfunktion InstanceSize: Longint;
Klassenfunktion InheritsFrom(AClass: TClass): Boolean;
Klassenfunktion MethodAddress(const Name: ShortString): Pointer;
Klassenfunktion MethodName(Adresse: Zeiger): ShortString;
Funktion FieldAddress(const Name: ShortString): Pointer;
function GetInterface(const IID: TGUID; out Obj): Boolean;
Klassenfunktion GetInterfaceEntry(const IID: TGUID): PInterfaceEntry;
Klassenfunktion GetInterfaceTable: PInterfaceTable;
Funktion SafeCallException(ExceptObject: TObject;
ExceptAddr: Pointer): HResult virtual;
Verfahren AfterConstruction; virtuell;
Prozedur BeforeDestruction; virtuell;
procedure Dispatch(var Message); virtual;
procedure DefaultHandler(var Message);
Klassenfunktion NewInstance: TObject virtual;
Prozedur FreeInstance virtuell;
Destruktor Zerstören; virtuell;
Ende;
Als nächstes werden wir nach und nach an die Tür der TObject-Atome klopfen, um zu sehen, welche Struktur sich darin befindet.
Wir wissen, dass TObject die Grundklasse aller Objekte ist. Was genau ist also ein Objekt?
Jedes Objekt in DELPHI ist ein Zeiger, der den vom Objekt im Speicher belegten Platz angibt! Obwohl das Objekt ein Zeiger ist, müssen wir beim Verweisen auf die Mitglieder des Objekts nicht den Code MyObject^.GetName schreiben, sondern können nur MyObject.GetName schreiben. Dies ist eine erweiterte Syntax der Object Pascal-Sprache vom Compiler unterstützt. Freunde, die C++ Builder verwenden, sind sich der Beziehung zwischen Objekten und Zeigern sehr bewusst, da Objekte in C++ Builder als Zeiger definiert werden müssen. Der Ort, auf den der Objektzeiger zeigt, ist der Objektraum, in dem das Objekt Daten speichert. Lassen Sie uns die Datenstruktur des Speicherraums analysieren, auf den der Objektzeiger zeigt.
Die ersten 4 Bytes des Objektraums verweisen auf die virtuelle Methodenadresstabelle (VMT – Virtual Method Table) der Objektklasse. Der nächste Raum ist der Raum zum Speichern der Mitgliedsdaten des Objekts selbst und wird in der Gesamtreihenfolge von den Datenmitgliedern der primitivsten Vorgängerklasse des Objekts bis zu den Datenmitgliedern der Objektklasse und in der Reihenfolge gespeichert, in der die Datenelemente werden in jeder Klassenebene definiert.
Die virtuelle Methodentabelle (VMT) einer Klasse enthält die Prozeduradressen der virtuellen Methoden aller Klassen, die von der ursprünglichen Vorgängerklasse der Klasse abgeleitet sind. Die virtuelle Methode einer Klasse ist eine Methode, die mit dem reservierten Wort „virtual“ deklariert wird. Die virtuelle Methode ist der grundlegende Mechanismus zur Erzielung von Objektpolymorphismus. Obwohl dynamische Methoden, die mit dem reservierten Wort „Dynamic“ deklariert werden, auch Objektpolymorphismus erreichen können, werden solche Methoden nicht in der virtuellen Methodenadresstabelle (VMT) gespeichert. Dies ist nur eine weitere von Object Pascal bereitgestellte Methode, mit der Klassenspeicherplatz eingespart werden kann. aber auf Kosten der Anrufgeschwindigkeit.
Selbst wenn wir selbst keine virtuelle Methode der Klasse definieren, verfügt das Objekt der Klasse immer noch über einen Zeiger auf die Adresstabelle der virtuellen Methode, aber die Länge des Adresseintrags ist Null. Wo werden jedoch die in TObject definierten virtuellen Methoden wie Destroy, FreeInstance usw. gespeichert? Es stellt sich heraus, dass ihre Methodenadressen relativ zum VMT-Zeiger in einem in negativer Richtung versetzten Raum gespeichert sind. Tatsächlich ist der um 76 Byte in der negativen Richtung der VMT-Tabelle versetzte Datenraum die Systemdatenstruktur der Objektklasse. Diese Datenstrukturen sind Compiler-bezogen und können in zukünftigen DELPHI-Versionen geändert werden.
Daher kann man davon ausgehen, dass VMT eine Datenstruktur ist, die vom negativen Offset-Adressraum ausgeht. Der negative Offset-Datenbereich ist der Systemdatenbereich von VMT und die positiven Offset-Daten von VMT sind der Benutzerdatenbereich (benutzerdefinierte virtuelle Methode). Adresstabelle). Die in TObject definierten Funktionen und Prozeduren im Zusammenhang mit Klasseninformationen oder Objektlaufzeitinformationen beziehen sich im Allgemeinen auf die Systemdaten von VMT.
VMT-Daten stellen eine Klasse dar. Tatsächlich ist VMT eine Klasse! In Object Pascal verwenden wir Bezeichner wie TObject, TComponent usw. zur Darstellung von Klassen, die intern in DELPHI als ihre jeweiligen VMT-Daten implementiert werden. Der mit der Klasse des reservierten Wortes definierte Klassentyp ist tatsächlich ein Zeiger auf die relevanten VMT-Daten.
Für unsere Anwendung sind VMT-Daten statische Daten. Nachdem der Compiler unsere Anwendung kompiliert hat, wurden diese Dateninformationen ermittelt und initialisiert. Die von uns geschriebenen Programmanweisungen können auf VMT-bezogene Informationen zugreifen, Informationen wie die Größe des Objekts, den Klassennamen oder Laufzeitattributdaten abrufen oder virtuelle Methoden aufrufen oder den Namen und die Adresse der Methode usw. lesen.
Wenn ein Objekt generiert wird, weist das System einen Speicherplatz für das Objekt zu und ordnet das Objekt der relevanten Klasse zu. Daher werden die ersten 4 Bytes im für das Objekt zugewiesenen Datenraum zu Zeigern auf Klassen-VMT-Daten.
Werfen wir einen Blick darauf, wie Objekte entstehen und sterben. Wenn ich meinem dreijährigen Sohn dabei zusehe, wie er im Gras herumspringt, kann ich den Sinn und die Größe des Lebens gerade deshalb wirklich verstehen, weil ich den Geburtsprozess des Lebens miterlebt habe. Nur wer den Tod erlebt hat, wird das Leben mehr verstehen und wertschätzen. Lassen Sie uns also den Prozess der Entstehung und des Todes von Objekten verstehen!
Wir alle wissen, dass das einfachste Objekt mit der folgenden Anweisung konstruiert werden kann:
AnObject := TObject.Create;
Der Compiler implementiert seine Kompilierung wie folgt:
Rufen Sie basierend auf dem VMT, das TObject entspricht, den Create-Konstruktor von TObject auf. Der Create-Konstruktor ruft den ClassCreate-Prozess des Systems auf, und der ClassCreate-Prozess des Systems ruft die virtuelle Methode NewInstance über die darin gespeicherte Klasse VMT auf. Der Zweck des Aufrufs der NewInstance-Methode besteht darin, den Instanzraum des Objekts festzulegen. Da wir diese Methode nicht überladen haben, handelt es sich um die NewInstance der TObject-Klasse. Die NewInstance-Methode der TObjec-Klasse ruft die GetMem-Prozedur auf, um dem Objekt Speicher basierend auf der vom Compiler in der VMT-Tabelle initialisierten Objektinstanzgröße (InstanceSize) zuzuweisen, und ruft dann die InitInstance-Methode auf, um den zugewiesenen Speicherplatz zu initialisieren. Die InitInstance-Methode initialisiert zunächst die ersten 4 Bytes des Objektraums auf einen Zeiger auf den VMT, der der Objektklasse entspricht, und löscht dann den verbleibenden Raum. Nach dem Einrichten der Objektinstanz wird auch eine virtuelle Methode AfterConstruction aufgerufen. Speichern Sie abschließend den Adresszeiger der Objektinstanzdaten in der AnObject-Variablen, und auf diese Weise wird das AnObject-Objekt geboren.
Ebenso kann ein Objekt mit der folgenden Anweisung zerstört werden:
AnObject.Destroy;
Der Destruktor von TObject, Destroy, wird als virtuelle Methode deklariert, die auch eine der inhärenten virtuellen Methoden des Systems ist. Die Destory-Methode ruft zunächst die virtuelle Methode BeforeDestruction und dann den ClassDestroy-Prozess des Systems auf. Der ClassDestory-Prozess ruft die virtuelle FreeInstance-Methode über die Klasse VMT auf, und die FreeInstance-Methode ruft den FreeMem-Prozess auf, um den Speicherplatz des Objekts freizugeben. Auf diese Weise verschwindet ein Objekt aus dem System.
Der Zerstörungsprozess von Objekten ist einfacher als der Konstruktionsprozess von Objekten, genauso wie die Geburt des Lebens ein langer Entstehungsprozess ist, der Tod jedoch relativ kurzlebig ist. Dies scheint eine unvermeidliche Regel zu sein.
Während des Konstruktions- und Zerstörungsprozesses des Objekts werden zwei virtuelle Funktionen, NewInstance und FreeInstance, aufgerufen, um den Speicherplatz der Objektinstanz zu erstellen und freizugeben. Der Grund, warum diese beiden Funktionen als virtuelle Funktionen deklariert werden, besteht darin, den Benutzern Raum für Erweiterungen zu geben, wenn sie spezielle Objektklassen schreiben, die erfordern, dass Benutzer ihren eigenen Speicher verwalten (z. B. in einigen speziellen industriellen Steuerungsprogrammen).
Durch die Deklaration von AfterConstruction und BeforeDestruction als virtuelle Funktionen soll der abgeleiteten Klasse in Zukunft auch die Möglichkeit gegeben werden, dem neu geborenen Objekt nach der Generierung des Objekts den ersten Atemzug frischer Luft zu ermöglichen und dem Objekt zu ermöglichen, die Nachwirkungen abzuschließen, bevor das Objekt stirbt . Das ist alles etwas, das Sinn macht. Tatsächlich werden das OnCreate-Ereignis und das OnDestroy-Ereignis des TForm-Objekts bzw. des TDataModule-Objekts in den beiden virtuellen Funktionsprozessen der TForm- und TDataModule-Überladung ausgelöst.
Darüber hinaus bietet TObjec auch eine Free-Methode, bei der es sich nicht um eine virtuelle Methode handelt. Sie dient speziell dazu, das Objekt sicher freizugeben, wenn unklar ist, ob das Objekt leer ist (Null). Wenn Sie nicht herausfinden können, ob das Objekt leer ist, liegt tatsächlich ein Problem der unklaren Programmlogik vor. Allerdings ist niemand perfekt und kann Fehler machen. Es ist auch eine gute Sache, Free zu verwenden, um versehentliche Fehler zu vermeiden. Das Schreiben korrekter Programme kann sich jedoch nicht ausschließlich auf solche Lösungen verlassen. Das erste Ziel der Programmierung sollte darin bestehen, die logische Korrektheit des Programms sicherzustellen!
Interessierte Freunde können den Originalcode der Systemeinheit lesen, wobei ein großer Teil des Codes in Assemblersprache geschrieben ist. Aufmerksame Freunde können feststellen, dass der Konstruktor „Create“ und der Destruktor „Destory“ keinen Code geschrieben haben. Tatsächlich kann der Assemblercode von „Create“ und „Destory“ im Debug-Status deutlich wiedergegeben werden. Denn die Meister, die DELPHI geschaffen haben, wollten den Benutzern nicht zu viele komplizierte Dinge bieten. Sie wollten, dass Benutzer Anwendungen schreiben, die auf einfachen Konzepten basieren, und die komplexe Arbeit innerhalb des Systems verbergen, damit sie sie ausführen können. Daher werden beim Veröffentlichen der System.pas-Einheit die Codes dieser beiden Funktionen speziell entfernt, um den Benutzern den Eindruck zu vermitteln, dass TObject die Quelle aller Dinge ist und vom Benutzer abgeleitete Klassen vollständig im Nichts beginnen. Dies ist an sich nicht falsch. Obwohl das Lesen dieser wichtigsten Codes von DELPHI ein geringes Maß an Assemblerkenntnissen erfordert, kann uns das Lesen solcher Codes ein tieferes Verständnis für den Ursprung und die Entwicklung der DELPHI-Welt vermitteln. Selbst wenn Sie nicht viel verstehen, wird es uns beim Schreiben von DELPHI-Programmen eine große Hilfe sein, zumindest einige grundlegende Dinge zu verstehen.
Abschnitt 2 TClass Atom
In der System.pas-Einheit ist TClass wie folgt definiert:
TClass = Klasse von TObject;
Das bedeutet, dass TClass die Klasse von TObject ist. Da TObject selbst eine Klasse ist, ist TClass die sogenannte Klasse von Klassen.
Konzeptionell ist TClass eine Art Klasse, also eine Klasse. Wir wissen jedoch, dass eine Klasse von DELPHI einen Teil der VMT-Daten darstellt. Daher kann die Klasse als der für das VMT-Datenelement definierte Typ betrachtet werden. Tatsächlich handelt es sich um einen Zeigertyp, der auf die VMT-Daten zeigt!
In der vorherigen traditionellen C++-Sprache konnte der Typ einer Klasse nicht definiert werden. Sobald das Objekt kompiliert ist, ist es fixiert, die Strukturinformationen der Klasse wurden in absoluten Maschinencode umgewandelt und die vollständigen Klasseninformationen sind nicht im Speicher vorhanden. Einige objektorientierte Sprachen höherer Ebenen können den dynamischen Zugriff und Aufruf von Klasseninformationen unterstützen, erfordern jedoch häufig einen komplexen internen Interpretationsmechanismus und mehr Systemressourcen. Die Object Pascal-Sprache von DELPHI übernimmt einige der hervorragenden Funktionen objektorientierter Hochsprachensprachen und behält gleichzeitig den traditionellen Vorteil der direkten Kompilierung von Programmen in Maschinencode bei, wodurch die Probleme erweiterter Funktionen und Programmeffizienz perfekt gelöst werden.
Gerade weil DELPHI vollständige Klasseninformationen in der Anwendung behält, kann es erweiterte objektorientierte Funktionen bereitstellen, wie z. B. das Konvertieren und Identifizieren von Klassen zur Laufzeit, wobei die VMT-Daten der Klasse eine Schlüsselrolle spielen. Interessierte Freunde können die beiden Assemblerprozesse von AsClass und IsClass in der Systemeinheit lesen. Sie sind die Implementierungscodes der as- und is-Operatoren, um ihr Verständnis von Klassen und VMT-Daten zu vertiefen.
DELPHIs Atomwelt (2)
Schlüsselwörter: Delphi steuert Verschiedenes
Abschnitt 2 TClass Atom
In der System.pas-Einheit ist TClass wie folgt definiert:
TClass = Klasse von TObject;
Das bedeutet, dass TClass die Klasse von TObject ist. Da TObject selbst eine Klasse ist, ist TClass die sogenannte Klasse von Klassen.
Konzeptionell ist TClass eine Art Klasse, also eine Klasse. Wir wissen jedoch, dass eine Klasse von DELPHI einen Teil der VMT-Daten darstellt. Daher kann die Klasse als der für das VMT-Datenelement definierte Typ betrachtet werden. Tatsächlich handelt es sich um einen Zeigertyp, der auf die VMT-Daten zeigt!
In der vorherigen traditionellen C++-Sprache konnte der Typ einer Klasse nicht definiert werden. Sobald das Objekt kompiliert ist, ist es fixiert, die Strukturinformationen der Klasse wurden in absoluten Maschinencode umgewandelt und die vollständigen Klasseninformationen sind nicht im Speicher vorhanden. Einige objektorientierte Sprachen höherer Ebenen können den dynamischen Zugriff und Aufruf von Klasseninformationen unterstützen, erfordern jedoch häufig einen komplexen internen Interpretationsmechanismus und mehr Systemressourcen. Die Object Pascal-Sprache von DELPHI übernimmt einige der hervorragenden Funktionen objektorientierter Hochsprachensprachen und behält gleichzeitig den traditionellen Vorteil der direkten Kompilierung von Programmen in Maschinencode bei, wodurch die Probleme erweiterter Funktionen und Programmeffizienz perfekt gelöst werden.
Gerade weil DELPHI vollständige Klasseninformationen in der Anwendung behält, kann es erweiterte objektorientierte Funktionen bereitstellen, wie z. B. das Konvertieren und Identifizieren von Klassen zur Laufzeit, wobei die VMT-Daten der Klasse eine Schlüsselrolle spielen. Interessierte Freunde können die beiden Assemblerprozesse von AsClass und IsClass in der Systemeinheit lesen. Sie sind die Implementierungscodes der as- und is-Operatoren, um ihr Verständnis von Klassen und VMT-Daten zu vertiefen.
Mit dem Klassentyp können Sie die Klasse als Variable verwenden. Eine Klassenvariable kann als spezielles Objekt verstanden werden, und Sie können auf die Methoden einer Klassenvariablen wie auf ein Objekt zugreifen. Schauen wir uns zum Beispiel das folgende Programmfragment an:
Typ
TSampleClass = Klasse von TSampleObject;
TSampleObject = Klasse( TObject )
öffentlich
Konstruktor Erstellen;
Destruktor Zerstören; überschreiben;
Klassenfunktion GetSampleObjectCount:Integer;
procedure GetObjectIndex:Integer;
Ende;
var
aSampleClass : TSampleClass;
aClass: TClass;
In diesem Code definieren wir eine Klasse TSampleObject und den zugehörigen Klassentyp TSampleClass sowie zwei Klassenvariablen aSampleClass und aClass. Darüber hinaus haben wir auch einen Konstruktor, einen Destruktor, eine Klassenmethode GetSampleObjectCount und eine Objektmethode GetObjectIndex für die Klasse TSampleObject definiert.
Lassen Sie uns zunächst die Bedeutung der Klassenvariablen aSampleClass und aClass verstehen.
Offensichtlich können Sie TSampleObject und TObject als konstante Werte behandeln und sie aClass-Variablen zuweisen, genau wie Sie der Ganzzahlvariablen i 123 konstante Werte zuweisen. Daher ist die Beziehung zwischen Klassentypen, Klassen und Klassenvariablen die Beziehung zwischen Typen, Konstanten und Variablen, jedoch auf der Ebene der Klasse und nicht auf der Ebene des Objekts. Natürlich ist es nicht zulässig, TObject aSampleClass direkt zuzuweisen, da aSampleClass eine Klassenvariable der von TObject abgeleiteten Klasse TSampleObject ist und TObject nicht alle mit dem TSampleClass-Typ kompatiblen Definitionen enthält. Im Gegenteil ist es legal, TSampleObject einer Class-Variablen zuzuweisen, da TSampleObject eine abgeleitete Klasse von TObject ist und mit dem TClass-Typ kompatibel ist. Dies ähnelt genau der Zuweisungs- und Typübereinstimmungsbeziehung von Objektvariablen.
Schauen wir uns dann an, was Klassenmethoden sind.
Die sogenannte Klassenmethode bezieht sich auf die auf Klassenebene aufgerufene Methode, z. B. die oben definierte GetSampleObjectCount-Methode, bei der es sich um eine mit dem reservierten Wort class deklarierte Methode handelt. Klassenmethoden unterscheiden sich von Objektmethoden, die auf Objektebene aufgerufen werden. Objektmethoden sind uns bereits bekannt, und Klassenmethoden werden immer auf der Ebene des Zugriffs und der Steuerung der gemeinsamen Eigenschaften aller Klassenobjekte und der zentralen Verwaltung von Objekten verwendet. In der Definition von TObject finden wir eine große Anzahl von Klassenmethoden wie ClassName, ClassInfo, NewInstance usw. Unter diesen ist NewInstance auch als virtuell definiert, dh als virtuelle Klassenmethode. Das bedeutet, dass Sie die Implementierungsmethode von NewInstance in einer abgeleiteten Unterklasse umschreiben können, um Objektinstanzen dieser Klasse auf besondere Weise zu erstellen.
Sie können den Bezeichner self auch in Klassenmethoden verwenden, seine Bedeutung unterscheidet sich jedoch von self in Objektmethoden. Das Selbst in der Klassenmethode stellt seine eigene Klasse dar, also den Zeiger auf den VMT, während das Selbst in der Objektmethode das Objekt selbst darstellt, also den Zeiger auf den Objektdatenraum. Obwohl Klassenmethoden nur auf Klassenebene verwendet werden können, können Sie Klassenmethoden dennoch über ein Objekt aufrufen. Beispielsweise kann die Klassenmethode ClassName des Objekts TObject über die Anweisung aObject.ClassName aufgerufen werden, da die ersten 4 Bytes im Objektdatenraum, auf die der Objektzeiger zeigt, Zeiger auf die Klasse VMT sind. Im Gegensatz dazu können Sie keine Objektmethoden auf Klassenebene aufrufen und Anweisungen wie TObject.Free müssen illegal sein.
Es ist erwähnenswert, dass der Konstruktor eine Klassenmethode und der Destruktor eine Objektmethode ist!
Was? Konstruktoren sind Klassenmethoden und Destruktoren sind Objektmethoden! Gab es einen Fehler?
Sie sehen, wenn Sie ein Objekt erstellen, verwenden Sie eindeutig eine Anweisung ähnlich der folgenden:
aObject := TObject.Create;
Es ruft eindeutig die Create-Methode der Klasse TObject auf. Verwenden Sie beim Löschen eines Objekts die folgende Anweisung:
aObject.Destroy;
Auch wenn Sie die Free-Methode zum Freigeben des Objekts verwenden, wird die Destroy-Methode des Objekts indirekt aufgerufen.
Der Grund ist sehr einfach: Bevor das Objekt erstellt wird, existiert es noch nicht, sondern nur die Klasse. Sie können nur Klassenmethoden zum Erstellen von Objekten verwenden. Im Gegenteil, das Löschen eines Objekts muss das vorhandene Objekt löschen, nicht die Klasse.
Lassen Sie uns abschließend das Problem der fiktiven Konstrukteure diskutieren.
In der traditionellen C++-Sprache können virtuelle Destruktoren implementiert werden, die Implementierung virtueller Konstruktoren ist jedoch ein schwieriges Problem. Denn in der traditionellen C++-Sprache gibt es keine Klassentypen. Instanzen globaler Objekte sind zur Kompilierungszeit im globalen Datenraum vorhanden, und lokale Objekte von Funktionen sind ebenfalls Instanzen, die zur Kompilierungszeit im Stapelbereich abgebildet sind. Auch dynamisch erstellte Objekte werden mithilfe des neuen Operators in die feste Klassenstruktur eingefügt im Heap-Bereich, und der Konstruktor ist nur eine Objektmethode, die die generierte Objektinstanz initialisiert. In der traditionellen C++-Sprache gibt es keine echten Klassenmethoden. Auch wenn sogenannte statische klassenbasierte Methoden definiert werden können, werden sie letztendlich als spezielle globale Funktion implementiert, ganz zu schweigen von virtuellen Klassenmethoden, die nur auf bestimmte Objekte abzielen können Instanzen. effizient. Daher geht die traditionelle C++-Sprache davon aus, dass es unmöglich ist, das Objekt selbst basierend auf dem zu generierenden Objekt zu erstellen, bevor eine bestimmte Objektinstanz generiert wird. Es ist in der Tat unmöglich, denn dies würde ein widersprüchliches Paradoxon in der Logik schaffen!
Gerade aufgrund der Schlüsselkonzepte dynamischer Klassentypinformationen, wirklich virtueller Klassenmethoden und klassenbasierter Konstruktoren in DELPHI können virtuelle Konstruktoren implementiert werden. Objekte werden von Klassen produziert. Das Objekt ist wie ein heranwachsendes Baby, und die Klasse ist seine Mutter. Das Baby selbst weiß nicht, was für ein Mensch es in Zukunft werden wird, aber die Mütter nutzen ihre eigenen Erziehungsmethoden, um verschiedene Kinder zu erziehen . Leute, die Prinzipien sind die gleichen.
In der Definition der TComponent-Klasse wird der Konstruktor Create als virtuell definiert, sodass verschiedene Arten von Steuerelementen ihre eigenen Konstruktionsmethoden implementieren können. Das ist die Großartigkeit von Konzepten wie den von TClass erstellten Klassen und auch die Großartigkeit von DELPHI.
................................................. ..
Kapitel 3 Die Sicht von Zeit und Raum in WIN32
Mein alter Vater sah seinen kleinen Enkel an, der mit Spielzeug auf dem Boden spielte, und sagte dann zu mir: „Dieses Kind ist genau wie du, als du ein Kind warst. Es liebt es, Dinge auseinanderzunehmen und hört erst auf, wenn er es zu Ende gesehen hat.“ " Wenn ich an meine Kindheit zurückdenke, zerlegte ich oft Spielzeugautos, kleine Wecker, Spieluhren usw. und wurde oft von meiner Mutter beschimpft.
Das erste Mal, dass ich die Grundprinzipien von Computern verstand, hatte mit einer Spieluhr zu tun, die ich auseinandernahm. Es stand in einem Comic, als ich in der High School war. Ein alter Mann mit weißem Bart erklärte die Theorie intelligenter Maschinen, und ein Onkel mit Schnurrbart sprach über Computer und Spieluhren. Sie sagten, dass die zentrale Verarbeitungseinheit eines Computers die Reihe von Notenblättern ist, die für die Aussprache in der Spieluhr verwendet werden, und dass das Computerprogramm die dicht gepackten Höcker auf dem kleinen Zylinder in der Spieluhr ist. Die Drehung des kleinen Zylinders ist äquivalent zur Drehung der Zentraleinheit, während die Höcker, die Musik auf dem kleinen Zylinder darstellen, die Vibration des Notenblatts steuern, um Anweisungen zu erzeugen, die der Ausführung des Programms durch den Zentralprozessor entsprechen. Die Spieluhr gibt eine wunderschöne Melodie ab, die entsprechend der vom Handwerker auf dem kleinen Zylinder eingravierten Musikpartitur abgespielt wird. Der Computer führt die komplexe Verarbeitung auf der Grundlage des vom Programmierer vorprogrammierten Programms durch. Als ich aufs College ging, erfuhr ich, dass der alte Mann mit dem weißen Bart der Wissenschaftsriese Turing war. Seine Theorie der endlichen Automaten förderte die Entwicklung der gesamten Informationsrevolution, und der Onkel mit dem Schnurrbart war der Vater der Computer, von Neumann . Die Computerarchitektur ist immer noch die Hauptarchitekturstruktur von Computern. Die Spieluhr wurde nicht umsonst abgebaut, Mutter kann beruhigt sein.
Nur mit einem einfachen und tiefgreifenden Verständnis können wir tiefgreifende und prägnante Kreationen schaffen.
In diesem Kapitel werden wir die grundlegenden Konzepte im Zusammenhang mit unserer Programmierung im Windows 32-Bit-Betriebssystem diskutieren und die richtige Sicht auf Zeit und Raum in WIN32 festlegen. Ich hoffe, dass wir nach der Lektüre dieses Kapitels ein tieferes Verständnis von Programmen, Prozessen und Threads erlangen, die Prinzipien ausführbarer Dateien, dynamischer Linkbibliotheken und Laufzeitpaketen verstehen und die Wahrheit über globale Daten, lokale Daten und Parameter im Speicher klar erkennen können .
Abschnitt 1 Den Prozess verstehen
Aus historischen Gründen entstand Windows aus DOS. Im DOS-Zeitalter hatten wir immer nur das Konzept eines Programms, nicht jedoch das Konzept eines Prozesses. Zu dieser Zeit verfügten nur normale Betriebssysteme wie UNIX und VMS über das Konzept von Prozessen, und Multiprozesse bedeuteten Minicomputer, Terminals und mehrere Benutzer, was auch Geld bedeutete. Meistens konnte ich nur relativ günstige Mikrocomputer und DOS-Systeme nutzen. Mit Prozessen und Minicomputern kam ich erst während meines Studiums von Betriebssystemen in Kontakt.
Erst nach Windows 3. Früher konnte unter DOS nur ein Programm gleichzeitig ausgeführt werden, unter Windows konnten jedoch mehrere Programme gleichzeitig ausgeführt werden. Das ist Multitasking. Während ein Programm unter DOS ausgeführt wird, kann dasselbe Programm nicht gleichzeitig ausgeführt werden. Unter Windows können jedoch mehr als zwei Kopien desselben Programms gleichzeitig ausgeführt werden, und jede ausgeführte Kopie des Programms ist ein Prozess. Genauer gesagt generiert jede Ausführung eines Programms eine Aufgabe, und jede Aufgabe ist ein Prozess.
Wenn Programme und Prozesse zusammen verstanden werden, bezieht sich das Wort Programm auf statische Dinge. Ein typisches Programm ist statischer Code und Daten, die aus einer EXE-Datei oder einer EXE-Datei plus mehreren DLL-Dateien bestehen. Ein Prozess ist die Ausführung eines Programms, bei dem es sich um Code und sich dynamisch ändernde Daten handelt, die dynamisch im Speicher ausgeführt werden. Wenn ein statisches Programm ausgeführt werden muss, stellt das Betriebssystem einen bestimmten Speicherplatz für diesen Vorgang bereit, überträgt den statischen Programmcode und die Daten in diese Speicherbereiche und positioniert den Programmcode und die Daten in diesem Bereich neu und ordnet sie zu ausgeführt und so einen dynamischen Prozess erzeugt.
Zwei gleichzeitig ausgeführte Kopien desselben Programms bedeuten, dass sich im Systemspeicher zwei Prozessbereiche befinden, deren Programmfunktionen jedoch gleich sind, sich jedoch in unterschiedlichen, sich dynamisch ändernden Zuständen befinden.
In Bezug auf die Laufzeit des Prozesses wird jeder Prozess gleichzeitig ausgeführt. Der Fachbegriff wird als parallele Ausführung oder gleichzeitige Ausführung bezeichnet. Dies ist jedoch hauptsächlich das oberflächliche Gefühl, das uns das Betriebssystem vermittelt. Tatsächlich wird jeder Prozess zeitgesteuert ausgeführt, das heißt, jeder Prozess belegt abwechselnd die CPU-Zeit, um die Programmanweisungen des Prozesses auszuführen. Bei einer CPU werden nur die Anweisungen eines Prozesses gleichzeitig ausgeführt. Das Betriebssystem ist der Manipulator hinter dem Betrieb des geplanten Prozesses. Es speichert und wechselt ständig den aktuellen Status jedes in der CPU ausgeführten Prozesses, sodass jeder geplante Prozess davon ausgeht, dass er vollständig und kontinuierlich ausgeführt wird. Da die Time-Sharing-Planung von Prozessen sehr schnell erfolgt, entsteht der Eindruck, dass die Prozesse alle gleichzeitig laufen. Tatsächlich ist ein echter gleichzeitiger Betrieb nur in einer Hardwareumgebung mit mehreren CPUs möglich. Wenn wir später über Threads sprechen, werden wir feststellen, dass Threads den Prozess wirklich vorantreiben und, was noch wichtiger ist, dass sie Prozessraum bereitstellen.
In Bezug auf den vom Prozess belegten Raum ist jeder Prozessraum relativ unabhängig und jeder Prozess wird in seinem eigenen unabhängigen Raum ausgeführt. Ein Programm umfasst sowohl Coderaum als auch Datenraum. Sowohl Code als auch Daten belegen Prozessraum. Windows weist tatsächlichen Speicher für den von jedem Prozess benötigten Datenraum zu und verwendet im Allgemeinen Freigabemethoden für Coderaum, um einen Code eines Programms mehreren Prozessen des Programms zuzuordnen. Das heißt, wenn ein Programm über 100 KB Code verfügt und 100 KB Datenraum benötigt, was bedeutet, dass insgesamt 200 KB Prozessraum erforderlich sind, weist das Betriebssystem beim ersten Ausführen des Programms 200 KB Prozessraum und 200 KB Prozessraum zu Der Speicherplatz wird beim zweiten Ausführen des Programms zugewiesen. Wenn ein Prozess gestartet wird, weist das Betriebssystem nur 100 KB Datenspeicherplatz zu, während der Codespeicherplatz den Speicherplatz des vorherigen Prozesses teilt.
Das Obige ist die grundlegende Zeit- und Raumansicht des Prozesses im Windows-Betriebssystem. Tatsächlich gibt es einen großen Unterschied in der Zeit- und Raumansicht des Prozesses zwischen dem 16-Bit- und dem 32-Bit-Betriebssystem.
In Bezug auf die Zeit ist die Prozessverwaltung von 16-Bit-Windows-Betriebssystemen wie Windows 3.x sehr einfach. Es handelt sich eigentlich nur um ein Multitasking-Management-Betriebssystem. Darüber hinaus ist die Aufgabenplanung des Betriebssystems passiv. Wenn eine Aufgabe die Verarbeitung der Nachricht nicht aufgibt, muss das Betriebssystem warten. Aufgrund der Mängel in der Prozessverwaltung des 16-Bit-Windows-Systems belegt ein laufender Prozess die CPU-Ressourcen vollständig. Damit 16-Bit-Windows die Möglichkeit hatte, andere Aufgaben zu planen, lobte Microsoft damals die Entwickler von Windows-Anwendungen dafür, dass sie aufgeschlossene Programmierer seien, so dass sie bereit seien, noch ein paar Codezeilen zu schreiben, um sie zu verschenken Betriebssystem. Im Gegenteil, WIN32-Betriebssysteme wie Windows 95 und NT verfügen über echte Multiprozess- und Multitasking-Betriebssystemfunktionen. Der Prozess in WIN32 wird vollständig vom Betriebssystem geplant. Sobald die Zeitspanne des laufenden Prozesses endet, wechselt das Betriebssystem aktiv zum nächsten Prozess, unabhängig davon, ob der Prozess noch Daten verarbeitet. Streng genommen kann das 16-Bit-Windows-Betriebssystem nicht als vollständiges Betriebssystem angesehen werden, aber das 32-Bit-Win32-Betriebssystem ist das wahre Betriebssystem. Natürlich wird Microsoft nicht sagen, dass Win32 die Mängel von 16-Bit-Fenstern ausgibt, behauptet jedoch, dass Win32 eine fortschrittliche Technologie namens "präventive Multitasking" implementiert, was eine kommerzielle Methode ist.
Aus der Sicht der Raumfahrt können Prozesse, obwohl der Prozessraum im 16-Bit-Windows-Betriebssystem relativ unabhängig ist, problemlos auf den Datenraum des anderen zugreifen. Da diese Prozesse tatsächlich unterschiedliche Datensegmente im selben physischen Raum sind und unsachgemäße Adressvorgänge leicht zu einem falschen Lese- und Schreiben von Raum zu führen und das Betriebssystem abstürzen können. Im Win32 -Betriebssystem ist jedoch jeder Prozessraum völlig unabhängig. Win32 bietet jedem Prozess einen virtuellen und kontinuierlichen Adressraum von bis zu 4G. Der sogenannte kontinuierliche Adressraum bedeutet, dass jeder Prozess einen Adressraum von $ 00000000 bis $ FFFFFFFF hat und nicht den segmentierten Platz von 16-Bit-Fenstern. In Win32 müssen Sie sich keine Sorgen über Ihre Lesen und Schreiben von Operationen machen, die die Daten in anderen Prozessräumen unbeabsichtigt beeinflussen, und Sie müssen sich keine Sorgen über andere Prozesse machen, um Ihre Arbeit zu belästigen. Gleichzeitig ist der von Win32 für Ihren Prozess bereitgestellte kontinuierliche virtuelle 4G -Raum der physische Speicher, der Ihnen vom Betriebssystem mit Unterstützung der Hardware zugeordnet ist. physische Erinnerung.
Abschnitt 2 Prozessraum
Wenn wir Delphi verwenden, um Win32 -Anwendungen zu schreiben, kümmern wir uns selten um die interne Welt des Prozesses, wenn er ausgeführt wird. Da Win32 4G kontinuierlicher virtueller Prozessraum für unseren Prozess bietet, verwendet die möglicherweise größte Anwendung der Welt derzeit nur einen Teil davon. Es scheint, dass der Prozessraum unbegrenzt ist, aber der 4G -Prozessraum ist virtuell und der tatsächliche Speicher Ihrer Maschine kann weit davon entfernt sein. Obwohl der Prozess einen so großen Raum hat, können einige komplexe Algorithmusprogramme aufgrund des Stapelüberlaufs immer noch nicht ausgeführt werden, insbesondere von Programmen, die eine große Anzahl rekursiver Algorithmen enthalten.
Daher hilft uns ein eingehendes Verständnis der Struktur des 4G-Prozessraums, seiner Beziehung zum physischen Gedächtnis usw. . Weltanschauung und Methodik, um verschiedene schwierige Probleme zu lösen.
Als nächstes werden wir ein einfaches Experiment verwenden, um die interne Welt des Process -Raums von Win32 zu verstehen. Dies erfordert möglicherweise einige Kenntnisse über Cup -Register und Versammlungssprache, aber ich habe versucht, es in einfacher Sprache zu erklären.
Wenn Delphi gestartet wird, wird ein Projekt1 -Projekt automatisch generiert und wir werden damit beginnen. Legen Sie beispielsweise einen Haltepunkt im ursprünglichen Programm von Project1.dpr fest, beispielsweise einen Haltepunkt am Startsatz. Führen Sie dann das Programm aus und es wird automatisch anhalten, wenn es den Haltepunkt erreicht. Zu diesem Zeitpunkt können wir das CPU -Fenster im Debugging -Tool öffnen, um die interne Struktur des Prozessraums zu beobachten.
Das aktuelle Anweisungszeigerregister EIP wird bei $ 0043E4b8 gestoppt. Process Space, der $ 00000000 bis zum hübschen kleinen Adressraum für $ ffffffff belegt.
Im Befehlsfeld im CPU -Fenster können Sie nach dem Inhalt des Prozessraums nachschlagen. Wenn Sie den Inhalt des Raums weniger als $ 00400000 anzeigen, finden Sie eine Reihe von Fragenmarkierungen "????". Wenn Sie sich den hexadezimalen Wert der globalen Variablen -Hinstanz zu diesem Zeitpunkt ansehen, werden Sie feststellen, dass sie auch $ 00400000 kostet. Obwohl Hinstance das Handle der Prozessinstanz widerspiegelt, ist es in der Tat der Startadresswert, wenn das Programm in den Speicher geladen wird, ebenfalls in 16-Bit-Fenstern. Daher können wir denken, dass das Programm des Prozesses ab $ 00400000 geladen ist, dh der Platz ab 4 m im virtuellen 4G -Bereich von 4G ist der Raum, in dem das Programm geladen wird.
Ab $ 00400000 und vor $ 0044D000 ist es hauptsächlich der Adressraum des Programmcodes und der globalen Daten. In der Stapelbox im CPU -Fenster können Sie die Adresse des aktuellen Stapels anzeigen. In ähnlicher Weise werden Sie feststellen, dass der aktuelle Stack -Adressraum zwischen $ 0067B000 bis 00680000 $ liegt, mit einer Länge von 5000 USD. Tatsächlich beträgt die minimale Stapelraumgröße des Prozesses 5000 US -Dollar, was basierend auf dem auf der Linkerseite der Projektoptionen festgelegten Min -Stapelgrößenwert beim Kompilieren des Delphi -Programms plus 1000 US -Dollar erhalten wird. Der Stapel wächst von der High-End-Adresse nach unten. Prozessraum. Beim Kompilieren eines Delphi -Programms können Sie den maximalen Stapelraum steuern, der erhöht werden kann, indem der Wert der maximalen Stapelgröße auf der Linkerseite in Projektoptionen festgelegt wird. Insbesondere in Programmen, die tiefe Unterprogramme aufrufen oder rekursive Algorithmen verwenden, muss der Wert der maximalen Stapelgröße vernünftig eingestellt werden. Da das Aufrufen eines Unterprogramms einen Stapelraum erfordert und nach dem Stapel des Stapels das System einen "Stapelüberlauf" -Fehler wirft.
Es scheint, dass der Prozessraum nach dem Stapelraum freier Platz sein sollte. Tatsächlich ist dies nicht der Fall. Es scheint, dass der Prozess wirklich nur 2G Raum besitzen kann. Tatsächlich ist der Raum, den ein Prozess wirklich besitzen kann, nicht einmal 2G, da der 4 -m -Platz von $ 00000000 bis 00400000 auch ein eingeschränkter Bereich ist.
Aber egal was passiert, die Adressen, die unser Prozess verwenden kann, sind immer noch sehr breit. Besonders nach dem Stapelraum und zwischen 80.000.000 US -Dollar ist es das Hauptschlachtfeld des Prozessraums. Der Speicherplatz, der vom System aus dem System zugewiesen wird Operationen, die eine Speicherzuweisung beinhalten, werden diesem Raum zugeordnet. Bitte beachten Sie, dass die hier erwähnte Zuordnung die Korrespondenz zwischen dem tatsächlichen Speicher und diesem virtuellen Speicherplatz bedeutet. ???? ".