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.
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.
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:
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) { ... }
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:
Contro:
Uno sviluppatore implementa correttamente il costruttore di copia, l'operatore di assegnazione e il distruttore. Ogni oggetto possiede la propria memoria.
Pro:
Contro: