Fondamenti di programmazione in C++, 2e - Capitolo 8

Page 1

Allocazione dinamica della memoria 8.1 8.2 8.3

Gestione dinamica della memoria L’operatore new L’operatore delete

8.4 8.5 8.6

8

F

Indice Esempi di new e delete Gestione dell’overflow della memoria Tipi di memoria in C++

INTRODUZIONE Abbiamo visto nei precedenti capitoli che le variabili possono essere globali o locali. Le globali occupano posizioni fisse di memoria all’interno del segmento dati assegnato al programma dal sistema operativo, e tutte le funzioni possono utilizzarle. Le variabili locali a una funzione vengono invece memorizzate nello stack ed esistono soltanto mentre è in esecuzione la funzione. È possibile inoltre creare variabili static , che occupano anch’esse posizioni fisse di memoria nel segmento dati, ma sono utilizzabili solo nel modulo (ovvero, nel file sorgente) o nella funzione in cui sono definite. Queste classi di variabili condividono una caratteristica, quella di

essere definite al momento della compilazione, e questo può essere anche un loro limite. Infatti non sempre è possibile sapere prima dell’esecuzione quanta memoria (cioè quante variabili) saranno necessarie al programma. In C è possibile assegnare dinamicamente memoria durante l’esecuzione del programma mediante le funzioni malloc() e free(). In questo capitolo vedremo che il C++ offre un metodo migliore e più efficiente. Invece che chiamare una funzione per assegnare o liberare memoria, C++ invoca due operatori: new() e delete(), che assegnano e liberano la memoria della zona denominata heap.

8.1 Gestione dinamica della memoria Con il termine heap si denota la parte della memoria principale che può essere allocata dinamicamente durante l’esecuzione del programma. Per poter essere riallocata, la memoria allocata a “run-time” può essere deallocata automaticamente mediante un garbage recollector (come avviene, per esempio, in Java) oppure rimanere occupata fino a che il programmatore la libera esplicitamente mediante opportune istruzioni. In C la gestione della memo-

08txt.indd 221

11/05/21 11:33


222 Parte I Fondamenti di programmazione

ria dinamica avviene tramite le funzioni malloc, calloc e free. In C++ esiste un’alternativa migliore, gli operatori new e delete. Con l’operatore new si definiscono variabili a run-time e con l’operatore delete si possono rilasciare le posizioni occupate dinamicamente perché possano eventualmente essere riutilizzate da altre variabili. Per esempio, consideriamo un programma per la valutazione degli studenti di un corso. I voti riportati dagli studenti devono essere memorizzati in un array sufficientemente grande da poterli contenere tutti. L’istruzione int corso [40];

riserva, per esempio, 40 interi. Questo perché supponiamo che gli studenti siano non più di 40, ma se il loro numero aumentasse, si dovrebbe cambiare la lunghezza dell’array e ricompilare il programma. In effetti, gli array sono la struttura dati più efficiente quando si conosce la loro lunghezza al momento di scrivere il programma, ma cosa succede se la dimensione del vettore può essere nota solo in fase di esecuzione? È abbastanza frequente non conoscere la quantità di memoria necessaria fino al momento dell’esecuzione del programma. Per esempio, se si vuole memorizzare una stringa di caratteri liberamente digitata dall’utente, non si può prevedere a priori la dimensione dell’array necessario per ospitarla, bisogna quindi preallocare un vettore molto grande con conseguente probabile spreco di memoria. Per risolvere questo problema si deve ricorrere ai puntatori e alle tecniche di allocazione dinamica della memoria. Il sistema operativo assegna al programma tre specifici segmenti della memoria principale: il code segment che ospita il codice stesso; il data segment che ospita costanti e variabili globali; lo stack che, a run-time, conterrà le variabili locali alle funzioni, i parametri delle funzioni e le copie dei registri per restituire il controllo alle funzioni chiamanti al termine delle funzioni chiamate. Quindi la memoria allocata in questo segmento cresce e decresce continuamente durante l’esecuzione del programma. La memoria che rimane nello stack, denominata heap, viene appunto usata per allocare dinamicamente variabili, e anche questa parte dello stack, lo heap, cresce e decresce dinamicamente, semplicemente lo fa dalla parte opposta. Importante

