Logo

Marco Cantù
L'essentiel sur Pascal

Traduit de l'anglais par Iannis Papageorgiadis ipapag@village.uunet.be

Chapitre 7

La gestion des chaînes

En Delphi, il est très simple de manipuler des chaînes de caractères, mais dans les coulisses la situation est plus complexe. Pascal utilise une manière traditionnelle de manipuler les chaînes, Windows possède la sienne, empruntée au langage C, et les versions 32 bits de Delphi comportent un puissant type de données chaîne longue qui est le type par défaut en Delphi.

Les types de chaînes

Dans le Turbo Pascal de Borland et dans le Delphi 16 bits, le type string typique est une séquence de caractères avec un byte longueur au début, indiquant la taille courante de la chaîne. Puisque la longueur est exprimée par un seul byte, elle ne peut pas dépasser 255 caractères, une valeur très faible qui engendre plusieurs problèmes lors des manipulations des chaînes. Chaque chaîne est définie avec une taille fixée (par défaut, la taille maximum : 255), bien que l'on puisse déclarer des chaînes plus courtes pour économiser de l'espace mémoire.

Un type string est semblable à un type tableau. En fait, une chaîne est presque un tableau de caractères. La preuve en est qu'il est possible d'accéder à un caractère spécifique de la chaîne en utilisant la notation [].

Pour dépasser les limites des chaînes Pascal traditionnelles, les versions 32 bits de Delphi supportent les chaînes longues. En réalité, il existe trois types de chaînes :

L'utilisation des chaînes longues

Si on utilise simplement le type de données string, on obtient soit des chaînes courtes soit des chaînes ANSI en fonction de la valeur de la directive de compilation $H. Le commutateur est mis en $H+ (option par défaut) pour les chaînes longues (le type ANSIString) qui sont utilisées par les composants de la bibliothèque Delphi.

Les chaînes longues de Delphi sont basées sur un mécanisme de comptage de références, lequel  retient le nombre de variables chaîne qui font référence à la même chaîne en mémoire. Ce comptage de références est également utilisé pour libérer la mémoire lorsqu'une chaîne n'est plus utilisée, c'est-à-dire lorsque le compteur de références arrive à zéro.

Si on souhaite augmenter la taille d'une chaîne en mémoire mais qu'il y a quelque chose d'autre dans la mémoire voisine, la chaîne ne peut pas s'étendre dans le même emplacement mémoire et une copie complète de la chaîne doit donc être effectuée dans un autre emplacement. Lorsque cette situation se présente, Delphi, pendant l'exécution, déplace la chaîne de façon complètement transparente. On fixe simplement la taille maximum de la chaîne à l'aide de la procédure SetLength en allouant effectivement la quantité de mémoire demandée :

SetLength (String1, 200);
La procédure SetLength effectue une demande de mémoire et non pas une véritable allocation de mémoire. Elle réserve l'espace mémoire demandé pour une utilisation future sans utiliser effectivement la mémoire. Cette technique est fondée sur une fonctionnalité des systèmes d'exploitation Windows et est utilisée par Delphi pour toutes les allocations dynamiques de mémoire. Lorsque, par exemple, on demande un très grand tableau, sa mémoire est réservée mais pas allouée.

Fixer la longueur d'une chaîne est rarement nécessaire. Le seul cas où l'on doit allouer de la mémoire pour une chaîne longue en utilisant SetLength est quand on doit passer la chaîne comme paramètre à une fonction API (après le transtypage adéquat), comme nous le montrerons bientôt.

Un coup d'oeil sur les chaînes en mémoire

Pour faciliter la compréhension des détails de la gestion de la mémoire pour les chaînes, nous présentons l'exemple simple StrRef. Dans ce programme, nous déclarons deux chaînes globales Str1 et Str2. Lorsque le premier des deux boutons est pressé, le programme affecte une constante chaîne à la première des deux variables et ensuite il affecte la première variable à la seconde :
Str1 := 'Hello';

Str2 := Str1;
En plus de travailler sur les chaînes, le programme affiche dans une boîte liste (list box) leur état interne en utilisant la fonction StringStatus ci-dessous :
 
function StringStatus (const Str: string): string;
begin
  Result := 'Address: ' + IntToStr (Integer (Str)) +
    ', Length: ' + IntToStr (Length (Str)) +
    ', References: ' + IntToStr (PInteger (Integer (Str) - 8)^) +
    ', Value: ' + Str;
