[Rubrica Programmazione]


coordinamento di Francesco Sileno





Allacciate le stringhe

Dopo aver introdotto nell'articolo precedente "Chi ha paura dei puntatori?" alcuni concetti sui puntatori, in queste righe cercherò di illustrarne due degli utilizzi più comuni: il passaggio dei parametri alle funzioni, e le operazioni sulle stringhe. Per rendere dilettevole la lettura, mi gioverò dell'aiuto di alcuni personaggi frutto della mia fantasia (ogni riferimento a persone è puramente casuale)

di Stefano Casini

Televisori...

Un giorno Eugenio, programmatore ingenuo, scoprì con sgomento il suo televisore megaschermo da 40 pollici che non dava più segni di vita. Con grande fatica se lo caricò sulle spalle, si fece quattro piani di scale a piedi (si era rotto anche l'ascensore), lo caricò sul tetto della macchina (nel portabagagli non c'entrava) ed andò a farlo riparare nel laboratorio specializzato. Il tecnico Alberto, programmatore esperto, vedendo entrare nel laboratorio quell'armadio, e volendosene liberare subito, armeggiò cinque minuti, dopodichè prese un fusibile da 3 Ampere (peso 2 grammi), lo sostituì a quello che si era bruciato ed il megatelevisore riprese vita. Mentre Eugenio si caricava nuovamente sulle spalle quel tubo catodico da 40 chili per riportarlo a casa, Alberto gli disse: "La prossima volta che hai bisogno di me, dammi il tuo indirizzo e verrò a riparartelo a casa, così eviterai la fatica di spostare avanti e indietro quella gigantesca struttura!".

...e puntatori.

Nelle parole di Alberto era nascosto un messaggio subliminale che, prendendo spunto da un episodio di vita comune, suggeriva ad Eugenio una delle regole fondamentali di buona programmazione in C: le strutture vanno passate alla funzione chiamata per indirizzo e non per valore, ovvero la funzione chiamante passerà alla funzione chiamata non la struttura intera, ma il puntatore a quella struttura. Quali sono i vantaggi di questo modo di agire? Nell'articolo precedente (Beta 1/96) ho spiegato come quell'essere gentile che risponde al nome di compilatore si occupa di gestire l'allocazione della memoria per le variabili che definiamo nel nostro codice: ma anche i parametri formali delle funzioni sono variabili, pertanto se una funzione ha come parametro una struttura, ogni qual volta viene chiamata, oltre alle variabili dichiarate all'interno il compilatore deve allocare spazio per la struttura.

typedef struct _tag

{

long fusibile;

int resistenze[1000];

char condensatori[1999];

}GRANDESTRUTTURA;

// GRANDESTRUTTURA è un tipo di dato da noi definito da 4000 byte

void FunzioneChiamante(void)

{

GRANDESTRUTTURA TV40pollici;

// definizione di una variabile di tipo GRANDESTRUTTURA

// di nome TV40pollici: il compilatore alloca 4000 byte

// in memoria e conserva l'indirizzo della locazione

long preventivo;

TV40pollici.fusibile = 0;

// rompiamo il fusibile

preventivo = FammiUnPreventivo(TV40pollici);

// portiamo il TV a riparare e chiediamo il preventivo

}


long FammiUnPreventivo(GRANDESTRUTTURA TVdiEugenio)

{

long ReturnValue;

// definizione di una variabile di tipo

// long di nome lReturnValue: il compilatore alloca 4 byte

// in memoria e conserva l'indirizzo della locazione

if(TVdiEugenio.fusibile == 0)

// il compilatore ha allocato lo spazio di memoria necessario // per contenere una variabile di tipo GRANDESTRUTTURA, e vi // ha copiato automaticamente dentro il contenuto della

// variabile presente nella funzione chiamante

ReturnValue = 5000;

else

ReturnValue = 50000;

return ReturnValue;

}