Il fatto che questo programma non venga compilato int dimensioneglobale; char vettore[dimensioneglobale]; // ERRORE! SPECIFICARE LA DIMENSIONE int main() { int dimensionelocale; char vettore[dimensionelocale];// OK! ALLOCATO NELLO STACK A RUN-TIME }

mostra che per allocare un vettore nel segmento dati bisogna conoscerne la dimensione già in fase di compilazione, mentre per allocarlo (a runtime) nello stack no.

08txt.indd 222

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 223

Memoria alta

Stack Tutta la memoria che rimane libera qui può essere allocata dinamicamente

SP

Heap Cresce verso l’alto con le chiamate di funzioni

SS

Dati non inizializzati

DS

Dati inizializzati

Code segment #n CS

... Code segment #2

Memoria bassa

Code segment #1

Figura 8.1 Mappa di memoria di un programma.

La Figura 8.1 mostra la generica mappa di memoria di un programma. L’heap è dentro lo stack e cresce verso l’alto, mentre lo stack cresce verso il basso. Gli operatori new e delete sono più evoluti e affidabili delle malloc() e free() del C perché allocano (o deallocano) memoria in funzione del tipo di dato da memorizzare ed effettuano ogni volta i relativi controlli. Inoltre new e delete si implementano come operatori e non come funzioni, e si possono utilizzare senza includere alcun header file. Un’altra loro caratteristica importante è che, essendo fortemente sovraccaricati, non richiedono casting di tipi e ciò li rende più facili da utilizzare che malloc() e free().

8.2 L’operatore new L’operatore new genera dinamicamente una variabile di un certo tipo assegnandole un blocco di memoria della dimensione di quel tipo. L’operatore restituisce poi l’indirizzo del blocco di memoria allocato, cioè della variabile, che verrà assegnata a un puntatore a quel tipo. La variabile dinamica sarà quindi accessibile dereferenziando il puntatore. La sintassi dell’operatore new è: tipo* puntatore = new tipo

dove puntatore sta per il nome del puntatore a cui si assegna l’indirizzo dell’oggetto generato. Per esempio: char* p = new char;

08txt.indd 223

11/05/21 11:33


224 Parte I Fondamenti di programmazione

Si può anche assegnare l’indirizzo di memoria di un blocco sufficientemente grande per contenere un array di elementi dello stesso tipo con questa sintassi: tipo* puntatore = new tipo[dimensione]

Per esempio: char* p = new char[100];

genera dinamicamente un vettore di cento char, l’indirizzo del primo dei quali viene assegnato al puntatore p. Si noti che l’operatore new può effettuare questo assegnamento proprio perché esso restituisce l’indirizzo del blocco di memoria allocata dinamicamente. Ogni volta che si invoca l’operatore new, il compilatore verifica che il tipo puntato dal puntatore (a sinistra) corrisponda al tipo della variabile allocata (a destra). Se i tipi non coincidono, il compilatore produce un messaggio d’errore. Il puntatore potrebbe essere già stato definito, per esempio si può scrivere anche: int* pi; ... pi = new int;

L’effetto è sempre quello di creare una variabile intera senza nome, accessibile solo dereferenziando il puntatore pi.

pi

variabile intera allocata a run-time nell’heap

Così per allocare dinamicamente un array di 100 interi si può anche scrivere: int* BloccoMem; BloccoMem = new int[100];

Se nello heap esiste un blocco libero della dimensione richiesta (400 byte in questo caso) new restituisce l’indirizzo del primo byte del blocco, altrimenti new restituisce 0 o NULL. La dimensione del blocco di memoria da allocare si può definire a runtime, per esempio richiedendolo dall’input: int n; cin >> n; char* s = new char[n];

Esempio 8.1 #include <cstring> // per strlen() e strcpy(,) int main() { char str[] = "Sierras di Cazorla, Segura e Magina";

08txt.indd 224

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 225 int lun = strlen(str); char* ptr = new char[lun+1]; // un byte in più per \0 strcpy(ptr, str); cout << "ptr = " << ptr; delete ptr; }

Esecuzione ptr = Sierras di Cazorla, Segura e Magina

