TStream und Strings

Für Fragen zur Programmiersprache auf welcher Lazarus aufbaut
wp_xyz
Beiträge: 5142
Registriert: Fr 8. Apr 2011, 09:01

Re: TStream und Strings

Beitrag von wp_xyz »

Als das Schreib-Programm gelaufen ist, lag der String "Hallo Welt" am Anfang des Heap. Die Adresse dieses Strings wurde in die Datei geschrieben. In dem zweiten Programm wird dieselbe Adresse aus der Datei gelesen und dem String s2 zugewiesen. Da du die Heapbelegung durch s1 und s2 im Leseprogramm nicht verändert hast, liegt dort wieder der String "Hallo Welt". Aber vertausche mal s1 und s2 in der Zuweisung, also

Code: Alles auswählen

 
  s2 := '----------';
  s1 := 'Hallo Welt'; 
 
Was passiert?

Leider ist das ganze ziemlich undurchsichtig, weil Strings eigentlich als Pointer realisiert sind.

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

Re: TStream und Strings

Beitrag von Warf »

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
Bild

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

mschnell
Beiträge: 3444
Registriert: Mo 11. Sep 2006, 10:24
OS, Lazarus, FPC: svn (Window32, Linux x64, Linux ARM (QNAP) (cross+nativ)
CPU-Target: X32 / X64 / ARMv5
Wohnort: Krefeld

Re: TStream und Strings

Beitrag von mschnell »

wp_xyz hat geschrieben:Als das Schreib-Programm gelaufen ist, lag der String "Hallo Welt" am Anfang des Heap. Die Adresse dieses Strings wurde in die Datei geschrieben. In dem zweiten Programm wird dieselbe Adresse aus der Datei gelesen und dem String s2 zugewiesen.
Du kannst nicht davon ausgehen, dass Speicher-Adressen in einem Programm für ein anderes Programm oder nach Neustart des selben Programms irgendeine Bedeutung haben.

-Michael

wp_xyz
Beiträge: 5142
Registriert: Fr 8. Apr 2011, 09:01

Re: TStream und Strings

Beitrag von wp_xyz »

Ich weiß, dass meine Erklärung nicht sauber formuliert ist. Aber wie würdest du die Beobachtung richtiger erklären?

Mathias
Beiträge: 6912
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: TStream und Strings

Beitrag von Mathias »

Die Lösung mit [1] finde ich schlecht, was passiert, wen der String Leer ist ?
Es knallt.

Ich nehme wohl besser die 2. oder 3. Lösung.

Code: Alles auswählen

            Stream.Write(Daten[i].Name[1], Len);
            Stream.Write(Pointer(Daten[i].Name)^, Len);  // FPC Lösung von TString.SaveFormStream
            Stream.Write(PChar(Daten[i].Name)^, Len);   
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Michl
Beiträge: 2511
Registriert: Di 19. Jun 2012, 12:54

Re: TStream und Strings

Beitrag von Michl »

Mathias hat geschrieben:Die Lösung mit [1] finde ich schlecht, was passiert, wen der String Leer ist ?
Es knallt.
Nein, da dann die Länge = 0 ist und somit auch kein Zugriff erfolgt. z.B.:

Code: Alles auswählen

procedure TForm1.Button1Click(Sender: TObject);
var
  MS: TFileStream;
  s: String;
begin
  MS:=TFileStream.Create('Test.txt', fmCreate);
  try
    s:='Test ';
    MS.Write(s[1], Length(s));
    MS.Write(Pointer(s)^, Length(s));
    MS.Write(PChar(s)^, Length(s));
    s:='';
    MS.Write(s[1], Length(s));
    MS.Write(Pointer(s)^, Length(s));
    MS.Write(PChar(s)^, Length(s));
  finally
    MS.Free;
  end;
end;
läuft problemlos durch.

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

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

Re: TStream und Strings

Beitrag von Warf »

Stream.Write(Daten.Name[1], Len); und Stream.Write(Pointer(Daten.Name)^, Len); und Stream.Write(PChar(Daten.Name)^, Len); sind äquivalent, alle sind das selbe.

Mathias
Beiträge: 6912
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: TStream und Strings

Beitrag von Mathias »

Bei mir kommt ein Debuggerfehler.

Ich habe Bereich und Überlauf-Prüfung aktiviert.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

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

Re: TStream und Strings

Beitrag von Warf »

Ein typischer fall von falschem Alarm, du greifst ja nur auf den Speicher zu wenn auch wirklich was drin steht, ohne Zugriff ist es egal ob str[1] existiert oder nicht. Passiert weil der Debugger nicht überprüft ob auch wirklich zugriff stattfindet. Der Debugger ist auch nicht perfekt

Mathias
Beiträge: 6912
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: TStream und Strings

Beitrag von Mathias »

Code: Alles auswählen

type
  TTestArray = array of byte;
 
  function Test(var c; len: integer): TTestArray;
  begin
    FillChar(c, len, $FF);
  end;
 
var
  TestArray: TTestArray;
  i: integer;
const
  len = 0;
 
begin
  SetLength(TestArray, len);
  Test(Pointer(TestArray)^, len);     // Funktionier auch mit len=0
  Test(TestArray[0], len);            // Bei len=0 kommt der Debugger
  for i := 0 to len - 1 do begin
    Write(TestArray[i]: 4);
  end;
  ReadLn;
end.     
Da sieht man es auch gut, der Debugger kann nicht wissen, was in der function Test passiert, aber mit dem Aufruf von TestArray[0] übergibt man eindeutig ein Variable die auf nichts zeigt.
Ich finde es gut, das der Debugger da motzt.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

Mathias
Beiträge: 6912
Registriert: Do 2. Jan 2014, 17:21
OS, Lazarus, FPC: Linux (die neusten Trunk)
CPU-Target: 64Bit
Wohnort: Schweiz

Re: TStream und Strings

Beitrag von Mathias »

Woher kommt die Abkürzungs fm bei folgenden Constanten ?

fmOpenRead, fmCreate, etc.

Ich sehe da keinen Zusammenhang.
Mit Lazarus sehe ich grün
Mit Java und C/C++ sehe ich rot

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

Re: TStream und Strings

Beitrag von Warf »

filemode

wp_xyz
Beiträge: 5142
Registriert: Fr 8. Apr 2011, 09:01

Re: TStream und Strings

Beitrag von wp_xyz »

FileMode

Michl
Beiträge: 2511
Registriert: Di 19. Jun 2012, 12:54

Re: TStream und Strings

Beitrag von Michl »

Über die "richtige" Formatierung kann sicherlich länger philosophieren. Ich finde den Styleguide vom Delphi-Treff ganz nützlich. Passend für deine Frage: http://www.delphi-treff.de/object-pascal/styleguide/3/ -> Bennennung von Attributen -> Aufzählbare Typen

Code: Alles auswählen

type
  TLiveSelection = (lsMoney, lsChilds, lsTime);
  TLive = Array[0..1] of TLiveSelection;  

Antworten