Come si vede, quando saremo dentro la funzione chiamata (FammiUnPreventivo) avremo allocato un sacco di memoria per il doppione della nostra struttura: questo non sarebbe il peggiore dei mali, ma guardate cosa succede se la funzione chiamata deve modificare i campi della struttura:

void FunzioneChiamante(void)

{

GRANDESTRUTTURA TV40pollici;

// definizione di una variabile di tipo GRANDESTRUTTURA

// di nome TV40pollici: il compilatore alloca 4000 byte

// in memoria e conserva l'indirizzo della locazione

long preventivo;

TV40pollici.fusibile = 0;

// rompiamo il fusibile

preventivo = FammiUnPreventivo(TV40pollici);

// portiamo il TV a riparare e chiediamo il preventivo

if(preventivo < 10000)

TV40Pollici = Riparalo(TV40Pollici);

// adesso vengono ricopiati nella variabile TV40Pollici tutti e // 4000 i byte ritornati dalla funzione Riparalo, quando

// basterebbe ricopiarne uno (il campo fusibile)

}

GRANDESTRUTTURA Riparalo(GRANDESTRUTTURA TVdiEugenio)

{

if(TVdiEugenio.fusibile == 0)

// il compilatore ha allocato lo spazio di memoria necessario // per contenere una variabile di tipo GRANDESTRUTTURA, e vi // ha copiato automaticamente dentro il contenuto della

// variabile presente nella funzione chiamante

TVdiEugenio.fusibile = 1;

// il fusibile è stato riparato

return TVdiEugenio;

// rimandiamo il pesante televisore ad Eugenio

}

Con un lampo di genio, Eugenio l'ingenuo farebbe notare: "Vabbé, metto una bella variabile globale ed ho risolto il problema!". Per motivi che ora è troppo oneroso spiegare (robustezza del codice, incapsulamento dei dati, etc.) è opportuno limitare al minimo l'uso delle variabili globali, soprattutto quelle extern, quando si vuole scrivere del buon C. Pertanto, tra la soluzione proposta da Eugenio:

GRANDESTRUTTURA TVdiEugenio;

void FunzioneChiamante(void)

{

long preventivo;

TVdiEugenio.fusibile = 0;

// rompiamo il fusibile

preventivo = FammiUnPreventivo();

// portiamo il TV a riparare e chiediamo il preventivo

if(preventivo < 10000)

Riparalo();

}

void Riparalo(void)

{

if(TVdiEugenio.fusibile == 0)

TVdiEugenio.fusibile = 1;

// il fusibile è stato riparato

return;

}

long FammiUnPreventivo(void)

{

long ReturnValue;

// definizione di una variabile di tipo

// long di nome lReturnValue: il compilatore alloca 4 byte

// in memoria e conserva l'indirizzo della locazione

if(TVdiEugenio.fusibile == 0)

ReturnValue = 5000;

else

ReturnValue = 50000;

return ReturnValue;

}

e la soluzione proposta da Alberto:

void FunzioneChiamante(void)

{

long preventivo;

GRANDESTRUTTURA TV40pollici;

TV40pollici.fusibile = 0;

// rompiamo il fusibile

preventivo = FammiUnPreventivo(&TV40pollici);

// portiamo il TV a riparare e chiediamo il preventivo

if(preventivo < 10000)

Riparalo(&TV40pollici);

}

void Riparalo(GRANDESTRUTTURA * pTVdiEugenio)

{

if(pTVdiEugenio->fusibile == 0)

pTVdiEugenio->fusibile = 1;

// il fusibile è stato riparato

return;

}

long FammiUnPreventivo(GRANDESTRUTTURA * pTVdiEugenio)

{

long ReturnValue;

// definizione di una variabile di tipo

// long di nome lReturnValue: il compilatore alloca 4 byte

// in memoria e conserva l'indirizzo della locazione

if(pTVdiEugenio->fusibile == 0)

ReturnValue = 5000;

else

ReturnValue = 50000;

return ReturnValue;

}

è senz'altro da preferire la seconda.

Mocassini e stringhe

