Logo

Marco Cantù's
Essential Pascal

Kapitel 7
Strings verwenden

Die Verwendung von Strings (Zeichenketten) ist in Delphi ganz einfach, aber hinter den Kulissen ist die Situation ziemlich komplex. Pascal besitzt eine traditionelle Weg zur Verwendung von Strings, Windows besitzt seinen eigenen Weg, angelehnt an die Sprache C, und die 32-Bit Versionen von Delphi enthalten einen leistungsfähigen Datentyp für lange Strings, welcher der Standardtyp für Strings in Delphi ist.

Arten von Strings

In Borland's Turbo Pascal und im 16-Bit Delphi ist der typische String-Typ eine Folge von Zeichen mit einem Längen-Byte am Anfang, der die momentane Größe des Strings kennzeichnet. Da diese Länge durch ein einzelnes Byte angegeben wird, kann es nicht mehr als 255 Zeichen umfassen, ein sehr niedriger Wert, der viele Probleme für die Manipulation von Strings hervorruft. Jeder String ist mit einer festen Länge definiert (welche standardmäßig das Maximum von 255 beträgt), sie können jedoch kürzere Strings vereinbaren, um Speicherplatz zu sparen.

Ein String ähnelt einem Array-Typ. Tatsächlich ist ein String fast ein Array von Zeichen. Das zeigt sich an dem Umstand, dass sie auf ein einzelnes String-Zeichen zugreifen können, indem Sie die []-Notation verwenden.

Um die Begrenzungen von traditionellen Pascal-Strings zu überwinden, unterstützt die 32-Bit Version von Delphi lange Strings. Daher existieren gegenwärtig drei String-Typen:

Verwendung von langen Strings

Wenn Sie einfach den Datentyp String verwenden, so erhalten Sie entweder kurze Strings oder ANSIStrings, in Abhängigkeit vom Wert der Compiler-Direktive $H. $H+ (die Vorgabe) steht für lange Strings (der ANSIString-Typ) welche auch durch die Komponenten der Delphi-Bibliothek verwendet wird.

Delphi's lange Strings beruhen auf einem Referenzzählungsmechanismus, welcher verfolgt, wie viele String-Variablen sich auf den selben String im Speicher beziehen. Diese Referenzzählung wird auch benutzt, um den Speicher wieder frei zu geben, wenn ein String nicht mehr weiter benutzt wird, d.h. wenn der Referenzzähler die Null erreicht.

Wenn Sie die Größe eines Strings im Speicher erhöhen möchten aber es befindet sich bereits etwas im benachbarten Speicher, dann kann der String nicht in den selben Speicherbereich wachsen. Daher muß eine vollständige Kopie an einem anderen Ort erzeugt werden. Wenn diese Situation auftritt, so gruppiert Delphi's Laufzeitunterstützung den String auf vollständig transparente Weise um. Sie setzen einfach die maximale Größe des Strings mit der Prozedur SetLength und bewirken damit die Zuteilung des erforderlichen Speicherbereichs:

SetLength (String1, 200);

Die Prozedur SetLength führt eine Speicheranforderung aus, keine tatsächliche Speicherzuteilung. Sie reserviert den erforderlichen Speicherplatz for die nachfolgende Verwendung, ohne tatsächlich den Speicher zu benutzen. Diese Technik beruht auf einer Eigenschaft des Windows-Betriebssystems und wird von Delphi für alle dynamischen Speicherzuteilungen verwendet. Wenn Sie z.B. ein sehr großes Array anfordern, so wird sein Speicher reserviert, aber nicht zugeteilt.

Die Einstellung der Länge eines Strings ist selten notwendig. Der einzige Fall im dem es erforderlich ist, die Speicher für einen langen String unter Verwendung von SetLength zuzuteilen ist der, wenn Sie den String als einen Parameter an eine API-Funktion übergeben müssen (nach einer korrekten Typumwandlung), wie ich es Ihnen gleich zeigen werde.

Strings im Speicher betrachten

Um Ihnen beim Verständnis der Details der Speicherverwaltung von Strings zu helfen, habe ich das einfache Beispiel StrRef geschrieben. In diesem Programm vereinbare ich zwei globale Strings: Str1 und Str2. Wenn der erste der zwei Schalter gedrückt wird, wiest das Programm einen konstanten String an die erste der zwei Variablen zu, und anschließend wird die zweite Variable mit der ersten zugewiesen:

