runtime polymorphism c
Une étude détaillée du polymorphisme d'exécution en C ++.
Le polymorphisme d'exécution est également appelé polymorphisme dynamique ou liaison tardive. Dans le polymorphisme d'exécution, l'appel de fonction est résolu au moment de l'exécution.
En revanche, pour compiler le temps ou le polymorphisme statique, le compilateur déduit l'objet au moment de l'exécution et décide ensuite quel appel de fonction se lier à l'objet. En C ++, le polymorphisme d'exécution est implémenté à l'aide de la substitution de méthode.
que signifie une référence non définie en c ++
Dans ce didacticiel, nous explorerons en détail tout sur le polymorphisme d'exécution.
=> Consultez TOUS les didacticiels C ++ ici.
Ce que vous apprendrez:
- Remplacement de fonction
- Fonction virtuelle
- Fonctionnement de la table virtuelle et de _vptr
- Fonctions virtuelles pures et classe abstraite
- Destructeurs virtuels
- Conclusion
- lecture recommandée
Remplacement de fonction
Le remplacement de fonction est le mécanisme par lequel une fonction définie dans la classe de base est à nouveau définie dans la classe dérivée. Dans ce cas, nous disons que la fonction est remplacée dans la classe dérivée.
Nous devons nous rappeler que le remplacement de fonction ne peut pas être effectué au sein d'une classe. La fonction est remplacée dans la classe dérivée uniquement. Par conséquent, l'héritage doit être présent pour la substitution de fonction.
La deuxième chose est que la fonction d'une classe de base que nous surchargons doit avoir la même signature ou prototype, c'est-à-dire qu'elle doit avoir le même nom, le même type de retour et la même liste d'arguments.
Voyons un exemple qui illustre le remplacement de méthode.
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'< Production:
Classe :: Base
Classe :: dérivée
Dans le programme ci-dessus, nous avons une classe de base et une classe dérivée. Dans la classe de base, nous avons une fonction show_val qui est remplacée dans la classe dérivée. Dans la fonction principale, nous créons un objet pour chacune des classes Base et Derived et appelons la fonction show_val avec chaque objet. Il produit la sortie souhaitée.
La liaison ci-dessus de fonctions utilisant des objets de chaque classe est un exemple de liaison statique.
Voyons maintenant ce qui se passe lorsque nous utilisons le pointeur de classe de base et assignons des objets de classe dérivés comme contenu.
L'exemple de programme est illustré ci-dessous:
#include using namespace std; class Base { public: void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() //overridden function { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //Early Binding }
Production:
Classe :: Base
Nous voyons maintenant que la sortie est «Class :: Base». Ainsi, quel que soit le type d'objet contenu dans le pointeur de base, le programme affiche le contenu de la fonction de la classe dont le pointeur de base est du type. Dans ce cas, une liaison statique est également effectuée.
Afin de rendre la sortie du pointeur de base, le contenu correct et la liaison appropriée, nous optons pour la liaison dynamique des fonctions. Ceci est réalisé en utilisant le mécanisme des fonctions virtuelles qui est expliqué dans la section suivante.
Fonction virtuelle
Pour que la fonction surchargée soit liée dynamiquement au corps de la fonction, nous rendons la fonction de classe de base virtuelle en utilisant le mot-clé «virtual». Cette fonction virtuelle est une fonction qui est remplacée dans la classe dérivée et le compilateur effectue une liaison tardive ou dynamique pour cette fonction.
Modifions maintenant le programme ci-dessus pour inclure le mot-clé virtuel comme suit:
#include using namespace std;. class Base { public: virtual void show_val() { cout << 'Class::Base'; } }; class Derived:public Base { public: void show_val() { cout <<'Class::Derived'; } }; int main() { Base* b; //Base class pointer Derived d; //Derived class object b = &d; b->show_val(); //late Binding }
Production:
Classe :: dérivée
Donc, dans la définition de classe ci-dessus de Base, nous avons fait de show_val la fonction «virtuelle». Comme la fonction de classe de base est rendue virtuelle, lorsque nous affectons un objet de classe dérivé au pointeur de classe de base et appelons la fonction show_val, la liaison se produit au moment de l'exécution.
Ainsi, comme le pointeur de classe de base contient un objet de classe dérivée, le corps de la fonction show_val dans la classe dérivée est lié à la fonction show_val et donc à la sortie.
En C ++, la fonction remplacée dans la classe dérivée peut également être privée. Le compilateur ne vérifie le type de l'objet qu'au moment de la compilation et lie la fonction au moment de l'exécution, donc cela ne fait aucune différence même si la fonction est publique ou privée.
Notez que si une fonction est déclarée virtuelle dans la classe de base, alors elle sera virtuelle dans toutes les classes dérivées.
Mais jusqu'à présent, nous n'avons pas discuté de la manière dont les fonctions virtuelles jouent un rôle dans l'identification de la fonction correcte à lier ou, en d'autres termes, de la fin de la liaison.
La fonction virtuelle est liée au corps de la fonction avec précision au moment de l'exécution en utilisant le concept de table virtuelle (VTABLE) et un pointeur caché appelé _vptr.
Ces deux concepts sont une implémentation interne et ne peuvent pas être utilisés directement par le programme.
Fonctionnement de la table virtuelle et de _vptr
Tout d'abord, comprenons ce qu'est une table virtuelle (VTABLE).
Le compilateur au moment de la compilation configure une VTABLE pour une classe ayant des fonctions virtuelles ainsi que les classes dérivées de classes ayant des fonctions virtuelles.
Un VTABLE contient des entrées qui sont des pointeurs de fonction vers les fonctions virtuelles qui peuvent être appelées par les objets de la classe. Il existe une entrée de pointeur de fonction pour chaque fonction virtuelle.
Dans le cas de fonctions virtuelles pures, cette entrée est NULL. (C'est la raison pour laquelle nous ne pouvons pas instancier la classe abstraite).
L'entité suivante, _vptr, appelée pointeur vtable, est un pointeur caché que le compilateur ajoute à la classe de base. Ce _vptr pointe vers la vtable de la classe. Toutes les classes dérivées de cette classe de base héritent de _vptr.
Chaque objet d'une classe contenant les fonctions virtuelles stocke en interne ce _vptr et est transparent pour l'utilisateur. Chaque appel à une fonction virtuelle à l'aide d'un objet est ensuite résolu à l'aide de ce _vptr.
Prenons un exemple pour démontrer le fonctionnement de vtable et _vtr.
#include using namespace std; class Base_virtual { public: virtual void function1_virtual() {cout<<'Base :: function1_virtual()
';}; virtual void function2_virtual() {cout<<'Base :: function2_virtual()
';}; virtual ~Base_virtual(){}; }; class Derived1_virtual: public Base_virtual { public: ~Derived1_virtual(){}; virtual void function1_virtual() { coutfunction2_virtual(); delete (b); return (0); }
Production:
Derived1_virtual :: function1_virtual ()
Base :: function2_virtual ()
Dans le programme ci-dessus, nous avons une classe de base avec deux fonctions virtuelles et un destructeur virtuel. Nous avons également dérivé une classe de la classe de base et en cela; nous n'avons remplacé qu'une seule fonction virtuelle. Dans la fonction main, le pointeur de classe dérivé est affecté au pointeur de base.
Ensuite, nous appelons les deux fonctions virtuelles à l'aide d'un pointeur de classe de base. Nous voyons que la fonction surchargée est appelée lorsqu'elle est appelée et non la fonction de base. Alors que dans le second cas, comme la fonction n'est pas remplacée, la fonction de classe de base est appelée.
Voyons maintenant comment le programme ci-dessus est représenté en interne en utilisant vtable et _vptr.
Selon l'explication précédente, comme il y a deux classes avec des fonctions virtuelles, nous aurons deux vtables - une pour chaque classe. De plus, _vptr sera présent pour la classe de base.
Ci-dessus, la représentation graphique de la disposition de la table virtuelle pour le programme ci-dessus. La vtable de la classe de base est simple. Dans le cas de la classe dérivée, seule function1_virtual est remplacée.
Nous voyons donc que dans la classe dérivée vtable, le pointeur de fonction pour function1_virtual pointe vers la fonction surchargée dans la classe dérivée. D'autre part, le pointeur de fonction pour function2_virtual pointe vers une fonction de la classe de base.
Ainsi, dans le programme ci-dessus, lorsque le pointeur de base se voit attribuer un objet de classe dérivée, le pointeur de base pointe vers _vptr de la classe dérivée.
Ainsi, lorsque l'appel b-> function1_virtual () est effectué, le function1_virtual de la classe dérivée est appelé et lorsque l'appel de fonction b-> function2_virtual () est effectué, comme ce pointeur de fonction pointe vers la fonction de classe de base, la fonction de classe de base est appelé.
Fonctions virtuelles pures et classe abstraite
Nous avons vu des détails sur les fonctions virtuelles en C ++ dans notre section précédente. En C ++, on peut également définir un ' fonction virtuelle pure »Qui est généralement égal à zéro.
La fonction virtuelle pure est déclarée comme indiqué ci-dessous.
virtual return_type function_name(arg list) = 0;
La classe qui a au moins une fonction virtuelle pure appelée ' classe abstraite ». Nous ne pouvons jamais instancier la classe abstraite, c'est-à-dire que nous ne pouvons pas créer un objet de la classe abstraite.
C'est parce que nous savons qu'une entrée est faite pour chaque fonction virtuelle dans la VTABLE (table virtuelle). Mais dans le cas d'une fonction virtuelle pure, cette entrée est sans adresse, ce qui la rend incomplète. Ainsi, le compilateur ne permet pas de créer un objet pour la classe avec une entrée VTABLE incomplète.
C'est la raison pour laquelle nous ne pouvons pas instancier une classe abstraite.
L'exemple ci-dessous démontrera la fonction virtuelle pure ainsi que la classe abstraite.
#include using namespace std; class Base_abstract { public: virtual void print() = 0; // Pure Virtual Function }; class Derived_class:public Base_abstract { public: void print() { cout <<'Overriding pure virtual function in derived class
'; } }; int main() { // Base obj; //Compile Time Error Base_abstract *b; Derived_class d; b = &d; b->print(); }
Production:
Remplacer la fonction virtuelle pure dans la classe dérivée
Dans le programme ci-dessus, nous avons une classe définie comme Base_abstract qui contient une fonction virtuelle pure qui en fait une classe abstraite. Ensuite, nous dérivons une classe «Derived_class» de Base_abstract et remplaçons la fonction virtuelle pure qui s'y trouve.
Dans la fonction principale, la première ligne n'est pas commentée. C'est parce que si nous le décommentons, le compilateur donnera une erreur car nous ne pouvons pas créer un objet pour une classe abstraite.
Mais à partir de la deuxième ligne, le code fonctionne. Nous pouvons créer avec succès un pointeur de classe de base, puis nous lui attribuer un objet de classe dérivé. Ensuite, nous appelons une fonction d'impression qui génère le contenu de la fonction d'impression remplacée dans la classe dérivée.
Laissez-nous énumérer quelques caractéristiques de la classe abstraite en bref:
- Nous ne pouvons pas instancier une classe abstraite.
- Une classe abstraite contient au moins une fonction virtuelle pure.
- Bien que nous ne puissions pas instancier une classe abstraite, nous pouvons toujours créer des pointeurs ou des références à cette classe.
- Une classe abstraite peut avoir une implémentation comme des propriétés et des méthodes avec des fonctions virtuelles pures.
- Lorsque nous dérivons une classe de la classe abstraite, la classe dérivée doit remplacer toutes les fonctions virtuelles pures de la classe abstraite. En cas d'échec, la classe dérivée sera également une classe abstraite.
Destructeurs virtuels
Les destructeurs de la classe peuvent être déclarés virtuels. Chaque fois que nous effectuons une conversion ascendante, c'est-à-dire en assignant l'objet de classe dérivé à un pointeur de classe de base, les destructeurs ordinaires peuvent produire des résultats inacceptables.
Par exemple,considérez la remontée suivante du destructeur ordinaire.
#include using namespace std; class Base { public: ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Production:
Classe de base :: Destructeur
Dans le programme ci-dessus, nous avons une classe dérivée héritée de la classe de base. Dans l'ensemble, nous affectons un objet de la classe dérivée à un pointeur de classe de base.
Idéalement, le destructeur qui est appelé lorsque «delete b» est appelé aurait dû être celui de la classe dérivée mais nous pouvons voir à partir de la sortie que le destructeur de la classe de base est appelé car le pointeur de la classe de base pointe vers cela.
Pour cette raison, le destructeur de classe dérivée n'est pas appelé et l'objet de classe dérivée reste intact, ce qui entraîne une fuite de mémoire. La solution à cela est de rendre le constructeur de classe de base virtuel de sorte que le pointeur d'objet pointe vers le destructeur correct et qu'une destruction correcte des objets soit effectuée.
L'utilisation du destructeur virtuel est illustrée dans l'exemple ci-dessous.
#include using namespace std; class Base { public: virtual ~Base() { cout << 'Base Class:: Destructor
'; } }; class Derived:public Base { public: ~Derived() { cout<< 'Derived class:: Destructor
'; } }; int main() { Base* b = new Derived; // Upcasting delete b; }
Production:
Classe dérivée :: Destructor
Classe de base :: Destructeur
C'est le même programme que le programme précédent, sauf que nous avons ajouté un mot-clé virtuel devant le destructeur de classe de base. En rendant le destructeur de classe de base virtuel, nous avons obtenu le résultat souhaité.
Nous pouvons voir que lorsque nous affectons un objet de classe dérivé au pointeur de classe de base, puis supprimons le pointeur de classe de base, les destructeurs sont appelés dans l'ordre inverse de la création d'objet. Cela signifie que le destructeur de classe dérivée est d'abord appelé et l'objet est détruit, puis l'objet de classe de base est détruit.
Remarque: En C ++, les constructeurs ne peuvent jamais être virtuels, car les constructeurs sont impliqués dans la construction et l'initialisation des objets. Par conséquent, nous avons besoin que tous les constructeurs soient exécutés complètement.
Conclusion
Le polymorphisme d'exécution est implémenté en utilisant la substitution de méthode. Cela fonctionne bien lorsque nous appelons les méthodes avec leurs objets respectifs. Mais lorsque nous avons un pointeur de classe de base et que nous appelons des méthodes surchargées à l'aide du pointeur de classe de base pointant vers les objets de classe dérivés, des résultats inattendus se produisent en raison de la liaison statique.
comment exécuter un fichier jnlp
Pour surmonter cela, nous utilisons le concept de fonctions virtuelles. Avec la représentation interne des vtables et _vptr, les fonctions virtuelles nous aident à appeler avec précision les fonctions souhaitées. Dans ce tutoriel, nous avons vu en détail le polymorphisme d'exécution utilisé en C ++.
Avec cela, nous concluons nos tutoriels sur la programmation orientée objet en C ++. Nous espérons que ce didacticiel sera utile pour acquérir une compréhension meilleure et approfondie des concepts de programmation orientée objet en C ++.
=> Visitez ici pour apprendre le C ++ à partir de zéro.
lecture recommandée
- Polymorphisme en C ++
- Héritage en C ++
- Fonctions Friend en C ++
- Classes et objets en C ++
- Utilisation de la classe Selenium Select pour la gestion des éléments déroulants sur une page Web - Tutoriel Selenium # 13
- Tutoriel sur les fonctions principales de Python avec des exemples pratiques
- Machine virtuelle Java: comment JVM aide à exécuter une application Java
- Comment configurer les fichiers de script LoadRunner VuGen et les paramètres d'exécution