J'apprends le C ++ et je me lance dans les fonctions virtuelles.
D'après ce que j'ai lu (dans le livre et en ligne), les fonctions virtuelles sont des fonctions de la classe de base que vous pouvez remplacer dans les classes dérivées.
Mais plus tôt dans le livre, lors de l'apprentissage de l'héritage de base, j'ai pu remplacer les fonctions de base dans les classes dérivées sans utiliser virtual
.
Alors qu'est-ce que je manque ici? Je sais qu'il y a plus dans les fonctions virtuelles, et cela semble être important, donc je veux être clair sur ce que c'est exactement. Je ne trouve tout simplement pas de réponse directe en ligne.
c++
virtual-functions
Jake Wilson
la source
la source
Réponses:
Voici comment j'ai compris non seulement quelles
virtual
sont les fonctions, mais pourquoi elles sont requises:Disons que vous avez ces deux classes:
Dans votre fonction principale:
Jusqu'ici tout va bien, non? Les animaux mangent des aliments génériques, les chats mangent des rats, tous sans
virtual
.Modifions-le un peu maintenant pour qu'il
eat()
soit appelé via une fonction intermédiaire (une fonction triviale juste pour cet exemple):Maintenant, notre fonction principale est:
Euh oh ... nous avons croisé un chat
func()
, mais il ne mangera pas de rats. Devriez-vous surchargerfunc()
donc il en faut unCat*
? Si vous devez dériver plus d'animaux d'Animal, ils auraient tous besoin du leurfunc()
.La solution est de faire
eat()
de laAnimal
classe une fonction virtuelle:Principale:
Terminé.
la source
virtual
introduit une liaison dynamique vs statique et oui, c'est bizarre si vous venez de langages comme Java.Sans "virtuel", vous obtenez une "liaison anticipée". L'implémentation de la méthode utilisée est décidée au moment de la compilation en fonction du type de pointeur que vous appelez.
Avec "virtuel" vous obtenez "liaison tardive". L'implémentation de la méthode utilisée est décidée au moment de l'exécution en fonction du type de l'objet pointé - en quoi il a été initialement construit. Ce n'est pas nécessairement ce que vous pensez en fonction du type de pointeur qui pointe vers cet objet.
EDIT - voir cette question .
Aussi - ce tutoriel couvre les liaisons précoces et tardives en C ++.
la source
main
fonction etc. pointeur à dérivée implicitement moulages pour pointer à la base (plus spécialisé jette implicitement vers plus général). Visa-versa, vous avez besoin d'un casting explicite, généralement adynamic_cast
. Autre chose - très sujet à un comportement indéfini, alors assurez-vous de savoir ce que vous faites. À ma connaissance, cela n'a pas changé depuis avant même C ++ 98.Vous avez besoin d'au moins 1 niveau d'héritage et d'un abattu pour le démontrer. Voici un exemple très simple:
la source
Vous avez besoin de méthodes virtuelles pour un abaissement , une simplicité et une concision sûrs .
C'est ce que font les méthodes virtuelles: elles abaissent en toute sécurité, avec un code apparemment simple et concis, en évitant les conversions manuelles dangereuses dans le code plus complexe et verbeux que vous auriez autrement.
Méthode non virtuelle ⇒ Liaison statique
Le code suivant est intentionnellement «incorrect». Il ne déclare pas la
value
méthode commevirtual
, et produit donc un «mauvais» résultat involontaire, à savoir 0:Dans la ligne commentée comme «mauvaise», la
Expression::value
méthode est appelée, car le type connu statiquement (le type connu au moment de la compilation) l'estExpression
, et lavalue
méthode n'est pas virtuelle.Méthode virtuelle ⇒ liaison dynamique.
Déclarer
value
commevirtual
dans le type statiquement connuExpression
garantit que chaque appel vérifiera de quel type d'objet il s'agit et appellera l'implémentation appropriée devalue
pour ce type dynamique :Ici, la sortie est
6.86
telle qu'elle devrait être, car la méthode virtuelle est appelée virtuellement . Ceci est également appelé liaison dynamique des appels. Une petite vérification est effectuée pour trouver le type d'objet dynamique réel et l'implémentation de la méthode appropriée pour ce type dynamique est appelée.L'implémentation pertinente est celle de la classe la plus spécifique (la plus dérivée).
Notez que les implémentations de méthode dans les classes dérivées ici ne sont pas marquées
virtual
, mais sont plutôt marquéesoverride
. Ils peuvent être marquésvirtual
mais ils sont automatiquement virtuels. Leoverride
mot-clé garantit que s'il n'y a pas une telle méthode virtuelle dans une classe de base, vous obtiendrez une erreur (ce qui est souhaitable).La laideur de le faire sans méthodes virtuelles
Sans cela,
virtual
il faudrait implémenter une version Do It Yourself de la liaison dynamique. C'est ce qui implique généralement une descente manuelle dangereuse, la complexité et la verbosité.Pour le cas d'une fonction unique, comme ici, il suffit de stocker un pointeur de fonction dans l'objet et d'appeler via ce pointeur de fonction, mais même ainsi, cela implique des rétrogradations dangereuses, de la complexité et de la verbosité, à savoir:
Une façon positive de voir les choses est que si vous rencontrez des abattages non sécurisés, de la complexité et de la verbosité comme ci-dessus, alors souvent une ou plusieurs méthodes virtuelles peuvent vraiment aider.
la source
Les fonctions virtuelles sont utilisées pour prendre en charge le polymorphisme d'exécution .
Autrement dit, le mot clé virtuel indique au compilateur de ne pas prendre la décision (de la liaison de fonction) au moment de la compilation, plutôt de la reporter pour l'exécution " .
Vous pouvez rendre une fonction virtuelle en faisant précéder le mot-clé
virtual
dans sa déclaration de classe de base. Par exemple,Lorsqu'une classe de base a une fonction membre virtuelle, toute classe qui hérite de la classe de base peut redéfinir la fonction avec exactement le même prototype, c'est-à-dire que seule la fonctionnalité peut être redéfinie, pas l'interface de la fonction.
Un pointeur de classe de base peut être utilisé pour pointer vers un objet de classe de base ainsi qu'un objet de classe dérivée.
la source
Si la classe de base est
Base
, et une classe dérivée l'estDer
, vous pouvez avoir unBase *p
pointeur qui pointe réellement vers une instance deDer
. Lorsque vous appelezp->foo();
, iffoo
n'est pas virtuel, alors saBase
version s'exécute, ignorant le fait qu'ilp
pointe réellement vers aDer
. Si foo est virtuel,p->foo()
exécute le remplacement "le plus à la feuille" defoo
, en tenant pleinement compte de la classe réelle de l'élément pointé. Ainsi, la différence entre virtuel et non virtuel est en fait assez cruciale: la première permet le polymorphisme d' exécution , le concept de base de la programmation OO, tandis que la seconde ne le permet pas.la source
Besoin de fonction virtuelle expliqué [Facile à comprendre]
La sortie sera:
Mais avec fonction virtuelle:
La sortie sera:
Par conséquent, avec la fonction virtuelle, vous pouvez obtenir un polymorphisme d'exécution.
la source
Je voudrais ajouter une autre utilisation de la fonction virtuelle bien qu'elle utilise le même concept que les réponses ci-dessus, mais je pense que cela vaut la peine d'être mentionné.
DESTRUCTEUR VIRTUEL
Considérez ce programme ci-dessous, sans déclarer le destructeur de classe Base comme virtuel; la mémoire de Cat ne peut pas être nettoyée.
Production:
Production:
la source
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.
C'est pire que ça. La suppression d'un objet dérivé via un pointeur / référence de base est un comportement non défini pur. Donc, ce n'est pas seulement qu'une certaine mémoire peut fuir. Au contraire, le programme est mal formé, de sorte que le compilateur peut le transformer en quelque chose: code machine qui arrive au travail bien, ou ne fait rien, ou citation démons de votre nez, ou , etc. C'est pourquoi, si un programme est conçu dans un tel de manière à ce qu'un utilisateur puisse supprimer une instance dérivée via une référence de base, la base doit avoir un destructeur virtuelVous devez faire la distinction entre la surcharge et la surcharge. Sans le
virtual
mot - clé, vous ne surchargez qu'une méthode d'une classe de base. Cela ne signifie rien d'autre que se cacher. Disons que vous avez une classe de baseBase
et une classe dérivéeSpecialized
qui implémentent toutes les deuxvoid foo()
. Vous avez maintenant un pointeur pourBase
pointer vers une instance deSpecialized
. Lorsque vous l'appelezfoo()
, vous pouvez observer la différence quivirtual
fait: Si la méthode est virtuelle, l'implémentation deSpecialized
sera utilisée, si elle est manquante, la version deBase
sera choisie. Il est recommandé de ne jamais surcharger les méthodes d'une classe de base. Rendre une méthode non virtuelle est le moyen pour son auteur de vous dire que son extension en sous-classes n'est pas prévue.la source
virtual
vous ne surchargez pas. Vous observez . Si une classe de baseB
a une ou plusieurs fonctionsfoo
et que la classe dérivéeD
définit unfoo
nom, celafoo
masque tous cesfoo
-s dansB
. Ils sont atteints enB::foo
utilisant la résolution de portée. Pour promouvoir desB::foo
fonctions enD
surcharge, vous devez utiliserusing B::foo
.Réponse rapide:
Dans Bjarne Stroustrup C ++ Programming: Principles and Practice, (14.3):
1.L'utilisation de l'héritage, du polymorphisme d'exécution et de l'encapsulation est la définition la plus courante de la programmation orientée objet .
2. Vous ne pouvez pas coder la fonctionnalité pour être plus rapide ou pour utiliser moins de mémoire en utilisant d'autres fonctionnalités linguistiques pour sélectionner parmi les alternatives au moment de l'exécution. Programmation Bjarne Stroustrup C ++: principes et pratique (14.3.1) .
3. Quelque chose pour dire quelle fonction est vraiment invoquée lorsque nous appelons la classe de base contenant la fonction virtuelle.
la source
J'ai ma réponse sous forme de conversation pour une meilleure lecture:
Pourquoi avons-nous besoin de fonctions virtuelles?
À cause du polymorphisme.
Qu'est-ce que le polymorphisme?
Le fait qu'un pointeur de base puisse également pointer vers des objets de type dérivé.
Comment cette définition du polymorphisme conduit-elle au besoin de fonctions virtuelles?
Eh bien, grâce à une liaison anticipée .
Qu'est-ce qu'une liaison anticipée?
La liaison anticipée (liaison au moment de la compilation) en C ++ signifie qu'un appel de fonction est fixé avant l'exécution du programme.
Donc...?
Donc, si vous utilisez un type de base comme paramètre d'une fonction, le compilateur ne reconnaît que l'interface de base, et si vous appelez cette fonction avec des arguments de classes dérivées, elle est coupée, ce qui n'est pas ce que vous voulez.
Si ce n'est pas ce que nous voulons faire, pourquoi est-ce autorisé?
Parce que nous avons besoin de polymorphisme!
Quel est l'avantage du polymorphisme alors?
Vous pouvez utiliser un pointeur de type de base comme paramètre d'une seule fonction, puis au cours de l'exécution de votre programme, vous pouvez accéder à chacune des interfaces de type dérivé (par exemple, leurs fonctions membres) sans aucun problème, en utilisant le déréférencement de cette seule pointeur de base.
Je ne sais toujours pas à quoi servent les fonctions virtuelles ...! Et c'était ma première question!
eh bien, c'est parce que vous avez posé votre question trop tôt!
Pourquoi avons-nous besoin de fonctions virtuelles?
Supposons que vous ayez appelé une fonction avec un pointeur de base, qui avait l'adresse d'un objet de l'une de ses classes dérivées. Comme nous en avons parlé ci-dessus, lors de l'exécution, ce pointeur est déréférencé, jusqu'ici tout va bien, cependant, nous nous attendons à ce qu'une méthode (== une fonction membre) "de notre classe dérivée" soit exécutée! Cependant, une même méthode (une qui a le même en-tête) est déjà définie dans la classe de base, alors pourquoi votre programme devrait-il prendre la peine de choisir l'autre méthode? En d'autres termes, je veux dire, comment pouvez-vous distinguer ce scénario de ce que nous avions l'habitude de voir se produire auparavant?
La réponse brève est "une fonction membre virtuelle dans la base", et une réponse un peu plus longue est que, "à cette étape, si le programme voit une fonction virtuelle dans la classe de base, il sait (se rend compte) que vous essayez d'utiliser polymorphisme "et va donc aux classes dérivées (en utilisant v-table , une forme de liaison tardive) pour trouver qu'une autre méthode avec le même en-tête, mais avec -pourvu - une implémentation différente.
Pourquoi une implémentation différente?
T'es la tête de l'articulation! Allez lire un bon livre !
OK, attendez, attendez, pourquoi se donner la peine d'utiliser des pointeurs de base, alors qu'il / elle pourrait simplement utiliser des pointeurs de type dérivé? Vous êtes le juge, tout ce mal de tête en vaut-il la peine? Regardez ces deux extraits:
//1:
// 2:
OK, bien que je pense que 1 est toujours meilleur que 2 , vous pouvez écrire 1 comme ceci:
//1:
et de plus, sachez que ce n'est encore qu'une utilisation artificielle de tout ce que je vous ai expliqué jusqu'à présent. Au lieu de cela, supposons par exemple une situation dans laquelle vous aviez une fonction dans votre programme qui utilisait respectivement les méthodes de chacune des classes dérivées (getMonthBenefit ()):
Maintenant, essayez de réécrire ceci, sans maux de tête!
Et en fait, cela pourrait être un exemple artificiel non plus!
la source
Lorsque vous avez une fonction dans la classe de base, vous pouvez
Redefine
ouOverride
elle dans la classe dérivée.Redéfinir une méthode : Une nouvelle implémentation de la méthode de la classe de base est donnée dans la classe dérivée. Ne facilite pas
Dynamic binding
.Substitution d'une méthode :
Redefining
avirtual method
de la classe de base dans la classe dérivée. La méthode virtuelle facilite la liaison dynamique .Alors, quand vous avez dit:
vous ne la surchargiez pas car la méthode dans la classe de base n'était pas virtuelle, vous la redéfinissiez plutôt
la source
Cela aide si vous connaissez les mécanismes sous-jacents. C ++ formalise certaines techniques de codage utilisées par les programmeurs C, les "classes" remplacées à l'aide de "superpositions" - des structures avec des sections d'en-tête communes seraient utilisées pour gérer des objets de différents types mais avec des données ou des opérations communes. Normalement, la structure de base de la superposition (la partie commune) a un pointeur vers une table de fonctions qui pointe vers un ensemble différent de routines pour chaque type d'objet. C ++ fait la même chose mais cache les mécanismes c'est-à-dire le C ++
ptr->func(...)
où func est virtuel comme C le serait(*ptr->func_table[func_num])(ptr,...)
, où ce qui change entre les classes dérivées est le contenu func_table. [Une méthode non virtuelle ptr-> func () se traduit simplement par mangled_func (ptr, ..).]Le résultat est que vous avez seulement besoin de comprendre la classe de base pour appeler les méthodes d'une classe dérivée, c'est-à-dire que si une routine comprend la classe A, vous pouvez lui passer un pointeur dérivé de classe B, puis les méthodes virtuelles appelées seront celles de B plutôt que de A puisque vous parcourez la table de fonctions B pointe sur.
la source
Le mot clé virtual indique au compilateur qu'il ne doit pas effectuer de liaison anticipée. Au lieu de cela, il devrait installer automatiquement tous les mécanismes nécessaires pour effectuer une liaison tardive. Pour ce faire, le compilateur typique1 crée une table unique (appelée VTABLE) pour chaque classe qui contient des fonctions virtuelles. Le compilateur place les adresses des fonctions virtuelles pour cette classe particulière dans la VTABLE. Dans chaque classe avec des fonctions virtuelles, il place secrètement un pointeur, appelé vpointer (abrégé en VPTR), qui pointe vers la VTABLE pour cet objet. Lorsque vous effectuez un appel de fonction virtuelle via un pointeur de classe de base, le compilateur insère silencieusement du code pour extraire le VPTR et rechercher l'adresse de fonction dans la VTABLE, appelant ainsi la fonction correcte et provoquant une liaison tardive.
Plus de détails dans ce lien http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html
la source
Le mot-clé virtuel force le compilateur à choisir l'implémentation de la méthode définie dans la classe de l' objet plutôt que dans la classe du pointeur .
Dans l'exemple ci-dessus, Shape :: getName sera appelé par défaut, sauf si getName () est défini comme virtuel dans la classe de base Shape. Cela oblige le compilateur à rechercher l'implémentation getName () dans la classe Triangle plutôt que dans la classe Shape.
La table virtuelle est le mécanisme dans lequel le compilateur assure le suivi des différentes implémentations de méthode virtuelle des sous-classes. Ceci est également appelé répartition dynamique, et il y a des frais généraux associés.
Enfin, pourquoi le virtuel est-il même nécessaire en C ++, pourquoi ne pas en faire le comportement par défaut comme en Java?
la source
Pourquoi avons-nous besoin de fonctions virtuelles?
Les fonctions virtuelles évitent les problèmes de transtypage inutiles, et certains d'entre nous peuvent débattre de la raison pour laquelle nous avons besoin de fonctions virtuelles lorsque nous pouvons utiliser le pointeur de classe dérivée pour appeler la fonction spécifique dans la classe dérivée! La réponse est - cela annule toute l'idée d'héritage dans un grand système développement, où avoir un objet de classe de base de pointeur unique est très souhaitable.
Comparons ci-dessous deux programmes simples pour comprendre l'importance des fonctions virtuelles:
Programme sans fonctions virtuelles:
PRODUCTION:
Programme avec fonction virtuelle:
PRODUCTION:
En analysant de près les deux sorties, on peut comprendre l'importance des fonctions virtuelles.
la source
Réponse POO: Polymorphisme de sous - type
En C ++, des méthodes virtuelles sont nécessaires pour réaliser le polymorphisme , plus précisément le sous - typage ou le polymorphisme de sous - type si vous appliquez la définition de wikipedia.
Wikipedia, Subtyping, 2019-01-09: Dans la théorie du langage de programmation, le sous-typage (également le polymorphisme de sous-type ou le polymorphisme d'inclusion) est une forme de polymorphisme de type dans lequel un sous-type est un type de données qui est lié à un autre type de données (le supertype) par une certaine notion de substituabilité, ce qui signifie que des éléments de programme, généralement des sous-programmes ou des fonctions, écrits pour fonctionner sur des éléments du supertype peuvent également fonctionner sur des éléments du sous-type.
REMARQUE: sous-type signifie classe de base et sous-type signifie classe héritée.
Pour en savoir plus sur le polymorphisme des sous-types
Réponse technique: Dynamic Dispatch
Si vous avez un pointeur sur une classe de base, l'appel de la méthode (qui est déclarée virtuelle) sera envoyé à la méthode de la classe réelle de l'objet créé. C'est ainsi que le polymorphisme de sous-type est réalisé en C ++.
Lectures complémentaires Polymorphism in C ++ and Dynamic Dispatch
Réponse d'implémentation: crée une entrée vtable
Pour chaque modificateur "virtuel" sur les méthodes, les compilateurs C ++ créent généralement une entrée dans la table virtuelle de la classe dans laquelle la méthode est déclarée. C'est ainsi que le compilateur C ++ commun réalise la répartition dynamique .
Lectures complémentaires
Exemple de code
Sortie de l'exemple de code
Diagramme de classe UML d'un exemple de code
la source
Voici un exemple complet qui illustre pourquoi la méthode virtuelle est utilisée.
la source
Concernant l'efficacité, les fonctions virtuelles sont légèrement moins efficaces que les fonctions de liaison anticipée.
"Ce mécanisme d'appel virtuel peut être rendu presque aussi efficace que le mécanisme" d'appel de fonction normale "(dans les 25%). Son surcharge d'espace est un pointeur dans chaque objet d'une classe avec des fonctions virtuelles plus un vtbl pour chacune de ces classes" [ A visite de C ++ par Bjarne Stroustrup]
la source
if(param1>param2) return cst;
où le compilateur peut réduire l'appel de fonction entier à une constante dans certains cas).Les méthodes virtuelles sont utilisées dans la conception d'interfaces. Par exemple, dans Windows, il existe une interface appelée IUnknown comme ci-dessous:
Ces méthodes sont laissées à l'utilisateur de l'interface à mettre en œuvre. Ils sont essentiels pour la création et la destruction de certains objets qui doivent hériter d'IUnknown. Dans ce cas, le run-time connaît les trois méthodes et attend qu'elles soient implémentées lors de leur appel. Donc, dans un sens, ils agissent comme un contrat entre l'objet lui-même et tout ce qui l'utilise.
la source
the run-time is aware of the three methods and expects them to be implemented
Comme ils sont purement virtuels, il n'y a aucun moyen de créer une instance deIUnknown
, et donc toutes les sous-classes doivent implémenter toutes ces méthodes afin de simplement compiler. Il n'y a aucun danger de ne pas les implémenter et de ne le découvrir qu'au moment de l'exécution (mais évidemment, on peut les implémenter à tort , bien sûr!). Et wow, aujourd'hui, j'ai appris Windows#define
sa macro avec le motinterface
, probablement parce que leurs utilisateurs ne peuvent pas simplement (A) voir le préfixeI
dans le nom ou (B) regarder la classe pour voir que c'est une interface. UghJe pense que vous faites référence au fait une fois qu'une méthode est déclarée virtuelle, vous n'avez pas besoin d'utiliser le mot-clé «virtuel» dans les remplacements.
Si vous n'utilisez pas «virtuel» dans la déclaration de foo de Base, alors le foo de Derived ne ferait que l'observer.
la source
Voici une version fusionnée du code C ++ pour les deux premières réponses.
Deux résultats différents sont:
Sans #define virtual , il se lie au moment de la compilation. Animal * ad et func (Animal *) pointent tous vers la méthode dit de l'animal ().
Avec #define virtual , il se lie au moment de l'exécution. Dog * d, Animal * ad et func (Animal *) pointent / font référence à la méthode dit du chien () car Dog est leur type d'objet. Sauf si la méthode [Dog's says () "woof"] n'est pas définie, ce sera celle recherchée en premier dans l'arbre des classes, c'est-à-dire que les classes dérivées peuvent remplacer les méthodes de leurs classes de base [Animal's says ()].
Il est intéressant de noter que tous les attributs de classe (données et méthodes) en Python sont effectivement virtuels . Étant donné que tous les objets sont créés dynamiquement au moment de l'exécution, il n'y a pas de déclaration de type ni de besoin de mot-clé virtual. Voici la version du code de Python:
La sortie est:
qui est identique à la définition virtuelle de C ++. Notez que d et ad sont deux variables de pointeur différentes faisant référence / pointant vers la même instance Dog. L'expression (ad is d) renvoie True et leurs valeurs sont les mêmes < objet .Dog principal à 0xb79f72cc>.
la source
Connaissez-vous les pointeurs de fonction? Les fonctions virtuelles sont une idée similaire, sauf que vous pouvez facilement lier des données à des fonctions virtuelles (en tant que membres de la classe). Il n'est pas aussi facile de lier des données à des pointeurs de fonction. Pour moi, c'est la principale distinction conceptuelle. Beaucoup d'autres réponses ici ne font que dire "parce que ... le polymorphisme!"
la source
Nous avons besoin de méthodes virtuelles pour prendre en charge le «polymorphisme d'exécution». Lorsque vous faites référence à un objet de classe dérivée à l'aide d'un pointeur ou d'une référence à la classe de base, vous pouvez appeler une fonction virtuelle pour cet objet et exécuter la version de la classe dérivée de la fonction.
la source
L'essentiel est que les fonctions virtuelles facilitent la vie. Utilisons certaines des idées de M Perry et décrivons ce qui se passerait si nous n'avions pas de fonctions virtuelles et ne pouvions utiliser que des pointeurs de fonction membre. On a, dans l'estimation normale sans fonctions virtuelles:
D'accord, c'est ce que nous savons. Essayons maintenant de le faire avec des pointeurs de fonction membre:
Bien que nous puissions faire certaines choses avec les pointeurs de fonction membre, ils ne sont pas aussi flexibles que les fonctions virtuelles. Il est difficile d'utiliser un pointeur de fonction membre dans une classe; le pointeur fonction membre presque, au moins dans ma pratique, doit toujours être appelé dans la fonction principale ou à partir d'une fonction membre comme dans l'exemple ci-dessus.
D'un autre côté, les fonctions virtuelles, bien qu'elles puissent avoir une surcharge de pointeur de fonction, simplifient considérablement les choses.
EDIT: Il existe une autre méthode similaire à eddietree: fonction virtuelle c ++ vs pointeur de fonction membre (comparaison des performances) .
la source