Si può invocare l’operatore new per allocare un vettore, perfino se non si sa in anticipo quanta memoria richiedono i suoi elementi. Tutto quello che si deve fare è invocare l’operatore new utilizzando un puntatore all’array. Anche se in fase di compilazione non si può calcolare la quantità di memoria necessaria, new aspetta il momento dell’esecuzione, quando sarà nota la dimensione del tipo del vettore. Per esempio, questo segmento di codice assegna memoria per un array di dieci stringhe la cui lunghezza sarà nota solo al momento dell’esecuzione: miaStringa* mioTesto = new miaStringa[10];

Si può inizializzare la variabile dinamica indicandone il valore fra parentesi tonde alla fine dell’istruzione che invoca l’operatore new. Per esempio: int* pi = new int(1000);

equivale a: int* pi = new int; *pi = 1000;

Per allocare dinamicamente un array multidimensionale si indica ogni dimensione dell’array. Per esempio, per assegnare un puntatore a un array contenente i nomi dei dodici mesi si può allocare dinamicamente una matrice di caratteri di dimensione [12] per [10]: char* p = new char[12][10];

Quando si utilizza new per assegnare un array multidimensionale, solo la dimensione più a sinistra può essere una variabile. Ciascuna delle altre dimensioni deve essere un valore costante, esattamente come quando si definisce un parametro formale come array dimensionale.

8.3 L’operatore delete L’operatore delete libera la memoria allocata dinamicamente perché possa eventualmente essere riallocata mediante successive chiamate all’operatore new. La sintassi dell’operatore delete è: delete puntatore delete [] puntatore

08txt.indd 225

// non array // per array

11/05/21 11:33


226 Parte I Fondamenti di programmazione

Per esempio, lo spazio assegnato per le variabili dinamiche: int* ad = new int; char* adc = new char[100];

si può liberare con le istruzioni: delete ad; delete [] adc; delete rende riutilizzabile la memoria puntata, ma non cancella il puntatore

che può quindi essere riutilizzato, per esempio per puntare un'altra variabile successivamente allocata con new. Se una variabile allocata dinamicamente non serve più è bene liberare lo spazio che essa occupa nell'heap perché altrimenti questo potrebbe esaurirsi anzitempo; l'operazione viene detta garbage collection e in C/C++ è totalmente lasciata alla responsabilità del programmatore. È importante cioè che esista sempre una corrispondenza new-delete. Non si può utilizzare delete per liberare memoria occupata da variabili ordinarie. int* p = new int; delete p; // corretto delete p; // non corretto perché duplicato int num = 10; int* pn = # delete pn; // non corretto perché memoria non assegnata da new

Attenzione

Non si possono creare due puntatori allo stesso blocco di memoria.

8.4 Esempi di new e delete Si può allocare dinamicamente qualunque tipo di dato. L’operatore new non è costretto a predeterminare la dimensione dei vettori. Utilizzando una variabile si può dimensionare l’array a tempo d’esecuzione. I programmi seguenti mostrano come assegnare una quantità variabile di memoria, a seconda del bisogno. Esempio 8.2 #include <cstring> int main() { int lunghezza_stringa; char *p; cout << "Quanti caratteri si assegnano? "; cin >> lunghezza_stringa; p = new char[lunghezza_stringa]; strcpy(p, "Carchel tambien esta en Sierra Magina"); cout << p << endl;

08txt.indd 226

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 227 delete p; }

Esecuzione Quanti caratteri si assegnano? 12 Carchel tambien esta en Sierra Magina

Esempio 8.3 #include <cstring> int main() { char str[] = "Mi pueblo es Carchelejo en Jaen"; int lung = strlen(str); char* p = new char[lung+1]; strcpy (p, str); cout << "p = " << p << endl; delete [] p; return 0; }

Esecuzione p = Mi pueblo es Carchelejo en Jaen

L’esempio seguente alloca dinamicamente memoria per una struttura, riempie i suoi campi, visualizza e successivamente libera la memoria allocata. Esempio 8.4 struct scheda { int numero; char nome[30]; }; int main () { scheda* punt_scheda = cout << "Introduca il cin >> punt_scheda -> cout << "Introduca il cin >> punt_scheda -> cout << "Numero: " << cout << "\nNome: " << delete punt_scheda; }

new scheda; numero del cliente: " ; numero; nome: "; nome; punt_scheda -> numero; punt_scheda -> nome;

Esecuzione Introduca il numero del cliente: 12

08txt.indd 227

11/05/21 11:33


228 Parte I Fondamenti di programmazione

Introduca il nome: Aldo Numero: 12 Nome: Aldo

