Distruttori, RAII e Metodi Costanti
~Classe): il metodo eseguito quando un oggetto viene distruttoconst: metodi che non possono modificare l’oggettodelete.
Matrix<ntype>): riservano la memoria quando vengono create e la liberano quando vengono distrutte, con il meccanismo che vediamo oggi.Sequential sono creati nell’heap, ma li possiede la rete, che li libera quando viene distrutta.const. I metodi che leggono soltanto sono const: get_type() const restituisce il nome del layer, size() const il numero di layer di Sequential.DenseLayer(int Nn, int Ni, int type = 0, int distro = 1): Nn neuroni, Ni ingressi; type e distro scelgono come inizializzare i pesi. Chi indica solo Nn e Ni ottiene type = 0 e distro = 1.&x è l’indirizzo di x; p, di tipo double* (puntatore a double), lo contiene.
*p è il valore all’indirizzo contenuto in p, cioè x: scrivere in *p modifica x.

Ogni riquadro è una variabile: nome sopra, valore dentro, indirizzo (di esempio) sotto. La freccia: p contiene l’indirizzo di x.
La memoria di un programma ha due zone (Lezione 08).
{ }. Una variabile locale viene liberata da sola quando il programma esce dal blocco in cui è dichiarata.new. Resta occupata finché il programma non la restituisce con delete (o delete[] per un array).Una variabile sullo stack può contenere l’indirizzo di memoria nell’heap: è il caso dei prossimi esempi. Quando la variabile viene liberata si perde l’indirizzo, ma la memoria nell’heap resta occupata.
new double[n] riserva nell’heap n valori double consecutivi e restituisce l’indirizzo del primo. v sta sullo stack e contiene quell’indirizzo.
v[i] è l’elemento i dell’array che comincia a quell’indirizzo, come in C.
delete[].
v è una variabile locale del main: sta sullo stack e contiene l’indirizzo 0x5600a0.double stanno nell’heap, uno dopo l’altro, a partire da quell’indirizzo.main v viene liberata da sola, perché è sullo stack. L’array no: se manca delete[] v;, i quattro double restano occupati e nessuna variabile ne conosce più l’indirizzo. È un memory leak (Lezione 08).struct e puntatorib sta sullo stack, i 4 double nell’heap: in b.data c’è solo il loro indirizzo.
CMatrix del Laboratorio 04.