Str1 := 'Hello';
Str2 := Str1;

Neben der Arbeit mit den Strings zeigt das Programm deren internen Status in einer Listbox unter Verwendung der folgenden Funktion StringStatus:

function StringStatus (const Str: string): string;
begin
  Result := 'Adresse: ' + IntToStr (Integer (Str)) +
    ', Länge: ' + IntToStr (Length (Str)) + 
    ', Referenzen: ' + IntToStr (PInteger (Integer (Str) - 8)^) +
    ', Wert: ' + Str;
end;

In der StringStatus-Funktion ist es unbedingt erforderlich, den String-Parameter als einen const-Parameter zu übergeben. Eine Übergabe dieses Parameters als Kopie würde den Seiteneffekt bewirken, dass eine zusätzliche Referenz auf den String erzeugt wird, solange die Funktion ausgeführt wird. Im Gegensatz dazu bewirkt die Übergabe des Parameters über eine Referenz (var) oder als einen konstanten Parameter (const) nicht, dass eine weitere Referenz auf den String erzeugt wird. In diesem Fall habe ich einen const-Parameter verwendet, da die Funktion nicht dazu gedacht ist, den String zu verändern.

Um die Speicheradresse des Strings zu ermitteln (nützlich, um die gegenwärtige Identität zu ermitteln und zu erkennen, ob zwei unterschiedliche Strings auf den selben Speicherbereich verweisen) habe ich einfach eine fest codierte Typumwandlung aus dem String-Typ in einen Integer-Typ vorgenommen. Strings sind praktisch Referenzen, sie sind Zeiger: Ihr Wert beinhaltet die momentane Speicherposition des Strings.

Um die Referenzanzahl zu extrahieren, habe ich den Code auf dem wenig bekannten Umstand basieren lassen, dass die Länge und die Referenzanzahl ebenfalls im String gespeichert werden, und zwar vor dem aktuellen Text und vor der Position auf den die String-Variable zeigt. Der (negative) Offset beträgt -4 für die String-Länge (einen Wert, den Sie viel einfacher durch Verwendung der Funktion Length ermitteln können) und -8 für die Referenzanzahl.

Beachten Sie dabei bitte, dass diese internen Informationen über den Offset in zukünftigen Versionen von Delphi geändert werden können; es gibt also keine Garantie dafür, dass derartige undokumentierte Eigenschaften auch in der Zukunft beibehalten werden.

Wenn Sie dieses Beispiel starten, so sollten Sie zwei Strings mit dem selben Inhalt erhalten, der gleichen Speicherposition und eine Referenzanzahl von 2, wie es im oberen Teil der ListBox in Abbildung 7.1 zu sehen ist. Wenn Sie jetzt den Wert von einem der zwei Strings verändern (es ist egal welchen) so wird sich die Speicherposition des modifizierten Strings ebenfalls ändern. Das ist die Auswirkung der copy-on-write Technik.

Abbildung 7.1: Das Beispiel StrRef zeigt den internen Status von zwei Strings einschließlich der gegenwärtigen Referenzanzahl.

Wir können den Effekt hervorrufen, der im zweiten Teil der ListBox von Abbildung 7.1 gezeigt wird, indem wir den folgenden Code zur Behandlung des OnClick-Ereignises für den zweiten Schalter schreiben:

procedure TFormStrRef.BtnChangeClick(Sender: TObject);
begin
  Str1 [2] := 'a';
  ListBox1.Items.Add ('Str1 [2] := ''a''');
  ListBox1.Items.Add ('Str1 - ' + StringStatus (Str1));
  ListBox1.Items.Add ('Str2 - ' + StringStatus (Str2));
end;

Beachten Sie, dass der Code der Methode BtnChangeClick erst nach der Methode BtnAssignClick ausgeführt werden kann. Um dies zu erzwingen, beginnt das Programm mit einem gesperrten zweiten Schalter (seine Eigenschaft Enabled ist auf False gesetzt) und gibt den Schalter am Ende der ersten Methode frei. Sie können dieses Beispiel beliebig erweitern und dabei die Funktion StringStatus verwenden, um das Verhalten von langen Strings unter vielen weiteren Umständen zu erforschen.

Delphi Strings und Window's PChars

Ein weiterer wichtiger Vorteil bei der Verwendung von langen Strings ist, dass sie null-terminiert sind. Das bedeutet, dass sie vollständig kompatibel mit den null-terminierten Strings der Sprache C sind, wie sie von Windows benutzt werden. Ein null-terminierter String ist eine Folge von Zeichen gefolgt von einem Byte mit dem Wert null (oder zero). Dieser kann in Delphi ausgedrückt werden durch Anwendung eines null-basierten Arrays aus Zeichen, dem Datentyp der gewöhnlich in der C-Sprache verwendet wird, um Strings zu implementieren. Das ist der Grund dafür, dass null-terminierte Zeichen-Arrays so weit verbreitet sind in den Windows API-Funktionen (welche auf der Sprache C beruhen). Da Pascal's lange Strings vollständig kompatibel zu null-terminierten Strings in C sind, können Sie einfach einen langen String verwenden und ihn in ein PChar umwandeln, wenn Sie einen String an eine Windows API-Funktion übergeben müssen.

Um z.B. die Beschriftung eines Formulars in einen PChar-String zu kopieren (unter Verwendung der API-Funktion GetWindowText) und ihn anschließend in die Beschriftung eines Schalters zu kopieren, können Sie den folgenden Code schreiben:

procedure TForm1.Button1Click (Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  Button1.Caption := S1;
end;

Sie finden diesen Code im Beispiel LongStr. Beachten Sie aber, dass das Programm wahrscheinlich abstürzen wird, wenn Sie diesen Code schreiben, aber vergessen, den Speicher für den String mit SetLength zuzuweisen. Falls Sie einen PChar verwenden müssen, um einen Wert zu übergeben (und nicht zu empfangen wie in dem Code zuvor), so wird der Code noch einfacher, da es hier nicht erforderlich ist, einen temporären String zu definieren und zu initialisieren. Die folgende Code-Zeile übergibt die Caption-Eigenschaft eines Labels als Parameter an eine API-Funktion, indem sie einfach eine Typumwandlung nach PChar vornimmt:

SetWindowText (Handle, PChar (Label1.Caption));

Falls Sie eine Umwandlung eines WideString in einen Windows-kompatiblen Typ vornehmen müssen, so nutzen Sie PWideChar anstatt PChar für diese Umwandlung. Breite Strings werden oft für OLE und COM-Programme benötigt.

Nachdem ich dieses schöne Bild vorgestellt habe, möchte ich jetzt auf die Fallen eingehen. Es können einige Probleme durch die Umwandlung eines langen Strings in einen PChar-Typ auftreten. Speziell das fundamentale Problem, dass Sie nach dieser Umwandlung verantwortlich werden für den String und seinen Inhalt, Delhpi kann Ihnen hier nicht weiter helfen. Betrachten Sie folgende kleine Änderung an dem ersten Programmausschnitt oben, Button1Click:

procedure TForm1.Button2Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := S1 + ' ist der Titel'; // dies tut's nicht
  Button1.Caption := S1;
end;

Dieses Programm wird übersetzt, aber wenn Sie es starten, so erleben sie eine Überraschung: Die Beschriftung des Schalters wird den ursprünglichen Text der Fensterbeschriftung zeigen, ohne den Text des konstanten Strings, den sie angefügt haben. Das Problem entsteht dann, wenn Windows in den String schreibt (innerhalb des API-Aufrufs GetWindowText), so setzt es die Länge des Pascal-Strings nicht korrekt. Delphi kann diesen String weiterhin zur Ausgabe verwenden und durch die Ermittlung des Endekennzeichens null feststellen wann er endet, aber wenn Sie weitere Zeichen hinter dem Endekennzeichen anhängen, so werden diese vollständig übergangen.

Wie können wir dieses Problem beheben? Die Lösung besteht darin, das System aufzufordern, den durch den API-Aufruf GetWindowText zurückgelieferten String zurück in einen Pascal-String umzuwandeln. Wenn Sie jedoch den folgenden Code schreiben:

S1 := String (S1);

... wird das System ihn ignorieren, da die Umwandlung eines Datentyps zurück in den selben eine nutzlose Operation ist. Um den richtigen langen Pascal-String zu erhalten, müssen Sie den String in ein PChar zurückwandeln und Delphi auffordern, diesen wieder in einen korrekten String zu verwandeln:

S1 := String (PChar (S1));

Häufig können Sie diese Umwandlung auslassen, da eine PChar-zu-String-Umwandlung in Delphi automatisch erfolgt. Hier ist der vollständige Code:

procedure TForm1.Button3Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := String (PChar (S1));
  S1 := S1 + ' ist der Titel';
  Button3.Caption := S1;
end;

Eine Alternative, die Länge eines Delphi-Strings zurückzusetzen, besteht darin, die Länge des PChar-Strings zu verwenden, indem Sie schreiben:

SetLength (S1, StrLen (PChar (S1)));

Im Beispiel LongStr können Sie drei Versionen dieses Codes finden. Er besitzt drei Schalter, um diese auszuführen. Wenn Sie jedoch nur den Titel eines Formulars benötigen, so können Sie einfach die Caption-Eigenschaft des Formulars selbst verwenden. Es besteht keine Notwendigkeit, all diesen verwirrenden Code zu schreiben, der nur dazu dienen sollte, die Probleme der String-Umwandlung zu demonstrieren. In der Praxis gibt es aber Fälle, wo sie Windows API-Funktionen aufrufen müssen. Dann sollten Sie diesen komplexen Umstand berücksichtigen.

Strings formatieren

Unter Verwendung des Plus-Operators (+) und einigen Umwandlungsfunktionen (wie z.B. IntToStr) können Sie tatsächlich komplexe Strings aus existierenden Werten erzeugen. Es gibt jedoch einen anderen Lösungsansatz zur Formatierung von Zahlen, Währungswerten und weiteren Strings in einen kompletten String. Sie können die leistungsfähige Format-Funktion oder einige ihrer Begleitfunktionen verwenden.

Die Format-Funktion erfordert als Parameter einen String mit dem Grundtext und einigen Platzhaltern (gewöhnlich gekennzeichnet durch das Symbol %) und ein Array der Werte, einen für jeden Platzhalter. Um z.B. zwei Zahlen in einem String zu formatieren können Sie folgendes schreiben:

Format ('First %d, Second %d', [n1, n2]);

wobei n1 und n2 zwei Integer-Werte sind. Der erste Platzhalter wird durch den ersten Wert ersetzt, der zweite entsprechend durch den zweiten usw. Wenn der Ausgabetyp des Platzhalters (gekennzeichnet durch den Buchstaben nach dem %-Symbol) nicht mit dem zugehörigen Parameter zusammenpasst, so tritt ein Laufzeitfehler auf. Das keine Typprüfung zur Übersetzungszeit existiert, ist der größte Nachteil bei der Verwendung der Format-Funktion.

Die Format-Funktion benutzt einen offenen Array-Parameter (ein Parameter, der eine beliebige Anzahl von Werten besitzen kann), etwas was ich am Ende dieses Kapitels noch erläutern werden. Beachten Sie im Moment jedoch nur die Array-ähnliche Syntax der Werteliste, die als zweiter Parameter dienen.

Neben der Verwendung von %d können Sie noch viele weitere Platzhalter verwenden, die durch diese Funktion definiert sind und in der Tabelle 7.1 kurz aufgelistet sind. Diese Platzhalter bieten eine Standardausgabe für den jeweiligen Datentyp. Sie können jedoch weitere Formatangaben verwenden, um die Standardausgabe zu verändern. Eine Formatangabe für die Breite bestimmt z.B. eine feste Anzahl von Zeichen in der Ausgabe, während eine Formatangabe für die Genauigkeit die Anzahl der Dezimalstellen bestimmt. Z.B.:

Format ('%8d', [n1]);

wandelt die Zahl n1 in einen String mit acht Zeichen und rechtsbündigem Text um (benutzen Sie das Minus-Symbol (-) um Linksbündig festzulegen), aufgefüllt mit Leerzeichen.

Tabelle 7.1: Die Formatbezeichner der Funktion Format

FORMATBEZEICHNER

BESCHREIBUNG

d (decimal) Der zugehörige Integer-Wert wird in einen String aus dezimalen Ziffern umgewandelt.
x (hexadecimal) Der zugehörige Integer-Wert wird in einen String aus hexadezimalen Ziffern umgewandelt.
p (pointer) Der zugehörige Zeigerwert wird in einen String bestehend aus hexadezimalen Ziffern umgewandelt.
s (string) Der zugehörige String, Char oder PChar wird in den Ausgabe-String kopiert.
e (exponential) Der zugehörige Gleitkommawert wird in einen String basierend auf der exponentialen Schreibweise umgewandelt.
f (floating point) Der zugehörige Gleitkommawert wird in einen String basierend auf der Gleitkommaschreibweise umgewandelt.
g (general) Der zugehörige Gleitkommawert wird in den kürzest möglichen Dezimal-String umgewandelt, basierend auf der exponentiellen oder der Gleitkommaschreibweise.
n (number) Der zugehörige Gleitkommawert wird in einen String basierend auf der Gleitkommaschreibweise, aber zusätzlich mit Tausendertrennzeichen umgewandelt.
m (money) Der zugehörige Gleitkommawert wird in einen String umgewandelt, der einen Währungsbetrag darstellt. Die Umwandlung erfolgt unter Berücksichtigung der regionalen Einstellungen - vergleichen Sie die Delphi-Hilfedatei unter den Punkten Währungswerte und Datums-/Zeitwerte (Formatvariablen).

Der beste Weg, um Beispiele für diese Umwandlungen zu sehen, besteht darin, selbst mit den Format-Strings zu experimentieren. Um dies zu vereinfachen, habe ich das Programm FmtTest geschrieben, welches dem Anwender gestattet, Format-Strings für Integer- und Gleitkommazahlen vorzugeben. Wie Sie in Abbildung 7.2 sehen können, zeigt dieses Programm ein in zwei Bereiche geteiltes Formular an. Der linke Bereich dient für Integer-Zahlen, der rechte Bereich für Gleitkommazahlen.

Jeder Bereich hat zunächst ein Eingabefeld mit dem Zahlenwert, den Sie als String formatieren möchten. Unter dem ersten Eingabefeld ist ein Schalter, um die Formatierung durchzuführen. Das Ergebnis wird in einer Message-Box angezeigt. Dann folgt ein weiteres Eingabefeld, in dem Sie den Formatierungs-String eingeben können. Alternativ können Sie einfach eine Zeile der darunterliegenden ListBox-Komponente anklicken, um einen vordefinierten Format-String auszuwählen. Jedes mal, wenn Sie einen neuen Format-String eingeben, wird er der dazugehörigen Liste angefügt (beachten Sie jedoch, dass diese neuen Einträge bei Beendigung des Programms verloren gehen).

Abbildung 7.2: Die Ausgabe eines Gleitkommawertes durch das Programm FmtTest.

Der Code dieses Beispiels verwendet einfach den Eingabetext der verschiedenen Steuerelemente, um die Ausgabe zu erzeugen. Dies ist eine der drei Methoden, die mit den Schaltern Show verbunden sind:

procedure TFormFmtTest.BtnIntClick(Sender: TObject);
begin
  ShowMessage (Format (EditFmtInt.Text,
    [StrToInt (EditInt.Text)]));
  // wenn das Element nicht dort ist, wird es angefügt
  if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then
    ListBoxInt.Items.Add (EditFmtInt.Text);
end;

Der Code beruht auf der Formatierungsoperation unter Verwendung der Eingabewerte aus den Steuerelementen EditFmtInt und EditInt. Wenn der Format-String noch nicht in der ListBox enthalten ist, so wird er anschließend hinzugefügt. Wenn der Anwender statt dessen auf einen Eintrag in der ListBox klickt, so kopiert der Code diesen Wert in die EditBox:

procedure TFormFmtTest.ListBoxIntClick(Sender: TObject);
begin
  EditFmtInt.Text := ListBoxInt.Items [
    ListBoxInt.ItemIndex];
end;

Zusammenfassung

Strings stellen mit Sicherheit einen sehr gebräuchlichen Datentyp dar. Obwohl Sie ihn in den meisten Fällen sicher verwenden können ohne verstehen zu müssen, wie er intern arbeitet, sollte dieses Kapitel das exakte Verhalten von Strings verständlich gemacht haben und es Ihnen ermöglichen, die volle Leistungsfähigkeit dieses Datentyps zu nutzen.

Strings werden im Speicher auf spezielle, dynamische Weise behandelt, genauso wie dynamische Arrays. Diese sind das Thema des nächsten Kapitels.

Nächstes Kapitel: Speicher

© Copyright Marco Cantù, Wintech Italia Srl 1995-2000
© Copyright der deutschen Übersetzung Immo Wache, 2000