Il seguente programma mostra come si possa utilizzare l’operatore new per allocare dinamicamente un array. Esempio 8.5 int main() { int* data = new int[3]; data[0] = 15; data[1] = 8; data[2] = 1999; cout << "L'appuntamento è il giorno " << data[0] << "/ " << data[1] << "/ " << data[2]; delete [] data; return 0; }

Esecuzione L'appuntamento è il giorno 15/ 8/ 1999

Il programma seguente chiede all’utente di digitare la dimensione dell’array allocato dinamicamente. Esempio 8.6 int main() { cout << "Quanti elementi ha il vettore? "; int lun; cin >> lun; int* mioArray = new int[lun]; for (int n = 0; n < lun; n++) mioArray[n] = n + 1; for (int n = 0; n < lun; n++) cout << ' ' << mioArray[n]; delete [] mioArray; return 0; }

Esecuzione Quanti elementi ha il vettore? 27 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

Il seguente programma definisce un tipo di struttura cane, poi utilizza l’operatore new per allocare un array di tre strutture di tipo cane e ne assegna l’in-

08txt.indd 228

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 229

dirizzo al puntatore pcane. Il programma assegna valori ai campi di ogni elemento utilizzando la funzione di libreria strcpy() per copiare costanti stringa nei campi razza e colore della struttura cane. Da ultimo, il programma visualizza il contenuto dei tre elementi dell’array. Esempio 8.7 #include <cstring> struct cane { char razza[20]; int eta; int altezza; char colore[15]; }; int main() { cane* pcane = new cane[3]; strcpy(pcane[0].razza, "Pastore tedesco"); strcpy(pcane[0].colore, "Biondo"); pcane[0].eta = 4; pcane[0].altezza = 120; strcpy(pcane[1].razza, "dalmata"); strcpy(pcane[1].colore, "bianco e nero"); pcane[1].eta = 5; pcane[1].altezza = 130; strcpy(pcane[2].razza, "doberman"); strcpy(pcane[2].colore, "nero"); pcane[2].eta = 4; pcane[2].altezza = 155; for (int i = 0; i < 3; i++) { cout << "\nRazza: "<< pcane[i].razza << endl; cout << "Colore: "<< pcane[i].colore << endl; cout << "Altezza: "<< pcane[i].altezza << endl; cout << "Eta: "<< pcane[i].eta << endl; } }

Esecuzione

08txt.indd 229

Razza: Colore: Altezza: Eta:

Pastore tedesco Biondo 120 4

Razza: Colore: Altezza: Eta:

dalmata bianco e nero 130 5

11/05/21 11:33


230 Parte I Fondamenti di programmazione

Razza: Colore: Altezza: Eta:

doberman nero 155 4

8.5 Gestione dell’overflow della memoria Per far fronte alla mancanza di spazio nello heap si può utilizzare la funzione C++ set_new_handler() che serve per gestire gli errori. Questa funzione è definita nell’header file <new>, e ha come argomento un puntatore a funzione. Quando la si invoca le si passa il nome di una funzione scritta per gestire l’errore. Per ridurre la probabilità del riempimento dello heap si deve utilizzare accuratamente delete per liberare memoria assegnata dinamicamente non più utilizzata. Esempio 8.8 #include <new>

// set_new_handler

void overflow() { cout << "Memoria insufficiente - fermare esecuzione" << endl; exit(1); } int main() { set_new_handler(overflow); long dimensione; cout << "Dimensione dei blocchi da allocare? "; cin >> dimensione; for (int nblocco = 1; ; nblocco++) { int* pi = new int[dimensione]; cout << "Allocazione blocco numero: " << pi << endl; } }

Esecuzione Allocazione blocco numero: 0x7fed1235c010 .... Allocazione blocco numero: 0x7ff83f3bd010 Memoria insufficiente - fermare esecuzione

Il programma seguente assegna dinamicamente un array con dimensione richiesta dall’utente; accetta valori dallo standard input, li visualizza a schermo e ne calcola la media aritmetica.

08txt.indd 230

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 231

Esempio 8.9 int main() { int n, somma = 0; cout << "Introdurre numero di elementi: "; cin >> n; int* p = new int[n]; for (int i=0; i<n; i++) { cout << "Introduca elemento " << i << ' '; cin >> p[i]; somma += p[i]; } cout << "elementi introdotti: "; for (int i = 0; i < n; i++) cout << p[i] << " , " ; cout << endl; cout << "Totale: " << somma << endl; cout << "Media: " << (double) somma /n << endl; delete [] p; return 0; }

