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.
- Que signifie le mot-clé virtual ?
- Pouvez-vous me donner une raison simple pour laquelle la virtualité est si importante ?
- Les fonctions virtuelles sont-elles un mécanisme important en C++ ?
- Qu'est-ce qu'une fonction virtuelle pure ?
- Qu'est-ce qu'un type de retour covariant ?
- Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?
Le mot-clé virtual permet de supplanter une fonction membre d'une classe parent depuis une classe dérivée, à condition qu'elle ait la même signature (à l'exception du retour covariant).
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 | class A { public: void F1() { cout << "A::F1()\n"; } virtual void F2() { cout << "A::F2()\n"; } }; class B : public A { public: void F1() { cout << "B::F1()\n"; } void F2() { cout << "B::F2()\n"; } }; int main() { A a; a.F1(); // affiche "A::F1()" a.F2(); // affiche "A::F2()" B b; b.F1(); // affiche "B::F1()" b.F2(); // affiche "B::F2()" // copie non polymorphe a = b; a.F1(); // affiche "A::F1()" a.F2(); // affiche "A::F2()" // utilisation polymorphe de B (par pointeur) A * pa = &b; pa->F1(); // affiche "A::F1()" pa->F2(); // affiche "B::F2()" <-- grâce à virtual // utilisation polymorphe de B (par référence) A & ra = b; ra.F1(); // affiche "A::F1()" ra.F2(); // affiche "B::F2()" <-- grâce à virtual } |
L'appel à une fonction membre virtuel n'est au contraire pas déterminé à la compilation, mais lors de l'exécution. Le fait que A::F2() soit déclarée virtual et supplantée par B::F2() signifie qu'à chaque appel de F2(), le compilateur va tester le type réel de l'objet afin d'appeler B::F2() si possible. Sinon, il appellera A::F2(). On parle alors de liaison dynamique (dynamic binding en anglais) par opposition à la liaison statique faite lors de l'édition de liens.
La virtualité implique l'utilisation de pointeurs ou de références. Ceci est illustré par le 3e exemple du code ci-dessus qui effectue une recopie non polymorphe d'un objet B vers un objet A. Dans ce cas, l'objet B est « tronqué » (pour éviter ce problème, il faut passer par une copie polymorphe, voir Comment effectuer la copie d'objets polymorphes ?) et on obtient un objet de type A malgré que l'on soit parti d'un objet de type B.
Ce n'est pas le cas avec l'utilisation de pointeurs ou références, qui bien que déclarés comme étant des pointeurs / références sur des objets de types A peuvent désigner des objets de type B comme dans les deux derniers exemples du code précédent.
Le type statique de pa est A* mais son type dynamique est B* . De même, le type dynamique de ra est B, ce qui explique que pa->F2() et ra.F2() provoquent l'appel de B::F2() alors que statiquement c'est A::F2() qui aurait du être appelé.
Notez que cet exemple n'inclut pas de destructeur virtuel par souci de simplification, mais ceci serait nécessaire. Pour plus d'explications, lire la question Pourquoi et quand faut-il créer un destructeur virtuel ?.
Attention : en l'absence de l'introduction du mot virtual, nous aurions affaire à un masquage de nom, et non à une redéfinition. Il y aurait alors absence totale de comportement polymorphique. |
L'appel dynamique permet d'augmenter la réutilisabilité en autorisant le « vieux » code à appeler du nouveau code.
Avant l'apparition de l'orientation objet, la réutilisation du code se faisait en appelant du vieux code à partir du nouveau code. Par exemple, un programmeur peut écrire du code appelant du code réutilisable comme printf().
Avec l'orientation objet, la réutilisation peut aussi être accomplie via l'appel de nouveau code par de l'ancien. Par exemple, un programmeur peut écrire du code qui est appelé par un framework qui a été écrit par son arrière grand-père. Il n'y a pas besoin de modifier le code écrit par l'arrière grand-père. En fait, il n'a même pas besoin d'être recompilé. Et si jamais il ne restait que le fichier objet, et que le code écrit par l'arrière grand-père ait été perdu depuis 25 ans, cet ancien fichier objet appellera le code avec les nouvelles fonctionnalités sans rien changer d'autre.
C'est cela l'extensibilité, et c'est cela l'orientation objet.
OUI
Sans les fonctions virtuelles, le C++ ne serait pas un langage orienté objet. La surcharge d'opérateur et les fonctions membres non virtuelles sont très pratiques, mais ne sont, finalement qu'une variante syntaxique de la notion beaucoup plus classique de passage de pointeur sur une structure à une fonction. La bibliothèque standard contient de nombreux templates illustrant les techniques de « programmation générique », qui sont aussi très pratiques, mais les fonctions virtuelles sont le cour même de la programmation orientée objet.
D'un point de vue 'business', il y a très peu de raison de passer du C pur au C++ sans les fonctions virtuelles (pour le moment, nous ferons abstraction de la programmation générique et de la bibliothèque standard). Les spécialistes pensent souvent qu'il a une grande différence entre le C et le C++ non orienté objet ; mais sans l'orientation objet, la différence n'est pas suffisante pour justifier le coût de formation des développeurs, des nouveaux outils.
En d'autres termes, si je devais conseiller un gestionnaire concernant le passage du C au C++ sans orientation objet (c'est-à-dire changer le langage sans changer de paradigme), je le découragerais probablement, à moins qu'il y ait des contraintes liées aux outils utilisés. D'un point de vue gestion, la programmation orientée objet permet de concevoir des systèmes extensibles et adaptables, mais la syntaxe seule sans l'orientation objet ne réduira jamais le coût de maintenance, mais augmentera probablement les coûts de formation de façon significative.
Nota : le C++ sans virtualité n'est pas orienté objet. Programmer avec des classes mais sans liaison dynamique est une programmation basée sur des objets, mais n'est pas de la programmation objet. Ignorer la virtualité est équivalent à ignorer l'orientation objet. Tout ce qui reste est une programmation basée sur des objets, tout comme la version originale d'ADA. (Soit dit en passant, le nouvel ADA supporte la véritable orientation objet, et non plus simplement la programmation basée sur les objets).
Syntaxiquement, une fonction virtuelle pure est une fonction virtuelle suivie de « = 0 » dans sa déclaration :
Code c++ : | Sélectionner tout |
1 2 3 4 5 | class Test { public: virtual void F() = 0; // = 0 signifie "virtuelle pure" }; |
Une fonction virtuelle pure signifie qu'elle doit être supplantée par une fonction d'une classe fille.
La classe qui déclare une fonction virtuelle pure n'est alors pas instanciable car elle possède au moins une fonction qui doit être supplantée. On dit alors que c'est une classe abstraite (lire Qu'est-ce qu'une classe abstraite ?).
Notez que virtuelle pure veut simplement dire que la fonction doit être supplantée par les classes filles que l'on veut instanciables, et non que la fonction n'est pas implémentée. Le C++ autorise une fonction virtuelle pure à disposer d'un corps. Une telle pratique sert généralement à forcer une classe à être abstraite (en rendant son destructeur virtuel pur) ou à proposer une implémentation type pour la fonction virtuelle pure.
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 | class A { public: virtual void f() = 0; // virtuelle pure }; void A::f() { // implémentation par défaut } // B se contente de l'implémentation par défaut de f() class B : public A { public: void f() { A::f(); } }; |
Lors de la réimplémentation d'une fonction membre virtuelle dans une classe dérivée, il est possible de ne pas tout à fait respecter le prototype de la fonction virtuelle en renvoyant un type dérivé du type originel :
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 | class A {}; class B : public A {}; class Base { public: virtual A* Test() { cout << "Base::Test\n"; return new A; } }; class Derived : public Base { public: virtual B* Test() // le type de retour est différent de Base::Test { cout << "Derived::Test\n"; return new B; } }; int main() { Base *b = new Derived; A *a = b->Test(); } |
Cette possibilité est en particulier utilisée dans le clonage de classes polymorphes. Les retours covariants permettent en effet de transformer le code suivant :
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 | class Clonable { public: virtual Clonable* Clone() const = 0; }; class A : public Clonable { public: virtual Clonable* Clone() const { return new A( *this ); } }; int main() { A *a1 = new A; // faire une copie A *a2 = static_cast<A*>( a1->Clone() ); // cast obligatoire ! } |
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 | class Clonable { public: virtual Clonable* Clone() const = 0; }; class A : public Clonable { public: virtual A* Clone() const // retour covariant { return new A( *this ); } }; int main() { A *a1 = new A; // faire une copie A *a2 = a1->Clone(); // plus de cast } |
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.