end;
(N.D.T. : PInteger est déclarée dans l'unité Windows.Pas en tant que ^Integer).

Il est indispensable, dans la fonction StringStatus, de passer le paramètre chaîne en tant que paramètre constante. Passer ce paramètre par copie (par valeur) provoquerait un effet de bord, celui d'avoir une référence supplémentaire à la chaîne lors de l'exécution de la fonction. Par contre, en le passant  par référence (var) ou par paramètre constante (const), il n'y aura pas de référence supplémentaire à la chaîne. Ici nous avons utilisé un paramètre constante, puisqu'il n'était pas prévu que la fonction modifie la chaîne.

Pour obtenir l'adresse en mémoire de la chaîne (utile pour déterminer sa véritable identité et pour voir quand deux chaînes différentes font référence à la même zone mémoire), nous avons simplement effectué un transtypage "en dur" au départ d'un type String vers le type Integer. Pratiquement, les chaînes sont des références; il s'agit de pointeurs : leur valeur contient le véritable emplacement mémoire de la chaîne.

Pour obtenir le compteur de références, nous avons basé le code sur le fait peu connu que la longueur et le compteur de références sont réellement stockés dans la chaîne, avant le texte réel et avant la position que pointe la variable chaîne. L'offset (négatif) est -4 pour la longueur de la chaîne (valeur que l'on peut obtenir plus facilement en utilisant la fonction Length) et -8 pour le compteur de références.

On retiendra que cette information interne à propos des offsets pourrait changer dans les versions futures de Delphi; il n'est pas garanti non plus que de telles fonctionnalités non documentées seront maintenues à l'avenir.

En exécutant cet exemple, vous devriez obtenir deux chaînes avec le même contenu, le même emplacement mémoire et un compteur de références de 2, comme on le voit dans la partie supérieure de la boîte liste (list box) de la figure 7.1. Si maintenant vous modifiez la valeur d'une des deux chaînes (n'importe laquelle), l'emplacement mémoire de la chaîne mise à jour changera. C'est l'effet de la technique copie-par-écriture (copy-on-write).

Figure 7.1 : L'exemple StrRef montre l'état interne de deux chaînes, y compris le compteur de références courant.

Nous pouvons effectivement produire cet effet, illustré dans la deuxième partie de la boîte liste ( list box) de la figure 7.1., en écrivant le code suivant pour le gestionnaire de l'événement OnClick du deuxième bouton :

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;
On notera que le code de la méthode BtnChangeClick peut être exécuté uniquement après la méthode BtnAssignClick. Pour forcer cela, le programme commence avec le deuxième bouton désactivé (sa propriété Enabled est mise à False); il active le bouton à la fin de la première méthode. On peut sans limite étendre cet exemple et utiliser la function StringStatus pour explorer le comportement des chaînes longues dans plusieurs autres situations.

Les chaînes Delphi et les PChar de Windows

Un autre point important en faveur de l'utilisation des chaînes longues est qu'elles sont à zéro terminal. Ceci signifie qu'elles sont entièrement compatibles avec les chaînes à zéro terminal du langage C utilisé par Windows. Une chaîne à zéro terminal est une séquence de caractères suivie par un byte mis à zéro (ou null). Ceci peut se formuler en Delphi en utilisant un tableau de caractères avec l'indice démarrant à zéro; c'est le type de données utilisé pour implémenter les chaînes dans le langage C. C'est pour cette raison que les tableaux de caractères à zéro terminal sont si courants dans les fonctions API Windows (qui sont basées sur le langage C). Puisque les chaînes longues de Pascal sont entièrement compatibles avec les chaînes à zéro terminal du C, on peut simplement utiliser des chaînes longues et les transtyper en PChar lorsqu'on a besoin de passer une chaîne à une fonction API Windows.

Par exemple, pour copier le titre d'une fiche dans une chaîne PChar (en utilisant la fonction API GetWindowText) et la copier ensuite dans le Caption d'un bouton, on peut écrire le code suivant :

procedure TForm1.Button1Click (Sender: TObject);

var

  S1: String;

begin

  SetLength (S1, 100);

  GetWindowText (Handle, PChar (S1), Length (S1));

  Button1.Caption := S1;

end;
Vous pouvez trouver ce code dans l'exemple LongStr. Notez que si vous écrivez ce code mais que vous n'allouez pas la mémoire pour la chaîne avec SetLength, le programme se bloquera probablement. Si vous utilisez un PChar pour passer une valeur (et non pas pour en recevoir une, comme dans le code ci-dessus), le code est même plus simple, parce qu'il ne faut pas définir une chaîne temporaire et l'initialiser. La ligne de code suivante passe la propriété Caption d'un libellé (label) en tant que paramètre à une fonction API en la transtypant simplement en PChar :
SetWindowText (Handle, PChar (Label1.Caption));
Lorsqu'on doit transtyper un WideString à un type compatible Windows, on doit utiliser pour la conversion PWideChar plutôt que PChar. Les chaînes étendues (WideString) sont souvent utilisées pour les programmes OLE et COM.