Esecuzione Introdurre numero di Introduca elemento 0 Introduca elemento 1 Introduca elemento 2 Introduca elemento 3 elementi introdotti: Totale: 233 Media: 58.25

elementi: 4 34 56 78 65 34 , 56 , 78 , 65 ,

8.6 Tipi di memoria in C++ In conclusione di questo capitolo ribadiamo le differenze fra i tre tipi di memoria del C++: automatica, statica e dinamica. Memoria automatica Le variabili definite all’interno di una funzione si denominano variabili automatiche: si allocano automaticamente nello stack quando viene invocata la funzione in cui sono definite e si cancellano quando essa termina. Tali variabili sono ovviamente locali alla funzione in cui sono definite, ma, di più, sono locali al blocco che le contiene, cioè alla più piccola sezione di codice rinchiusa tra parentesi graffe in cui sono definite.

08txt.indd 231

11/05/21 11:33


232 Parte I Fondamenti di programmazione

Memoria statica L’allocazione statica avviene nel data segment durante l’esecuzione di un programma completo. Ci sono due modi per allocare staticamente una variabile:

Soluzioni dei problemi sul sito web www.mheducation.it

1. definirla al di fuori da qualsiasi funzione, main() compresa; 2. anteporre alla sua definizione la parola riservata static. static double temperatura = 25.75;

Memoria dinamica Viene allocata a run-time nello heap dagli operatori new e delete. Non avendo C/C++ un garbage collector, come quelli di Java e Visual Basic, la memoria rimane allocata fino a che viene affrancata dall’esecuzione di un’opportuna istruzione di delete. L’operatore new compie una chiamata al sistema operativo chiedendo una certa quantità di memoria. Il sistema risponde verificandone la disponibilità. Negli odierni elaboratori è raro aver problemi di disponibilità di memoria, ma l’esaurimento dello heap è sempre possibile.

SOMMARIO

L’assegnamento dinamico della memoria permette di utilizzare quantità di memoria non note a priori. Abbiamo visto che in C++ si può allocare una variabile nell’heap quando se ne ha bisogno ed eliminarla quando non serve più mediante due operatori: new e delete. L’operatore new assegna un blocco di memoria della dimensione

del tipo specificato. Quando si termina di utilizzare un blocco di memoria definito dinamicamente con new, lo si può liberare con l’operatore delete perché possa essere riallocato ad altre variabili dinamiche. Per gestire l’eventualità dello svuotamento dello heap si può utilizzare la funzione di libreria set_new_handler().

CONCETTI CHIAVE

• • • •

Array dinamico Array statico Overflow di memoria Stile C (free)

• • • •

Stile C (malloc) Gestione dinamica Operatore delete Operatore new

Esercizi 8.1 Un programma contiene il seguente codice per creare un array dinamico: int* entrata = new int[10];

08txt.indd 232

Se non esiste sufficiente memoria nell’heap, la chiamata fallirà. Scrivere un codice che verifichi questa eventualità e visualizzi un relativo messaggio d’errore.

11/05/21 11:33


Capitolo 8 Allocazione dinamica della memoria 233

8.3 Con riferimento all’esercizio precedente, scrivere il codice per sommare gli elementi del vettore. 8.4 Data la seguente dichiarazione, definire un puntatore b alla struttura, riservare memoria dinamicamente per una struttura assegnando il suo indirizzo a b. struct bottone { char* cartellino;

08txt.indd 233

int codice; };

8.5 Una volta assegnata memoria al puntatore b dell’Esercizio 8.4 scrivere istruzioni per leggere i campi cartellino e codice. 8.6 Dichiarare una struttura per rappresentare un punto nello spazio tridimensionale con un nome. Dichiarare un puntatore alla struttura che abbia l’indirizzo di un array dinamico di n strutture punto. Assegnare memoria all’array e verificare che si è potuto assegnare la memoria richiesta.

Soluzioni dei problemi sul sito web www.mheducation.it

8.2 Con riferimento all’esercizio precedente, scrivere il codice per riempire questo array dinamico con 10 numeri letti in input.

11/05/21 11:33


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.