Logo

Marco Cantù
Essential Pascal


Tradotto dall'inglese da: Paolo Rossi

Capitolo 7
La gestione delle stringhe

La gestione delle stringhe in Delphi e' abbastanza semplice, ma dietro le quinte la situazione e' abbastanza complessa. Il Pascal ha un modo tradizionale di gestire le stringhe, Windows ha il proprio, derivato dal linguaggio C, e le versioni a 32 bit di Delphi includono il potente tipo long string, che e' ora il tipo di default di Delphi.

Tipi di Stringhe

Nel Turbo Pascal di Borland e nella versione a 16 bit di Delphi, la tipica stringa e' una sequenza di caratteri con il byte di lunghezza all'inizio, che indica la dimensione della stringa. Siccome la lunghezza e' espressa da un solo byte, non puo' eccedere i 255 caratteri, un valore davvero basso che crea diversi problemi nella manipolazione di stringhe. Ogni stringa e' definita con una lunghezza fissa (che per default e' la massima, 255), anche se si possono dichiarare stringhe piu' corte per salvare memoria.

Un tipo stringa e' simile ad un tipo array. Di fatto, una stringa e' praticamente un array di caratteri. Questo e' dimostrato dal fatto che si puo' accedere ad uno specifico carattere della stringa usando la notazione [].

Per superare i limiti delle stringhe tradizionali del Pascal, le versioni a 32 bit di Delphi supportano le long string. Attualmente ci sono tre tipi di stringhe:

Usare le Long Strings

Se si usa semplicemente il tipo stringa, si ottiene il tipo short string o ANSI string, dipendente dal valore della direttiva $H del compilatore. $H+ (il default) indica le long string (il tipo ANSIString), che e' quello usato dai componenti della VCL.

Le long string di Delphi sono basate sul meccanismo del reference-counting, che tengono traccia di quante variabili stringa stanno referenziando la stessa stringa in memoria. Questo reference-counting e' anche usato per liberare memoria quando una stringa non e' piu' usata, quando il reference-count raggiunge zero.

Se si vuole incrementare la dimensione di una stringa in memoria ma c'e' qualcosa d'altro nella memoria adiacente, allora la stringa non puo' aumentare nella stessa locazione di memoria, e una completa copia della stringa deve percio' essere fatta in un'altra locazione. Quando capita questa situazione, il supporto run-time di Delphi rialloca la stringa in modo completamente trasparente. Semplicemente regolare la dimensione della stringa con la procedura SetLength procedure, allocando efficacemente la quantita' di memoria richiesta:

SetLength (String1, 200);

La procedura SetLength esegue la richiesta di memoria, non una vera allocazione di memoria. Essa riserva la memoria richiesta per un uso futuro, senza realmente usare la memoria. Questa tecnica e' basata su una caratteristica del sistema operativo Windows ed e' usata da Delphi per tutte le allocazioni dinamiche di memoria. Ad esempio, quando si richiede un array di notevoli dimensioni, la memoria e' riservata ma non allocata.

Impostare la lunghezza di una stringa raramente necessario. Il solo caso nel quale si deve allocare memoria per una long string usando la procedura SetLength e' quando bisogna passare la stringa come un parametro ad una funzione API (dopo un opportuno typecast), come mostrato piu' avanti.

Un'occhiata alle Stringhe in Memoria

Per aiutare a comprendere meglio i dettagli della gestione della memoria per le stringhe, ho scritto un semplice esempio, StrRef. In questo programma dichiaro due stringhe globali: Str1 e Str2. Quando il primo dei due bottoni e' premuto, il programma assegna una stringa costante alla prima delle due variabili e assegna la seconda variabile alla prima:

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

Oltre a lavorare sulle stringhe, il programma mostra il suo stato interno in una list box, usando la seguente funzione StringStatus:

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;

