FAQ C++Consultez toutes les FAQ
Nombre d'auteurs : 34, nombre de questions : 368, dernière mise à jour : 14 novembre 2021 Ajouter une question
Cette FAQ a été réalisée à partir des questions fréquemment posées sur les forums de http://www.developpez.com et de l'expérience personnelle des auteurs.
Je tiens à souligner que cette FAQ ne garantit en aucun cas que les informations qu'elle propose sont correctes ; les auteurs font le maximum, mais l'erreur est humaine. Cette FAQ ne prétend pas non plus être complète. Si vous trouvez une erreur ou si vous souhaitez devenir rédacteur, lisez ceci.
Sur ce, nous vous souhaitons une bonne lecture.
- Qu'est-ce qu'un constructeur ?
- Qu'est-ce qu'un constructeur par défaut ?
- Quand dois-je définir un constructeur par défaut ?
- Qu'est-ce qu'un constructeur de copie ?
- Quand dois-je définir un constructeur par copie ?
- Quelles sont les différences fondamentales entre le constructeur d'une classe et sa méthode Init() ?
- Y a-t-il une différence quelconque entre MaClasse x; et MaClasse x(); ?
- Un constructeur d'une classe peut-il appeler un autre constructeur de la même classe pour initialiser 'this' ?
- Est-ce que le constructeur par défaut pour Fred est toujours Fred::Fred() ?
- Quel constructeur est appelé quand je crée un tableau d'objets Fred ?
- Mes constructeurs doivent-ils utiliser les listes d'initialisation ou l'affectation ?
- Puis-je utiliser le pointeur this dans un constructeur ?
- Qu'est-ce que l'idiome du constructeur nommé (Named Constructor) ?
- Que faire en cas d'échec du constructeur ?
- Qu'est-ce que « l'idiome des paramètres nommés » ?
- Dans quel ordre sont construits les différents composants d'une classe ?
- Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?
Un constructeur est ce qui construit un objet, initialise éventuellement les membres de la classe, alloue de la mémoire, etc.
On peut le comparer à une fonction d'initialisation de la classe.
On le reconnaît au fait qu'il porte le même nom que la classe elle-même. Sa déclaration se fait de la façon suivante :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class MaClasse { public: MaClasse(); }; |
Un constructeur par défaut est un constructeur qui peut être appelé sans paramètre. À noter qu'il peut s'agir d'un constructeur sans paramètres, ou d'un constructeur dont les paramètres ont des valeurs par défaut.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class MaClasse1 { public: MaClasse1(); // ceci est le constructeur par défaut }; class MaClasse2 { public: MaClasse2( int i = 1 ); // ceci est le constructeur par défaut }; class MaClasse3 { public: MaClasse3( int i ); // ceci n'est pas le constructeur par défaut }; |
Le constructeur par défaut est le constructeur qui ne prend aucun argument ou dont les arguments ont une valeur par défaut :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class CMyClass { public: CMyClass(); // constructeur par défaut }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class CMyClass { public: CMyClass(T1 param1=def_value, T2 param2=T2()); // constructeur par défaut }; |
Une exception : si la classe définit un autre constructeur (constructeur avec paramètres sans valeur par défaut ou constructeur par copie), alors le compilateur ne génère pas de constructeur par défaut. Dans ce cas, si cela a un sens pour la classe, un constructeur par défaut doit alors être explicitement défini.
Un constructeur de copie, comme son nom l'indique, sert à copier l'objet, à partir d'un autre objet. Ce constructeur est utilisé pour
- la duplication d'un objet lors de la création d'un nouvel objet
- lors du passage en paramètre à une fonction, un template… pour générer l'objet utilisé en interne par cette fonction
- au retour d'une fonction renvoyant un objet, dans le but de mettre cet objet à disposition sur la pile pour l'appelant
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class MaClasse { public: MaClasse( const MaClasse & ); // ceci est le constructeur de copie }; |
Le constructeur par copie se base sur un autre objet du même type pour construire l'objet en cours :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class CMyClass { public: CMyClass(CMyClass const&); // constructeur par copie }; |
Si on souhaite donner une sémantique de copie et que le constructeur par copie ne convient pas (par exemple, parce qu'une ressource est gérée), alors il faut définir un constructeur par copie.
Dans le cadre d'une utilisation polymorphe, on peut vouloir définir un constructeur par copie pour permettre le clonage. Celui-ci est alors protégé car la copie n'a de sens que dans ce cadre. La fonction Clone() est, elle, publique.
Le constructeur est une fonction spéciale appelée automatiquement à certains endroits, une fonction init() n'a aucune sémantique spéciale, l'utilisateur doit l'appeler manuellement (risque d'oubli). En revanche, un constructeur ne peut pas être polymorphe, ni appeler une fonction de sa classe de manière polymorphe. L'appel de la fonction init() est donc une nécessité dans les cas extrêmes rares où l'on a besoin de « postconstruction polymorphe ».
Cela revient à dire qu'il faut utiliser uniquement un constructeur. Dans certains cas, on n'a pas assez d'infos à la construction de l'objet pour tout initialiser, d'où une fonction init() (toutefois, cela reste assez rare).
Pour signaler une erreur dans le constructeur, il peut lancer une exception, et dans ce cas, l'objet n'est pas construit du tout.
Si on suit le code exemple suivant :
Code c++ : | Sélectionner tout |
1 2 3 | T * t = new T(); t->init(); // peut lever une exception, parce que la clé de connexion n'est pas valide... return t; |
- Fusionner le résidu init() dans le constructeur principal des classes qui sont refactorisées.
- Utiliser les « unique_ptr<> ».
Une grande différence !
Supposez que MaClasse soit le nom d'une classe. Dans l'exemple suivant :
Code c++ : | Sélectionner tout |
1 2 | MaClasse x; MaClasse y(); |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | #include "MaClasse.h" int main() { MaClasse x; // ok MaClasse y(); // déclaration de la fonction y ! } |
C++11
Notez que C++11 permet maintenant au constructeur d'une classe de faire directement appel à un autre constructeur. Cette technique s'appelle le delegating constructor.
C++03
Absolument pas.
Prenons un exemple. Supposons que l'on veuille que le constructeur Foo::Foo(char) appelle un autre constructeur de la même classe, Foo::Foo( char, int ) de façon que ce dernier initialise l'objet this. Malheureusement, ce n'est pas possible en C++.
Pourtant, certaines personnes le font, mais cela ne fait pas ce qu'elles désirent. Par exemple,
Code c++ : | Sélectionner tout |
line Foo( x, 0 );
Code c++ : | Sélectionner tout |
Foo::Foo(char,int)
Code c++ : | Sélectionner tout |
Foo::Foo( char, int )
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | class Foo { public: Foo( char x ); Foo( char x, int y ); }; Foo::Foo( char x ) { Foo( x, 0 ); // Cette ligne n'initialise pas l'objet ! } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class Foo { public: Foo( char x, int y = 0 ); // cette ligne combine les 2 constructeurs }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class Foo { public: Foo( char x ); Foo( char x, int y ); private: void init( char x, int y ); }; Foo::Foo( char x ) { init( x, int( x ) + 7 ); } Foo::Foo( char x, int y ) { init( x, y ); } void Foo::init( char x, int y ) { } |
Non. Un « constructeur par défaut » est un constructeur qui peut s'appeler sans arguments. Ainsi un constructeur qui ne prend aucun argument est certainement un constructeur par défaut :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class Fred { public: Fred(); // constructeur par défaut : peut s'appeler sans argument }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class Fred { public: Fred( int i = 3, int j = 5 ); // constructeur par défaut : peut s'appeler sans argument }; |
Le constructeur par défaut.
Il n'y a aucun moyen de demander au compilateur d'appeler un constructeur différent. Si votre class Fred n'a pas de constructeur par défaut, une tentative de créer un tableau de Fred, se soldera par une erreur de compilation.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | class Fred { public: Fred( int i, int j ); // Suppose qu'il n'y a aucun constructeur par défaut }; int main() { Fred a[ 10 ]; // ERREUR : Fred n'a pas de constructeur par défaut Fred * p = new Fred[ 10 ]; // ERREUR : Fred n'a pas de constructeur par défaut } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | #include <vector> int main() { std::vector <Fred> a(10, Fred(5,7)); // 10 objets Fred dans le vecteur a seront initialisés avec Fred(5,7) } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Fred { public: Fred(int i, int j); // suppose qu'il n'y a aucun constructeur par défaut }; int main() { Fred a[10] = { Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7) }; // 10 objets Fred dans le tableau a seront initialisés avec Fred(5,7) } |
Souvenez-vous : les tableaux sont mauvais, à moins qu'il y ait une raison valable d'en utiliser un, utilisez plutôt un vecteur.
Les listes d'initialisation. En fait, les constructeurs devraient initialiser tous les objets membres dans la liste d'initialisation.
Par exemple, ce constructeur initialise l'objet membre x_ en utilisant une liste d'initialisation :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | class Fred { Type x_; public: Fred(); }; Fred::Fred() : x_(n importe quoi) { } |
Comme x_ peut être de strictement n'importe quel type, il se peut parfaitement qu'il s'agisse d'une classe créée par l'utilisateur.
Selon les circonstances, le type de x_ pourrait tout aussi bien avoir sémantique de valeur que sémantique d'entité.
Pour les classes ayant sémantique de valeur, l'affectation sous une forme proche de
Code c++ : | Sélectionner tout |
1 2 3 4 | Fred::Fred() : x_() { x_ = n importe quoi; } |
Mais les classes ayant sémantique d'entité n'ont vocation à être ni copiées, ni affectées.
Un tel code serait donc refusé par le compilateur.
Certains pensent qu'on ne devrait pas utiliser le pointeur this dans un constructeur parce que l'objet n'est pas complètement formé. Pourtant il est possible d'utiliser le pointeur this dans le corps du constructeur et même dans la liste d'initialisation si l'on est prudent.
Voilà quelque chose qui fonctionne toujours : le corps du constructeur (ou d'une fonction appelée depuis le constructeur) peut accéder aux données membres déclarées dans une classe de base et/ou à celles déclarées dans la classe elle-même en toute sécurité. Ceci parce que le langage nous assure que toutes ces données membres ont été complètement construites au moment où le corps du constructeur est exécuté.
Voilà quelque chose qui ne fonctionne jamais : le corps du constructeur (ou d'une fonction qu'il appelle) ne peut pas descendre dans une classe dérivée en appelant une fonction membre virtual qui est redéfinie dans une classe dérivée. Si votre but était d'exécuter le code de la fonction virtuelle, cela ne fonctionnera pas. Notez que vous n'obtiendrez pas la version de la classe dérivée indépendamment de la manière d'appeler la fonction membre virtuelle : en utilisant explicitement this( this->method() ), ou implicitement sans utiliser le pointeur this( method() ), ou même en appelant quelque autre fonction qui appelle la fonction membre virtuelle en question a partir du pointeur this.
Ceci parce que l'appelant est en train de construire un objet d'un type dérivé, et donc que la construction de ce dernier n'est pas encore terminée. Donc votre objet qui fait office de classe de base n'appartient pas encore à cette classe dérivée.
Voilà quelque chose qui fonctionne parfois : si vous passez n'importe quelle donnée membre de l'objet au constructeur d'initialisation d'une autre donnée membre, vous devez vous assurer que l'autre donnée membre a déjà été initialisée. La bonne nouvelle est que vous pouvez déterminer si l'autre donnée membre a (ou non) déjà été initialisée en utilisant des règles du langage indépendantes du compilateur que vous utilisez. La mauvaise nouvelle est qu'il vous faut connaître ces règles. Les sous-objets de la classe de base sont initialisés en premier (vérifiez l'ordre si vous avez de l'héritage multiple et/ou de l'héritage virtuel !), ensuite viennent les données membres définies dans la classe qui est initialisée dans l'ordre dans lequel elles apparaissent dans la déclaration de la classe. Si vous ne connaissez pas ces règles alors ne passez aucune donnée membre depuis l'objet this (cela ne dépend pas de l'utilisation explicite de this) vers l'initialiseur d'une autre donnée membre ! Si vous connaissez ces règles, soyez tout de même très vigilants.
Une technique qui fournit des exécutions plus intuitives et/ou plus sûres de construction pour des utilisateurs de votre classe.
Le problème est que les constructeurs ont toujours le même nom que la classe. Par conséquent la seule façon de les différencier se fait via la liste des paramètres. Mais s'il y a beaucoup de constructeurs, les différences entre les constructeurs deviennent quelque peu subtiles et sujettes à erreur.
Avec l'idiome du constructeur nommé, vous déclarez les constructeurs de toute la classe dans l'une des sections private ou protected.
Vous fournissez des fonctions déclarées statiques dans la section public: qui renvoient un objet. Ces fonctions statiques sont connues comme « constructeurs nommés ». En général il y a une telle fonction statique pour chaque manière différente de construire l'objet.
Par exemple, supposez que nous construisions une classe Point qui représente une position sur le plan X/Y. Il s'avère qu'il y a deux façons d'indiquer une coordonnée dans un espace bidimensionnel : les coordonnées cartésiennes (X+Y) et les coordonnées polaires (Distance+Angle). (Pour cet exemple, l'essentiel est de retenir qu'il y a plusieurs façons de créer un point.) Malheureusement les paramètres pour ces deux systèmes de coordonnées sont identiques : deux réels. Ceci créerait une ambiguïté dans les constructeurs surchargés :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | class Point { public: Point(float x, float y); // Coordonnées rectangulaires Point(float r, float a); // Coordonnées polaires (distance et angle) // ERREUR : Surcharge ambiguë: Point::Point(float, float) }; int main() { Point p = Point(5.7, 1.2); // Ambigu : De quel système de coordonnées parle-t-on ? } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | #include <cmath> // Pour avoir sin() et cos() class Point { public: static Point rectangular(float x, float y); // Coordonnées rectangulaires static Point polar(float radius, float angle); // Coordonnées polaires // Ces fonctions static sont les « constructeurs nommés » // ... private: Point(float x, float y); // coordonnées rectangulaires float x_, y_; }; inline Point::Point(float x, float y) : x_(x), y_(y) { } inline Point Point::rectangular(float x, float y) { return Point(x, y); } inline Point Point::polar(float radius, float angle) { return Point( radius * cos(angle), radius * sin(angle) ); } |
Code c++ : | Sélectionner tout |
1 2 3 4 5 | int main() { Point p1 = Point::rectangular(5.7, 1.2); // Évidemment rectangulaire Point p2 = Point::polar(5.7, 1.2); // Évidemment polaire } |
L'idiome du constructeur nommé peut aussi être utilisé pour vous assurer que les objets d'une classe sont toujours créés avec l'opérateur new.
Dans ce cas, le mieux est de lancer une exception. Lire à ce sujet Peut-on lever des exceptions dans les constructeurs ?.
C'est une méthode très utile pour exploiter le chaînage des fonctions.
Le principal problème solutionné par l'idiome des paramètres nommés est que le C++ ne supporte que les « paramètres par position ». Par exemple, une fonction appelante ne peut pas dire « Voici la valeur pour le paramètre xyz, et voici autre chose pour le paramètre pqr ». Tout ce que vous pouvez faire en C++ (ou en C ou en Java) est « voici le premier paramètre, le second, etc. » L'alternative, appelée « paramètres nommés » et implémentée en Ada, est particulièrement utile si une fonction prend un nombre important de paramètres dont la plupart supportent des valeurs par défaut.
Au cours des années, de nombreuses personnes ont mis au point des astuces pour contourner ce manque de paramètres nommés en C et en C++. Une d'entre elles implique d'intégrer la valeur du paramètre dans une chaîne et de découper cette chaîne à l'exécution. C'est ce qui se passe pour le second paramètre de la fonction fopen(), par exemple. Une autre astuce est de combiner tous les paramètres booléens dans un champ de bits, et la fonction fait un ou logique pour obtenir la valeur du paramètre désiré. C'est ce qui se passe avec le second paramètre de la fonction open(). Cette approche fonctionne, mais la technique exposée ci-dessous produit un code plus simple à écrire et à lire, plus élégant.
L'idée est de transformer les paramètres de la fonction en des fonctions membres d'une nouvelle classe, dans laquelle toutes ces fonctions renvoient *this par référence. Vous renommez ensuite simplement la fonction principale en une fonction sans paramètre de cette classe.
Prenons un exemple pour rendre les choses plus claires.
L'exemple sera pour le concept 'ouvrir un fichier'. Ce concept a besoin logiquement d'un paramètre pour le nom du fichier, et éventuellement d'autres paramètres pour spécifier si le fichier doit être ouvert en lecture, en écriture, ou encore en lecture/écriture ; si le fichier doit être créé s'il n'existe pas déjà ; s'il doit être ouvert en ajout (append) ou en sur-écriture (overwrite) ; la taille des blocs à lire ou écrire ; si les entrées-sorties sont bufferisées ou non ; la taille du buffer ; si le mode est partagé ou exclusif ; et probablement d'autres encore. Si nous implémentions ce concept via une fonction classique avec des paramètres par position, l'appel de cette fonction serait assez désagréable à lire. Il y aurait au moins 8 paramètres, ce qui est source d'erreur. Utilisons donc à la place l'idiome des paramètres nommés.
Avant de nous lancer dans l'implémentation, voici à quoi devrait ressembler le code appelant, en supposant que l'on accepte toutes les valeurs par défaut des paramètres.
Code c++ : | Sélectionner tout |
File f = OpenFile("foo.txt");
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | File f = OpenFile("foo.txt"). readonly(). createIfNotExist(). appendWhenWriting(). blockSize(1024). unbuffered(). exclusiveAccess(); |
Voici comment l'implémenter. Nous créons d'abord une nouvelle classe OpenFile qui contient toutes les valeurs des paramètres en tant que membres de données privées. Ensuite, toutes les fonctions membres (readonly(), blockSize(unsigned), etc.) renvoient *this (c'est-à-dire qu'elles renvoient une référence sur l'objet OpenFile, autorisant ainsi le chaînage des appels des fonctions). Pour terminer, nous spécifions les paramètres requis (le nom du fichier dans le cas présent) dans un paramètre normal (c'est-à-dire par position) passé au constructeur de OpenFile
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class File; class OpenFile { public: OpenFile(const string& filename); // initialise chaque donnée membre à sa valeur par défaut OpenFile& readonly(); // change readonly_ à true OpenFile& createIfNotExist(); OpenFile& blockSize( unsigned nbytes ); // ... private: friend File; bool readonly_; // false par défaut [exemple] unsigned blockSize_; // 4096 par défaut [exemple] // ... }; |
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 | class File { public: File(const OpenFile& params); // s'initialise à partir des paramètres de l'objet OpenFile reçu // ... }; |
Étant donné que chaque fonction membre de la chaîne renvoie une référence, il n'y a pas de copie d'objets et le tout est très efficace. De plus, si les différentes fonctions membres sont déclarées inline, le code généré ressemblera probablement à du code C qui positionne certains membres d'une structure. Bien sur, si les fonctions membres ne sont pas déclarées inline, il risque d'y avoir une légère augmentation de la taille du code et une légère perte de performance (mais uniquement si la construction se passe dans certaines circonstances, comme nous l'avons vu précédemment). Cela peut donc, dans ce cas être un compromis qui rendra le code plus robuste.
Les constructeurs sont appelés dans l'ordre suivant :
- le constructeur des classes de base héritées virtuellement en profondeur croissante et de gauche à droite ;
- le constructeur des classes de base héritées non virtuellement en profondeur croissante et de gauche à droite ;
- le constructeur des membres dans l'ordre de leur déclaration ;
- le constructeur de la classe.
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | #include <iostream> #include <string> struct MembreA{ MembreA() { std::cout << "MembreA" << std::endl; } }; struct A { A() { std::cout << "A" << std::endl; } MembreA m; }; struct MembreB { MembreB() { std::cout << "MembreB" << std::endl; } }; struct B : A { B() { std::cout << "B" << std::endl; } MembreB m; }; struct MembreC { MembreC() { std::cout << "MembreC" << std::endl; } }; struct C : A { C() { std::cout << "C" << std::endl; } MembreC m; }; struct MembreD { MembreD() { std::cout << "MembreD" << std::endl; } }; struct D : B, C {D() { std::cout << "D" << std::endl; MembreD m; }; struct MembreE{MembreE() { std::cout << "MembreE" << std::endl; }; struct E : virtual A {E() { std::cout << "E" << std::endl; } MembreE m; }; struct MembreF{MembreF() { std::cout << "MembreF" << std::endl; }; struct F : virtual A { F() { std::cout << "F" << std::endl; } MembreF m; }; struct MembreG { MembreG() { std::cout << "MembreG" << std::endl; } }; struct G { G() { std::cout << "G" << std::endl; } MembreG m; }; struct MembreH { MembreH() { std::cout << "MembreH" << std::endl; } }; struct H : G, F { H() { std::cout << "H" << std::endl; } MembreH m; }; struct MembreI { MembreI() { std::cout << "MembreI" << std::endl; } }; struct I : E, G, F { I() { std::cout << "I" << std::endl; } MembreI m; }; template<class T> void Creation() { std::cout << "Creation d'un " << typeid(T).name() << " : " << std::endl; T t; } int main() { Creation<A>(); Creation<B>(); Creation<C>(); Creation<D>(); Creation<E>(); Creation<F>(); Creation<G>(); Creation<H>(); Creation<I>(); return 0; } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | Creation d'un struct A : MembreA A Creation d'un struct B : MembreA A MembreB B Creation d'un struct C : MembreA A MembreC C Creation d'un struct D : MembreA A MembreB B MembreA A MembreC C MembreD D Creation d'un struct E : MembreA A MembreE E Creation d'un struct F : MembreA A MembreF F Creation d'un struct G : MembreG G Creation d'un struct H : MembreA A MembreG G MembreF F MembreH H Creation d'un struct I : MembreA A MembreE E MembreG G MembreF F MembreI I |
Remarque 2 : L'ordre de construction est fixé par la norme et ne dépend pas des listes d'initialisation du code :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | struct Membre1 { Membre1() { std::cout << "Membre1" << std::endl; } }; struct Membre2 { Membre2() { std::cout << "Membre2" << std::endl; } }; struct A { A() { std::cout << "A" << std::endl; } }; struct B { B() { std::cout << "B" << std::endl; } }; struct C : A,B { C() :m2(), B(), m1(), A() { std::cout << "C" << std::endl; } Membre1 m1; Membre2 m2; }; int main() { C c; return 0; } |
Code : | Sélectionner tout |
1 2 3 4 5 | A B Membre1 Membre2 C |
Remarque 3 : pour l'héritage virtuel, le constructeur appelé est celui spécifié par le type effectivement instancié et non par celui spécifié par le type demandant l'héritage. Si le type instancié ne spécifie pas de constructeur, alors c'est celui par défaut :
Code c++ : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | struct A { A(std::string appelant_="defaut") { std::cout << "A construit par " << appelant_ << std::endl; } }; struct B : virtual A { B() :A("B") { } }; struct C : B { C() { } }; struct D : B { D() :A("D") { } }; template<class T> void Creation() { std::cout << "Creation d'un " << typeid(T).name() << " : " << std::endl; T t; } int main() { Creation<B>(); Creation<C>(); Creation<D>(); return 0; } |
Code : | Sélectionner tout |
1 2 3 4 5 6 | Creation d'un struct B : A construit par B Creation d'un struct C : A construit par defaut Creation d'un struct D : A construit par D |
Le constructeur d'une classe doit monter sa liste d'initialisation suivant cet ordre :
- les constructeurs des classes héritées virtuellement dans tout l'arbre d'héritage en profondeur croissante et de gauche à droite ;
- les constructeurs des classes de base directement héritées dans l'ordre de gauche à droite ;
- les membres dans l'ordre de leur déclaration.
Ceci a comme conséquences :
- Ce sont d'éventuelles contraintes dans l'ordre de construction qui imposeront l'ordre d'héritage (et non des approches de type d'abord le public, puis le privé).
- Toute dépendance de construction entre les variables membres devra être explicitement commentée à défaut de pouvoir être évitée. Par cette documentation, les lecteurs du code sont avertis qu'il s'agit d'un comportement compris et maîtrisé par le développeur : la fiabilité est accrue et la maintenance est facilitée.
Oui, c'est possible, mais attention, ça ne fait pas toujours ce qu'on pense. La première approche consiste à comprendre que lors de l'appel du constructeur d'une classe de base, la classe dérivée n'a pas encore été construite. Donc, c'est la méthode spécialisée à ce niveau qui est appelée.
Voyons en détail :
La règle est que le type dynamique d'une variable en cours de construction est celui du constructeur qui est en train d'être exécuté. Pour bien comprendre ce qui se passe, il faut donc revenir sur la différence entre le type statique d'une variable, et son type dynamique.
Prenons par exemple trois classes, C qui dérive de B qui dérive de A. Par exemple, dans :
Code c++ : | Sélectionner tout |
A* a = new B();
Par contre, son type dynamique est B*. Une fonction virtuelle est simplement une fonction dont on va chercher le code en utilisant le type dynamique de la variable, au lieu de son type statique, comme une fonction classique.
Maintenant, quand on crée un objet de type C, les choses se passent ainsi :
- On alloue assez de mémoire pour un objet de la taille de C.
- On initialise la sous partie correspondant à A de l'objet.
- On appelle le corps du constructeur de A. Pendant cet appel, l'objet crée a pour type dynamique A.
- On initialise la sous partie correspondant à B de l'objet.
- On appelle le corps du constructeur de B. Pendant cet appel, l'objet crée a pour type dynamique B.
- On initialise la sous partie correspondant à C de l'objet.
- On appelle le corps du constructeur de C. Pendant cet appel, l'objet crée a pour type dynamique C.
Donc, dans le corps du constructeur de la classe B, un appel d'une fonction virtuelle appellera la version de la fonction définie dans la classe B (ou à défaut celle définie dans A si la fonction n'a pas été définie dans B), et non pas celle définie dans la classe C.
D'ailleurs, si la fonction est virtuelle pure dans B, ça causera quelques problèmes, puisqu'on tentera alors d'appeler une fonction qui n'existe pas. En général, le programme va planter, si on a de la chance, il affichera une message du style « Pure function called ».
La problématique est exactement la même pour les destructeurs, mais dans l'ordre inverse.
Pourquoi cette règle ? Une fonction définie dans C a accès aux données membre de C. Or, on a vu que au moment où on exécute l'appel au corps du constructeur de B, ces dernières ne sont pas encore créées. On a donc préféré jouer la sécurité.
Proposer une nouvelle réponse sur la FAQ
Ce n'est pas l'endroit pour poser des questions, allez plutôt sur le forum de la rubrique pour çaLes sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2024 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.