- Programmazione » Programmazione » Guida C - Manuale programmazione con articoli e risorse interessanti
I puntatori: le stringhe
PUNTATORI
Abbiamo anticipato che non esiste, in C, il tipo di dato "stringa". Queste sono gestite dal compilatore come sequenze di caratteri, cioè di dati di tipo char. Un metodo comunemente utilizzato per dichiarare e manipolare stringhe nei programmi è offerto proprio dai puntatori, come si vede nel programma dell'esempio seguente, che visualizza "Ciao Ciao!" e porta a capo il cursore.
#include
char *string = "Ciao";
void main(void)
{
printf(string);
printf(" %s!
" ,string);
}
La dichiarazione di string può apparire, a
prima vista, anomala. Si tratta infatti, a tutti gli effetti,
della dichiarazione di un puntatore e la stranezza consiste
nel fatto che a questo non è assegnato un indirizzo di
memoria, come ci si potrebbe aspettare, bensì una
costante stringa. Ma è proprio questo l'artificio
che consente di gestire le stringhe con normali puntatori a
carattere: il compilatore, in realtà, assegna a
string, puntatore a 16 bit, l'indirizzo della
costante "Ciao". Dunque la word occupata
da string non contiene la parola
"Ciao", ma i 16 bit che esprimono la parte
offset del suo indirizzo. A sua volta,
"Ciao" occupa 5 byte di memoria. Proprio
5, non si tratta di un errore di stampa: i 4 byte necessari a
memorizzare i 4 caratteri che compongono la parola,
più un byte, nel quale il compilatore memorizza il
valore binario 0, detto terminatore di stringa o
null terminator. In C, tutte le stringhe sono chiuse
da un null terminator, ed occupano perciò un byte in
più del numero di caratteri "stampabili" che
le compongono.
La prima chiamata a printf() passa quale argomento
proprio string: dunque la stringa parametro
indispensabile di printf() non deve essere
necessariamente una stringa di formato quando l'unica
cosa da visualizzare sia proprio una stringa. Lo è,
però, quando devono essere visualizzati caratteri o
numeri, o stringhe formattate in un modo particolare, come
avviene nella seconda chiamata.
Qui va sottolineato che per visualizzare una stringa con
printf() occore fornirne l'indirizzo, che nel
nostro caso è il contenuto del puntatore
string. Se string punta alla stringa
"Ciao", che cosa restituisce
l'espressione *string? La tentazione di
rispondere "Ciao" è forte, ma se
così fosse perché per visualizzare la parola
occorre passare a printf() string e non
*string? Il problema non si poneva con gli esempi
precedenti, perché tutti i puntatori esaminati
indirizzavano un unico dato di un certo tipo. Con le
dichiarazioni
float numero = 12.5;
float *numPtr = №
si definisce il puntatore numPtr e lo si inizializza in modo che contenga l'indirizzo della variabile numero, la quale, in fondo proprio come string, occupa più di un byte. In questo caso, però, i 4 byte di numero contengono un dato unitariamente considerato. In altre parole, nessuno dei 4 byte che la compongono ha significato in sé e per sé. Con riferimento a string, al contrario, ogni byte è un dato a sé stante, cioè un dato di tipo char: bisogna allora precisare che un puntatore indirizza sempre il primo byte di tutti quelli che compongono il tipo di dato considerato, se questi sono più d'uno. Se ne ricava che string contiene, in realtà, l'indirizzo del primo carattere di "Ciao", cioè la 'C'. Allora *string non può che restituire proprio quella, come si può facilmente verificare con la seguente chiamata a printf():
printf("%c è il primo carattere...
" ,*string);
Non dimentichiamo che le stringhe sono, per il compilatore C,
semplici sequenze di char: la stringa del nostro
esempio inizia con il char che si trova
all'indirizzo contenuto in string (la
'C') e termina con il primo byte nullo
incontrato ad un indirizzo uguale o superiore a quello (in
questo caso il byte che segue immediatamente la
'o').
Per accedere ai caratteri che seguono il primo è
sufficiente incrementare il puntatore o, comunque, sommare ad
esso una opportuna quantità (che rappresenta
l'offset, cioè lo spostamento, dall'inizo
della stringa stessa). Vediamo, come al solito, un
esempio:
int i = 0;
while(*(string+i) != 0) {
printf("%c
" ,*(string+i));
++i;
}
L'esempio si basa sull'aritmetica dei puntatori, cioè sulla possibilità di accedere ai dati memorizzati ad un certo offset rispetto ad un indirizzo sommandovi algebricamente numeri interi. Il ciclo visualizza la stringa "Ciao" in senso verticale. Infatti l'istruzione while (finalmente una "vera" istruzione C!) esegue le istruzioni comprese tra le parentesi graffe finché la condizione espressa tra le parentesi tonde è vera (se questa è falsa la prima volta, il ciclo non viene mai eseguito): in questo caso la printf() è eseguita finché il byte che si trova all'indirizzo contenuto in string aumentato di i unità è diverso da 0, cioè finché non viene incontrato il null terminator. La printf() visualizza il byte a quello stesso indirizzo e va a capo. Il valore di i è inizialmente 0, pertanto nella prima iterazione l'indirizzo espresso da string non è modificato, ma ad ogni loop i è incrementato di 1 (tale è il significato dell'operatore ++, pertanto ad ogni successiva iterazione l'espressione string+i restituisce l'indirizzo del byte successivo a quello appena visualizzato. Al termine, i contiene il valore 4, che è anche la lunghezza della stringa: questa è infatti convenzionalmente pari al numero dei caratteri stampabili che compongono la stringa stessa; il null terminator non viene considerato. In altre parole la lunghezza di una stringa è inferiore di 1 al numero di byte che essa occupa effettivamente in memoria. La lunghezza di una stringa può quindi essere calcolata così:
unsigned i = 0;
while(*(string+i))
++i;
La condizione tra parentesi è implicita: non viene
specificato alcun confronto. In casi come questo il
compilatore assume che il confronto vada effettuato con il
valore 0, che è proprio quel che fa al nostro
caso. Inoltre, dato che il ciclo si compone di una sola riga
(l'autoincremento di i), le graffe non sono
necessarie (ma potrebbero essere utilizzate
ugualmente[16]).
Tutta questa chiacchierata dovrebbe avere reso evidente una
cosa: quando ad una funzione viene passata una costante
stringa, come in
printf("Ciao!
");
il compilatore, astutamente, memorizza la costante da qualche
parte (non preoccupiamoci del "dove" , per il
momento) e ne passa l'indirizzo.
Inoltre, il metodo visto poco fa per "prelevare"
uno ad uno i caratteri che compongono una stringa vale anche
nel caso li si voglia modificare:
char *string = "Rosso
";
void main(void)
{
printf(string);
*(string+3) = 'p';
printf(string);
}
Il programma dell'esempio visualizza dapprima la parola
"Rosso" e poi "Rospo".
Si noti che il valore di string non è mutato:
esso continua a puntare alla medesima locazione di memoria,
ma è mutato il contenuto del byte che si trova ad un
offset di 3 rispetto a quell'indirizzo. Dal momento che
l'indirezione di un puntatore a carattere restituisce un
carattere, nell'assegnazione della lettera
'p' è necessario esprimere
quest'ultima come un char, e pertanto tra apici
(e non tra virgolette). La variabile string non a
caso è dichiarata all'esterno di
main().
E' possibile troncare una stringa? Sì, basta
inserire un NULL dove occorre:
*(string+2) = NULL;
A questo punto una chiamata a printf() visualizzerebbe la parola "Ro". NULL è una costante manifesta definita in STDIO.H, e rappresenta lo zero binario; infatti la riga di codice precedente potrebbe essere scritta così:
*(string+2) = 0;
E' possibile allungare una stringa? Sì, basta...
essere sicuri di avere spazio a disposizione. Se si
sovrascrive il NULL con un carattere, la stringa si
allunga sino al successivo NULL. Occorre fare alcune
considerazioni: in primo luogo, tale operazione ha senso, di
solito, solo nel caso di concatenamento di stringhe (quando
cioè si desidera accodare una stringa ad un'altra
per produrne una sola, più lunga). In secondo luogo,
se i byte successivi al NULL sono occupati da altri
dati, questi vengono perduti, sovrascritti dai caratteri
concatenati alla stringa: l'effetto può essere
disastroso. In effetti esiste una funzione di libreria
concepita appositamente per concatenare le stringhe: la
strcat(), che richiede due stringhe quali parametri.
L'azione da essa svolta consiste nel copiare i byte che
compongono la seconda stringa, NULL terminale
compreso, in coda alla prima stringa, sovrascrivendone il
NULL terminale.
In una dichiarazione come quella di string, il
compilatore riserva alla stringa lo spazio strettamente
necessario a contenere i caratteri che la compongono,
più il NULL. E' evidente che concatenare
a string un'altra stringa sarebbe un grave
errore (peraltro non segnalato dal compilatore, perché
esso lascia il programmatore libero di gestire la memoria
come crede: se sbaglia, peggio per lui). Allora, per potere
concatenare due stringhe senza pericoli occorre riservare in
anticipo lo spazio necessario a contenere la prima stringa e
la seconda... una in fila all'altra. Affronteremo il
problema parlando di array e di allocazione dinamica della
memoria.
Avvertenza: una dichiarazione del tipo:
char *sPtr;
riserva in memoria lo spazio sufficiente a memorizzare il
puntatore alla stringa, e non una (ipotetica) stringa. I byte
allocati sono 2 se il puntatore è, come
nell'esempio, near; mentre sono 4 se è
far o huge. In ogni caso va ricordato che
prima di copiare una stringa a quell'indirizzo bisogna
assolutamente allocare lo spazio necessario a contenerla e
assegnarne l'indirizzo a sPtr. Anche a questo
proposito occorre rimandare gli approfondimenti alle pagine
in cui esamineremo l'allocazione dinamica della
memoria.
E' meglio sottolineare che le librerie standard del C
comprendono un gran numero di funzioni (dichiarate in
STRING.H) per la manipolazione delle stringhe, che
effettuano le più svariate operazioni: copiare
stringhe o parte di esse (strcpy(),
strncpy()), concatenare stringhe (strcat(),
strncat()), confrontare stringhe (strcmp(),
stricmp()), ricercare sottostringhe o caratteri
all'interno di stringhe (strstr(),
strchr(), strtok())... insomma, quando si
deve trafficare con le stringhe vale la pena di consultare il
manuale delle librerie e cercare tra le funzioni il cui nome
inizia con "str": forse la soluzione al
problema è già pronta.