Nella funzione StringStatus e' vitale passare il parametro stringa come parametro costante. Passare questo parametro come copia causa l'effetto collaterale di avere un riferimento extra alla stringa mentre la funzione e' eseguita. Al contrario, passare il parametro come riferimento (var) o costante (const) non porta ad un ulteriore riferimento alla stringa. In questo caso ho usato un parametro const, siccome la funzione in oggetto non modifica la stringa.

Per ottenere l'indirizzo di memoria della stringa (utile per determinare la sua vera identita' e per vedere quando due stringhe differenti puntano alla stessa area di memoria), ho semplicemente effettuato un typecast dal tipo stringa al tipo intero. Le stringhe sono riferimenti, in pratica sono puntatori: Il loro valore contiene la reale locazione di memoria della stringa.

Per estrarre il reference count, ho basato il codice sul fatto poco conosciuto che la lunghezza e il reference count sono memorizzati nella stringa, prima del testo reale e prima della posizione a cui punta la variabile stringa. L'offset (negativo) e' -4 per la lunghezza della stringa (un valore che si puo' recuperare piu' facilmente usando la funzione Length!) e -8 per il reference count.

Da ricordare che questa informazione interna riguardo l'offset puo' cambiare nelle future versioni di Delphi, non c'e' nemmeno sicurezza che simili funzionalita' non documentate saranno mantenute in futuro.

Eseguendo questo esempio, si possono ottenere due stringhe con lo stesso contenuto, la stessa locazione di memoria e un reference count di 2, come mostrato nella parte superiore della list box in Figura 2.1. Adesso se si cambia il valore di una delle due stringhe (non importa quale), la locazione di memoria della stringa modificata cambiera'. Questo e' l'effetto della tecnica copy-on-write.

Figura 7.1: L'esempio StrRef mostra lo stato interno di due stringhe, incluso il reference-count corrente.

Si puo' produrre questo effetto, mostrato nella seconda parte della list box di Figura 2.1, scrivendo il seguente codice per il gestore dell'evento OnClick del secondo pulsante:

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;

Notare che il codice del metodo BtnChangeClick puo' essere eseguito solamente dopo il metodo BtnAssignClick. Per far rispettare cio', il programma parte con il secondo pulsante disabilitato (la sua proprieta' Enabled e' messa a False), il programma abilita il pulsante alla fine del primo metodo. Si puo' liberamente estendere questo esempio e usare la funzione StringStatus per esplorare il comportamento delle long string in diverse altre circostanze.

Le Stringhe in Delphi e i Windows PChars

Un altro importante punto in favore dell'uso delle long string e' che sono null-terminated. Questo vuol dire che sono completamente compatibili con le stringhe null-terminated del linguaggio C usate da Windows. Una stringa null-terminated e' una sequenza di caratteri seguita da un byte che e' messo a zero (o null). Questo puo' essere espresso in Delphi usando un array di caratteri zero-based, il tipo di dato usato per implementare le stringhe nel linguaggio C. Questo e' il motivo per cui gli array null-terminated sono cosi' comuni nelle funzioni API di Windows (che e' basato sul linguaggio C). Siccome le long string Pascal sono completamente compatibili con le stringhe null-terminated del C, si possono tranquillamente usare le long string e convertirle (cast) in PChar quando bisogna passarle a funzioni Windows API.

Ad esempio, per copiare la Caption di un form in una stringa PChar (usando la funzione API GetWindowsText) e poi copiarla nella Caption del pulsante, si puo' scrivere il codice seguente:

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

Si puo' trovare questo codice nell'esempio LongStr. Notare che se si scrive questo codice ma non si riesce ad allocare memoria per la stringa con SetLength, il programma probabilmente andra' in crash. Se si sta usando un PChar per passare un valore (e non per riceverne uno come nel codice sopra), il codice sara' anche piu' semplice, visto che non c'e' necessita' di definire una stringa temporanea ed inizializzarla. Il codice seguente passa la proprieta' Caption di una label come parametro ad una funzione API, semplicemente facendo il typecasting ad un PChar:

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

