ProgrammazioneSviluppatore C++

Spiega la differenza tra shallow copy e deep copy per classi con memoria dinamica. Quando e perché è necessario un costruttore di copia profondo? Come implementare manualmente il deep copy?

Supera i colloqui con l'assistente IA Hintsage

Risposta.

Storia della questione

In C++, la copia degli oggetti avviene inizialmente tramite il meccanismo di memberwise copying: per ogni membro dell'oggetto viene chiamata l'operazione di copia appropriata. Questo è sicuro per i tipi incorporati, ma per le risorse dinamiche si presenta un problema: vengono copiati solo i puntatori, non i dati stessi.

Problema

Se un oggetto contiene un puntatore a memoria allocata nel heap, dopo aver copiato due oggetti, entrambi punteranno alla stessa area di memoria. Quando uno degli oggetti viene distrutto, la memoria viene liberata e il puntatore dell'altro diventa non valido ("puntatore selvaggio"). Questo porta a errori di runtime e perdite di memoria.

Soluzione

Perché la copia sia indipendente, è necessario un deep copy — una copia byte-per-byte e l'allocazione di un proprio buffer. Questo si realizza scrivendo un costruttore di copia personalizzato e un operatore di assegnazione.

Esempio di codice:

class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // Costruttore di copia profondo MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // Assegnazione profonda MyString& operator=(const MyString& src) { if (this != &src) { delete[] data; data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } return *this; } ~MyString() { delete[] data; } };

Caratteristiche chiave:

  • Necessario per le classi che gestiscono risorse esterne
  • Necessaria l'implementazione di copy constructor e operator=
  • Possibili perdite di memoria senza una corretta implementazione della Regola dei Tre

Domande insidiose.

Perché è necessario un distruttore personalizzato se la classe contiene solo un puntatore, ma non viene allocata memoria?

Il distruttore è necessario solo se hai esplicitamente allocato memoria (o un'altra risorsa) all'interno della classe. Se il puntatore non alloca memoria, il distruttore predefinito è sufficiente.


Cosa succede se non implementi operator= in una classe con memoria dinamica, ma il costruttore di copia è dichiarato?

Se hai definito manualmente il costruttore di copia, il compilatore non implementerà automaticamente operator=; sarà dichiarato implicitamente oppure il compilatore genererà un errore/avviso (a seconda dello standard). Questo porterà a comportamenti mal definiti durante l'assegnazione: si verificherà una memberwise copy e si potrebbe verificare un double free o una perdita di memoria.

Esempio di codice:

MyString a("hi"); MyString b = a; // Ok: il tuo costruttore di copia MyString c("bye"); c = a; // Problema! Se operator= non è implementato manualmente, si verificherà un shallow copy

Quali sono i rischi di un'assegnazione a se stesso durante l'implementazione manuale di operator=?

Se si condividono risorse, senza controllare this!=&rhs, durante l'assegnazione a se stessi verrà eseguito delete[] data, seguito dalla copia di un array già distrutto, portando a un segfault. Autodifesa: controlla sempre l'auto-assegnazione.

if (this != &rhs) { ... }

Errori comuni e antipattern

  • Non è stata implementata una delle regole della Regola dei Tre (costruttore di copia, operator=, distruttore)
  • Non viene controllata l'auto-assegnazione
  • Perdita di memoria a causa di un delete[] dimenticato
  • Double free se i puntatori sono condivisi

Esempio dalla vita reale

Caso negativo

Uno sviluppatore copia un oggetto di una classe con un puntatore incorporato, senza implementare un deep copy. Dopo la copia, più oggetti condividono una stessa area di memoria. Due distruttori causano un double delete, causando il crash del programma.

Pro:

  • È facile scrivere il codice ("la copia funziona" a prima vista)

Contro:

  • Crash del programma, bug imprevedibili
  • Perdite di memoria o double-free

Caso positivo

Uno sviluppatore implementa correttamente il costruttore di copia, l'operatore di assegnazione e il distruttore. Ogni oggetto possiede la propria memoria.

Pro:

  • Sicurezza durante la copia e la distruzione
  • Nessuna perdita di memoria

Contro:

  • Con un gran numero di copie aumenta l'overhead della copia