// ex7_2_1.cpp #include <iostream.h> void main() { int* n = new int; double* x = new double; *n = 7; *x = 2.71; cout << "n = " << *n << "\n"; cout << "x = " << *x << "\n"; }
output:
n = 7
x = 2.71
Le variabili allocate dinamicamente seguono le medesime regole di quelle automatiche, con l'unica importante differenza che esse non sono identificate da un nome, ma devono essere riferite da un puntatore definito al momento della loro creazione. Si veda il seguente:
// ex7_2_2 #include <iostream.h> void main() { int* a = new int; *a = 7; cout << "a = " << *a << "\n"; a = new int; // attenzione: la variabile intera il cui // valore e' 7 NON e` piu` accessibile *a = 5; cout << "a = " << *a << "\n"; }
output:
a = 7
a = 5
Si faccia attenzione all'esempio precedente: inizialmente creiamo una
variabile nello heap e assegnamo ad essa il valore 7
; successivamente
ad a
, che è un puntatore, viene assegnato l'indirizzo di una
nuova variabile dinamica, che viene assegnata con il valore
5
. In questa maniera abbiamo ``perso'' la prima variabile, non potremo
cioè mai più accederla o utilizzarla in nessuna maniera, né
distruggerla, per il semplice fatto che non sappiamo come
``chiamarla''. Abbiamo creato quello che è detto in gergo garbage (=
``spazzatura''): memoria allocata da un programma che non è più
utilizzabile e, tuttavia, resta ad occupare spazio nella memoria
centrale. Ecco il motivo per il quale bisogna stare bene attenti a non perdere
il puntatore ad un oggetto allocato nello heap. Vediamo un altro esempio di
creazione di garbage, molto più insidioso del precedente:
// ex7_2_3 #include <iostream.h> void funzione (int* p) { p = new int; *p = 19; } void main() { int* a; funzione (a); cout << "a = " << *a << "\n"; }
Vediamo cosa sembra che il programma esegua: viene creato un puntatore
ad intero, il quale viene passato a funzione
; all'interno di essa
tramite tale puntatore viene allocata una variabile intera nello heap, che
viene assegnata con il valore 19
; infine si stampa il valore di tale
variabile. Dov'è l'errore? Nel fatto che il puntatore p
locale a
funzione
è una copia del puntatore a
della main
:
esso (il puntatore) viene distrutto alla fine della funzione, mentre la
memoria da esso puntata resta allocata, diventando garbage. Affinché il
programma funzioni correttamente dobbiamo invece passare il puntatore a
per riferimento:
// ex7_2_4 #include <iostream.h> void funzione (int*& p) { p = new int; *p = 19; } void main() { int* a; funzione (a); cout << "a = " << *a << "\n"; }
Fino ad ora abbiamo sempre allocato memoria (tramite l'operatore new) e successivamente assegnato alla neonata variabile un valore; è possibile effettuare queste due operazione con un solo statement, cioè è possibile inizializzare anche le variabili dinamiche:
tipo* nome_variabile = new tipo
(valore_iniziale)
|
// ex7_2_5 #include <iostream.h> // passaggio per indirizzo int dimezza (int* n) { return *n / 2; } // passaggio per valore double dimezza (double x) { return x / 2; } void main() { int* n = new int(7); double* x = new double(5e-5); cout << "n = " << *n << "\n"; cout << "x = " << *x << "\n"; cout << "n/2 = " << dimezza(n) << "\n"; cout << "x/2 = " << dimezza(*x) << "\n"; }
output:
n = 7
x = 5e-05
n/2 = 3
x/2 = 2.5e-05
In questo esempio abbiamo due funzioni, le quali mostrano due modalità di
passaggio delle variabili dinamiche: per valore e per indirizzo; la differenza
con le variabili automatiche è che nel passare per valore una variabile
bisogna utilizzare l'operatore *
, come nella seconda funzione
dimezza
.
Le variabili dinamiche, come quelle automatiche, vengono interamente distrutte al termine del programma. Tuttavia le variabili dinamiche possono essere a piacere deallocate in qualunque momento tramite l'operatore delete; esso non ha nessun valore di ritorno e ha il compito di segnare come libera la memoria puntata dall'argomento fornitogli, che deve dunque essere un puntatore. Vediamo un esempio.
// ex7_2_6 #include <iostream.h> void main() { int* n = new int(7); cout << "n = " << *n; // stampa: `n = 7' delete n; // dealloca la memoria puntata da n // cout << "n = " << *n; // NO: *n e` stata deallocata }
Se nell'esempio precedente non avessimo commentato l'ultimo statement, il
programma non potrebbe essere correttamente eseguito: con lo statement
delete n
la memoria puntata da n
viene segnata dal compilatore
come libera, e pertanto può essere utilizzata da altri programmi. A questo
punto il puntatore n
diventa un floating pointer (= ``puntatore
fluttuante''): la memoria da esso puntata contiene dati potenzialmente privi
di significato e diventa pericoloso effettuare una nuova operazione di
deallocazione tramite l'operatore delete. Per evitare problemi con
puntatori di questo tipo è conveniente mettere a zero un puntatore dopo che
la memoria da esso puntata viene deallocata, come vediamo nel seguente
// ex7_2_7 #include <iostream.h> void main() { int* a = new int(8); cout << "indirizzo di a = " << a << "\n"; delete a; // ora l'indirizzo di a rimane invariato ma // la memoria e` stata liberata: // a e` un floating pointer // delete a; // operazione pericolosa a = 0; // cosi` mettiamo a zero il puntatore a cout << "indirizzo di a = " << a << "\n"; delete a; // operazione sicura cout << "indirizzo di a = " << a << "\n"; }
esempio di output:
indirizzo di a = 0x8049dc8
indirizzo di a = (nil)
indirizzo di a = (nil)
Come si vede, effettuando un delete su di un puntatore nullo non si ha nessun effetto, per cui tale operazione è sicura; perché non è altrettanto sicuro distruggere un puntatore fluttuante? Il motivo è il seguente: siccome la memoria che esso puntava è stata liberata, è possibile che un altro programma (o il sistema operativo stesso) la occupi per utilizzarla per i propri scopi; se noi chiamiamo delete sul nostro puntatore, il programma non può sapere che la memoria è attualmente impegnata da un altro programma e distrugge comunque il suo contenuto: tale operazione può quindi causare problemi ad altri programmi che vengono avviati contemporaneamente al nostro, o al sistema operativo.