ProgramaciónDesarrollador C++ Middle

Explique las diferencias entre shallow y deep copy en C++ con un ejemplo de un contenedor con memoria dinámica. ¿Cómo implementar la copia profunda manualmente?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

El mecanismo de copia de objetos en C++ se divide en copia superficial (shallow copy) y copia profunda (deep copy). La diferencia es especialmente importante para las clases con memoria asignada dinámicamente.

Contexto de la pregunta

En C++, muchas estructuras de datos trabajan con memoria dinámica (new/delete). Por defecto, el compilador genera un constructor de copia y un operador de asignación que realizan una copia byte a byte (shallow copy). Esto es rápido, pero peligroso si el objeto gestiona recursos externos.

Problema

La shallow copy copia solo las direcciones de los recursos dinámicamente asignados. Al eliminar un objeto, la memoria será liberada, y el otro objeto quedará con un puntero "colgante" (dangling). Como resultado, surgen problemas de doble liberación, fugas de memoria y fallos.

Solución

La copia profunda implica la creación explícita de copias de todos los recursos dinámicos. Para ello, en la clase es necesario implementar el constructor de copia y el operador de asignación manualmente, para asegurar la copia de cada elemento.

Ejemplo de código para una clase con un arreglo:

class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // Constructor de copia profundo 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]; } // Asignación de copia profunda 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; } };

Aspectos clave:

  • La shallow copy copia punteros, la deep copy crea nuevas instancias de memoria dinámica.
  • Para la deep copy es necesario implementar la lógica de copia en el constructor y en el operador =
  • Ignorar la necesidad de deep copy puede llevar a errores difíciles de rastrear.

Preguntas trampa.

¿El compilador siempre genera correctamente el constructor de copia y el operador de asignación, verdad?

Respuesta:

Incorrecto. Para las clases con recursos dinámicos, la copia por defecto es incorrecta: ambos objetos poseerán el mismo recurso. La deep copy debe implementarse explícitamente cuando se gestionan recursos externos.

¿Es necesario implementar un destructor si se ha escrito solo el constructor de copia/operador de asignación profundo?

Respuesta:

Sí, de lo contrario habrá una fuga de memoria: si se libera memoria en el constructor de copia personalizado, pero no se implementa un destructor, la memoria no será liberada al destruir los objetos.

¿Puede std::vector almacenar punteros y por qué al copiarlo pueden ocurrir fugas?

Respuesta:

Sí, std::vector puede almacenar punteros. Al copiar dicho std::vector, se copian los propios punteros, no los objetos a los que apuntan. Esto es una shallow copy: si se necesita una deep copy de todo el contenido, será necesario copiar manualmente cada objeto y colocarlos en memoria de forma independiente.

Ejemplo:

std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // Se copian punteros, no *int

Errores comunes y anti-patrones

  • Ignorar la necesidad de implementar la Regla de Tres.
  • Copiar punteros, asumiendo que es una copia del objeto.
  • No liberar recursos dinámicos en el destructor.
  • Usar shallow copy para clases con recursos gestionados.

Ejemplo de la vida real

Caso negativo

Un programador implementa una clase envoltura de un arreglo sin sobrecargar el constructor de copia/operador de asignación. Como resultado, ambos objetos poseen la misma memoria, y destruir uno lleva a un fallo al acceder al segundo.

Ventajas:

  • Funciona rápidamente (sin copias).

Desventajas:

  • Errores difíciles de rastrear en tiempo de ejecución; presencia de doble libre/señal de error.

Caso positivo

Un desarrollador implementa la copia profunda: el contenido del arreglo es copiado, hay su propio destructor y un operador de asignación con protección contra la auto-asignación.

Ventajas:

  • Copia segura y liberación de memoria.
  • Código mantenible y extensible.

Desventajas:

  • Un poco más de código y consumo de memoria.
  • Más complicado para clases con varios recursos dinámicos.