Après avoir présenté le beau côté de la chose, il nous faut maintenant en examiner les pièges. Quelques problèmes pourraient surgir lors de la conversion d'une chaîne longue en un PChar. Le problème fondamental est que, après la conversion, l'utilisateur devient responsable de la chaîne et de ses contenus, et Delphi ne l'aidera plus. Considérons la petite modification suivante du premier fragment de code ci-dessus, Button1Click :

procedure TForm1.Button2Click(Sender: TObject);

var

  S1: String;

begin

  SetLength (S1, 100);

  GetWindowText (Handle, PChar (S1), Length (S1));

  S1 := S1 + ' est le titre'; // ceci ne fonctionnera pas

  Button1.Caption := S1;

end;
Ce programme se compile, mais lorsque vous l'exécuterez, vous serez surpris. Le Caption du bouton contiendra le texte d'origine du titre de la fenêtre sans le texte de la constante chaîne que vous lui avez ajoutée. Le problème provient du fait que lorsque Windows écrit dans la chaîne (à l'intérieur de l'appel API GetWindowText), il n'établit pas correctement la longueur de la chaîne longue Pascal. Delphi peut encore très bien utiliser cette chaîne comme sortie et peut arriver à comprendre quand elle se termine en cherchant le zéro terminal, mais si vous ajoutez des caractères supplémentaires après le zéro terminal, ils seront complètement ignorés.

Comment résoudre ce problème? En disant au système de convertir la chaîne retournée par le retour d'appel de l'API GetWindowText en une chaîne Pascal. Toutefois, si vous écrivez le code suivant :

S1 := String (S1);
le système l'ignorera parce que convertir un type de données en lui-même est une opération inutile. Pour obtenir la  chaîne longue Pascal appropriée, vous devez re-typer la chaîne en un PChar et laisser à Delphi le soin de la convertir correctement de nouveau en une String :
S1 := String (PChar (S1));
En fait, vous pouvez éviter la conversion de la chaîne, parce que les conversion de PChar vers string sont automatiques en Delphi. Voici le code final :
procedure TForm1.Button3Click(Sender: TObject);

var

  S1: String;

begin

  SetLength (S1, 100);

  GetWindowText (Handle, PChar (S1), Length (S1));

  S1 := String (PChar (S1));

  S1 := S1 + ' est le titre';

  Button3.Caption := S1;

end;
Une alternative est de rétablir la longueur de la chaîne Delphi en utilisant la longueur de la chaîne PChar, en écrivant :
SetLength (S1, StrLen (PChar (S1)));
Vous pouvez trouver trois versions de ce code dans l'exemple LongStr qui comporte trois boutons pour les exécuter. Cependant, si vous devez seulement accéder au titre de la fiche, vous pouvez simplement utiliser la propriété Caption de l'objet fiche même. Il n'est pas nécessaire d'écrire tout ce code confus, qui n'avait comme but que d'illustrer les problèmes de la conversion des chaînes. Il existe en pratique des cas dans lesquels on doit appeler des fonctions API Windows et où il faut alors tenir compte de cette situation complexe.

Formater des chaînes

En utilisant l'opérateur plus (+) et quelques-unes des fonctions de conversion (comme IntToStr), on peut en effet construire des chaînes complexes en-dehors des valeurs existantes. Toutefois, il existe une approche différente pour formater des nombres, des valeurs monétaires et d'autres chaînes dans une chaîne finale. On peut utiliser la puissante fonction Format ou une de ses fonctions partenaires.

La fonction Format demande comme paramètres une chaîne avec le texte de base, quelques spécificateurs de format (habituellement marqués par le symbole %) et un tableau de valeurs, une par spécificateur de format. Par exemple, pour formater deux nombres en une chaîne, vous pouvez écrire :

Format ('First %d, Second %d', [n1, n2]);
n1 et n2 sont deux valeurs de type Integer. Le premier spécificateur de format est remplacé par la première valeur, le second par la seconde et ainsi de suite. Si le type résultant du spécificateur de format (indiqué par la lettre qui suit le symbole %) ne s'accorde pas avec le type du paramètre correspondant, il se produit une erreur d'exécution. L'absence de vérification à la compilation est en réalité l'inconvénient majeur de l'utilisation de la fonction Format.

La fonction Format utilise un paramètre tableau ouvert (un paramètre qui peut avoir un nombre arbitraire de valeurs); nous en discuterons vers la fin de ce chapitre. Mais, pour le moment, remarquez uniquement la syntaxe type tableau de la liste des valeurs passées en tant que second paramètre.

