ProgrammationDéveloppeur C++ Middle

Expliquez les différences entre shallow et deep copy en C++ en utilisant l'exemple d'un conteneur avec de la mémoire dynamique. Comment réaliser une copie profonde manuellement ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Le mécanisme de copie des objets en C++ se divise en copie superficielle (shallow copy) et copie profonde (deep copy). La différence est particulièrement importante pour les classes avec de la mémoire dynamique.

Historique de la question

En C++, de nombreuses structures de données fonctionnent avec de la mémoire dynamique (new/delete). Par défaut, le compilateur génère un constructeur de copie et un opérateur d'assignation qui effectuent une copie au niveau des octets (shallow copy). C'est rapide, mais dangereux si l'objet gère des ressources externes.

Problème

La shallow copy ne copie que les adresses des ressources allouées dynamiquement. Lors de la suppression d'un objet, la mémoire sera libérée, et l'autre instance restera avec un pointeur "dangling". En conséquence, des double delete, des fuites de mémoire et des crashes peuvent survenir.

Solution

La copie profonde implique la création explicite d'une copie de toutes les ressources dynamiques. Pour cela, il est nécessaire de réaliser manuellement un constructeur de copie et un opérateur d'assignation dans la classe afin de s'assurer que chaque élément soit copié.

Exemple de code pour une classe avec un tableau :

class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // Constructeur de copie profonde 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]; } // Assignation profonde 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; } };

Caractéristiques clés :

  • La shallow copy copie des pointeurs, la deep copy crée de nouvelles instances de mémoire dynamique.
  • La deep copy nécessite une logique de copie à implémenter dans le constructeur et l'opérateur=
  • Ignorer la nécessité de deep copy conduit à des bugs difficiles à attraper.

Questions pièges.

Le compilateur génère-t-il toujours correctement le constructeur de copie et l'opérateur d'assignation ?

Réponse :

Faux. Pour les classes avec des ressources dynamiques, la copie par défaut est incorrecte : les deux objets posséderont la même ressource. La deep copy doit être réalisée explicitement lorsqu'il y a des ressources externes.

Faut-il implémenter un destructeur si seul le constructeur de copie/assignation profond est écrit ?

Réponse :

Oui, sinon il y aura une fuite de mémoire : si vous libérez de la mémoire dans le constructeur de copie utilisateur mais que vous ne mettez pas en œuvre un destructeur, la mémoire ne sera libérée par personne lors de la destruction des objets.

std::vector peut-il stocker des pointeurs et pourquoi y a-t-il des fuites possibles lors de sa copie ?

Réponse :

Oui, std::vector peut stocker des pointeurs. Lors de la copie d'un tel std::vector, ce sont les pointeurs eux-mêmes qui sont copiés, pas les objets auxquels ils pointent. C'est une shallow copy : si un deep copy de tout le contenu est nécessaire, il faudra copier manuellement chaque objet et les placer en mémoire de manière indépendante.

Exemple :

std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // Les pointeurs sont copiés, pas *int

Erreurs typiques et anti-patterns

  • Ignorer la nécessité d'implémenter la Règle de Trois.
  • Copier des pointeurs en supposant qu'il s'agit d'une copie de l'objet.
  • Ne pas libérer les ressources dynamiques dans le destructeur.
  • Utiliser shallow copy pour les classes avec des ressources possédées.

Exemple de la vie réelle

Cas négatif

Un programmeur implémente une classe d'enveloppement de tableau sans redéfinir le constructeur de copie/l'opérateur d'assignation. Par conséquent, les deux objets possèdent la même mémoire, la destruction de l'un entraîne un crash lors de l'accès à l'autre.

Avantages :

  • Fonctionne rapidement (pas de copies).

Inconvénients :

  • Erreurs de runtime très difficiles à attraper ; présence de double free/segfault.

Cas positif

Un développeur implémente une copie profonde : le contenu du tableau est copié, il y a son propre destructeur et un opérateur d'assignation avec protection contre l'auto-assignation.

Avantages :

  • Copie sécurisée et libération de mémoire.
  • Code entretenu et extensible.

Inconvénients :

  • Un peu plus de code et de frais de mémoire.
  • Plus complexe pour les classes avec plusieurs ressources dynamiques.