coordinamento di Francesco Sileno
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.