Effectuez des copies entières de vos objets
Nous avons vu précédemment que le compilateur pouvait créer automatiquement le constructeur par copie et l'opérateur d'affectation. Ces éléments par défaut suffisent la plupart du temps, à moins que les objets à copier contiennent des pointeurs, des constantes ou des références. Si vous avez écrit le constructeur par copie et l'opérateur d'affectation, sachez qu'il y a quelques pièges à éviter.
Copiez tous les membres d'une classe
Lorsque vous utilisez les versions par défaut du constructeur par copie et de l'opérateur d'affectation, vous êtes certains que tous les membres de vos classes seront copiés, car le compilateur n'en oubliera aucun. Cependant, les copies de pointeurs ne se feront sans doute pas à votre goût et c'est alors pour cela que vous déclarerez vos propres fonctions de copie. Le compilateur vous fait confiance, il ne vous préviendra pas si vous oubliez de copier un des membres.
Imaginons la classe de base suivante :
class Person { public: Person(unsigned int _age) : m_age(_age) {} // constructeur. Person(const Person& _pers) : m_age(_pers.m_age) {} // constructeur par copie. virtual ~Person() {} // destructeur virtuel, on utilise l'héritage dans les exemples suivants. Person& operator=(const Person& _pers) // opérateur d'affectation. { if (this != &_pers) // test d'auto-affectation. m_age = _pers.m_age; return *this; // retour de la référence sur l'objet courant. } private: unsigned int m_age; };
Les constructeurs par copie et opérateur d'affectation fonctionnent parfaitement. Imaginons maintenant que, lors d'une opération de maintenance du logiciel, nous devons ajouter des informations dans la classe. Le compilateur ne vous indiquera pas si vous avez oublié de copier le nouveau membre, que ce soit dans le constructeur par copie ou dans l'opérateur d'affectation.
Nous allons donc ajouter une nouvelle information : le sexe de la personne. Au même titre que l'initialisation dans le constructeur, il ne faut surtout pas oublier d'effectuer la copie de cette nouvelle information dans le constructeur par copie et dans l'opérateur d'affectation. Faites-le dès que vous ajoutez un membre, car si vous l'oubliez, le compilateur ne vous le dira pas et vos objets seront alors partiellement copiés.
class Person { public: enum Gender { Male, Female }; // ajout d'un type Genre. Person(unsigned int _age, Gender _gender) : m_age(_age), m_gender(_gender) {} // on n'oublie pas d'initialiser le genre. Person(const Person& _pers) : m_age(_pers.m_age), m_gender(_pers.m_gender) // on n'oublie pas de copier le genre. { } virtual ~Person() {} Person& operator=(const Person& _pers) { if (this != &_pers) { m_age = _pers.m_age; m_gender = _pers.m_gender; // on n'oublie pas de copier le genre. } return *this; } private: unsigned int m_age; Gender m_gender; // ajout d'une nouvelle information. };
Copiez la base de votre classe dérivée
Vous avez remarqué le virtual devant le destructeur, nous allons utiliser notre classe Person en tant que classe de base pour créer la classe Student. Cette classe contiendra le diplôme en cours, par exemple.
Voici notre classe dérivée :
class Student : public Person { public: enum Diploma { Diploma1, Diploma2, Diploma3 }; // ajout d'un type Diplôme. Student(unsigned int _age, Gender _gender, Diploma _dipl) : Person(_age, _gender), // on n'oublie pas d'appeler le constructeur de base. m_diploma(_dipl) // on n'oublie pas d'initialiser m_diploma. {} Student(const Student& _stud) : Person(_stud), // on n'oublie pas d'appeler le constructeur par copie de la classe de base. m_diploma(_stud.m_diploma) // on initialise les membres... {} virtual ~Student() {} Student& operator=(const Student& _stud) { if (this != &_stud) { // on n'oublie pas d'appeler l'opérateur d'affectation de la classe de base. Person::operator=(_stud); m_diploma = _stud.m_diploma; // on copie les membres. } return *this; } private: Diploma m_diploma; // le diplôme. };
À chaque fois qu'on doit copier un objet dérivé d'un autre, on ne doit pas oublier de copier la partie de base. Généralement, les membres de la classe de base sont privés, mais même s'ils sont protégés ou publics, ne les initialisez pas manuellement depuis la classe dérivée. Utilisez plutôt la fonction de copie correspondante de la classe de base tel que cela est fait dans l'exemple ci-dessus.
Conclusion
Copier entièrement les objets signifie qu'il faut copier tous les membres locaux à une classe et qu'il faut appeler les fonctions appropriées de copie des classes de base. On pourrait être tenté d'éviter la duplication de code en utilisant l'opérateur = dans le constructeur par copie et inversement, mais c'est une très mauvaise idée. Utilisez plutôt une troisième méthode pour mettre le code commun et appelez la depuis ces deux fonctions. En général, cette méthode est déclarée privée, et on a pour coutume de l'appeler init. Cette technique est fiable et permet d'éliminer la duplication de code.
Vous pourrez approfondir le sujet avec la sélection des livres suivants : Pour mieux développer avec C++, Standards de programmation en C++ et Le langage C++.
Commentaires
Un constructeur de copie est un constructeur comme les autres : la liste d'initialisation y est tout indiquée.
Effectivement, les deux premiers exemples n'utilisaient pas la liste d'initialisation pour le constructeur par copie, c'est maintenant chose faite. Merci.