AnsiStrings sind in Pascal durch Zeiger Realisiert. Im Klartext heißt dass, mit der Deklaration einer Variable (var Name: Typ) wird ein Speicherblock der Größe SizeOf(Typ) belegt, auf diesen hast du direkt Zugriff mit der Variable. Dieser Speicherblock wird im so genannten Stack/Stapelspeicher/Kellerspeicher organisiert. Der Stack funktioniert nach dem LiFo, Last in First out system organisiert, also was zuletzt reingeschrieben wurde liegt ganz oben auf dem Speicher. Damit ist es möglich den Speicher recht gut zu organisieren. Das Problem daran ist, dass dieser Speicher Statisch ist, das heißt bereits zur Compilerzeit liegt vor wie viel Speicher benötigt wird. Für viele Zwecke reicht dieser Statische Speicher allerdings nicht, als Beispiel Strings, da ein String Beliebig lang werden Kann, und immer unterschiedlich viele Zeichen enthält muss man den Speicher anders Lösen. Dafür kommt eine andere Speicherlösung zum Einsatz, der so genannte Heap/Haldenspeicher (Hat nicht so sehr viel mit der Datenstruktur Heap zu tun).
Wie der Heap Organisiert ist ist relativ egal, darum kümmert sich das System, allgemein kann man es sich vorstellen wie der Name bereits schon sagt, wie eine Halde. Überall liegt irgendwo was rum, kommt was dazu sucht man sich einfach noch einen Platz wo es hin passt, und dort wirft man es einfach hin. Das wird auch im Speicher gemacht, wenn man GetMem(Allgemein), SetLength(Strings und Arrays), new(Typisierte Pointer) oder malloc(in C) sucht das System nach einem Freien stück Speicher im Heap und gibt dir die Position. Durch Freemem(Allgemein), dispose(Typisierte Pointer) oder Free(in C) wird der Speicher aufgelöst und damit kann er wieder verwendet werden.
Da es keine Ordnung gibt muss man wenn man etwas Holen will wissen wo es liegt (oder suchen, aber das tut relativ wenig zur Sache). Um nun zu wissen wo auf der Halde das zu findende Element liegt musst du die Position zwischenspeichern.
Wenn wir von Speicher Reden handelt es sich um Adressen, je nach Architektur 8, 16, 32 oder 64 bit Großen Wörtern (typen Byte, Word, DWord, QWord) über welche jeweils eine Reihe von 8 Bit Registern (8*1Bit FlipFlops) angesteuert werden.
Beispiel Grafik für den Aufbau des Speichers eines Prozesses
Das Zwischenspeichern wird über Zeiger/Pointer Realisiert, ein Datentyp dessen Größe durch den Compiler immer an den Adressraum des Systems angepasst wird (Also bei einem 32 Bit Programm ist Pointer ein DWord). Durch einen Aufruf von z.B. SetLength bei einem String (z.b. String := 'Hallo' wird zu nächst einmal SetLength(String, 5) aufgerufen intern) wird nun der Speicher im Heap Reserviert, und kann damit frei Genutzt werden. Die Addresse wo sich die Daten des Strings, also die Zeichen im Heap befinden wird dann in der Variable, dem Pointer/Zeiger gespeichert. Veränderst du den String, hängst zum Beispiel ein Zeichen an den alten String dran wird der Speicher für die neue Größe gesucht, reserviert, der Alte wert reingeschrieben, und dann an den Letzten platz das neue Zeichen eingefügt. Dann wird der Alte Speicher freigegeben, und die neue Adresse dann in die Variable des String geschrieben.
Somit werden, egal wie groß der String ist im Stack nur 4 Byte Belegt, und der Heap wird mit den Zeichen Belastet.
Zugriff auf diesen Speicher erhälst du nun über die Zeiger Operationen. Bei einem Zeiger z.B. PChar oder PInteger kannst du auf den Wert über ein "Dach" ^ zugreifen:
Code: Alles auswählen
var IntPointer: PInteger; //Zeiger Variable (32 Bit groß)
begin
new(IntPointer); // Belegt im Heap die Benötigte größe für einen Integer und speichert die Adresse in IntPointer
IntPointer^:=14; // Schreibt 14 in den Speicher im Heap
dispose(IntPointer); // Gibt den Speicher wieder frei, daten werden gelöscht
end;
Bei Strings oder Dynamischen Arrays geht dass über Eckige Klammern
Code: Alles auswählen
var MyString: AnsiString; // Zeiger Variable
begin
MyString := 'Hallo'; // ist das selbe wie:
SetLength(MyString, 5); // 5 Byte für den String Reservieren
MyString[1] := 'H'; // Erstes Feld mit dem Char 'H' Belegen (bei Arrays wäre der Index 0)
MyString[2] := 'a'; // Zweites Feld Belegen
MyString[3] := 'l'; // ...
MyString[4] := 'l';
MyString[5] := 'o'; //letztes Feld belegen
end;
Die Variable MyString ist also nur Indirekt der String.
Befehle wie TStream.Write oder auch Move (Kopiert n Bytes von Variable 1 in Variable 2) wird nicht auf den Typen Geachtet, sondern ab dem Register welches durch die Variable angezeigt wird gearbeitet. TStream.Write(MyString, SizeOf(Pointer)); würde z.B. nur den Zeiger auf den String Schreiben, da die Variable den Speicher im Stack enthält, nicht den im Heap, wo die Daten Stehen. Über TStream.Write(MyString[1], Len); würden Len Bytes angefangen von dem Ersten Element des Strings (im Heap) geschrieben werden. Da die Bytes des Strings hintereinander im Heap stehen wird damit der Gesamte String Geschrieben.
Was kann passieren wenn man nicht drauf achtet ob man einen Pointer oder einen Ordinal Typen (typen deren Größe fix im Stack liegt) hat.
Ein Befehl wie z.B. TStream.Read(MyString, 20) würde einfach 20 Bytes in den Stack lesen. Nun hält der String nur 4 Byte im Stack. Nun Schreibt er aber weiter, sagen wir im Stack liegt, die Variable MyString, ein Integer, noch ein Double und die Rücksprungaddresse. Die ersten 4 Byte werden in den Pointer MyString gelesen. Mit den Nächsten 4 Zeichen des Strings wird der Integer überschrieben. Darauf folgen 8 Byte die der Double aufnehmen kann. Schlussendlich überschreibst du noch die Rücksprungsadresse. Nun hast du das Folgende erreicht: 1. Dein String zeigt auf eine Wahrscheinlich nicht exsistierende Addresse, in deinem Integer und Double steht unverwertbarerer Müll (grade wenn man z.b. in einer For schleife ist und der Zähler plötzlich von 5 auf -32158211 gesetzt wurde kann das fehlen geben) und dein Programm weiß nicht mehr wo es im Code war (rücksprungadresse) und führt nur noch sinnlosen Code aus, der an dieser Stelle gar nicht stehen sollte.
Jetzt setzen wir noch einen Drauf und sagen du verwendest ein Altes System z.b. Windows 9x da kannst du über Falsche Adressräume den Speicher anderer Prozesse überschreiben. Überschreibst du also zu viel Speicher kann es vorkommen (besser gesagt konnte, als windows 9x noch im gebrauch war) das du plötzlich Fehler in Anderen Prozessen, z.B. dem System selber verursachst, nur wegen einem kleinen Denkfehler.
Also die Moral von der Geschichte ist: benutze einfach das [1], alles andere endet in Fiasko