Esercizio - Quiz e domande su funzioni virtuali, passaggio degli argomenti, dangling pointer e doppia deallocazione (quarto appello 2023-24, teoria)
In questa pagina 4
Testo. Prova scritta di Laboratorio di Programmazione, quarto appello 2023-24, parte di teoria (90 minuti): 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++ - ereditarietà, funzioni virtuali e layout degli oggettiL'ereditarietà pubblica (class D : public B) esprime la relazione "is a": D eredita da B i membri public e protected e li estende; la composizione esprime invece "has a". Una classe base di una gerarchia disabilita copia e assegnamento (=delete) perché copiando una derivata in una base si ha lo slicing. Una funzione virtuale può essere ridefinita nelle derivate (override: stesso nome, tipi e constness) e la funzione chiamata si decide a run-time in base al tipo dell'oggetto, non del puntatore o del riferimento; override esplicito con la keyword override; una funzione virtuale pura (= 0) obbliga le derivate a ridefinirla e rende astratta la classe; il distruttore di una classe base polimorfa è virtuale. Il meccanismo è realizzato con una vtbl per classe (tabella di puntatori alle funzioni) e un vptr in ogni oggetto; i dati della derivata seguono quelli della base.C++ - ereditarietà, funzioni virtuali e layout degli oggetti →, C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →, 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 →. La parte di programmazione dello stesso appello è Esercizio - Numeri complessi (quarto appello 2023-24, programmazione).
Quiz
1. In C++ il polimorfismo dinamico:
| affermazione | risposta | motivo |
|---|---|---|
| viene gestito tramite l'ereditarietà e le funzioni virtuali | V | |
| si concretizza specializzando il template di una classe/funzione con un tipo specifico | F | quello è il polimorfismo statico |
| presuppone un binding a tempo di compilazione | F | il binding è a run-time |
richiede la keyword template<typename T> nella definizione della classe |
F | i template servono al polimorfismo statico |
2. In C++11 la keyword constexpr:
| affermazione | risposta | motivo |
|---|---|---|
è in tutto e per tutto equivalente a const |
F | const dice "non modificabile"; constexpr dice "valore noto a compile-time" |
| identifica una variabile costante il cui valore è noto solo a tempo di esecuzione | F | è noto a tempo di compilazione |
una variabile costante che può essere solo di tipo std::string |
F | tipi letterali (interi, double, ...); std::string in C++11 non lo è |
| una variabile costante senza tipo | F | ha un tipo, come ogni variabile |
3. Il frammento v[i] = ++i;:
| affermazione | risposta | motivo |
|---|---|---|
il valore assegnato all'i-esimo elemento di v è sempre pari a i+1 |
F | dipende dall'ordine di valutazione, che lo standard (prima di C++17) non fissa: non è garantito |
il valore assegnato all'i-esimo elemento non è conoscibile a priori |
V | si legge i e lo si modifica nella stessa espressione: comportamento non specificato/indefinito |
l'operatore ++ non ha side effect |
F | modifica i |
| può generare segmentation fault | V | se l'indice calcolato esce dall'array |
Il compilatore usato per le prove, con -std=c++11 -Wall, segnala unsequenced modification and access to 'i'; con v di 4 elementi inizializzati a -1 scrive -1 1 -1 -1 (ha valutato prima ++i, quindi v[1] = 1), ma un altro compilatore potrebbe scrivere v[0] = 1. (Da C++17 il lato destro di un'assegnamento è valutato prima del sinistro: il frammento diventa definito, ma un'affermazione "non conoscibile a priori" resta la risposta attesa dal corso.) L'esperimento:
#include <iostream>
#include <vector>
int main()
{
std::vector<int> v(4, -1);
int i = 0;
v[i] = ++i; // quale elemento riceve il valore? dipende dall'ordine di valutazione
for (int x : v) std::cout << x << ' ';
std::cout << "i=" << i << '\n';
}4. Una lambda expression in C++:
5. L'allocazione student_list = new students[size];:
| affermazione | risposta | motivo |
|---|---|---|
per la deallocazione si usa delete[] students; |
F | si fa delete[] sul puntatore (student_list), non sul nome del tipo |
| provoca un'allocazione di memoria nel free store | V | |
provoca la chiamata del costruttore della classe students |
V | per ciascuno dei size elementi (costruttore di default) |
se ne deduce che student_list è un oggetto std::list<student> |
F | è un puntatore a students (students*) |
6. La compilazione di un progetto C++:
| affermazione | risposta | motivo |
|---|---|---|
| il compilatore controlla la corretta deallocazione della memoria dinamica | F | i leak non sono errori di compilazione |
| un errore di linking si può verificare quando manca una parte del software esterna al progetto (es. una libreria) | V | |
| un errore di linking si può verificare quando una funzione è dichiarata, chiamata, ma non definita | V | undefined reference |
| i commenti presenti nel codice sono tradotti in linguaggio macchina | F | sono rimossi dal preprocessore |
7. Gli iteratori begin e end di un container STL:
8. Uno std::unique_ptr:
| affermazione | risposta | motivo |
|---|---|---|
| è una classe template | V | unique_ptr<T> |
| supporta lo spostamento ma non la copia | V | |
| ha un costruttore che effettua l'allocazione dinamica della memoria | F | il costruttore riceve un puntatore già ottenuto con new; è make_unique ad allocare |
| ha un distruttore che dealloca la memoria puntata | V |
9. Lo scope in C++:
| affermazione | risposta | motivo |
|---|---|---|
| definisce una regione del testo di un programma | V | |
| si può definire uno scope di funzione dentro uno scope di classe | V | le funzioni membro |
può essere definito dentro un blocco { } (scope locale) |
V | |
| in un file header è attivato dalle include guard | F | le include guard evitano le inclusioni multiple: non creano scope |
10. Il layout della memoria di un programma in esecuzione:
Domanda aperta 1 (5 punti): funzioni virtuali e virtuali pure
Testo. Spiegare che cosa sono una funzione virtuale e una funzione virtuale pura. Descrivere che caratteristiche ha una classe che contiene almeno una funzione virtuale pura. Illustrare le conseguenze sulle classi derivate, corredando la descrizione di un esempio di codice. Descrivere i casi in cui è utile ricorrere a questi strumenti.
Risposta.
- Funzione virtuale: funzione membro dichiarata con
virtualin una classe base, che le derivate possono ridefinire (override: stesso nome, stessi tipi dei parametri, stessaconst). La chiamata fatta tramite puntatore o riferimento alla base esegue la versione dell'oggetto effettivo (decisione a run-time, polimorfismo dinamico), grazie alvptre allavtbl. La base ne fornisce un comportamento di default. - Funzione virtuale pura: si dichiara
virtual T f(...) = 0;; di norma non ha implementazione nella base ed esprime "ogni forma sa fare questo, ma la base non sa come". - Classe con almeno una virtuale pura = classe astratta: non si possono creare oggetti di quel tipo (solo puntatori e riferimenti ad essa); può avere dati membro, costruttori (usati dalle derivate), funzioni non virtuali e virtuali non pure; serve come interfaccia.
- Conseguenze sulle derivate: ogni derivata deve fare override di tutte le pure per essere istanziabile; se ne dimentica una resta astratta a sua volta (e
Forma f;, o l'istanza della derivata incompleta, dà errore di compilazione). Le virtuali non pure possono essere ereditate così come sono.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Forma { // classe ASTRATTA: ha una funzione virtuale pura
public:
virtual double area() const = 0; // pura: nessuna implementazione nella base, override obbligatorio
virtual string nome() const { return "forma"; } // virtuale NON pura: ha un comportamento di default
void stampa() const { cout << nome() << " con area " << area() << '\n'; } // non virtuale: usa le virtuali
virtual ~Forma() {} // distruttore virtuale
};
class Cerchio : public Forma {
public:
explicit Cerchio(double r) : r_{r} {}
double area() const override { return 3.0 * r_ * r_; } // pi greco ~ 3 per semplicita'
string nome() const override { return "cerchio"; }
private:
double r_;
};
class Rettangolo : public Forma {
public:
Rettangolo(double b, double h) : b_{b}, h_{h} {}
double area() const override { return b_ * h_; } // nome() resta quello della base
private:
double b_, h_;
};
class SenzaArea : public Forma { // non ridefinisce area(): e' ancora astratta
public:
string nome() const override { return "senza area"; }
};
int main()
{
// Forma f; // errore: Forma e' astratta
// SenzaArea s; // errore: anche SenzaArea e' astratta
Cerchio c{2.0};
Rettangolo r{3.0, 4.0};
vector<const Forma*> v{&c, &r}; // un puntatore alla base puo' puntare a qualunque derivata
for (const Forma* f : v) f->stampa(); // chiamata virtuale: area() e nome() sono quelle della derivata
}Uscita eseguita: cerchio con area 12 e forma con area 12. Forma::stampa() non è virtuale ma chiama le virtuali nome() e area(): per il Cerchio escono i valori del cerchio, per il Rettangolo (che non ridefinisce nome()) il nome di default forma e l'area 12 del rettangolo . Forma f; e SenzaArea s; non compilano.
- Quando usarle: quando si ha una famiglia di tipi con interfaccia comune e comportamenti diversi (forme, robot con politiche di movimento, widget), e si vogliono gestire insieme in un contenitore di puntatori (
vector<const Forma*>) o in funzioni che accettano la base; la virtuale pura quando la base non ha un comportamento sensato e si vuole obbligare le derivate a fornirlo; nelle basi polimorfe si dichiara sempre il distruttorevirtual. Non si usano per tipi senza parentela, o quando la scelta è nota a compile-time (qui i template, 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): passaggio per copia, riferimento e puntatore
Testo. Illustrare il passaggio di argomenti per copia, reference e puntatore, evidenziando le differenze e i punti in comune tra le tre opzioni e fornendo un esempio di codice per ciascuna (senza usare l'allocazione dinamica della memoria). In quali casi la funzione chiamante può vedere le variabili passate per argomento modificate dopo la chiamata?
Risposta.
| per copia | per reference | per puntatore | |
|---|---|---|---|
| firma | void f(int a) |
void f(int& a) |
void f(int* a) |
| chiamata | f(x) |
f(x) |
f(&x) |
| che cosa riceve la funzione | una copia del valore | un alias (un altro nome) dell'argomento | una copia dell'indirizzo dell'argomento |
| accesso | a |
a |
*a |
| modifica dell'originale | no | sì | sì (tramite *a) |
| costo | copia dell'oggetto | nessuna copia | copia di un indirizzo |
| può essere nullo | no | no | sì (nullptr) |
Punti in comune: in tutte e tre le forme l'argomento è valutato prima della chiamata e il parametro vive nel record di attivazione della funzione (per riferimento e puntatore occupa al più un indirizzo); reference e puntatore possono essere const (const int&, const int*) per garantire che la funzione non modifichi l'originale; tutte e tre si usano per passare oggetti grandi senza che il chiamante debba cambiare la logica della funzione.
#include <iostream>
using namespace std;
void scambia_copia(int a, int b) { int t = a; a = b; b = t; cout << " dentro (copia): a=" << a << " b=" << b << '\n'; }
void scambia_ref(int& a, int& b) { int t = a; a = b; b = t; }
void scambia_ptr(int* a, int* b) { int t = *a; *a = *b; *b = t; }
int main()
{
int x = 1, y = 2;
scambia_copia(x, y); // gli argomenti sono COPIATI: scambia le copie
cout << "dopo copia: x=" << x << " y=" << y << '\n'; // 1 2: nessun effetto
scambia_ref(x, y); // a e b sono alias di x e y
cout << "dopo riferimento: x=" << x << " y=" << y << '\n'; // 2 1
scambia_ptr(&x, &y); // si passano gli indirizzi: *a e *b sono x e y
cout << "dopo puntatore: x=" << x << " y=" << y << '\n'; // 1 2
}Uscita eseguita:
dentro (copia): a=2 b=1
dopo copia: x=1 y=2
dopo riferimento: x=2 y=1
dopo puntatore: x=1 y=2La funzione di scambio è l'esempio classico: con la copia scambia_copia scambia le due copie (dentro vale 2 e 1) e le variabili del chiamante non cambiano; con il riferimento e con il puntatore lo scambio avviene sulle variabili originali (x,y passano da 1,2 a 2,1 e poi tornano a 1,2 con la terza chiamata).
Quando il chiamante vede le variabili modificate: solo se l'argomento è passato per reference non const o per puntatore non a const (e la funzione le modifica). Con il passaggio per copia no, e con const& / const* la modifica è proibita dal compilatore. (Eccezione di cui tenere conto: se si passa per copia un puntatore o un oggetto che contiene puntatori, la funzione può modificare ciò a cui puntano: si modifica il puntato, non l'argomento.)
Domanda aperta 3 (4 punti): dangling pointer e doppia deallocazione
Testo. Spiegare i problemi del dangling pointer e della doppia deallocazione di memoria. Per ciascuno dei due, produrre un esempio di codice affetto dal relativo problema evidenziando quali sono le istruzioni che lo causano, disegnando le relazioni tra i puntatori e i blocchi di memoria nel free store. Per ciascuno degli esempi precedenti produrre un ulteriore esempio che corregge il problema.
Risposta.
- Dangling pointer: un puntatore che contiene l'indirizzo di memoria già rilasciata (o di una variabile locale non più viva). Usarlo (leggere o scrivere) è un accesso a memoria non più propria: comportamento indefinito, spesso senza errore immediato, con corruzione silenziosa di altri dati.
- Doppia deallocazione: fare
deletedue volte sullo stesso blocco. La seconda corrompe la gestione dello heap e può far terminare il programma. Nasce tipicamente da due puntatori che condividono un blocco e da una copia superficiale di una classe.
Esempi difettosi (le funzioni non vengono chiamate: eseguirle è comportamento indefinito; il file compila):
// DUE PROGRAMMI DIFETTOSI: le funzioni non vengono chiamate dal main (eseguirle e' comportamento indefinito).
void dangling()
{
int* p = new int{5}; // p ----> [5]
int* q = p; // p ----> [5] <---- q (due puntatori allo stesso blocco)
delete p; // il blocco e' restituito: p ---> (libero) <--- q
*q = 7; // ERRORE: q e' un dangling pointer, si scrive su memoria non piu' nostra
}
void doppia_delete()
{
int* a = new int{1}; // a ----> [1]
int* b = a; // a ----> [1] <---- b
delete a; // il blocco e' liberato
delete b; // ERRORE: seconda deallocazione dello stesso blocco
}
int main() { return 0; }Relazioni nel free store, passo per passo:
| istruzione | free store | puntatori |
|---|---|---|
int* p = new int{5}; |
blocco B1 = [5] |
p -> B1 |
int* q = p; |
B1 = [5] |
p -> B1 <- q |
delete p; |
B1 liberato |
p -> (libero) <- q |
*q = 7; |
scrittura in B1, che non è più nostro |
q è dangling (e p pure) |
| istruzione | free store | puntatori |
|---|---|---|
int* a = new int{1}; |
B2 = [1] |
a -> B2 |
int* b = a; |
B2 = [1] |
a -> B2 <- b |
delete a; |
B2 liberato |
a -> (libero) <- b |
delete b; |
seconda delete di B2 |
doppia deallocazione |
Le istruzioni che causano i problemi sono *q = 7; (uso dopo la delete) e delete b; (seconda delete): la causa profonda è la presenza di due puntatori allo stesso blocco senza una regola su chi lo possiede.
Esempi corretti (compilati ed eseguiti):
#include <iostream>
#include <memory>
using namespace std;
void dangling_corretto_1()
{
int* p = new int{5}; // p ----> [5]
int* q = p; // p ----> [5] <---- q
*q = 7; // si usa il blocco MENTRE e' vivo
cout << " *p = " << *p << '\n';
delete p; // un'unica delete
p = nullptr; // si azzerano ENTRAMBI i puntatori che puntavano al blocco:
q = nullptr; // un uso successivo diventa un errore visibile, non un'alterazione silenziosa
}
void dangling_corretto_2()
{
unique_ptr<int> p = make_unique<int>(5); // un solo proprietario: nessun secondo puntatore che possa restare dangling
*p = 7;
cout << " *p = " << *p << '\n';
} // liberato qui, automaticamente
void doppia_delete_corretta_1()
{
int* a = new int{1}; // a ----> [1]
int* b = a; // a ----> [1] <---- b
delete a; // una sola delete...
a = nullptr;
b = nullptr; // ...e b non viene piu' usato per liberare
cout << " a e b nulli: " << (a == nullptr && b == nullptr) << '\n';
}
void doppia_delete_corretta_2()
{
shared_ptr<int> a = make_shared<int>(1); // il blocco ha un contatore dei proprietari
shared_ptr<int> b = a; // contatore = 2
cout << " use_count = " << a.use_count() << '\n';
} // a e b muoiono: contatore a 0, UNA sola deallocazione
int main()
{
cout << "dangling, versione con nullptr\n"; dangling_corretto_1();
cout << "dangling, versione con unique_ptr\n"; dangling_corretto_2();
cout << "doppia delete, versione con una sola delete\n"; doppia_delete_corretta_1();
cout << "doppia delete, versione con shared_ptr\n"; doppia_delete_corretta_2();
}Uscita eseguita: *p = 7 (due volte), a e b nulli: 1, use_count = 2. Le correzioni: (1) usare il blocco solo mentre è vivo e azzerare con nullptr tutti i puntatori che puntavano al blocco dopo la delete (un uso successivo diventa un errore visibile); (2) avere un solo proprietario con unique_ptr (il blocco è rilasciato all'uscita dallo scope, non esistono altri puntatori che possano restare dangling) oppure, se la proprietà è davvero condivisa, shared_ptr con il contatore dei riferimenti: la deallocazione è una sola, quando muore l'ultimo; (3) per la doppia delete, fare una sola delete dal proprietario. Dettagli: C++ - smart pointerGli smart pointer (classi template di
Versione ripasso
- Quiz (risposte V/F): 1) dinamico = ereditarietà e virtuali V; specializzare template F; binding a compile-time F; richiede
template<typename T>F. 2)constexpr=constF; valore noto solo a run-time F; solostd::stringF; senza tipo F. 3) semprei+1F; non conoscibile a priori V;++senza side effect F; può dare segmentation fault V. 4) lambda anonima V; non accetta argomenti F; senza tipo restituito F;[&]cattura per riferimento V. 5)delete[] studentsF; free store V; chiama il costruttore V; èstd::listF. 6) il compilatore controlla le deallocazioni F; linking con libreria mancante V; funzione non definita V; commenti in codice macchina F. 7)begin() == end()possibile V;endpunta all'ultimo F; definiti anche per container con[]V; solo con almeno un elemento F. 8) classe template V; sposta ma non copia V; costruttore che alloca F; distruttore che dealloca V. 9) regione del testo V; scope di funzione in scope di classe V; scope locale in{ }V; attivato dalle include guard F. 10) uninitialized data V; free store con allocazione dinamica V; initialized data solo read-only F; text segment = nome dell'eseguibile F. - Virtuale e virtuale pura: virtuale = ridefinibile, scelta a run-time sul tipo dell'oggetto (vptr, vtbl), con comportamento di default. Pura (
= 0) = nessuna implementazione, override obbligatorio. Classe con una pura = astratta: niente oggetti, solo puntatori e riferimenti, serve da interfaccia; le derivate devono fare override di tutte le pure o restano astratte. Si usano per famiglie di tipi con interfaccia comune in un contenitore di puntatori; basi polimorfe con distruttorevirtual.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Forma { // classe ASTRATTA: ha una funzione virtuale pura
public:
virtual double area() const = 0; // pura: nessuna implementazione nella base, override obbligatorio
virtual string nome() const { return "forma"; } // virtuale NON pura: ha un comportamento di default
void stampa() const { cout << nome() << " con area " << area() << '\n'; } // non virtuale: usa le virtuali
virtual ~Forma() {} // distruttore virtuale
};
class Cerchio : public Forma {
public:
explicit Cerchio(double r) : r_{r} {}
double area() const override { return 3.0 * r_ * r_; } // pi greco ~ 3 per semplicita'
string nome() const override { return "cerchio"; }
private:
double r_;
};
class Rettangolo : public Forma {
public:
Rettangolo(double b, double h) : b_{b}, h_{h} {}
double area() const override { return b_ * h_; } // nome() resta quello della base
private:
double b_, h_;
};
class SenzaArea : public Forma { // non ridefinisce area(): e' ancora astratta
public:
string nome() const override { return "senza area"; }
};
int main()
{
// Forma f; // errore: Forma e' astratta
// SenzaArea s; // errore: anche SenzaArea e' astratta
Cerchio c{2.0};
Rettangolo r{3.0, 4.0};
vector<const Forma*> v{&c, &r}; // un puntatore alla base puo' puntare a qualunque derivata
for (const Forma* f : v) f->stampa(); // chiamata virtuale: area() e nome() sono quelle della derivata
}Passaggio degli argomenti: per copia (copia del valore, l'originale non cambia), per reference (alias: modifica l'originale, nessuna copia, mai nullo), per puntatore (copia dell'indirizzo, accesso con
*, modifica l'originale, può esserenullptr); reference e puntatore possono essereconst. Esempioscambia: la copia non scambia (x=1 y=2), reference e puntatore sì. Il chiamante vede le modifiche solo con reference nonconsto puntatore non aconst.Dangling pointer (puntatore a memoria rilasciata:
p,q = p,delete p,*q = 7) e doppia deallocazione (a,b = a,delete a,delete b): nascono da due puntatori allo stesso blocco senza un proprietario. Correzioni: usare il blocco solo finché è vivo e azzerare tutti i puntatori dopo ladelete; un solo proprietario conunique_ptr; proprietà condivisa conshared_ptr(un'unica deallocazione); una soladelete. Vedi 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 → .Codice (a4t/scambia.cpp):
#include <iostream>
using namespace std;
void scambia_copia(int a, int b) { int t = a; a = b; b = t; cout << " dentro (copia): a=" << a << " b=" << b << '\n'; }
void scambia_ref(int& a, int& b) { int t = a; a = b; b = t; }
void scambia_ptr(int* a, int* b) { int t = *a; *a = *b; *b = t; }
int main()
{
int x = 1, y = 2;
scambia_copia(x, y); // gli argomenti sono COPIATI: scambia le copie
cout << "dopo copia: x=" << x << " y=" << y << '\n'; // 1 2: nessun effetto
scambia_ref(x, y); // a e b sono alias di x e y
cout << "dopo riferimento: x=" << x << " y=" << y << '\n'; // 2 1
scambia_ptr(&x, &y); // si passano gli indirizzi: *a e *b sono x e y
cout << "dopo puntatore: x=" << x << " y=" << y << '\n'; // 1 2
}Teoria collegata
- C++ - ereditarietà, funzioni virtuali e layout degli oggetti
- C++ - funzioni, reference e passaggio dei parametri
- C++ - allocazione dinamica della memoria con new e delete
- STL - algoritmi, predicati e lambda
- STL - sequenze, iteratori e container (vector, list, string)
- C++ - chiamate a funzione e layout della memoria
- C++ - polimorfismo
- C++ - smart pointer