Un'altra caratteristica che contraddistingue Eugenio è il suo amore per i mocassini: alcuni malignano che questo amore è dovuto al fatto che ha dei problemi con le stringhe, non impara ancora ad allacciarle. Qualcuno si domanderà: ciò si ripercuote sul suo stile di programmazione? Si ripercuote. In C non esiste un tipo di dato speciale per dichiarare una stringa: la stringa è semplicemente un array di caratteri, terminato da un carattere il cui valore è zero (null byte). Pertanto c'è questa sottile differenza: la stringa, o meglio le funzioni di manipolazione delle stringhe, se ne fregano della dimensione dell'array con cui la stringa è stata dichiarata, regolandosi solo in base al primo null byte che trovano mentre effettuano la scansione delle celle di memoria. In parole povere:

char Mocassino[8] = {'p', 'i', 's', 'o', 'l', 'i', 'n', 'o'};

// questo è un array di 8 caratteri

char Stringa[8] = {'p', 'i', 's', 't', 'o', 'l', 'a', '\0'};

// questa è una stringa di 8 caratteri

char Stivale[8] = {'P', 'I', 'S', 'A', '\0', 'i', 'n', 'o'};

// questo è un array di 8 caratteri con dentro la stringa PISA

Se proviamo a concatenare tra loro due di questi array, otteniamo i seguenti risultati:

concatenando Stringa e Stivale, otteniamo una nuova stringa di 11 caratteri, che necessiterà di un array di caratteri da 12 elementi (11 + il null byte);

concatenando Stringa o Stivale con Mocassino, otteniamo una nuova stringa di non si sa quanti caratteri, perchè non sappiamo, nella mappa di memoria, dove si trova il primo null byte dopo la fine di Mocassino, per cui non sappiamo neanche quanto dovrà essere grande l'array che deve contenere la concatenazione (il risultato pratico di un'operazione del genere è un bel messaggio di sistema che ci avverte di aver fatto operazioni poco lecite nella memoria).

Osserviamo che le funzioni di manipolazione delle stringhe vogliono come parametri i puntatori alle stringhe, e non le stringhe intere: questo perchè, analogamente a quanto illustrato nel precedente paragrafo per le strutture, il codice eseguibile risulta enormemente più veloce e "leggero" per lo stack. Eugenio, appena sente parlare di puntatori, inizia a sudare freddo ed a cambiare discorso, ma la cosa è meno terribile di quanto si pensi. Se ricordiamo che il nome dell'array rappresenta l'indirizzo in memoria dello stesso (vedi Beta 1/96), la notazione si semplifica notevolmente: non occorre usare né parentesi quadre [] né l'operatore di referenza &. Allora perché questa funzione mi blocca il programma, si chiede Eugenio?

void FunzioneSbagliata(void)

{

char Stringa[8] = {'p', 'i', 's', 't', 'o', 'l', 'a', '\0'};

// questa è una stringa di 8 caratteri

char Stivale[8] = {'P', 'I', 'S', 'A', '\0', 'i', 'n', 'o'};

// questo è un array di 8 caratteri con dentro la stringa PISA

strcat(Stringa, Stivale);

// concateniamo Stivale a Stringa

}

L'errore consiste nell'avere usato come contenitore di destinazione delle stringhe concatenate un array di dimensioni insufficienti: infatti la funzione di libreria strcat(string1, string2) appende i byte contenuti nell'indirizzo puntato da string2 a partire dal primo null byte dell'array puntato da string1 senza controllare che la dimensione di quest'ultimo sia in grado di accogliere i byte in più: il risultato è che la striga risultante dalla concatenazione è di 12 caratteri (11 + null byte) e va a "sporcare" 4 byte di memoria adiacenti agli 8 di Stringa. Se in questi 4 byte il compilatore non aveva allocato altre variabili possiamo anche passarla liscia (purtroppo), altrimenti comparirà il messaggio di violazione di memoria: il brutto del linguaggio C è che errori di questo genere in genere la passano liscia, salvo poi manifestarsi durante una demo di fronte al cliente o, peggio ancora, una volta che il software è stato rilasciato sul mercato (la legge di Murphy non è un'opinione). Come ovviare a questi errori? Dichiarando la stringa contenitore di dimensione sufficiente:

