Constructeurs par défaut, par copie, destructeur et opérateur d'affectation
La plupart des classes C++ que l'on crée possèdent généralement au moins un constructeur, un destructeur et un opérateur d'affectation. Si vous ne les avez pas déclarés, le compilateur le fera pour vous.
En effet.
- Si vous n'avez déclaré aucun constructeur, le compilateur en créera un par défaut. Le constructeur par défaut ne prend aucun argument.
- Si le destructeur n'est pas défini, le compilateur en créera également un pour vous. Ce destructeur ne sera pas virtuel à moins que celui de la classe mère le soit.
- Si vous ne déclarez pas le constructeur par copie ou l'opérateur d'affectation, le compilateur les déclarera automatiquement pour vous si vous en avez besoin.
Plutôt que de parler théorie, nous allons directement passer à la pratique à travers quelques exemples commentés.
Constructeur par défaut
Il est important de savoir que si au moins un constructeur est déclaré dans votre classe, aucun constructeur par défaut ne sera généré automatiquement par le compilateur, donc on pourra, grâce à cette technique, empêcher la création d'un objet via son constructeur par défaut.
// Fichier main.cpp #include <iostream> class MyFirstClass { public: // un constructeur est déclaré, le constructeur par défaut ne sera donc pas // créé par le compilateur. MyFirstClass(int a) {} // pour corriger le problème, on déclarera manuellement un constructeur par défaut. // MyFirstClass() {} void doSomething() {} }; int main(int argc, char **argv) { // MyFirstClass a; // erreur : pas de constructeur par défaut. MyFirstClass b(12); b.doSomething(); return EXIT_SUCCESS; }
Il est bien évidemment possible de déclarer plusieurs constructeurs dans une classe à condition que leurs signatures (liste d'arguments) soient différentes. Un constructeur peut être le constructeur par défaut si tous ses arguments possèdent une valeur par défaut mais attention, il ne peut y en avoir qu'un seul.
class MyFirstClass { public: // erreur : il ne peut y avoir deux constructeurs par défaut. MyFirstClass(); // constructeur par défaut. MyFirstClass(int a = 0); // erreur : autre constructeur par défaut. MyFirstClass(int a, int b); // celui-ci n'est pas un constructeur par défaut. };
Constructeur par copie et opérateur d'affectation
Nous allons voir quand sont générés automatiquement les constructeurs par copie et les opérateurs d'affectation. L'exemple suivant indique quelles sont les instructions qui font appel au constructeur par copie et à l'opérateur d'affectation. Si MyClass ne possède pas de constructeur par copie ni d'opérateur d'affectation, le compilateur les créera automatiquement puisque nous les avons utilisés au moins une fois.
MyClass c1; // utilise le constructeur par défaut. MyClass c2(c1); // utilise le constructeur par copie. MyClass c3 = c1; // utilise le constructeur par copie car c3 est construit avec c1. c3 = c2; // utilise l'opérateur d'affectation.
Le constructeur par copie et l'opérateur d'affectation effectuent simplement des copies des variables (non statiques) de l'objet original vers l'objet destination : les variables automatiques (allouées sur la pile) sont copiées via leur constructeur par copie ou leur opérateur d'affectation et les variables dynamiques (allouées sur le tas) pointent alors sur la même adresse mémoire… Ce dernier point est problématique car la libération de la mémoire de l'objet original a une incidence sur l'objet destination et vice et versa. D'autres problèmes peuvent survenir si des membres sont des références ou des constantes. Il faudra donc gérer ces problèmes manuellement en déclarant vos propres versions du constructeur par copie et de l'opérateur d'affectation. L'exemple suivant démontre le problème que l'on obtiendra en utilisant l'opérateur de copie généré par le compilateur si la classe contient des variables dynamiques.
// fichier main.cpp #include <iostream> class TestClass { public: // unique constructeur. alloue la mémoire pour un entier et l'initialise. TestClass(int v) : m_val(new int(v)) {} // la mémoire pour l'entier est détruit dans le destructeur. ~TestClass() { delete m_val; } // setter void setVal(int v) { if (m_val) *m_val = v; } // getter int val() const { return (m_val) ? *m_val : 0; } private: int *m_val; }; int main(int argc, char **argv) { // initialisation de l'objet avec la valeur 6. TestClass x(6); // le constructeur par copie est celui créé par le compilateur. il ne // s'occupe pas de recopier la mémoire allouée sur le tas. x::m_val et // y::m_val pointent sur le même espace mémoire. TestClass y(x); // affichera 6 et 6. std::cout << x.val() << std::endl; std::cout << y.val() << std::endl; // on change x::m_val à 2. x.setVal(2); // affichera 2 et 2. std::cout << x.val() << std::endl; std::cout << y.val() << std::endl; // les deux valeurs de m_val pointent sur le même espace mémoire. // lorsque x est détruit (automatiquement à la fin du bloc puisqu'il // est alloué sur la pile) x::m_val est détruit aussi. // y::m_val pointe sur un espace mémoire maintenant invalide donc lors // de la destruction de y, une erreur a lieu lors de la destruction de // y::m_val. return EXIT_SUCCESS; }
Voici à quoi ressemblent le constructeur par copie et l'opérateur d'affectation que le compilateur a générés automatiquement.
// constructeur par copie. TestClass (const TestClass& obj) : m_val(obj.m_val) {} // opérateur d'affectation. TestClass& operator=(const TestClass& obj) { this->m_val = obj.m_val; return *this; }
Mais pour éviter les problèmes rencontrés, ils devraient ressembler à cela. Dorénavant, chaque instance de TestClass possède un espace mémoire pour m_val qui lui est propre, les erreurs lors de la libération de la mémoire n'ont plus lieu d'être.
// constructeur par copie. TestClass (const TestClass& obj) : m_val(new int(*(obj.m_val))) {} // opérateur d'affectation. TestClass& operator=(const TestClass& obj) { this->m_val = new int(*(obj.m_val)); return *this; }
Enfin, notez que le compilateur interdit l'opérateur d'affectation d'une classe fille si ce même opérateur est privé dans la classe mère. Le même principe s'applique au constructeur par copie.
Conclusion
Il faut faire très attention avec les constructeurs et opérateurs générés automatiquement par le compilateur. En effet, le constructeur par défaut n'initialisera pas les variables membres de la classe, ceci est problématique surtout pour les pointeurs. Le constructeur par copie et l'opérateur d'affectation copient les variables allouées sur la pile, mais pas celles allouées sur le tas. Si un pointeur, une référence ou une constante est présente dans votre classe, vous devriez déclarer un constructeur par copie ainsi que l'opérateur d'affectation pour gérer correctement la copie. Enfin, n'oubliez pas de libérer la mémoire dans le destructeur si vous en avez allouée dans le constructeur (par exemple).
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++.
Derniers Commentaires