Barninga Z
a- a+

I puntatori: le stringhe

PUNTATORI 

Le stringhe 

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. 

 

Ti potrebbe interessare anche

commenta la notizia

C'è 1 commento
Marcello
Ti è piaciuto l'articolo?