void FunzioneEsatta(void)

{

char Stringa[12] = {'p', 'i', 's', 't', 'o', 'l', 'a', '\0', 'e', 'ò', 'r', '3'};

// questa è un array di 12 caratteri con dentro una stringa di 7

char Stivale[8] = {'P', 'I', 'S', 'A', '\0', 'i', 'n', 'o'};

// questo è un array di 8 caratteri con dentro la stringa PISA

strcat(Stringa, Stivale);

// concateniamo Stivale a Stringa

}

Una soluzione dinamica

Ma non sempre si lavora con stringhe di dimensioni fisse: si pensi al caso in cui Stringa e Stivale siano delle stringhe prese a run-time da un input di tastiera, e si voglia farne la concatenazione; o si surdimensiona il contenitore, o si procede in maniera intelligente, come nell'esempio seguente:

void FunzioneDinamica(void)

{

char Stringa[] = "Questa è la prima stringa:";

char Stivale[] = "questa è la seconda stringa, che si concatena alla prima.";

// inizializziamo 2 stringhe

char * stringatotale;

stringatotale = ConcatenazioneIntelligente(Stringa, Stivale);

// concateniamo Stivale a Stringa

...

...

free(stringatotale);

// quando non ne abbiamo più bisogno, liberiamo la memoria

// allocata dinamicamente

}

char * ConcatenazioneIntelligente(char * string1, char * string2)

{

size_t totalsize;

char * dinamicmemory;

totalsize = strlen(string1) + strlen(string2) + 1;

// misuriamo la lunghezza delle stringhe ed aggiungiamo lo

// spazio per il null byte

dinamicmemory = malloc(totalsize);

// allochiamo dinamicamente la memoria di cui abbiamo bisogno // per concatenare le stringhe

if(dinamicmemory == NULL)

return NULL;

// controlliamo se l'allocazione dinamica di memoria ha avuto // successo

dinamicmemory[0] = '\0';

// mettiamo un null byte all'inizio della memoria allocata

// dinamicamente, così simuliamo una stringa di lunghezza zero

strcat(dinamicmemory, string1);

// concateniamo la prima stringa alla memoria dinamica

strcat(dinamicmemory, string2);

// concateniamo la seconda stringa alla memoria dinamica

return dinamicmemory;

// ritorniamo l'indirizzo della stringa contenitore

}

Conclusioni

invece di passare le strutture come parametri di funzione, è meglio passare gli indirizzi delle strutture;

la funzione chiamata lavorerà non sulla copia della struttura, ma sul contenuto della struttura stessa: aumenta la velocità e la "magrezza" dell'eseguibile;

poiché si lavora sull'originale, e non sulla copia, le modifiche fatte sulla struttura dalla funzione chiamata rimangono tali anche quando si ritorna alla funzione chiamante;

il passaggio dei parametri per indirizzo è usatissimo dalle funzioni di libreria che manipolano le stringhe;

la stringa non è un tipo di dato: è, per convenzione, un array di caratteri terminato da un null byte;

la dimensione di una stringa non dipende dalla dimensione dell'array di caratteri che la contiene, ma dalla posizione del primo null byte;

operazioni sulle stringhe, se non sono ben controllate, possono sporcare la memoria: gli effetti però si manifestano in modo aleatorio;

se non si conoscono a priori le dimensioni delle stringhe da manipolare, si può ricorrere all'allocazione dinamica di memoria per risolvere i problemi.

La possibilità di allocare dinamicamente la memoria è una delle caratteristiche che rende versatile e potente il linguaggio C ma richiede, per essere utilizzata, la conoscenza dell'uso dei puntatori: ne parleremo la prossima volta.

Copyright © 1996 Beta Working Group. Tutti i diritti riservati.