ProgrammationDéveloppeur C++

Expliquez la différence entre shallow copy et deep copy pour les classes avec mémoire dynamique. Quand et pourquoi un constructeur de copie profond est-il nécessaire ? Comment réaliser un deep copy manuellement ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

Historique de la question

En C++, lors de la copie d'objets, le mécanisme de copie par membre est utilisé : pour chaque membre de l'objet, son propre opérateur de copie est appelé. Pour les types intégrés, cela est sans danger, mais pour les ressources dynamiques, un problème survient : seuls les pointeurs sont copiés, mais pas les données elles-mêmes.

Problème

Si un objet contient un pointeur vers de la mémoire allouée dans le tas, alors après la copie de deux objets, ils pointeront vers le même espace mémoire. Ainsi, à la destruction d'un objet, la mémoire sera libérée et le pointeur de l'autre deviendra invalide ("pointeur sauvage"). Cela entraîne des erreurs d'exécution et des fuites de mémoire.

Solution

Pour que la copie soit indépendante, un deep copy est nécessaire — une copie au niveau des octets et l'allocation d'un tampon propre. Cela s'implémente en écrivant un constructeur de copie et un opérateur d'assignation personnalisés.

Exemple de code :

class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // Constructeur de copie profond MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // Assignation de copie profonde 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; } };

Caractéristiques clés :

  • Nécessaire pour les classes qui gèrent des ressources externes
  • Nécessite la mise en œuvre de son propre constructeur de copie et opérateur =
  • Des fuites de mémoire sont possibles sans mise en œuvre correcte de la Règle des Trois

Questions piégeuses.

Pourquoi un destructeur personnalisé est-il nécessaire si la classe ne contient qu'un pointeur, mais aucune mémoire n'est allouée ?

Le destructeur n'est nécessaire que si vous avez explicitement alloué de la mémoire (ou une autre ressource) dans la classe. Si le pointeur n'alloue pas de mémoire, le destructeur par défaut est suffisant.


Que se passera-t-il si l'opérateur = n'est pas implémenté dans une classe avec mémoire dynamique, mais le constructeur de copie est déclaré ?

Si vous avez défini manuellement le constructeur de copie, le compilateur n'implementera pas automatiquement l'opérateur = ; il sera soit déclaré implicitement, soit le compilateur générera une erreur/un avertissement (selon le standard). Cela conduira à un comportement mal défini lors de l'assignation : une copie par membre se produira et entraînera un double free ou une fuite.

Exemple de code :

MyString a("hi"); MyString b = a; // Ok : votre constructeur de copie MyString c("bye"); c = a; // Problème ! Si l'opérateur = n'est pas implémenté manuellement, ce sera une shallow copy

Pourquoi l'assignation à soi-même est-elle dangereuse lors de l'implémentation manuelle de l'opérateur = ?

Si vous partagez des ressources sans vérifier this!=&rhs, alors en s'assignant à soi-même, delete[] data sera exécuté, puis la copie d'un tableau déjà détruit, ce qui entraînera un segfault. Auto-protection : vérifiez toujours l'auto-assignation.

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

Erreurs typiques et anti-patrons

  • Un des méthodes de la Règle des Trois (constructeur de copie, opérateur =, destructeur) n'est pas implémenté
  • Auto-assignation non vérifiée
  • Fuite de mémoire si delete[] est oublié
  • Double free si les pointeurs se sont avérés communs

Exemple de la vie quotidienne

Cas négatif

Un développeur copie un objet d'une classe avec un pointeur intégré, sans réaliser de deep copy. Après la copie, plusieurs objets partagent le même espace mémoire. Deux destructeurs provoquent un double delete, la programme plante.

Avantages :

  • Il est facile de coder ("la copie fonctionne" à première vue)

Inconvénients :

  • Crash du programme, bugs imprévisibles
  • Fuites de mémoire ou double-free

Cas positif

Un développeur implémente correctement le constructeur de copie, l'opérateur d'assignation et le destructeur. Chaque objet possède sa propre mémoire.

Avantages :

  • Sécurité lors de la copie et de la destruction
  • Pas de fuites

Inconvénients :

  • Avec un grand nombre de copies, le coût de la copie augmente