Salta al contenuto
Note per Studenti 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 funzioni virtuali, passaggio degli argomenti, dangling pointer e doppia deallocazione (quarto appello 2023-24, teoria)

Esame
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:

a4t/ordine.cppcpp
#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++:

affermazione risposta motivo
è una funzione di cui non è definito il nome V funzione anonima, definita nel punto d'uso
non può accettare argomenti F [](int x) { ... }
non ha un tipo restituito F ce l'ha: dedotto o indicato con -> tipo
è in grado di catturare le variabili locali per riferimento con la clausola [&] V (STL - algoritmi, predicati e lambdaLa STL ha circa 80 algoritmi generici (header e ) che lavorano su sequenze [first, last) di qualunque contenitore tramite iteratori: find/find_if, count/count_if, sort, copy/copy_if/unique_copy, merge, equal_range, equal, accumulate, inner_product. find cerca un valore e restituisce l'iteratore alla prima occorrenza o end(); find_if verifica un predicato (funzione che restituisce bool) passato come puntatore a funzione, function object (classe con operator()) o lambda expression. Una variabile globale per passare la soglia al predicato è da evitare; il function object la memorizza in un dato membro. Una lambda cattura -> tipo { corpo } definisce la funzione sul posto e può catturare le variabili locali per valore ([=], [x]) o per riferimento ([&], [&x]). sort(b, e) ordina con < e sort(b, e, cmp) con un criterio, solo su iteratori random access; copy richiede spazio a destinazione.STL - algoritmi, predicati e lambda →)

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 virtual in una classe base, che le derivate possono ridefinire (override: stesso nome, stessi tipi dei parametri, stessa const). La chiamata fatta tramite puntatore o riferimento alla base esegue la versione dell'oggetto effettivo (decisione a run-time, polimorfismo dinamico), grazie al vptr e alla vtbl. 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.
a4t/virtuali.cppcpp
#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 3⋅43 \cdot 4. Forma f; e SenzaArea s; non compilano.

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.

a4t/scambia.cppcpp
#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=2

La 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 delete due 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):

a4t/difetti.cppcpp
// 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):

a4t/corretti.cppcpp
#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 ) 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 →.

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 = const F; valore noto solo a run-time F; solo std::string F; senza tipo F. 3) sempre i+1 F; 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[] students F; free store V; chiama il costruttore V; è std::list F. 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; end punta 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 distruttore virtual.
a4t/virtuali.cppcpp
#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
}
a4t/scambia.cppcpp
#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