Alla fine del main viene liberata b, con l’indirizzo contenuto in b.data, ma non l’array: senza delete[] b.data; resta occupato.
Con il distruttore il delete[] si scrive una volta sola, dentro la classe.
Il distruttore è il metodo eseguito quando l’oggetto viene distrutto:
~ (riga 6);r vive nel blocco delle righe 10–13 e viene distrutta alla graffa della riga 13. Output: costruttore, dentro il blocco, distruttore, fine.
Le variabili locali vengono distrutte nell’ordine inverso a quello di creazione: l’ultima creata è la prima distrutta (LIFO, last in, first out).
#include <iostream>
class Elemento {
private:
int id_;
public:
Elemento(int id) : id_(id) { std::cout << "creato " << id_ << "\n"; }
~Elemento() { std::cout << "distrutto " << id_ << "\n"; }
};
int main() {
Elemento a(1);
Elemento b(2);
Elemento c(3);
1 return 0;
}creato 1, creato 2, creato 3, poi distrutto 3, distrutto 2, distrutto 1.
Il motivo: un oggetto creato dopo può usare quelli creati prima. Se viene distrutto per primo, durante il suo distruttore gli altri esistono ancora.
La struct del ripasso diventa una classe:
b viene distrutto, e con lui l’array.Nel main nessun delete; con -fsanitize=address nessun leak.
return anticipato senza leak#include <iostream>
class Buffer {
private:
double* data_;
public:
Buffer(int size) : data_(new double[size]) {}
~Buffer() { delete[] data_; }
};
void leggiFile() {
Buffer buffer(1000);
bool errore_lettura = true;
if (errore_lettura) {
std::cerr << "Errore di lettura\n";
return;
}
}
int main() { leggiFile(); return 0; }Nella Lezione 08 il delete[] mancava sul ramo d’errore. Qui l’array lo alloca il costruttore di buffer (riga 12), variabile locale.
return si esce dalla funzione: buffer viene distrutto e il distruttore libera l’array.Il distruttore viene chiamato a ogni uscita: nessun delete[] da ricordare.
Questo modo di scrivere le classi si chiama RAII (Resource Acquisition Is Initialization, “l’acquisizione di una risorsa avviene nell’inizializzazione”). È un idioma del C++, con tre regole:
Buffer.Così il distruttore viene eseguito da solo a ogni uscita dal blocco.
Anche la libreria standard segue il RAII: std::vector libera la sua memoria, std::ofstream chiude il suo file.
newAnche con new il costruttore viene eseguito: new riserva la memoria e chiama il costruttore. Il distruttore è automatico solo per le variabili locali; per un oggetto creato con new lo chiama delete, che poi libera la memoria.
Buffer b(1000); |
Buffer* p = new Buffer(1000); |
|
|---|---|---|
| Dove sta | sullo stack | nell’heap; p contiene il suo indirizzo |
| Costruttore | alla dichiarazione | eseguito da new |
| Distruttore | all’uscita dal blocco | solo con delete p; |
Senza delete |
nessun problema | il distruttore non parte: si perdono il Buffer e il suo array |
Buffer è scritta secondo il RAII, ma usata così non lo rispetta. Il RAII non vieta new, ma un new senza proprietario: il new va nel costruttore di un oggetto che fa il delete nel suo distruttore, come in Buffer.
#include <fstream>
#include <string>
class Registro {
private:
std::ofstream file_;
public:
Registro(const std::string& nomefile) {
file_.open(nomefile);
}
~Registro() { file_.close(); }
void scrivi(const std::string& riga) {
file_ << riga << "\n";
}
};
int main() {
Registro registro("registro.txt");
registro.scrivi("inizio del calcolo");
registro.scrivi("fine del calcolo");
return 0;
}La risorsa può essere un file: std::ofstream (da <fstream>) ci scrive con <<, come std::cout.
registro viene distrutto e il file, con le due righe, è chiuso.Il close() è esplicito per mostrare il rilascio: std::ofstream chiuderebbe il file anche da solo.
const: il problemaIl metodo valore() serve a leggere l’attributo, non a modificarlo. Se per errore lo modifica, il compilatore non lo segnala: non ha modo di sapere che il metodo doveva soltanto leggere.
#include <iostream>
class Grandezza {
private:
double valore_;
public:
Grandezza(double v) : valore_(v) {}
double valore() {
1 valore_ = 0.0;
return valore_;
}
};
int main() {
Grandezza g(9.81);
std::cout << g.valore() << "\n"; // stampa 0
return 0;
}valore_. Il programma compila senza avvisi, anche con -Wall, e stampa 0 invece di 9.81.
const: la soluzione#include <iostream>
class Grandezza {
private:
double valore_;
public:
Grandezza(double v) : valore_(v) {}
void imposta(double v) { valore_ = v; }
double valore() const {
// valore_ = 0.0;
return valore_;
}
};
int main() {
Grandezza g(9.81);
g.imposta(1.62);
std::cout << g.valore() << "\n"; // 1.62
return 0;
}Riga 9: il const dopo la parentesi dei parametri dichiara che il metodo non modifica l’oggetto; imposta() (riga 8) lo modifica e non è const. Il compilatore lo verifica: dentro un metodo const gli attributi si possono leggere, non modificare.
Togliendo il commento della riga 10 la compilazione si ferma (assignment of member ... in read-only object).
Il const fa parte della firma: se il metodo è definito fuori dalla classe, si ripete, double Grandezza::valore() const { ... }.
const e const reference#include <iostream>
class Vettore2D {
private:
double x_, y_;
public:
Vettore2D(double x, double y) : x_(x), y_(y) {}
void scala(double f) { x_ *= f; y_ *= f; }
double norma2() const { return x_ * x_ + y_ * y_; }
};
void stampaNorma2(const Vettore2D& v) {
std::cout << v.norma2() << "\n";
// v.scala(2.0);
}
int main() {
Vettore2D v(3.0, 4.0);
stampaNorma2(v); // 25
return 0;
}Il parametro const Vettore2D& v (riga 12, Lezione 05) evita la copia e vieta di modificare v: su v si possono chiamare solo metodi const.
norma2(), il quadrato della norma, è const: ammessa.scala() non lo è: errore, se si toglie il commento.Se norma2() non fosse const, anche la riga 13 darebbe errore: i metodi che non modificano l’oggetto vanno tutti dichiarati const.
#include <iostream>
class Contatore {
private:
int conteggio_ = 0;
public:
void incrementa(int passo = 1);
int valore() const { return conteggio_; }
};
void Contatore::incrementa(int passo) {
conteggio_ += passo;
}
int main() {
Contatore c;
c.incrementa(); // passo = 1
c.incrementa(5); // passo = 5
std::cout << c.valore() << "\n"; // 6
return 0;
}Un argomento di default (Lezione 05) è il valore usato quando l’argomento manca: c.incrementa() equivale a c.incrementa(1).
Se dichiarazione e definizione sono separate (Lezione 08), il default si scrive solo nella dichiarazione (riga 7, nel .hpp).
Ripeterlo nella definizione (riga 11) è un errore di compilazione (default argument given for parameter 1 con g++).
Un costruttore con tutti gli argomenti di default si può chiamare anche senza argomenti. Se nella classe c’è anche un costruttore senza argomenti, per Grandezza b; vanno bene tutti e due, e il compilatore non può scegliere.
Togliendo il commento dalla riga di b la compilazione si ferma; con g++: call of overloaded 'Grandezza()' is ambiguous.
Il rimedio è tenere uno solo dei due costruttori.
#include <iostream>
#include <cmath>
#include <string>
void stampa(const std::string& msg,
bool a_capo = true) {
std::cout << msg;
if (a_capo) std::cout << "\n";
}
class Vettore2D {
private:
double x_, y_;
public:
Vettore2D(double x, double y)
: x_(x), y_(y) {}
Vettore2D(double angolo)
: x_(std::cos(angolo)), y_(std::sin(angolo)) {}
};
int main() {
stampa("prima riga");
stampa("seconda riga, senza a capo", false);
Vettore2D v(1.0, 2.0); // componenti
Vettore2D u(0.0); // versore con angolo 0
return 0;
}Argomento di default (righe 4–5): il codice è lo stesso e cambia solo un’opzione, andare a capo o no.
Overloading (Lezione 05: più funzioni con lo stesso nome e parametri diversi; righe 13–16): i due costruttori calcolano x_ e y_ in modi diversi, dalle componenti cartesiane oppure dall’angolo di un versore. Un solo costruttore con un argomento di default non potrebbe fare entrambe le cose.
DynamicArray alloca un array di double nell’heap nel costruttore e lo libera nel distruttore. Header e main sono dati; voi completate DynamicArray.cpp.
In una cartella create DynamicArray.hpp, main.cpp e DynamicArray.cpp, copiando il codice dalle due slide seguenti.
Compilate subito, prima di scrivere codice:
Lo scheletro compila: stampa una riga di zeri, uno 0 e la riga del distruttore.
Completate DynamicArray.cpp: in ciascuna delle quattro funzioni manca una sola riga, indicata dal commento // da completare. Il controllo sugli indici è già scritto.
main, e l’AddressSanitizer non deve stampare nulla.delete[] nel distruttore, ricompilate ed eseguite. L’AddressSanitizer deve segnalare 40 byte(s) leaked in 1 allocation(s): l’array di 5 double da 8 byte non è stato liberato. Su macOS usate leaks, come nella Lezione 08. Poi rimettete il delete[].mainDynamicArray.hpp
Output atteso, a esercizio completato:
0 0.5 1 1.5 2
0
distruttore: liberati 5 elementi
La seconda riga è leggi(7): gli indici validi vanno da 0 a 4, quindi leggi restituisce 0.
main.cpp
#include <iostream>
#include "DynamicArray.hpp"
int main() {
DynamicArray a(5);
for (int i = 0; i < 5; i++) {
a.imposta(i, 0.5 * i);
}
for (int i = 0; i < 5; i++) {
std::cout << a.leggi(i) << " ";
}
std::cout << "\n";
a.imposta(7, 1.0); // indice non valido
std::cout << a.leggi(7) << "\n";
return 0;
}DynamicArray.cpp
#include <iostream>
#include "DynamicArray.hpp"
DynamicArray::DynamicArray(int size) : size_(size), data_(nullptr) {
// da completare (una riga): data_ riceve un array di size_ double (new[])
}
DynamicArray::~DynamicArray() {
// da completare (una riga): liberate data_ con delete[]
std::cout << "distruttore: liberati " << size_ << " elementi\n";
}
void DynamicArray::imposta(int indice, double valore) {
if (indice >= 0 && indice < size_) { // indice valido
// da completare (una riga): scrivete valore in data_[indice]
}
}
double DynamicArray::leggi(int indice) const {
if (indice >= 0 && indice < size_) { // indice valido
// da completare (una riga): restituite data_[indice]
}
return 0.0; // indice non valido
}DynamicArray.cpp
#include <iostream>
#include "DynamicArray.hpp"
DynamicArray::DynamicArray(int size) : size_(size), data_(nullptr) {
data_ = new double[size_];
}
DynamicArray::~DynamicArray() {
delete[] data_;
std::cout << "distruttore: liberati " << size_ << " elementi\n";
}
void DynamicArray::imposta(int indice, double valore) {
if (indice >= 0 && indice < size_) { // indice valido
data_[indice] = valore;
}
}
double DynamicArray::leggi(int indice) const {
if (indice >= 0 && indice < size_) { // indice valido
return data_[indice];
}
return 0.0; // indice non valido
}Header e main sono quelli dati.
main.cpp
#include <iostream>
#include "DynamicArray.hpp"
int main() {
DynamicArray a(5);
for (int i = 0; i < 5; i++) {
a.imposta(i, 0.5 * i);
}
for (int i = 0; i < 5; i++) {
std::cout << a.leggi(i) << " ";
}
std::cout << "\n";
a.imposta(7, 1.0); // indice non valido
std::cout << a.leggi(7) << "\n";
return 0;
}Supponiamo di aggiungere al main la riga DynamicArray b = a;.
a in b: size_ e data_. data_ è un indirizzo, quindi b riceve lo stesso indirizzo di a, non una copia dell’array.a e b puntano allo stesso array nell’heap.main vengono distrutti entrambi, e i due distruttori eseguono delete[] sullo stesso indirizzo: è il double free della Lezione 08, e il programma termina con un errore.Come scrivere una classe che si copia correttamente è l’argomento della Lezione 10.
fine del blocco
distrutto 2
fine del main
distrutto 3
distrutto 1
b è dichiarata nel blocco interno: viene distrutta alla sua graffa di chiusura, prima che venga creata c.main restano a e c. Vengono distrutte in ordine inverso a quello di creazione: prima c, poi a.