Ciao a tutti, eccoci giunti alla quattordicesima lezione del nostro corso completo di programmazione in C. Anche oggi parleremo dei puntatori e nello specifico approfondiremo alcuni aspetti legati alla gestione dinamica della memoria, prima di partire, però, facciamo un pò il punto di quello che abbiamo detto e visto fino ad ora sui puntatori:
- Sono un tipo di variabile
- Devono essere tipizzati (int, char etc.)
- Possono essere usati con l’operatore di dereferenziazione ( * ) per accedere al contenuto della cella di memoria puntata
- L’operatore di indirizzamento ( & ) restituisce l’indirizzo al quale una variabile è memorizzata, quindi può essere considerato il duale dell’operatore ” * “. (Cosa succede ad esempio scrivento ” *(&nome_variabile) ” ??
- Possono essere inseriti tra i parametri delle funzioni, riuscendo così a superare il problema del passaggio dati per valore.
- Sono alla base della creazione di strutture statiche come array, matrici e matrici multidimensionali.
- Con l’aritmetica dei puntatori possiamo modificare la cella di memoria puntata dal puntatore per muoverci agevolmente su di essa.
Ma se dovessi scegliere un solo motivo per il quale sono stati inventati i puntatori, sceglierei proprio quello di cui parleremo in questa lezione: l’allocazione dinamica della memoria.
Negli esempi che abbiamo scritto nella precedente lezione, quando abbiamo parlato di strutture dati come array e matrici, abbiamo detto che per dichiarare un array di 10 elementi (indicizzati da 0 a 9) bisogna scrivere:
int nomeArray[10];
in questo modo il compilatore riserverà ad ogni esecuzione del nostro programma 10 * (dimensione di un intero) byte in memoria per questa variabile e sarà accessibile tramite:
nomeArray[n]; //n compreso tra 0 e 9
Questo approccio ha il difetto di allocare sempre lo spazio per 10 interi in memoria, il che può andar bene se il valore “10” è fisso e indipendente dall’esecuzione, ma supponiamo che quel parametro dipenda dalle scelte dell’utente, come potremmo fare?
Supponiamo per esempio di voler realizzare un programma che calcoli alcuni valori statistici su un numero di campioni scelti dall’utente memorizzandoli in un array. (in questo esempio calcolo solo la somma)
#include
int somma(int *array, int size){
int i; //variabile utilizzata per i cicli.
int somma = 0; //variabile per memorizzare la somma.
for (i = 0; i < size; i++){
somma = somma + array[i]; //aggiungo alla somma parziale il valore dell'array in posizione i
}
return somma; //restituisco il valore della somma.
}
int main (int argc, const char * argv[]) {
int qt_numeri;
int numero;
int i;
int arrayDiNumeri[255]; //array dove memorizzare i numeri insiti dall'utente.
printf("Inserisci la quantià di numeri: ");
scanf("%d",&qt_numeri);
printf("Dovrai inserire %d numeri:",qt_numeri);
for (i = 0 ; i < qt_numeri ; i++ ) {
scanf("%d",&numero);
arrayDiNumeri[i] = numero;
}
printf("la somma è: %d", somma(arrayDiNumeri, qt_numeri));
return 0;
}
A questo punto del corso non dovreste avere grosse difficoltà a capire questo codice, ma come potete ben notare c’è un problema enorme: siamo costretti a dichiarare un array di dimensione 255 perché non sappiamo in anticipo quanti numeri l’utente deciderà di inserire… quindi adremo ad occupare 255 * (dimensione di un intero) ciascuna volta che l’utente eseguirà il programma…certo occupare circa 1000 byte (1 kb) può non sembrare un grosso spreco, vista la quantità in gigabyte di memoria che hanno a disposizione i nostri computer, ma per realizzare un programma efficiente bisogna essere come accaniti risparmiatori, intenti a rosicchiare byte dove è possibile! Ma c’è un errore ancora più grave, cosa succede se l’utente decide di inserire 256 numeri? all’interno del ciclo for tenteremmo di scrivere nell’allocazione arrayDiNumeri[255] ma l’ultima allocazione effettivamente utilizzabile è arrayDiNumeri[254] come si vede in questa immagine:
e questo è un errore gravissimo, perché non abbiamo idea di che cosa sia memorizzato in quella allocazione di memoria, magari stiamo sovrascrivendo un dato importante, infatti molto spesso un errore di questo tipo porta al crash dell’intero programma.
Sottolineo il termine molto spesso e non ho detto sempre! Come abbiamo avuto modo di notare più volte il linguaggio C suppone che il programmatore sappia quello che sta facendo, quindi si limita ad “eseguire gli ordini” senza fare troppi controlli, e purtroppo l’errore di scrivere in un’allocazione di memoria errata può alcune volte passare inosservata anche agli sguardi molto attenti. Per darvi un’idea anche una vecchia versione di SSH era afflitta da un errore simile, con le conseguenze del caso.
Torniamo al problema originario, come facciamo ad allocare un’array di dimensione variabile a seconda delle scelte dell’utente? In C esiste l’allocazione dinamica della memoria che permette proprio di fare quanto richiesto.
È d’obbligo fare una digressione su come viene gestita la memoria all’interno del computer, ma ci accontentiamo di sapere che viene divisa in due parti, lo stack dove vengono allocate le variabili statiche ovvero tutte quelle dichiarate fino ad adesso e l’heap dove vengono allocate le variabili allocate dinamicamente. La differenza sostanziale è che lo stack è gestito come una pila, tutte le variabili vengono messe una sull’altra, quando una funzione viene invocata nello stack vengono allocate tutte le variabili che appartengono a quella funzione, quando una funzione termina la parte di stack viene svuotata e così viene liberata la memoria. L’heap invece ha una funzionalità diversa, per ottenere dello spazio nell’heap bisogna utilizzare delle funzioni particolari (che vedremo tra un attimo) questo spazio, però, non viene rilasciato quando la variabile esce dallo scope, dovremmo invece rilasciarla esplicitamente.
La funzione principale da utilizzare per ottenere dello spazio nell’heap è la malloc, il cui prototipo è il seguente:
void *malloc(size_t size);
e la pagina di manuale recita:
The malloc() function allocates size bytes of memory and returns a pointer to the allocated memory.
La funzione è dichiarata dentro la libreria stdlib.h, quindi ricordiamoci di inserire la direttiva #include in cima ai nostri programmi se intendiamo utilizzarla.
Ma che tipo di parametro richiede? è la prima volta che incontriamo il tipo di dato size_t ma niente paura è semplicemente un tipo di dato come un altro e si usa quando ci si riferisce alla dimensione in byte di un oggetto in memoria. Difficilmente però ci troveremo a dichiarare delle variabili di tipo size_t, più probabilmente utilizzeremo la funzione sizeof() che restituisce proprio la dimensione dell’argomento espressa in size_t.
Con un esempio tutto risulterà più chiaro: dichiariamo un puntatore e facciamolo puntare ad un’allocazione di memoria dell’heap in grado di ospitare 10 interi
int *array;
array = malloc( 10 * sizeof(int) );
Dichiariamo un array nell’heap in grado di contenere 5 caratteri:
char *array;
array = malloc(5 * sizeof(char));
A questo punto le strutture possono essere utilizzare come dei normali array statici, accedento alle singole variabili tramite array[n]
Ci sono due cose da non dimenticare quando si ha a che fare con l’allocazione dinamica della memoria:
- Controllare il valore di ritorno della funzione malloc
- Ricordarsi di liberare esplicitamente la memoria allocata quando non più necesaria.
La funzione malloc restituisce NULL se non c’è sufficiente memoria disponibile per essere allocata, questo non succede molto spesso, ma in alcuni casi potrebbe accadere, quindi è buona norma assicurarsi sempre che il puntatore restituito dalla malloc sia diverso da NULL.
Per liberare lo spazio allocato tramite la funzione malloc bisogna richiamare la funzione free() passandole come parametro proprio il puntatore restituito dalla malloc. Da notare che per liberare lo spazio abbiamo bisogno del puntatore, quindi lo spazio va liberato mentre si è nello scope della variabile, usciti dallo scope si ha quello che si chiama “memory leak” (link) ovvero della memoria allocata, inutilizzabile e non più liberabile.
Vediamo quindi il precedente esempio come viene modificato utilizzando la gestione dinamica della memoria:
#include
#include
int somma(int *array, int size){
int i;
int somma = 0;
for (i = 0; i < size; i++){
somma = somma + array[i];
}
return somma;
}
int main (int argc, const char * argv[]) {
int qt_numeri;
int numero;
int i;
int *arrayDiNumeri; //dichiariamo come semplice puntatore
printf("Inserisci la quantià di numeri: ");
scanf("%d",&qt_numeri);
printf("Dovrai inserire %d numeri:",qt_numeri);
//alloco dinamicamente la memoria
arrayDiNumeri = malloc(qt_numeri * sizeof(int));
//verifico che il puntatore non sia null,
// la condizione è più spesso abbreviata in questo modo "if (! arrayDiNumeri)"
if (arrayDinumeri == NULL) {
//esco dal programma riportando un errore.
return -1;
}
for (i = 0 ; i < qt_numeri ; i++ ) {
scanf("%d",&numero);
arrayDiNumeri[i] = numero;
}
printf("la somma è: %d", somma(arrayDiNumeri, qt_numeri));
//libero la memoria allocata
free(arrayDiNumeri);
return 0;
}
Per tutti coloro che hanno già avuto a che fare con la programmazione in objective-c, vorrei far notare che la dichiarazione di un nuovo oggetto, ad esempio tramite l’istruzione
NSString *myName = [[NSString alloc] init];
praticamente è la verione “evoluta” della malloc che abbiamo appena visto, viene riservato infatti nell’heap lo spazio sufficiente a memorizzare un oggetto di tipo NSString. Addirittura per conformità con il linguaggio hanno mantenuto il simbolo ” * ” davanti al nome della variabile, proprio ad indicare che si tratta di un puntatore.
Una variante sul tema
Vorrei precisare che la funzione malloc non è l’unica in grado di allocare della memoria nell’heap, una valida alternativa è la funzione:
void * calloc(size_t count, size_t size);
La sintassi è molto simile, ma questa prevede due parametri separati, in pratica per dichiarare un array da 10 interi bisogna scrivere:
int *array;
array = calloc(10, sizeof(int));
Un’ulteriore differenza è che la calloc restituisce la zona di memoria già azzerata, mentre la malloc restituisce la memoria così com’è.
Esiste poi la funzione realloc() il cui prototipo è il seguente:
void *realloc(void *ptr, int dim_totale)
Il suo scopo è quello di modificare la dimensione di un blocco di memoria puntato da ptr per farlo diventare di dim_totale bytes.
Se dim_totale è 0, l’invocazione è equivalente ad una free(ptr). Naturalmente il valore di ptr deve provenire da una precedente invocazione di malloc() o calloc() o realloc()
Come ormai consueto vi lascio con qualche esercizio, alla prossima!!
Esercizi
- Scrivere una funzione che scambi il valore di due variabili, passate come parametro. (hint: utlizzare i puntatori come parametro non servono array)
- Scrivere una funzione che chieda all’utente quanti numeri inserire, quindi allochi dinamicamente un’array della dimensione corretta e prenda in input tutti i valori, al termine dell’inserimento calcoli la media dei valori inseriti.
- Scrivere una funzione che presi in input due valori ne restituisca un terzo lungo il doppio ottenuto dalla concatenazione dei due array in input.
- Scrivere una funzione che preso in inut un array lo restituisca in ordine inverso.
- Scrivere un programma che allochi dinamicamente lo spazio per una matrice 10×10.
Letture consigliate:
Il linguaggio C. Principi di programmazione e manuale di riferimento (Accademica)
Brian W. Kernighan – Dennis M. Ritchie
Editore: Pearson | Lingua: Italiano | Brossura: 313 pagine
Prezzo Listino: EUR 27,00
Prezzo Promozione: EUR 22,95 con Spedizione gratuita
C. Corso completo di programmazione
Paul J. Deitel – Harvey M. Deitel
Editore: Apogeo | Lingua: Italiano | Brossura: 640 pagine
Prezzo Listino: EUR 39,00
Prezzo Promozione: EUR 33,15 con Spedizione gratuita











12 Responses to “14. I Puntatori – La gestione dinamica della memoria – parte 3”
18 Maggio 2011
cikpisf (arrayDinumeri != NULL) {
//esco dal programma riportando un errore.
return -1;
}
C’è un’errore!
La condizione dell’if deve avere come operatore == e non !=…
Per il resto tutto ok!
19 Maggio 2011
ignaziocConfermo!! modifico subito l’articolo
24 Maggio 2011
andrea90rmsempre nella parte finale..hai scordato il contenuto degli include
30 Maggio 2011
peppenon ci abbandonare……………..
31 Maggio 2011
Ignaziocpurtroppo i caratteri “” sono un problema in html 🙁
31 Maggio 2011
Ignaziocnon vi ho abbandonato 🙂 sono sempre qui!
26 Giugno 2011
Lucaforse mi sono perso un pezzo ma si inizia a parlare di scope….che cosa vuol dire?
4 Ottobre 2011
NicolaHo capito che si potrebbe verificare un errore nel caso si dichiari un array di capacità più piccola di quella dichiarata come nel primo esempio.
Tale problema dovrebbe essere risolto utilizzando la memoria dinamica nell’heap…perchè?
Solo perchè vi è la possibilità di controllare tale errore con il NULL?
Grazie
Nic
4 Ottobre 2011
ignaziocnon ho capito bene il tuo dubbio. Un array statico avrà sempre la stessa dimensione, scelta nel momento in cui scrivi il programma. Alcune volte si ha la necessità di array la cui dimensione sia variabile nel tempo, per questo motivo si dichiarano “array dinamici” (che poi sono sempre puntatori) che vengono memorizzati nell’heap.
5 Ottobre 2011
NicolaPerchè posso dichiarare malloc(variabile_che _inserisce_utente * sizeof(int))?
14 Febbraio 2013
Corso di programmazione in C – I Puntatori – La gestione dinamica della memoria – parte 3 | devAPP | DiFuscoFrancesco.it[…] http://www.devapp.it/wordpress/14-i-puntatori-la-gestione-dinamica-della-memoria-parte-3.html […]
9 Gennaio 2014
abeNoooooooooooooooooooooooo…
non ci sono le listeeeeeeeeeeeeeee!!
E’ l’unico sito da cui capisco le cose.
LE LISTE! LE LISTE! LE LISTE!