Copie profonde et copie superficielle en C++ avec un exercice d'entretien

La copie d'ojbets en C++ peut être effectuée de deux manières : copie superficielle (shallow copy) et copie profonde (deep copy). Le compilateur génère un constructeur de copie par défaut qui réalise une copie superficielle, ce qui peut entraîner des problèmes lorsque la classe contient des pointeurs ou des ressources allouées dynamiqumeent. Il est alors nécessaire de définir explicitement un constructeur de copie et un opérateur d'affectation.

Quand le constructeur de copie est-il appelé ?

  • Lors de l'initialisation d'un objet par un autre objet du même type.
  • Lors du passage d'un objet par valeur à une fonction (pas par référence).
  • Lors du retour d'un objet par valeur depuis une fonction (même si la fonction retourne une référence, une copie peut être générée pour l'objet temporaire).

Exemple simple

#include <iostream>
using namespace std;

class MonEntier {
private:
    int valeur;
public:
    MonEntier(int v) : valeur(v) {}
    MonEntier(const MonEntier& autre); // constructeur de copie
    int obtenirValeur() const { return valeur; }
};

MonEntier::MonEntier(const MonEntier& autre) {
    valeur = autre.valeur;
    cout << "Constructeur de copie appelé" << endl;
}

int fonctionValeur(MonEntier obj) { // appel par valeur → copie
    return obj.obtenirValeur();
}

int fonctionRef(const MonEntier& obj) { // appel par référence → pas de copie
    return obj.obtenirValeur();
}

MonEntier retourObjet() {
    MonEntier tmp(10);
    return tmp; // copie de l'objet temporaire
}

MonEntier& retourRef() {
    MonEntier tmp(20);
    return tmp; // attention : référence à un objet local détruit
}

int main() {
    MonEntier a(5);
    MonEntier b(a);           // constructeur de copie
    MonEntier c = a;          // constructeur de copie
    
    int i = fonctionValeur(a);  // copie
    int j = fonctionRef(a);     // pas de copie
    
    MonEntier d = retourObjet();    // copie de l'objet temporaire
    // MonEntier e = retourRef();   // comportement indéfini
    return 0;
}

Exercice d'entretien : corriger le code ci-dessous

#include <iostream>
using namespace std;

class ChaineNom {
private:
    char* pointeurNom;
    char* pointeurData;
public:
    ChaineNom() {
        static const char nomDefaut[] = "Nom par défaut";
        static const char dataDefaut[] = "Chaîne par défaut";
        strcpy(pointeurNom, nomDefaut);        // erreur : pointeur non alloué
        strcpy(pointeurData, dataDefaut);
    }
    ~ChaineNom() {}
    ChaineNom(const char* nom, const char* data) {
        pointeurData = new char[strlen(data)];      // manque +1
        pointeurNom = new char[strlen(nom)];        // manque +1
    }
    void afficher() {
        cout << "Nom : " << pointeurNom << endl;
        cout << "Données : " << pointeurData << endl;
    }
};

int main() {
    ChaineNom* tableau = NULL;
    tableau = new ChaineNom[10];
    ChaineNom obj("hello", "world");
    delete tableau; // doit être delete[]
    return 0;
}

Problèmes identifiés

  1. Les pointeurs pointeurNom et pointeurData ne sont pas initialisés dans le constructeur par défaut, strcpy écrit dans une zone mémoire invalide.
  2. Dans le constructeur paramétré, new char[strlen(data)] alloue un espace insuffisant ; il faut strlen(data)+1 pour le caractère nul de fin.
  3. Le destructeur ne libère pas la mémoire allouée.
  4. Absence de constructeur de copie et d'opérateur d'affectation : le membre pointeur provoque une copie superficielle (deux objets partagent le même bloc mémoire).
  5. delete tableau doit être delete[] tableau pour un tableau.

Version corrigée

#include <iostream>
#include <cstring>
using namespace std;

class ChaineNom {
private:
    char* pNom;
    char* pData;
public:
    // Constructeur par défaut
    ChaineNom() {
        const char* nomDefaut = "Nom par défaut";
        const char* dataDefaut = "Chaîne par défaut";
        pNom = new char[strlen(nomDefaut) + 1];
        pData = new char[strlen(dataDefaut) + 1];
        strcpy(pNom, nomDefaut);
        strcpy(pData, dataDefaut);
    }

    // Destructeur
    ~ChaineNom() {
        delete[] pNom;
        delete[] pData;
    }

    // Constructeur paramétré
    ChaineNom(const char* nom, const char* data) {
        pNom = new char[strlen(nom) + 1];
        pData = new char[strlen(data) + 1];
        strcpy(pNom, nom);
        strcpy(pData, data);
    }

    // Constructeur de copie (copie profonde)
    ChaineNom(const ChaineNom& autre) {
        pNom = new char[strlen(autre.pNom) + 1];
        pData = new char[strlen(autre.pData) + 1];
        strcpy(pNom, autre.pNom);
        strcpy(pData, autre.pData);
    }

    // Opérateur d'affectation (copie profonde)
    ChaineNom& operator=(const ChaineNom& autre) {
        if (this == &autre) return *this; // auto-affectation
        delete[] pNom;
        delete[] pData;
        pNom = new char[strlen(autre.pNom) + 1];
        pData = new char[strlen(autre.pData) + 1];
        strcpy(pNom, autre.pNom);
        strcpy(pData, autre.pData);
        return *this;
    }

    void afficher() const {
        cout << "Nom : " << pNom << endl;
        cout << "Données : " << pData << endl;
    }
};

int main() {
    ChaineNom* tableau = new ChaineNom[3]; // 3 objets par défaut
    ChaineNom obj("hello", "world");
    delete[] tableau;

    ChaineNom copie = obj; // test constructeur de copie
    copie.afficher();
    return 0;
}

Étiquettes: C++ copie profonde copie superficielle Constructeur de Copie pointeurs

Publié le 25 juillet à 08h26