Le
constructeur de copie est une
surcharge particulière du constructeur.
Le constructeur de copie devient généralement indispensable dans une classe qui contient des pointeurs, et ça tombe bien vu que c'est justement notre cas ici.
Le problème
Pour bien comprendre l'intérêt du constructeur de copie, voyons voir concrètement ce qui se passe lorsqu'on crée un objet en l'affectant par... un autre objet ! Par exemple :
Code : C++ | int main()
{
Personnage goliath("Epée aiguisée", 20);
Personnage david(goliath); // On crée david à partir de goliath. David sera une "copie" de goliath.
return 0;
}
|
Lorsqu'on construit un objet en lui affectant directement un autre objet, comme on vient de le faire ici, le compilateur appelle une méthode appelée constructeur de copie.
Le rôle du constructeur de copie est de
copier la valeur de tous les attributs du premier objet dans le second. Donc david récupère la vie de goliath, la mana de goliath, etc.
Dans quels cas le constructeur de copie est-il appelé ?
On vient de le voir, le constructeur de copie est appelé lorsqu'on crée un nouvel objet en l'affectant par la valeur d'un autre :
Code : C++ | Personnage david(goliath); // Appel du constructeur de copie (cas 1)
|
Ceci est strictement équivalent à écrire :
Code : C++ | Personnage david = goliath; // Appel du constructeur de copie (cas 2)
|
Dans ce second cas le constructeur de copie est là aussi appelé.
Mais ce n'est pas tout ! Lorsque vous envoyez un objet à une fonction sans utiliser de pointeur ni de référence, l'objet est là aussi copié !
Imaginons la fonction :
Code : C++ | void maFonction(Personnage unPersonnage)
{
}
|
Si vous appelez cette fonction qui n'utilise pas de pointeur ni de référence, alors l'objet sera copié en utilisant un constructeur de copie au moment de l'appel de la fonction :
Code : C++ | maFonction(Goliath); // Appel du constructeur de copie (cas 3)
|
Bien entendu, il est préférable d'utiliser une référence en général car l'objet n'a pas besoin d'être copié, donc ça va bien plus vite et ça prend moins de mémoire. Toutefois, il arrivera des cas où vous aurez besoin de créer une fonction comme ici qui fait une copie de l'objet.
Le problème ? Eh bien justement, il se trouve qu'un des attributs est un pointeur dans notre classe Personnage ! Que fait l'ordinateur ? Il copie la valeur du pointeur, donc l'adresse de l'arme. Au final, les 2 objets ont un pointeur qui pointe vers le même objet de type Arme !
Ah les fourbes !
L'ordinateur a copié le pointeur, et donc les 2 pointeurs pointent vers la même arme !
Si on ne fait rien pour régler ça, imaginez ce qu'il va se passer lorsque les 2 personnages seront détruits... Le premier sera détruit, ainsi que son arme car le destructeur ordonnera la suppression de l'arme avec un
delete. Et quand arrivera le tour du second personnage, le
delete va planter (et votre programme avec

) parce que l'arme aura
déjà été détruite !
Le constructeur de copie généré automatiquement par le compilateur n'est pas assez intelligent pour comprendre qu'il faut allouer de la mémoire pour une autre arme... Qu'à cela ne tienne, nous allons le lui expliquer.
Création du constructeur de copie
Le constructeur de copie, comme je vous l'ai dit un peu plus haut, est une surcharge particulière du constructeur. C'est un constructeur qui prend pour paramètre... une référence constante vers un objet du même type !
Si vous ne trouvez pas ça clair, peut-être qu'un exemple vous aidera.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 | class Personnage
{
public:
Personnage();
Personnage(Personnage const& personnageACopier); // Le prototype du constructeur de copie
Personnage(std::string nomArme, int degatsArme);
~Personnage();
/*
... plein d'autres méthodes qui ne nous intéressent pas ici
*/
private:
int m_vie;
int m_mana;
Arme *m_arme;
};
|
En résumé, le prototype d'un constructeur de copie est :
Code : C++ | Objet(Objet const& objetACopier);
|
Le
const indique juste qu'on n'a pas le droit de modifier les valeurs de l'
objetACopier (c'est logique, on a juste besoin de "lire" ses valeurs pour le copier).
Écrivons l'implémentation de ce constructeur. Il va falloir copier tous les attributs du
personnageACopier dans le personnage actuel. Commençons par les attributs "simples", c'est-à-dire ceux qui ne sont pas des pointeurs :
Code : C++ | Personnage::Personnage(Personnage const& personnageACopier)
: m_vie(personnageACopier.m_vie), m_mana(personnageACopier.m_mana), m_arme(0)
{
}
|
Il reste maintenant à "copier"
m_arme. Si on écrit :
Code : C++ | m_arme = personnageACopier.m_arme;
|
... on fait exactement la même erreur que le compilateur, c'est-à-dire qu'on ne copie que l'adresse de l'objet de type Arme, et pas l'objet en entier !
Pour résoudre le problème, il va falloir copier l'objet de type Arme en faisant une allocation dynamique, donc un
new. Attention, accrochez-vous parce que ce n'est pas simple.
Si on fait :
Code : C++
... on va bien créer une nouvelle arme, mais on utilisera le constructeur par défaut, donc cela créera l'arme de base. Or, on veut avoir exactement la même arme que celle du
personnageACopier (ben oui, c'est un constructeur de copie

).
La bonne nouvelle, comme je vous l'ai dit plus haut, c'est que le constructeur de copie est automatiquement généré par le compilateur. Tant que la classe n'utilise pas de pointeurs vers des attributs, il n'y a pas de danger. Et ça tombe bien, la classe Arme n'utilise pas de pointeurs, on va donc pouvoir se contenter du constructeur qui a été généré.
Il faut donc appeler le constructeur de copie de
Arme, en envoyant en paramètre l'objet à copier. Vous pourriez penser qu'il faut faire ceci :
Code : C++ | m_arme = new Arme(personnageACopier.m_arme);
|
Presque ! Sauf que
m_arme est un pointeur, et le prototype du constructeur de copie est :
Code : C++
... ce qui veut dire qu'il faut envoyer l'objet lui-même et pas son adresse. Vous vous souvenez comment on fait pour obtenir l'objet (ou la variable) à partir de son adresse ? On utilise l'étoile * !
Ce qui donne au final :
Code : C++ | m_arme = new Arme(*(personnageACopier.m_arme));
|
Cette ligne alloue dynamiquement une nouvelle arme, en se basant sur l'arme du
personnageACopier. Pas simple je le reconnais, mais relisez plusieurs fois les étapes de mon raisonnement et vous allez comprendre.

Pour bien suivre tout ce que j'ai dit, il faut vraiment que vous soyez au point sur tout : les pointeurs, les références, et les... constructeurs de copie.
Le constructeur de copie une fois terminé
Le bon constructeur de copie ressemblera donc à ceci au final :
Code : C++ | Personnage::Personnage(Personnage const& personnageACopier)
: m_vie(personnageACopier.m_vie), m_mana(personnageACopier.m_mana), m_arme(0)
{
m_arme = new Arme(*(personnageACopier.m_arme));
}
|
Ainsi, nos 2 personnages ont tous deux une arme identique, mais dupliquée afin d'éviter les problèmes que je vous ai expliqués plus haut :
L'opérateur d'affectation
Nous avons déjà parlé de la surcharge des opérateurs. Mais il y en a un que je ne vous ai pas présenté. Il s'agit de l'opérateur d'affectation (
operator=).
Le compilateur écrit un opérateur d'affectation par défaut automatiquement, mais c'est un opérateur "bête". Cet opérateur bête se contente de copier les valeurs des attributs un à un dans le nouvel objet. Comme pour le constructeur de copie généré par le compilateur.
La méthode
operator= sera appelée dès qu'on essaie d'affecter une valeur à notre objet. C'est le cas par exemple si on affecte à notre objet la valeur d'un autre objet :
Code : C++
Ne confondez pas le constructeur de copie avec la surcharge de l'opérateur = (
operator=). Ils se ressemblent beaucoup, mais il y a une différence : le constructeur de copie est appelé lors de l'initialisation (à la création de l'objet) tandis que la méthode
operator= est appelée si on essaie d'affecter un autre objet par la suite, après son initialisation.
Code : C++ | Personnage david = goliath; // Constructeur de copie
david = goliath; // operator=
|
Cette méthode effectue le même travail que le constructeur de copie. Écrire son implémentation est donc relativement simple. Une fois qu'on a compris le principe bien sûr.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12 | Personnage& Personnage::operator=(Personnage const& personnageACopier)
{
if(this != &personnageACopier) //On vérifie que notre objet n'est pas le même que celui reçu en argument
{
m_vie = personnageACopier.m_vie; //On copie tous les champs
m_mana = personnageACopier.m_mana;
delete m_arme;
m_arme = new Arme(*(personnageACopier.m_arme));
}
return *this; //On renvoie l'objet lui-même
}
|
Il y a tout de même quatre différences :
- Comme ce n'est pas un constructeur, on ne peut pas utiliser la liste d'initialisation et donc tout se passe entre les accolades.
- Il faut penser à vérifier que l'on n'est pas en train de faire david=david. On doit donc vérifier que l'on travaille avec deux objets distincts. Il faut donc vérifier que leurs adresses mémoires (this et &personnageACopier) sont différentes.
- Il faut renvoyer *this comme pour les opérateurs +=, -=, etc. C'est une règle à respecter.
- Il faut penser à supprimer l'ancienne arme avant de créer la nouvelle. C'est ce qui est fait à l'instruction delete surlignée du code. Ceci n'était pas nécessaire dans le constructeur de copie puisque le personnage ne possédait pas d'arme avant.
Cet opérateur est toujours similaire à celui que je vous donne pour la classe
Personnage. Les seuls éléments qui changent d'une classe à l'autre sont les lignes qui se trouvent dans le
if. Je vous ai en quelque sorte donné la recette universelle.
Il y a une chose importante à retenir au sujet de cet opérateur : il va toujours de paire avec le constructeur de copie.
Si l'on a besoin d'écrire un constructeur de copie, alors il faut aussi obligatoirement écrire une surcharge de operator=.
C'est une règle très importante à respecter. Vous risquez de graves problèmes de pointeurs si vous ne la respectez pas.
La POO n'est pas simple comme vous commencez à vous en rendre compte, surtout quand on commence à manipuler des objets avec des pointeurs. Heureusement, vous aurez l'occasion de pratiquer tout cela par la suite, et vous allez petit à petit prendre l'habitude d'éviter les pièges des pointeurs.