ProgrammazioneSviluppatore C++ Middle

Spiega le differenze tra shallow e deep copy in C++ con l'esempio di un contenitore con memoria dinamica. Come implementare manualmente la copia profonda?

Supera i colloqui con l'assistente IA Hintsage

Risposta.

Il meccanismo di copia degli oggetti in C++ si divide in copia superficiale (shallow copy) e copia profonda (deep copy). La differenza è particolarmente importante per le classi con memoria allocata dinamicamente.

Storia della domanda

In C++, molte strutture dati lavorano con la memoria dinamica (new/delete). Per impostazione predefinita, il compilatore genera un costruttore di copia e un operatore di assegnazione che esegue una copia byte per byte (shallow copy). Questo è veloce, ma pericoloso se l'oggetto gestisce risorse esterne.

Problema

La shallow copy copia solo gli indirizzi delle risorse allocate dinamicamente. Quando si elimina un oggetto, la memoria viene liberata, mentre un'altra istanza rimane con un puntatore "dangling". Di conseguenza, si verificano double delete, perdite di memoria e crash.

Soluzione

La copia profonda implica la creazione esplicita di copie di tutte le risorse dinamiche. Per fare ciò, nella classe è necessario implementare personalmente il costruttore di copia e l'operatore di assegnazione, in modo da garantire una copia di ogni elemento.

Esempio di codice per una classe con un array:

class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // Costruttore di copia profonda DynArray(const DynArray& other) : size(other.size), data(new int[other.size]) { for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } // Assegnazione profondamente copia DynArray& operator=(const DynArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } return *this; } };

Caratteristiche principali:

  • La shallow copy copia i puntatori, la deep copy crea nuove istanze di memoria dinamica.
  • Per la deep copy è necessario implementare la propria logica di copia nel costruttore e nell'operatore=
  • Ignorare la necessità di deep copy porta a bug difficili da individuare.

Domande trabocchetto.

Il compilatore genera sempre correttamente il costruttore di copia e l'operatore di assegnazione, vero?

Risposta:

Falso. Per le classi con risorse dinamiche, la copia per impostazione predefinita è errata: entrambi gli oggetti possederanno la stessa risorsa. La deep copy deve essere implementata esplicitamente quando si possiedono risorse esterne.

È necessario implementare un distruttore se è stato scritto solo il costruttore/assegnazione a copia profonda?

Risposta:

Sì, altrimenti si verificherà una perdita di memoria: se si libera la memoria nel costruttore di copia utente, ma non si implementa un distruttore, la memoria non verrà liberata durante la distruzione degli oggetti.

Può std::vector contenere puntatori e perché durante la sua copia possono verificarsi perdite?

Risposta:

Sì, std::vector può contenere puntatori. Durante la copia di tale std::vector, vengono copiati solo i puntatori, non gli oggetti a cui puntano. Questa è una shallow copy: se è necessaria una deep copy di tutto il contenuto, sarà necessario copiare manualmente ogni oggetto e allocarli in memoria in modo indipendente.

Esempio:

std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // Vengono copiati i puntatori, non *int

Errori tipici e antipattern

  • Ignorare la necessità di implementare la Rule of Three.
  • Copiare puntatori pensando che sia una copia dell'oggetto.
  • Non liberare le risorse dinamiche nel distruttore.
  • Usare shallow copy per classi con risorse possedute.

Esempio dalla vita reale

Caso negativo

Un programmatore implementa una classe avvolgente per un array senza ridefinire il costruttore di copia/l'operatore di assegnazione. Di conseguenza, entrambi gli oggetti possiedono la stessa memoria, la distruzione di uno porta a un crash durante l'accesso all'altro.

Vantaggi:

  • Funziona rapidamente (senza copie).

Svantaggi:

  • Errori di runtime molto difficili da individuare; presenza di double free/segfault.

Caso positivo

Un sviluppatore implementa la copia profonda: si copia il contenuto dell'array, c'è un proprio distruttore e un operatore di assegnazione con protezione contro self-assignment.

Vantaggi:

  • Copia e rilascio di memoria sicuri.
  • Codice manutenibile e estensibile.

Svantaggi:

  • Qualche riga di codice in più e oneri di memoria.
  • Più complesso per classi con più risorse dinamiche.