C++ - allocazione dinamica della memoria con new e delete
In questa pagina 8
Come si costruisce un contenitore come std::vector: la memoria con dimensione nota solo a tempo di esecuzione si prende dal free store con new, si rilascia con delete, e una classe garantisce che questo avvenga sempre tramite il suo costruttore e il suo distruttore. Si basa su C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast → e sul layout visto in C++ - chiamate a funzione e layout della memoriauna chiamata a funzione crea un record di attivazione (RA) con una copia dei parametri e le variabili locali, impilato nello stack (LIFO) e tolto all'uscita; la dimensione dipende dalle variabili locali e il tempo per inserirlo è costante; il processo ha cinque regioni di memoria: text (istruzioni, spesso read-only), initialized data (globali e statiche inizializzate), uninitialized data o BSS (globali e statiche non inizializzate, azzerate dal sistema), stack (RA) e heap (allocazione dinamica con new e delete); la durata (storage duration) di una variabile è diversa dallo scope: le globali e le static sono statiche (esistono per tutto il programma, inizializzate una volta), le locali sono automatiche (create all'entrata nel blocco, distrutte in ordine inverso all'uscita); le globali sono inizializzate prima del main ma l'ordine fra file diversi non è definito.C++ - chiamate a funzione e layout della memoria →. In C lo stesso tema è malloc/free (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →, Puntatori, struct e memoria dinamica in CPuntatori e passaggio per riferimento in C, array e aritmetica dei puntatori, stringhe, struct e typedef con l'operatore ->, malloc e free, puntatore a puntatore per modificare una testa; compilazione con Makefile; errori tipici (puntatori pendenti, perdite di memoria, off-by-one).Puntatori, struct e memoria dinamica in C →); la struttura dati costruita è una lista o un array dinamico (Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →). Gli smart pointer che automatizzano tutto questo sono in C++ - smart pointerGli smart pointer (classi template di
Perché serve: costruire un vector
std::vector ha un comportamento "intelligente": cambia dimensione per ospitare gli elementi, conosce la propria dimensione (size()), si legge con [], aggiunge con push_back(). Tutto ciò non è supportato dall'hardware: è una sovrastruttura scritta dalla libreria standard. Per capire come, bisogna costruirne uno.
Una classe come class vector { int size, age0, age1, age2, age3; }; è statica: i membri sono decisi a tempo di compilazione. Per una struttura dinamica servono vettori le cui dimensioni sono note a tempo di esecuzione: esiste una zona di memoria adatta, il free store, e uno strumento di basso livello per usarla, l'allocazione dinamica. Il vettore è allora composto da due membri:
class vector {
int sz; // numero di elementi
double* elem; // puntatore al buffer di sz double sul free store
public:
vector(int s); // crea un array di s double e fa puntare elem ad esso
int size() const { return sz; }
};Il puntatore gestisce un buffer. Il resto (copia, dimensione variabile) si costruirà sopra questo: C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list
new: allocare dal free store
double* p = new double[4]; // 4 double sul free store; p punta al primonew restituisce sempre un puntatore, anche se si alloca un solo elemento: l'indirizzo dell'oggetto, o del primo elemento di un array. La memoria restituita non ha un nome: la si gestisce tramite il puntatore. Il puntatore non sa quanti oggetti ci sono nell'array.
| espressione | risultato |
|---|---|
new int |
un int |
new int[4] |
quattro int consecutivi |
new double[n] |
n può essere una variabile (a differenza degli array C) |
Si accede come a un array (C++ - puntatori, array e castun puntatore è una variabile che contiene un indirizzo di memoria e sa il tipo dell'oggetto puntato (T*); si ottiene l'indirizzo con & e si accede al dato con * (dereference, un lvalue), nullptr indica nessun oggetto e due puntatori a tipi diversi sono tipi diversi; void* è un puntatore a memoria grezza (accetta qualsiasi puntatore ma non si dereferenzia) e i cast (static_cast, reinterpret_cast, const_cast) servono per interfacciarsi con hardware e codice non modificabile; il puntatore, a differenza della reference, si può spostare, può essere nullptr e va dereferenziato, e assegnarlo copia l'indirizzo (shallow copy) mentre assegnare una reference copia l'oggetto; un array C è una sequenza contigua di elementi dello stesso tipo, con dimensione costante, senza controllo dei limiti, che passato a una funzione decade in un puntatore al primo elemento; l'aritmetica dei puntatori somma o sottrae interi spostando di n elementi (non di n byte) e la differenza fra puntatori è in elementi; gli errori classici sono nullptr, puntatori non inizializzati, accessi fuori dai limiti e puntatori a oggetti usciti dallo scope.C++ - puntatori, array e cast →): *p e p[0] sono equivalenti, p[2] è il terzo elemento. Non esiste controllo dei limiti: pd[4] = 4.4 oltre un array di 4 elementi, o pd[-3], compilano; il programma "termina misteriosamente" in modo diverso a ogni esecuzione. Sono bug difficilissimi.
Inizializzazione
| scrittura | effetto |
|---|---|
double* p0; |
grosso problema: puntatore non inizializzato |
double* p1 = new double; |
double non inizializzato (il tipo built-in non viene azzerato) |
double* p2 = new double{5.5}; |
double inizializzato a 5.5 |
double* p3 = new double[5]; |
array non inizializzato |
double* p4 = new double[5]{0,1,2,3,4}; |
array inizializzato con una lista |
double* p5 = new double[]{0,1,2,3,4}; |
dimensione dedotta (5) |
Per un tipo definito dall'utente new X e new X[17] chiamano il costruttore di default. Se la classe non ce l'ha (per esempio Y ha solo un costruttore che prende un int), new Y e new Y[17] sono errori; si scrive new Y{13} o new Y[17]{0, 2, 3, ..., 16}.
#include <iostream>
using namespace std;
struct X { // ha un costruttore di default
X() { cout << " X()\n"; }
~X() { cout << " ~X()\n"; }
};
int main()
{
// new restituisce SEMPRE un puntatore: all'oggetto, o al primo elemento di un array
int* pi = new int; // 1 int NON inizializzato
int* qi = new int[4]; // 4 int NON inizializzati
double* p2 = new double{5.5}; // 1 double inizializzato a 5.5
double* p4 = new double[5]{0, 1, 2, 3, 4}; // array inizializzato
double* p5 = new double[]{0, 1, 2, 3}; // dimensione dedotta: 4
*pi = 10; // la memoria non ha nome: la raggiungo col puntatore
for (int i = 0; i < 4; ++i) qi[i] = i * i;
cout << *pi << ' ' << qi[3] << ' ' << *p2 << ' ' << p4[4] << ' ' << p5[3] << '\n';
// *p e p[0] sono equivalenti; p[2] e' il terzo elemento
cout << (*p4 == p4[0]) << ' ' << p4[2] << '\n';
// senza controllo dei limiti: p4[5] o p4[-1] compilano, ma sono errori (comportamento indefinito)
cout << "costruzione di un oggetto:\n";
X* px = new X; // new: alloca E chiama il costruttore
cout << "costruzione di un array di 3:\n";
X* parr = new X[3];
cout << "delete:\n";
delete px; // delete: chiama il distruttore E libera
cout << "delete[]:\n";
delete[] parr; // delete[] per ogni new[]
delete pi; delete[] qi; delete p2; delete[] p4; delete[] p5;
return 0;
}10 9 5.5 4 3
1 2
costruzione di un oggetto:
X()
costruzione di un array di 3:
X()
X()
X()
delete:
~X()
delete[]:
~X()
~X()
~X()Letto con il codice: new X costruisce un solo X; new X[3] chiama tre volte il costruttore; delete px chiama il distruttore e libera; delete[] parr chiama il distruttore su tutti gli elementi (in ordine inverso) e libera il blocco.
Un puntatore non è inizializzato automaticamente a nullptr, e dopo la liberazione resta col vecchio valore; per controllare: if (p0 != nullptr) oppure if (p0).
Garbage e memory leak
Un puntatore si può spostare da un oggetto a un altro (a differenza delle reference), e questo genera problemi:
double* p = new double;
double* q = new double[1000];
q[700] = 7.7; // ok
q = p; // attenzione: q ora punta al singolo double
double d = q[700]; // errore: fuori dall'oggetto puntatoDopo q = p, l'array di 1000 double non ha più nessun puntatore: è garbage (spazzatura), memoria allocata ma irraggiungibile e inservibile. Questo fenomeno è il memory leak, e può portare all'esaurimento della memoria.
Liberare la memoria
In C++ nessuno libera la memoria al posto nostro: non c'è un garbage collector, a differenza di Java e di altri linguaggi. Produrre garbage è un errore tecnico grave. Perché lasciarlo al programmatore? Per due ragioni:
- efficienza: il garbage collector impiega molte risorse (deve capire quale memoria è ancora raggiungibile);
- controllo: decidiamo noi quando la memoria è rilasciata, e quindi di nuovo disponibile.
Si usano istruzioni dedicate, applicate a puntatori: delete per la memoria allocata con new, delete[] per quella allocata con new[] (usare quella sbagliata è un errore).
Caccia al memory leak
double* calc(int res_size, int max) {
double* p = new double[max]; // buffer di lavoro
double* res = new double[res_size]; // risultato
return res;
}
double* r = calc(100, 1000); // ... e nessun delete di rI leak sono due: p (il puntatore locale sparisce all'uscita ma la memoria no) e r (il chiamante non libera). La correzione: liberare p prima del return e liberare r nel chiamante.
#include <cstdlib>
#include <iostream>
#include <new>
using namespace std;
// Sostituiamo gli operatori globali per contare le allocazioni
static int n_new = 0, n_delete = 0;
void* operator new(size_t s) { ++n_new; return malloc(s); }
void* operator new[](size_t s) { ++n_new; return malloc(s); }
void operator delete(void* p) noexcept { if (p) ++n_delete; free(p); }
void operator delete[](void* p) noexcept { if (p) ++n_delete; free(p); }
void operator delete(void* p, size_t) noexcept { if (p) ++n_delete; free(p); }
void operator delete[](void* p, size_t) noexcept { if (p) ++n_delete; free(p); }
double* calc_con_leak(int res_size, int max)
{
double* p = new double[max]; // buffer di lavoro: MAI liberato -> leak
double* res = new double[res_size];
p[0] = 0;
return res; // solo il puntatore e' copiato: la memoria resta la stessa
}
double* calc_corretta(int res_size, int max)
{
double* p = new double[max];
double* res = new double[res_size];
p[0] = 0;
delete[] p; // libero ciò che non esce dalla funzione
return res; // res passa al chiamante, che dovra' liberarlo
}
int main()
{
int a0 = n_new, d0 = n_delete; // l'ambiente puo' avere allocato per conto suo
double* r = calc_con_leak(100, 1000);
delete[] r; // il chiamante libera il risultato
cout << "versione con leak: new = " << n_new - a0 << ", delete = " << n_delete - d0 << '\n';
a0 = n_new; d0 = n_delete;
double* s = calc_corretta(100, 1000);
delete[] s;
cout << "versione corretta: new = " << n_new - a0 << ", delete = " << n_delete - d0 << '\n';
return 0;
}versione con leak: new = 2, delete = 1
versione corretta: new = 2, delete = 2Il programma sostituisce gli operatori globali new/delete per contare le allocazioni: nella prima versione ci sono 2 new e 1 delete (l'altro blocco è perso), nella seconda 2 e 2.
Passaggio di memoria tra funzioni
L'allocazione dinamica permette di creare un oggetto in uno scope e passarlo a un altro (il chiamante). Non esiste un meccanismo analogo a quello che libera una variabile locale automatica (per questo si chiama "automatica"). Dopo return res; viene copiato solo il puntatore: la memoria allocata rimane la stessa e il chiamante ne diventa responsabile.
Stack Free store
calc: res = 0xac00 ----+
main: r = 0xac00 ----+-----> [ 1000 double ] (da liberare con delete[] r in main)Anche per un oggetto: vector* p = f(4); ... delete q; nel chiamante.
Errori frequenti
| errore | codice | perché è grave |
|---|---|---|
| doppia cancellazione | int* p = new int{5}; delete p; delete p; |
dopo il primo delete, p non è più a nostra disposizione: il proprietario è il gestore del free store e ci potrebbe già essere un altro oggetto |
| dangling pointer (puntatore pendente) | int* p = new int{5}; delete p; *p = 1; |
dopo delete, p ha ancora lo stesso valore, ma non è più valido: usarlo è un errore |
| rimedio | delete p; p = nullptr; |
un delete su nullptr non fa nulla e if (p) funziona |
Il distruttore
Torniamo a vector: a fine vita bisogna deallocare. Una funzione clean_up() richiede che l'utente se ne ricordi. Come garantire che avvenga in modo automatico?
Definizione. Il distruttore
~T()è una funzione membro che il compilatore chiama implicitamente quando un oggetto deve essere distrutto: esce dallo scope, oppure è deallocato condelete.
class vector {
int sz;
double* elem;
public:
vector(int s) : sz{s}, elem{new double[s]} { for (int i = 0; i < s; ++i) elem[i] = 0; }
~vector() { delete[] elem; }
// ...
};Così vector fa automaticamente quello che con p si fa a mano:
void f3(int n) {
double* p = new double[n];
vector v(n); // v è liberato automaticamente
// ... uso p e v ...
delete[] p; // p va liberato a mano
}I distruttori sono utili per ogni risorsa da rilasciare: memoria, file, thread, lock, stream. Un oggetto acquisisce le risorse nel costruttore, durante la vita può acquisirne altre o rilasciarne, a fine vita le rilascia tutte. Questa è l'idea di RAII, sviluppata in C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →.
Azioni di
newedelete.newalloca la memoria e invoca il costruttore.deleteinvoca il distruttore e libera la memoria.
Se il distruttore non è scritto, ne esiste uno generato dal compilatore: non è intelligente (non libera al posto nostro la memoria puntata da un puntatore nudo) ma chiama i distruttori degli oggetti membro. Per questo struct Customer { std::string name; std::vector<std::string> address; }; non perde memoria: alla distruzione di Fred si chiamano i distruttori di string e vector<string>.
#include <iostream>
using namespace std;
class Vector {
public:
explicit Vector(int s)
: sz{s}, elem{new double[s]} // alloca il buffer sul free store
{
for (int i = 0; i < s; ++i) elem[i] = 0; // new non inizializza i double
cout << " Vector(" << sz << "): buffer allocato\n";
}
~Vector() // chiamato da solo all'uscita dallo scope
{
delete[] elem; // libera il buffer
cout << " ~Vector(" << sz << "): buffer liberato\n";
}
int size() const { return sz; }
double& operator[](int n) { return elem[n]; } // lettura e scrittura
double operator[](int n) const { return elem[n]; } // solo lettura, su oggetti const
private:
int sz; // numero di elementi
double* elem; // puntatore al buffer
};
void f3(int n)
{
double* p = new double[n]; // gestione MANUALE: serve il delete[]
Vector v(n); // gestione AUTOMATICA: ci pensa il distruttore
v[0] = 3.5;
v[n - 1] = 7;
p[0] = v[0] + v[n - 1];
cout << " p[0] = " << p[0] << ", v.size() = " << v.size() << '\n';
delete[] p;
} // qui v viene distrutto
int main()
{
cout << "entro in f3\n";
f3(4);
cout << "esco da f3\n";
const Vector cv(2); // oggetto const
cout << "cv[1] = " << cv[1] << '\n'; // usa la versione const di operator[]
return 0;
}entro in f3
Vector(4): buffer allocato
p[0] = 10.5, v.size() = 4
~Vector(4): buffer liberato
esco da f3
Vector(2): buffer allocato
cv[1] = 0
~Vector(2): buffer liberatoIl distruttore di v scatta alla } di f3, mentre il buffer p richiede il delete[] esplicito. cv è const: chiama operator[] nella versione const.
Accedere al buffer: operator[]
Il vettore si legge e scrive con [], più naturale di get()/set(). Un primo tentativo double operator[](int n) { return elem[n]; } ritorna una copia: v[3] = x; non compila perché v[3] non è un lvalue. Un double* come risultato costringerebbe a scrivere *v[3] = x;. La soluzione è la reference:
double& operator[](int n) { return elem[n]; } // lettura e scritturaResta un problema: non funziona su oggetti const. Si fa l'overloading su const: due versioni della funzione, una const e una no. Quella const può restituire una copia o una const reference.
double& operator[](int n); // non const: v[1] = 2.0;
double operator[](int n) const; // const: double d = cv[1];void ff(const vector& cv, vector& v) {
double d = cv[1]; // ok: versione const
cv[1] = 2.0; // errore: versione const non restituisce un lvalue
d = v[1]; // ok: versione non const
v[1] = 2.0; // ok
}Restituire una reference a un dato privato (elem[n]) non viola l'incapsulamento? L'accesso è deciso dalla classe: l'utente non ottiene il puntatore elem, ma un alias a un elemento, e operator[] è comunque l'interfaccia pubblica scelta dal progettista. L'attenzione è su due punti: non restituire mai una reference a una variabile locale e ricordare che la reference resta valida solo finché esiste il buffer (se il vettore viene riallocato, vecchie reference diventano dangling).
Pro e contro dell'allocazione dinamica
| vantaggi | svantaggi |
|---|---|
| accedere a grandi quantità di memoria | gestione esplicita della deallocazione |
| blocchi di dimensione nota solo a run-time | gestione tramite puntatori: sorgente di molti errori subdoli |
| controllare quando la memoria è allocata e deallocata | maggior carico computazionale: allocazioni e deallocazioni passano per chiamate al sistema operativo |
L'allocazione dinamica va giustificata: bisogna sapere perché è necessaria invece di altri strumenti. Usarla a sproposito è un errore di design.
Domande d'esame
1. Descrivere l'allocazione dinamica in C++ e gli strumenti usati, con l'effetto sul layout della memoria e un esempio dentro una classe.
Traccia. Il programma ha text, data, BSS, stack e heap (free store); new riserva memoria sull'heap a run-time, il puntatore (sullo stack o nei dati) contiene l'indirizzo; si libera con delete/delete[]; classe con new nel costruttore e delete[] nel distruttore (vector). Soluzione completa: Esercizio - Allocazione dinamica e layout della memoria (primo appello 2022-23, teoria).
2. Che cos'è un memory leak, e che cos'è un dangling pointer? Come si evitano?
Traccia. Leak: memoria allocata e non più raggiungibile né liberata (garbage). Dangling pointer: puntatore con il vecchio valore dopo delete, che non va più usato. Si evitano liberando con delete/delete[] e ponendo il puntatore a nullptr; meglio con classi con distruttore o smart pointer.
3. Che cosa succede con student_list = new students[size]; Come si dealloca? Può esserci stack overflow?
Traccia. Alloca size elementi dal free store, con chiamata al sistema operativo; si dealloca con delete[] student_list;. L'overflow dello stack no: la memoria è nell'heap; semmai new fallisce con eccezione std::bad_alloc. student_list è un puntatore, non uno std::list.
Versione ripasso
- Perché:
vectorsi costruisce sul free store: la dimensione è nota a run-time. Struttura:int sz; double* elem;(puntatore a un buffer). Rimandi: Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →, Realizzare contenitori su array e listeCome si realizza un ADT contenitore partendo da un array: lunghezza logica e capacità con raddoppio (costo ammortizzato), dizionario su array ordinato con ricerca binaria, coda doppia su array circolare, coda con priorità a livelli, ADT costruiti sopra altri ADT (pila di code, pila reversibile); tabella dei costi.Realizzare contenitori su array e liste →. newrestituisce sempre un puntatore (all'oggetto o al primo elemento); la memoria non ha nome; il puntatore non sa quanti elementi ci sono.*pp[0]. Nessun controllo dei limiti (pd[4],pd[-3]compilano).
| scrittura | effetto |
|---|---|
new double; / new double[5]; |
non inizializzati |
new double{5.5}; |
inizializzato |
new double[5]{0,1,2,3,4}; / new double[]{0,1,2,3}; |
array inizializzato, dimensione dedotta |
new X; / new X[17]; |
chiamano il costruttore di default (errore se non c'è) |
new Y{13}; / new Y[17]{...}; |
con costruttore a un argomento |
- Il puntatore non è inizializzato a
nullptr; controlloif (p0 != nullptr)oif (p0). - Garbage / memory leak: area allocata senza più riferimenti (
q = pconqche puntava a 1000double); può esaurire la memoria. - Niente garbage collector (a differenza di Java): per efficienza e controllo.
deletepernew,delete[]pernew[]. - Caccia al leak:
double* calc(int res_size, int max) {
double* p = new double[max]; // leak se non liberato
double* res = new double[res_size];
delete[] p; // libero il buffer di lavoro
return res; // passa al chiamante (copia solo il puntatore)
}
double* r = calc(100, 1000); /* ... */ delete[] r; // il chiamante libera Contatori sostituendo operator new/delete: versione con leak new = 2, delete = 1, corretta 2, 2.
- Errori: doppia cancellazione (
delete p; delete p;), dangling pointer (usarepdopodelete). Rimedio:delete p; p = nullptr;. - Distruttore
~T(): chiamato dal compilatore quando l'oggetto esce dallo scope o condelete.vector: costruttoreelem{new double[s]}, distruttoredelete[] elem. Utile per memoria, file, thread, lock, stream (RAII: C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →). Il distruttore generato dal compilatore non libera la memoria puntata da puntatori nudi ma distrugge i membri (string,vector).new= alloca + costruttore;delete= distruttore + libera. operator[]:double operator[](int)non permettev[3] = x;double*costringe a*v[3];double& operator[](int n)+double operator[](int n) const(overloading suconst:cv[1] = 2.0errore,v[1] = 2.0ok). Non restituire mai reference a locali.- Pro: molta memoria, dimensione a run-time, controllo di quando; contro: deallocazione esplicita, puntatori (errori subdoli), costo (chiamate al sistema operativo). Usarla senza motivo è un errore di design.
- Programma di prova di
new:int* pi = new int; int* qi = new int[4]; double* p2 = new double{5.5}; double* p4 = new double[5]{0,1,2,3,4};e poidelete pi; delete[] qi; delete p2; delete[] p4;; con una classeXche stampa nei costruttore e distruttore:new XX();new X[3]treX();delete px~X();delete[] parrtre~X(). - Classe con distruttore, uso:
void f3(int n) { double* p = new double[n]; Vector v(n); /* ... */ delete[] p; }vè distrutto da solo alla},pva liberato a mano;Vector(4)alloca,~Vector(4)libera;const Vector cv(2);+cv[1]usa l'operator[]const. - Passare memoria al chiamante: si copia solo il puntatore (
return res;), la memoria resta la stessa e il chiamante la libera (delete[] r;odelete q;per un oggetto). - Traccia d'esame: layout (heap),
new/delete[], classe con distruttore; leak = memoria irraggiungibile e non liberata; dangling = puntatore usato dopodelete, si evita connullptr;new T[n]si libera condelete[], overflow dello stack non è possibile (è heap). - Il
vectorcome esempio completo: rappresentazioneint sz; double* elem;(due membri, il puntatore regge il buffer); costruttorevector(int s) : sz{s}, elem{new double[s]};size() const;operator[]in due versioni; distruttore~vector() { delete[] elem; }. Una classe statica comeclass vector { int size, age0, age1, age2, age3; };non può cambiare dimensione: serve il free store. - Spostare un puntatore crea garbage:
double* p = new double; double* q = new double[1000]; q = p;l'array di 1000doublenon ha più riferimenti. Con una reference non succede (non si sposta). - Controllo di validità:
if (p0 != nullptr)oif (p0); un puntatore non è azzerato da solo né dadelete. - Errori tipici:
deleteper un array; dimenticare ildelete[]di un buffer di lavoro; liberare il risultato nella funzione che lo restituisce; usare l'oggetto dopodelete; credere chenewazzeri idouble; dimenticare il distruttore in una classe connew.
Esercizi su questo argomento
- Esercizio - Allocazione dinamica e layout della memoria (primo appello 2022-23, teoria)
- Esercizio - Array dinamico ordinato come classe template (quarto appello 2022-23, programmazione)
- Esercizio - Array stile C, vector e la classe MyArray (terzo appello 2022-23, teoria)
- Esercizio - Classe Image con buffer dinamico (terzo appello 2023-24, programmazione)
- Esercizio - Distruttore, copia e spostamento della classe Elements (secondo appello 2021-22, programmazione)
- Esercizio - MyVector 2.0 con buffer ridimensionabile (laboratorio 4)
- Esercizio - MyVector con memoria dinamica (laboratorio 3)
- Esercizio - Quiz e domande su funzioni virtuali, passaggio degli argomenti, dangling pointer e doppia deallocazione (quarto appello 2023-24, teoria)
- Esercizio - Quiz e domande su polimorfismo, allocazione dinamica e garanzie sulle eccezioni (secondo appello 2023-24, teoria)
- Esercizio - Quiz su conversioni, constexpr, free store e downcasting (terzo appello 2021-22, quiz)
- Esercizio - Quiz su inizializzazione, free store, polimorfismo e passaggio di argomenti (quarto appello 2022-23, quiz)
- Esercizio - Quiz su operatore =, dangling pointer, enumerazioni e at() (quarto appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)
- Esercizio - RAII e garanzie sulle eccezioni (quarto appello 2021-22, teoria)
- Esercizio - Smart pointer, unique_ptr e shared_ptr (primo appello 2021-22, teoria)
Teoria collegata
- C++ - buona interfaccia e progetti su più file
- C++ - chiamate a funzione e layout della memoria
- C++ - costruttori, inizializzazione, copia e spostamento
- C++ - eccezioni e gestione delle risorse (RAII)
- C++ - enumerazioni e overloading degli operatori
- C++ - puntatori, array e cast
- C++ - smart pointer
- C++ - template e programmazione generica