Quando si desidera convertire una WideString ad un tipo compatibile con le funzioni di Windows, bisogna usare il tipo PWideChar invece di PChar. Le WideString sono spesso usate per la programmazione OLE e COM.

Dopo aver mostrato il lato positivo, adesso ci si focalizza sulle trappole. Ci sono alcuni problemi che possono presentarsi quando si converte una long string in un PChar. Essenzialmente, il problema fondamentale e' che dopo questa conversione, si e' responsabili per la stringa ed il suo contenuto mentre Delphi non puo' essere di nessun aiuto. Esaminare il seguente piccolo cambiamento al codice mostrato sopra, ButtonClick:

procedure TForm1.Button2Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := S1 + ' is the title'; // this won't work
  Button1.Caption := S1;
end;

Il programma viene compilato, ma quando si esegue, ecco una sorpresa: La caption del pulsante avra' il testo del titolo della finestra, senza il testo della costante aggiunto. Il problema e' che quando Windows scrive sulla stringa (all'interno della chiamata GetWindowText), non setta correttamente la lunghezza della long string Pascal. Delphi puo' ancora usare questa stringa per l'output e puo' calcolare quando finisce cercando il carattere di terminazione (null), ma se si aggiungono ulteriori caratteri dopo il null terminator, saranno igorati completamente.

Come fissare questo problema? La soluzione e' di dire al sistema di convertire la stringa restituita dalla funzione API GetWindowText in una stringa Pascal. Tuttavia, se si scrive il seguente codice:

S1 := String (S1);

il sistema lo ignorera', siccome convertire un tipo di dato in se stesso e' un'operazione senza senso. Per ottenere l'opportuna long string, bisogna riconvertire la stringa in un PChar e lasciare che Delphi la converta ancora in una stringa:

S1 := String (PChar (S1));

In effetti, si puo' saltare la conversione in stringa, perche' la conversione PChar -> stringa e' automatica in Delphi. Questo e' il codice finale:

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

Un alternativa e' resettare la lunghezza della stringa Delphi, usando la lunghezza della stringa PChar, scrivendo:

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

Si possono trovare tre versioni di questo codice nell'esempio LongStr. Tuttavia, se serve solo accedere al titolo di un form, si puo' semplicemente usare la proprieta' Caption dell'oggetto form stesso. Non c'e' bisogno di scrivere tutto questo codice, che serve solo allo scopo di mostrare i problemi della conversione tra stringhe. Ci sono casi concreti in cui bisogna chiamare funzioni API e considerare questa complessa situazione.

Formattare le stringhe

Usando l'operatore piu' (+) e alcune funzioni di conversione (ad esempio IntToStr) si possono effettivamente costruire stringhe complesse partendo dai valori esistenti. Tuttavia, c'e' un differente modo per formattare numeri, valute e altre stringhe in una stringa finale. Si puo' usare la potente funzione Format o una delle funzioni correlate.

La funzione Format richiede come parametri una stringa con il testo di base, diversi marcatori (di solito preceduti dal simbolo %) e un array di valori, uno per ogni marcatore. Ad esempio, per formattare due numeri in una stringa si puo' scrivere:

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

dove n1 e n2 sono due valori interi. Il primo marcatore e' sostituito con il primo valore, il secondo dal secondo valore e cosi' via. Se il tipo del marcatore (indicato dalla lettera dopo il simbolo %) non corrisponde al tipo del parametro, viene generato un errore run-time. Non avendo un controllo dei tipi in compilazione e' il maggior svantaggio nell'usare la funzione Format.

La funzione Format usa un parametro open-array (un parametro, cioe', che puo' avere un numero arbitrario di valori), che discutero' piu' avanti in questo capitolo. Per il momento, comunque, notare che solo la sintassi simil-array della lista di valori passata come secondo parametro.

Oltre a %d, si possono usare uno dei diversi marcatori definiti da questa funzione e sommariamente elencati nella Tabella 7.1. Questi marcatori forniscono un output di default per il tipo corrispondente. Tuttavia, si possono usare ulteriori specificatori del formato per modificare l'output di dafault. Lo specificatore width, ad esempio, determina un numero fisso di caratteri in output, mentre lo specificatore di precisione indica il numero di decimali dopo la virgola. Ad esempio,

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

converte il numero n1 in una stringa ad otto caratteri, allineata a destra (usare il simbolo meno (-) per specificare l'allineamento a sinistra) e riempita con spazi.

Tabella 7.1: Specificatori di tipo per la funzione Format

Specificatore di tipo Descrizione
d (decimale) Il corrispondente valore intero e' convertito in una stringa di numeri decimali
x (esadecimale) Il corrispondente valore intero e' convertito in una stringa di numeri esedecimali.
p (puntatore) Il corrispondente valore puntatore e' convertito in una stringa espressa con numeri esedecimali.
s (stringa) Il corrispondente valore stringa, carattere, o PChar e' copiato nella stringa di oputput.
e (esponenziale) Il corrispondente valore floating-point e' convertito in una stringa basata sulla notazione esponenziale.
f (floating-point) Il corrispondente valore floating-point e' convertito in una stringa basata sulla notazione floating point.
g (generale) Il corrispondente valore floating-point e' convertito nella stringa piu' piccola possibile usando la notazione esponenziale o floating point.
n (numero) Il corrispondente valore floating-point e' convertito in una stringa usando i separatori di migliaia.
m (valuta) Il corrispondente valore floating-point e' convertito in una stringa basata sulla notazione in valuta. La conversione e' basata sui settaggi internazionali di Windows (guardare l'help di Delphi alla voce Currency and date/time variables).

Il miglior modo di vedere esempi su queste conversioni e' di sperimentare personalmente le stringhe di formato. Per rendere questo piu' facile ho scritto il programma FmtTest, il quale permette all'utente di inserire le stringhe di formattazione per interi e floating-point. Come si puo' vedere in Figura 7.2, questo programma mostra un form suddiviso in due parti. La parte di destra e' per i numeri floating-point.

Ogni parte ha un primo edit box con il valore numerico che si vuole formattare in una stringa. Sotto al primo edit box c'e' un pulsante per eseguire la formattazione e mostrare il risultato in un message box. Apparrira' quindi un altro edit box, dove si puo' scrivere una stringa di formattazione. Come alternativa si puo' semplicemente cliccare su una delle linee del compoennte ListBox per selezionare una stringa di formattazione predefinita. Ogni volta che si scrive una nuova stringa di formattzione valida, essa e' aggiunta al ListBox (notare che chiudendo il programma verranno perse questi nuovi elementi).

Figura 7.2: L'output di un valore floating-point dal programma FmtTest.

Il codice di questo esempio usa semplicemente il testo di vari controlli per produrre il suo output. Questo e' uno dei tre metodi connessi ai pulsanti Show:

procedure TFormFmtTest.BtnIntClick(Sender: TObject);
begin
  ShowMessage (Format (EditFmtInt.Text,
    [StrToInt (EditInt.Text)]));
  // if the item is not there, add it
  if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then
    ListBoxInt.Items.Add (EditFmtInt.Text);
end;

Il codice semplicemente esegue l'operazione di formattazione usando il testo dell'edit box EditFmtInt e il valore del controllo EditInt. Se la stringa di formattazione non e' gia' nel List Box, essa viene aggiunta. Se l'utente invece seleziona un elemento del List Box, il codice sposta questo valore nell'Edit Box:

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

Conclusioni

Le stringhe sono certamente un tipo di dati molto comune. Anche se si possono usare in molti casi senza capire completamente come lavorano, questo capitolo ha mostrato l'esatto comportamento delle stringhe, rendendo disponibile la potenza di questo tipo di dato.

Le stringhe sono gestite in memoria in modo dinamico e particolare, come succede con gli array dinamici. Questo e' l'argomento del prossimo capitolo.

Prossimo Capitolo: La Memoria

© Copyright Marco Cantù, Wintech Italia Srl 1995-2000