next up previous contents index
Next: Array dinamici Up: Memoria dinamica. Strutture. Liste Previous: Memoria dinamica   Indice   Indice analitico

Gli operatori new e delete

Ora che abbiamo capito cosa è la memoria dinamica di un programma e perché essa debba essere utilizzata, vediamo in pratica come il C++ la gestisce. Il meccanismo di gestione dello heap avviene tramite i due operatori new e delete, che corrispondono alla creazione (allocazione) e alla distruzione (deallocazione) di un oggetto, in particolare una variabile. Cominciamo con il primo di essi. La parola chiave new è un operatore, il quale ha come argomento il tipo di oggetto da allocare (quindi la sua grandezza in memoria) e come valore di ritorno un puntatore a tale zona di memoria7.2. Cosa vuol dire questo? Semplice: le variabili dinamiche non hanno un nome che le identifichi, bensì solo un puntatore alla zona di memoria da loro occupata; nel caso (non infrequente) che ``si perda'' tale puntatore, la variabile non è più utilizzabile né deallocabile: si faccia bene attenzione dunque a non cambiare il valore di un puntatore che funge da ``riferimento'' per una variabile dinamica. Vediamo un esempio:

// 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)
Ad esempio:

// 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.

ex-2
si scriva un programma che accetti due numeri interi e li scambi; si utilizzino esclusivamente variabili allocate dinamicamente;
ex-3
si scriva una funzione che accetti due puntatori a double e torni un puntatore al più grande di essi


next up previous contents index
Next: Array dinamici Up: Memoria dinamica. Strutture. Liste Previous: Memoria dinamica   Indice   Indice analitico
Claudio Cicconetti
2000-09-06