Outre %d, on peut utiliser un des nombreux autres spécificateurs de format définis par cette fonction et brièvement listés dans la table 7.1. Ces spécificateurs de format fournissent un résultat par défaut pour les types de données donnés. Cependant, on peut utiliser des spécificateurs de format supplémentaires pour modifier le résultat par défaut. Un spécificateur de taille (width), par exemple, détermine un nombre précis de caractères dans la réponse, tandis qu'un spécificateur de précision indique le nombre de chiffres décimaux. Par exemple :

Format ('%8d', [n1]);
convertit le nombre n1 en une chaîne de huit caractères alignés à droite (utiliser le signe moins (-) pour spécifier un alignement à gauche), en le complétant par des espaces.
 
Table 7.1 : Types des spécificateurs de format pour la fonction Format
 
TYPE DU SPECIFICATEUR DESCRIPTION
d (décimal) La valeur entière correspondante est convertie en une chaîne de chiffres décimaux.
x (hexadécimal) La valeur entière correspondante est convertie en une chaîne de chiffres hexadécimaux.
p (pointeur) La valeur entière correspondante est convertie en une chaîne exprimée en chiffres hexadécimaux.
s (chaîne de caractères) La valeur chaîne, caractère, ou PChar correspondante est copiée dans la chaîne résultante.
e (exponentielle) La valeur à virgule flottante correspondante est convertie en une chaîne indiquée en notation scientifique.
f (virgule flottante) La valeur à virgule flottante correspondante est convertie en une chaîne indiquée en notation en virgule flottante.
g (général) La valeur à virgule flottante correspondante est convertie en une chaîne la plus courte possible en utilisant le format en virgule flottante ou la notation scientifique.
n (nombre) La valeur à virgule flottante correspondante est convertie en une chaîne format virgule flottante mais utilise aussi les séparateurs de milliers.
m (monétaire) La valeur à virgule flottante correspondante est convertie en une chaîne représentant un montant monétaire. La conversion est basée sur les paramètres régionaux. Voir l'aide Delphi à la rubrique variables de format date/heure ou variables de format monétaire.
La meilleure façon d'examiner des exemples de ces conversions est d'essayer soi-même. Pour  faciliter la chose,  nous avons écrit le programme FmtTest, qui permet à un utilisateur de fournir des chaînes formatées pour des nombres entiers et pour des nombres à virgule flottante. Comme on le voit à la figure 7.2, ce programme affiche une fiche divisée en deux parties. La partie gauche est destinée aux nombres entiers et la partie droite aux nombres à virgule flottante.

Chaque partie comporte une première boîte de saisie (edit box) avec la valeur numérique que l'on souhaite formater. Au-dessous de la première boîte de saisie se trouve un bouton pour réaliser l'opération de formatage et pour afficher le résultat dans une boîte de message (message box). Vient ensuite une autre boîte de saisie dans laquelle on peut écrire une chaîne de format. On peut aussi simplement cliquer sur une des lignes du composant ListBox, au-dessous, pour sélectionner une chaîne de format prédéfinie. Chaque fois qu'on écrit une nouvelle chaîne de format, elle est ajoutée dans le ListBox correspondant (notez que, en quittant le programme, on perd ces nouveaux éléments).
 

Figure 7.2 : Le résultat d'une valeur en virgule flottante du programme FmtTest


 

Le code de cet exemple utilise simplement le texte de différents contrôles pour produire ses résultats. Voici une des trois méthodes connectées aux boutons Show :
procedure TFormFmtTest.BtnIntClick(Sender: TObject);

begin

  ShowMessage (Format (EditFmtInt.Text,

    [StrToInt (EditInt.Text)]));

  // si l'élément ne s'y trouve pas, l'y ajouter

  if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then

    ListBoxInt.Items.Add (EditFmtInt.Text);

end;
Le code effectue l'opération de formatage en utilisant le texte de la boîte de saisie EditFmtInt et la valeur du contrôle EditInt. Si la chaîne de format ne se trouve pas déjà dans la boîte liste (List box), elle y est ajoutée. Si par contre l'utilisateur clique sur un élément de la boîte liste, le code place cette valeur dans la boîte de saisie :
procedure TFormFmtTest.ListBoxIntClick(Sender: TObject);

begin

  EditFmtInt.Text := ListBoxInt.Items [

    ListBoxInt.ItemIndex];

end;

Conclusion

Les chaines constituent un type de données très courant. Bien que vous puissiez les utiliser dans de nombreux cas sans comprendre comment elles travaillent, ce chapitre devrait vous faire comprendre le comportement exect des chaînes et vous permettre d'utiliser toute la puissance de ce type de données.

Les chaînes sont manipulées en mémoire d'une façon dynamique spéciale, comme celle des tableaux dynamiques. Ce sera le sujet du chapitre suivant.

Chapitre suivant: La mémoire