En C++, por defecto al copiar objetos se utiliza el mecanismo de copia miembro a miembro: se llama a la operación de copia correspondiente para cada miembro del objeto. Para tipos incorporados, esto es seguro, pero para recursos dinámicos surge un problema: solo se copian los punteros, pero no los propios datos.
Si un objeto contiene un puntero a memoria asignada en heap, después de copiar dos objetos, ambos apuntarán a la misma área de memoria. Entonces, al destruir un objeto, la memoria se liberará y el puntero del segundo se volverá no válido ("puntero salvaje"). Esto provoca errores en tiempo de ejecución y fugas de memoria.
Para que la copia sea independiente, se necesita deep copy: una copia byte a byte y la asignación de un propio búfer. Esto se implementa escribiendo un constructor de copia y un operador de asignación personalizados.
Ejemplo de código:
class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // Constructor de copia profundo MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // Asignación de copia profunda 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; } };
Características clave:
¿Por qué se necesita un destructor personalizado si la clase solo contiene un puntero, pero no se asigna memoria?
El destructor es necesario solo si se ha asignado explícitamente memoria (u otro recurso) dentro de la clase. Si el puntero no asigna memoria, el destructor predeterminado es suficiente.
¿Qué sucederá si no se implementa el operator= en una clase con memoria dinámica, pero se declara el constructor de copia?
Si has definido manualmente el constructor de copia, el compilador no implementará automáticamente el operator=; se declarará de manera implícita o el compilador generará un error/advertencia (dependerá del estándar). Esto llevará a un comportamiento mal definido al asignar: se realizará una copia miembro a miembro y se producirá un doble liberación o una fuga.
Ejemplo de código:
MyString a("hi"); MyString b = a; // Ok: tu constructor de copia MyString c("bye"); c = a; // ¡Problema! Si operator= no se ha implementado manualmente, habrá una copia superficial
¿Qué riesgos implica asignarse a uno mismo al implementar manualmente operator=?
Si compartes recursos sin verificar this!=&rhs, al asignarte a ti mismo se ejecutará delete[] data, y luego se intentará copiar un arreglo ya destruido, lo que causará un segfault. Auto-protección: siempre verifica la auto-asignación.
if (this != &rhs) { ... }
Un desarrollador copia un objeto de clase con un puntero incorporado, sin implementar deep copy. Después de la copia, varios objetos comparten la misma área de memoria. Dos destructores provocan una doble eliminación, el programa falla.
Ventajas:
Desventajas:
Un desarrollador implementa correctamente el constructor de copia, el operador de asignación y el destructor. Cada objeto es dueño de su propia memoria.
Ventajas:
Desventajas: