Esercizio - Quiz e domande su polimorfismo, allocazione dinamica e garanzie sulle eccezioni (secondo appello 2023-24, teoria)
In questa pagina 4
Testo. Prova scritta di Laboratorio di Programmazione, secondo appello 2023-24, parte di teoria (90 minuti): un quiz di 10 domande da 4 affermazioni vere/false (corretta +1, sbagliata -0,25, non data 0) e tre domande aperte (5, 4 e 4 punti).
Teoria: C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →, C++ - allocazione dinamica della memoria con new e deletela memoria dinamica si prende dal free store (heap) con new, che restituisce un puntatore all'oggetto o al primo elemento dell'array (new T, new T[n], con inizializzatori {}), e si restituisce con delete (per new) e delete[] (per new[]); il puntatore non sa quanti elementi ci sono, la memoria non ha nome e non ha controllo dei limiti; in C++ non c'è garbage collector (scelta per efficienza e controllo), quindi un'area che perde ogni riferimento diventa garbage e produce un memory leak, e gli errori tipici sono la doppia cancellazione e il dangling pointer (si mette il puntatore a nullptr dopo delete); il distruttore ~T() è chiamato dal compilatore quando l'oggetto esce dallo scope o con delete e libera le risorse, per cui una classe come vector acquisisce la memoria nel costruttore e la rilascia nel distruttore; new alloca e chiama il costruttore, delete chiama il distruttore e libera; operator[] con due versioni (T& e const) dà accesso in lettura e scrittura al buffer; si alloca dinamicamente solo se serve davvero (memoria grande, dimensione nota a run-time, durata controllata).C++ - allocazione dinamica della memoria con new e delete →, 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) →. La parte di programmazione dello stesso appello è Esercizio - ConvertToArray da un container STL (secondo appello 2023-24, programmazione).
Quiz
1. Codice: float* ptr1; float* ptr2; for (int i = 0; i < 4; ++i) { ptr1 = new float[5]; ptr2 = ptr1; }
| affermazione | risposta | motivo |
|---|---|---|
| dà errore in fase di compilazione | F | il codice è corretto sintatticamente |
| causa memory leak | V | si fanno 4 new[] e nessuna delete[]: dei 4 blocchi restano raggiungibili solo l'ultimo (da ptr1 e ptr2) e, a fine programma, nessuno viene liberato; i primi tre sono persi a ogni iterazione |
| produce un problema di dangling pointer | F | nessun blocco è stato liberato: nessun puntatore punta a memoria già rilasciata |
| utilizza l'allocazione dinamica | V | new[] |
2. I tipi che implementano una deep copy:
3. Una variabile locale automatica definita in una funzione f:
| affermazione | risposta | motivo |
|---|---|---|
| ha scope globale | F | scope locale (il blocco) |
ha durata di memorizzazione pari alla durata di esecuzione di f |
V | nasce nel record di attivazione e muore quando f ritorna |
deve essere deallocata in uscita da f per non causare memory leak |
F | la deallocazione è automatica |
è definita dalla keyword auto |
F | in C++11 auto indica la deduzione del tipo (la "storage class" auto è di un C/C++ antico e mai usata) |
4. Il record di attivazione di una funzione:
5. Un errore di linking può essere causato da:
| affermazione | risposta | motivo |
|---|---|---|
| una funzione dichiarata, chiamata, ma non definita né nel progetto né nelle librerie linkate | V | undefined reference |
| una variabile non inizializzata | F | è un problema di esecuzione (valore indeterminato) |
| un puntatore usato senza essere inizializzato | F | come sopra |
| una libreria usata ma il cui header non è incluso in modo appropriato | F | senza header il compilatore non conosce il nome: errore di compilazione |
6. L'overloading degli operatori:
| affermazione | risposta | motivo |
|---|---|---|
| è possibile anche per i tipi built-in | F | almeno un operando deve essere di un tipo definito dall'utente |
| permette di trasformare operatori unari in binari | F | non si cambia l'arità dell'operatore |
per operator+ si può usare funzione libera o membro, ma il numero di argomenti cambia |
V | libera: due argomenti; membro: uno (il primo operando è *this) |
per operator<< si può usare funzione libera o membro, ma il numero di argomenti non cambia |
F | cambia come per +: libera operator<<(ostream&, const T&) con due argomenti, membro con uno |
7. Gli iteratori dei container STL:
| affermazione | risposta | motivo |
|---|---|---|
si usano tramite operator[] |
F | si usano con *, ++, == (solo i random access hanno anche []) |
| possono essere dereferenziati | V | *it |
| quando sono distrutti deallocano l'oggetto puntato | F | non possiedono l'elemento |
| sono implementati con classi, ciascuna definita nel relativo container | V | vector<T>::iterator, list<T>::iterator |
8. Il distruttore di una classe:
| affermazione | risposta | motivo |
|---|---|---|
| è chiamato quando un oggetto locale automatico esce dallo scope | V | |
| è chiamato quando un oggetto statico esce dallo scope e il programma non termina | F | gli oggetti statici sono distrutti alla fine del programma |
può avere un argomento di tipo int |
F | il distruttore non ha parametri |
è chiamato effettuando una delete su un oggetto allocato dinamicamente |
V |
9. Puntatori e riferimenti:
| affermazione | risposta | motivo |
|---|---|---|
| un riferimento può essere spostato da un oggetto a un altro | F | si lega una volta sola, in inizializzazione |
| un puntatore può essere inizializzato in modo da non puntare a nessun elemento | V | nullptr |
| un riferimento può essere inizializzato in modo da non essere collegato a niente | F | non esiste il riferimento nullo |
| la copia tra due riferimenti equivale alla copia tra gli elementi a cui sono collegati | V | r1 = r2 assegna il valore riferito, non ricollega |
10. L'istruzione std::cout << "Hello, world!\n";:
| affermazione | risposta | motivo |
|---|---|---|
| contiene un literal | V | la stringa |
| contiene una definizione | F | è un'istruzione-espressione, nessuna dichiarazione |
| contiene due operandi | V | std::cout e la stringa |
| contiene un operatore | V | << |
Domanda aperta 1 (5 punti): polimorfismo
Testo. Descrivere il concetto di polimorfismo in C++ e le due tipologie di polimorfismo viste durante il corso, evidenziandone le differenze. Descrivere gli strumenti che le implementano. Per ogni tipologia riportare un esempio di codice in cui sia chiara la sua applicazione.
Risposta. Polimorfismo = capacità di associare comportamenti diversi a un'unica notazione generica. Le due forme (terminologia di Vandevoorde e Josuttis):
| dinamico | statico | |
|---|---|---|
| strumenti | ereditarietà + funzioni virtuali | template |
| quando si sceglie la funzione | a run-time (tramite vptr e vtbl) | a compile-time (si genera una funzione per ogni tipo) |
| vincolo | vincolato: l'interfaccia delle derivate è fissata dalla classe base | svincolato: non c'è base, basta che i tipi abbiano la stessa sintassi |
| collezione di tipi diversi | sì (vector<Base*>) |
no |
| costo | indirezione per chiamata e un puntatore per oggetto | nessun costo a run-time, ma codice generato per ogni tipo; errori in compilazione se un tipo non ha l'operazione |
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// ---------- POLIMORFISMO DINAMICO: ereditarieta' + funzioni virtuali ----------
class Animale {
public:
virtual string verso() const = 0; // interfaccia comune
virtual ~Animale() {}
};
class Cane : public Animale { public: string verso() const override { return "bau"; } };
class Gatto : public Animale { public: string verso() const override { return "miao"; } };
void fai_parlare(const Animale& a) { cout << a.verso() << ' '; } // una sola funzione, decisione a RUN-TIME
// ---------- POLIMORFISMO STATICO: template ----------
struct Auto { string verso() const { return "brum"; } }; // nessuna base comune
struct Barca { string verso() const { return "tut"; } };
template <typename T>
void fai_rumore(const T& x) { cout << x.verso() << ' '; } // una funzione per tipo, decisione a COMPILE-TIME
int main()
{
Cane c;
Gatto g;
vector<const Animale*> zoo{&c, &g}; // collezione eterogenea: possibile solo con il dinamico
for (const Animale* a : zoo) fai_parlare(*a); // bau miao
cout << '\n';
Auto a;
Barca b;
fai_rumore(a); // fai_rumore<Auto>
fai_rumore(b); // fai_rumore<Barca>
cout << '\n';
}Uscita eseguita: bau miao (le due chiamate passano per Animale, scelta a run-time) e brum tut (due istanze fai_rumore<Auto> e fai_rumore<Barca>, scelta a compile-time). Nel primo caso Cane e Gatto devono derivare da Animale e ridefinire verso; nel secondo Auto e Barca non hanno parentela, basta che abbiano verso(). Altre forme di polimorfismo: overloading di funzioni e operatori. Dettagli in C++ - polimorfismoPolimorfismo = abilità di associare comportamenti diversi a un'unica notazione generica. In C++ ci sono due forme principali: dinamico (ereditarietà più funzioni virtuali: vincolato dall'interfaccia della classe base, binding a run-time, permette collezioni eterogenee) e statico (template: svincolato, basta che i tipi abbiano la stessa sintassi, binding a tempo di compilazione, nessun costo a run-time ma una funzione per ogni tipo); il polimorfismo statico è lo strumento della programmazione generica. Anche l'overloading di funzioni e di operatori può essere visto come polimorfismo. Nella gerarchia si naviga con l'upcasting (implicito, sempre lecito) e il downcasting con dynamic_cast, l'unico cast con verifica a run-time: su un puntatore dà nullptr se non lecito, su un riferimento lancia bad_cast.C++ - polimorfismo →.
Domanda aperta 2 (4 punti): allocazione dinamica
Testo. Descrivere come avviene l'allocazione dinamica della memoria in C++, indicando quale zona di memoria è usata e quali sono i vantaggi/svantaggi rispetto alle variabili locali automatiche. Specificare gli elementi (istruzioni, variabili) usati per allocare e deallocare la memoria, corredando la discussione con un esempio di codice. Descrivere le problematiche di cui il programmatore deve tenere conto.
Risposta.
Dove e come. L'allocazione dinamica usa il free store (heap): una zona di memoria gestita a run-time, separata dallo stack. new T(args) alloca lo spazio per un T, chiama il costruttore e restituisce un puntatore T*; new T[n] alloca un array di n elementi; delete p chiama il distruttore e libera lo spazio; delete[] p libera un array. Le istruzioni sono new, new[], delete, delete[]; la variabile che tiene l'indirizzo è un puntatore (che vive, per esempio, nello stack).
| variabile locale automatica (stack) | oggetto dinamico (free store) | |
|---|---|---|
| durata | quella dello scope: automatica | scelta dal programmatore: fino alla delete |
| dimensione | nota a compile-time | decisa a run-time (new int[n]) |
| costo | allocazione quasi gratuita | più lenta (gestione dello heap) |
| rilascio | automatico | manuale |
| rischi | pochi (dangling se si restituisce l'indirizzo) | leak, dangling, doppia delete |
| spazio | limitato (stack overflow) | grande |
Esempio verificato:
#include <iostream>
using namespace std;
struct Punto {
int x, y;
Punto(int xx, int yy) : x{xx}, y{yy} { cout << " Punto(" << x << "," << y << ")\n"; }
~Punto() { cout << " ~Punto(" << x << "," << y << ")\n"; }
};
int* crea_dinamico()
{
return new int{5}; // vive nel free store: sopravvive alla funzione
}
int main()
{
cout << "1) variabile automatica (stack): nasce e muore con lo scope\n";
{
Punto p{1, 2};
} // ~Punto automatico
cout << "2) new / delete (free store)\n";
Punto* q = new Punto{3, 4}; // allocazione + costruttore
delete q; // distruttore + deallocazione
q = nullptr; // niente dangling pointer
cout << "3) new[] / delete[]\n";
int* v = new int[5]{}; // 5 int inizializzati a 0
v[4] = 9;
cout << " v[4] = " << v[4] << '\n';
delete[] v; // con le quadre, perche' e' stato fatto new[]
cout << "4) il dato dinamico sopravvive alla funzione\n";
int* d = crea_dinamico();
cout << " *d = " << *d << '\n';
delete d;
cout << "5) memory leak, dangling pointer e doppia delete (descritti, non eseguiti)\n";
// int* p = new int{1}; p = new int{2}; // LEAK: il primo blocco non e' piu' raggiungibile
// int* a = new int{1}; delete a; *a = 3; // DANGLING: uso dopo la delete
// int* b = new int{1}; delete b; delete b; // DOPPIA delete: comportamento indefinito
// int* f() { int x = 5; return &x; } // DANGLING: indirizzo di una variabile automatica restituito
}Uscita eseguita: Punto(1,2)/~Punto(1,2) (automatico: il distruttore è chiamato alla chiusura delle graffe), Punto(3,4)/~Punto(3,4) (con new/delete il costruttore e il distruttore sono chiamati dalle due istruzioni), v[4] = 9, *d = 5.
Problematiche.
- Memory leak: si perde l'ultimo puntatore a un blocco senza averlo liberato (
p = new int{2};sopra un puntatore che puntava a un altro blocco; uscita di una funzione, anche per eccezione, prima delladelete). - Dangling pointer: si usa un puntatore dopo la
delete, o si restituisce l'indirizzo di una variabile locale; per prevenirlo si azzera il puntatore (p = nullptr). - Doppia deallocazione: due
deletesullo stesso blocco (tipico con la copia superficiale di una classe). - Mancata corrispondenza:
deletesu un blocco fatto connew[](e viceversa). - Dimensione e confini: accesso fuori dai limiti, uso di memoria non inizializzata.
- Soluzioni moderne: contenitori (
vector) e smart pointer (C++ - smart pointerGli smart pointer (classi template di) sono oggetti che detengono un puntatore e lo deallocano automaticamente all'uscita dallo scope (RAII), eliminando memory leak e dangling pointer con un overhead minimo. unique_ptr ha un solo proprietario: non si copia (costruttore e assegnamento di copia cancellati) ma si sposta (std::move, ritorno da funzione), ha reset(), release(), get(), * e ->; due unique_ptr costruiti con lo stesso puntatore grezzo sono un errore (doppia delete) che il compilatore non vede, e si evita con make_unique. shared_ptr ha più proprietari: si copia, tiene un contatore di riferimenti (use_count) e libera l'oggetto quando l'ultimo shared_ptr viene distrutto, a un costo maggiore; weak_ptr osserva senza possedere, per rompere i cicli. auto_ptr è deprecato (rimosso in C++17).C++ - smart pointer → ), che applicano RAII.
Domanda aperta 3 (4 punti): eccezioni e memoria dinamica
Testo. Illustrare un caso in cui chiamare una funzione che può lanciare un'eccezione può creare problemi alla gestione della memoria allocata dinamicamente. Spiegare che cosa si intende per strong guarantee, basic guarantee, no-throw guarantee, e come queste permettono di risolvere tale problema.
Risposta.
Il caso. La funzione alloca un blocco, poi chiama qualcosa che può lanciare (qui vector::at()), e rilascia in fondo: se l'eccezione parte, la funzione è abbandonata e la delete[] non viene eseguita: memory leak.
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
// 1) PROBLEMA: se v.at(x) lancia, la delete[] non viene eseguita e la memoria e' persa
void con_problema(int s, int x)
{
int* p = new int[s];
vector<int> v;
p[0] = v.at(x); // lancia out_of_range (v e' vuoto)
delete[] p; // non raggiunta
}
// 2) BASIC GUARANTEE con try/catch: libera e rilancia
void con_basic(int s, int x)
{
int* p = new int[s];
vector<int> v;
try {
p[0] = v.at(x);
} catch (...) {
delete[] p; // nessuna perdita
throw; // l'eccezione continua verso chi sa gestirla
}
delete[] p;
}
// 3) RAII: nessun try/catch e nessuna delete, ci pensa il distruttore di vector
void con_raii(int s, int x)
{
vector<int> p(static_cast<size_t>(s));
vector<int> v;
p[0] = v.at(x);
}
// 4) STRONG GUARANTEE: lavoro su una copia, poi commit con operazioni che non lanciano
void aggiungi_due(vector<int>& dati, int a, int b)
{
vector<int> copia = dati; // se lancia, dati e' intatto
copia.push_back(a); // puo' lanciare (bad_alloc): sempre sulla copia
copia.push_back(b);
dati.swap(copia); // swap non lancia: commit
}
int main()
{
for (auto f : {con_problema, con_basic, con_raii}) {
try { f(10, 3); } catch (const exception& e) { cout << "catch: " << e.what() << '\n'; }
}
vector<int> d{1, 2};
aggiungi_due(d, 3, 4);
cout << d.size() << '\n';
}(Il programma compila per Linux senza avvisi e non è stato eseguito: le eccezioni non funzionano in WebAssembly. Il comportamento è determinato dal codice: con v vuoto at(3) lancia out_of_range; le tre funzioni terminano tutte con eccezione, ma solo con_problema lascia 40 byte non liberati; il testo di what() dipende dalla libreria. aggiungi_due lascia d con 4 elementi.)
Le garanzie.
| garanzia | che cosa promette | nell'esempio |
|---|---|---|
| basic | se lancia non ci sono perdite di risorse e lo stato resta valido (non necessariamente quello di prima) | con_basic: try/catch (...) che fa delete[] e rilancia (throw;); le STL offrono almeno questa |
| strong | in più, in caso di fallimento lo stato osservabile è identico a quello precedente alla chiamata (commit or rollback) | aggiungi_due: lavora su una copia e poi swap |
| no-throw | la funzione non lancia mai (noexcept) |
swap, il distruttore: sono i mattoni per costruire il commit della strong |
Come risolvono il problema. La basic impone che l'eventuale eccezione trovi la funzione in grado di rilasciare tutto (catch (...) { delete[] p; throw; }), quindi niente leak, ma il codice si duplica e diventa pesante a ogni punto che può lanciare. La soluzione generale è RAII: l'acquisizione nel costruttore e il rilascio nel distruttore di un oggetto locale (vector, unique_ptr) fanno sì che lo stack unwinding chiami da solo i distruttori all'uscita dallo scope, anche per eccezione (con_raii): la basic guarantee senza try/catch. La strong si ottiene separando la fase che può fallire (su una copia) dal commit (operazioni no-throw come swap).
Versione ripasso
- Quiz (risposte V/F): 1) errore di compilazione F; leak V; dangling F; allocazione dinamica V. 2) deep copy non con derivate F; solo membri pubblici F; più lenta V; definire copia ed assegnamento V. 3) scope globale F; durata = esecuzione di
fV; va deallocata F; keywordautoF. 4) creato in compilazione F; impilato V; una sola copia per processo F; dimensione dipende dalla funzione V. 5) funzione non definita V; variabile non inizializzata F; puntatore non inizializzato F; header non incluso F (errore di compilazione). 6) built-in F; unari in binari F;operator+cambia numero di argomenti V;operator<<non cambia F. 7)[]F; dereferenziabili V; deallocano F; classi nei container V. 8) locale automatico V; statico a fine scope F; argomentointF;deleteV. 9) riferimento spostabile F; puntatore a niente V; riferimento a niente F; copia tra riferimenti = copia dei riferiti V. 10) literal V; definizione F; due operandi V; un operatore V. - Polimorfismo: associa comportamenti diversi a una notazione. Dinamico = ereditarietà + virtuali, vincolato dalla base, run-time, collezioni eterogenee. Statico = template, svincolato (stessa sintassi), compile-time, una funzione per tipo. Esempi:
fai_parlare(const Animale&)conCane/Gatto→bau miao;fai_rumore<T>conAuto/Barca→brum tut.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// ---------- POLIMORFISMO DINAMICO: ereditarieta' + funzioni virtuali ----------
class Animale {
public:
virtual string verso() const = 0; // interfaccia comune
virtual ~Animale() {}
};
class Cane : public Animale { public: string verso() const override { return "bau"; } };
class Gatto : public Animale { public: string verso() const override { return "miao"; } };
void fai_parlare(const Animale& a) { cout << a.verso() << ' '; } // una sola funzione, decisione a RUN-TIME
// ---------- POLIMORFISMO STATICO: template ----------
struct Auto { string verso() const { return "brum"; } }; // nessuna base comune
struct Barca { string verso() const { return "tut"; } };
template <typename T>
void fai_rumore(const T& x) { cout << x.verso() << ' '; } // una funzione per tipo, decisione a COMPILE-TIME
int main()
{
Cane c;
Gatto g;
vector<const Animale*> zoo{&c, &g}; // collezione eterogenea: possibile solo con il dinamico
for (const Animale* a : zoo) fai_parlare(*a); // bau miao
cout << '\n';
Auto a;
Barca b;
fai_rumore(a); // fai_rumore<Auto>
fai_rumore(b); // fai_rumore<Barca>
cout << '\n';
}Allocazione dinamica: free store (heap);
new/new[]alloca (e costruisce),delete/delete[]distrugge e libera, il puntatore tiene l'indirizzo. Rispetto all'automatica: durata scelta, dimensione a run-time, rilascio manuale, più lenta. Problemi: leak, dangling, doppia delete,deleteal posto didelete[], accessi fuori dai limiti. Soluzioni:vector, smart pointer (C++ - smart pointerGli smart pointer (classi template di) sono oggetti che detengono un puntatore e lo deallocano automaticamente all'uscita dallo scope (RAII), eliminando memory leak e dangling pointer con un overhead minimo. unique_ptr ha un solo proprietario: non si copia (costruttore e assegnamento di copia cancellati) ma si sposta (std::move, ritorno da funzione), ha reset(), release(), get(), * e ->; due unique_ptr costruiti con lo stesso puntatore grezzo sono un errore (doppia delete) che il compilatore non vede, e si evita con make_unique. shared_ptr ha più proprietari: si copia, tiene un contatore di riferimenti (use_count) e libera l'oggetto quando l'ultimo shared_ptr viene distrutto, a un costo maggiore; weak_ptr osserva senza possedere, per rompere i cicli. auto_ptr è deprecato (rimosso in C++17).C++ - smart pointer → ).Eccezioni e memoria:
p = new int[s]; ... v.at(x) ...; delete[] p;con eccezione perde il blocco. Basic = nessuna perdita e stato valido (try/catch (...)condelete[]ethrow;); strong = commit or rollback (copia di lavoro eswap); no-throw =noexcept. Soluzione generale: RAII (vector,unique_ptr), lo stack unwinding chiama i distruttori. Vedi 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) →.Codice (a2t/eccezione_memoria.cpp):
#include <iostream>
#include <stdexcept>
#include <vector>
using namespace std;
// 1) PROBLEMA: se v.at(x) lancia, la delete[] non viene eseguita e la memoria e' persa
void con_problema(int s, int x)
{
int* p = new int[s];
vector<int> v;
p[0] = v.at(x); // lancia out_of_range (v e' vuoto)
delete[] p; // non raggiunta
}
// 2) BASIC GUARANTEE con try/catch: libera e rilancia
void con_basic(int s, int x)
{
int* p = new int[s];
vector<int> v;
try {
p[0] = v.at(x);
} catch (...) {
delete[] p; // nessuna perdita
throw; // l'eccezione continua verso chi sa gestirla
}
delete[] p;
}
// 3) RAII: nessun try/catch e nessuna delete, ci pensa il distruttore di vector
void con_raii(int s, int x)
{
vector<int> p(static_cast<size_t>(s));
vector<int> v;
p[0] = v.at(x);
}
// 4) STRONG GUARANTEE: lavoro su una copia, poi commit con operazioni che non lanciano
void aggiungi_due(vector<int>& dati, int a, int b)
{
vector<int> copia = dati; // se lancia, dati e' intatto
copia.push_back(a); // puo' lanciare (bad_alloc): sempre sulla copia
copia.push_back(b);
dati.swap(copia); // swap non lancia: commit
}
int main()
{
for (auto f : {con_problema, con_basic, con_raii}) {
try { f(10, 3); } catch (const exception& e) { cout << "catch: " << e.what() << '\n'; }
}
vector<int> d{1, 2};
aggiungi_due(d, 3, 4);
cout << d.size() << '\n';
}