Pourquoi avons-nous besoin de fonctions virtuelles en C ++?

1312

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.

Jake Wilson
la source
13
J'ai créé une explication pratique pour les fonctions virtuelles ici: nrecursions.blogspot.in/2015/06/…
Nav
4
C'est peut-être le plus grand avantage des fonctions virtuelles - la possibilité de structurer votre code de telle manière que les nouvelles classes dérivées fonctionneront automatiquement avec l'ancien code sans modification!
user3530616
tbh, les fonctions virtuelles sont une fonction de base de la POO, pour l'effacement de type. Je pense que ce sont les méthodes non virtuelles qui rendent Object Pascal et C ++ spéciaux, en optimisant les grandes tables inutiles et en autorisant les classes compatibles POD. De nombreux langages POO s'attendent à ce que chaque méthode puisse être remplacée.
Swift - Friday Pie
C'est une bonne question. En effet, cette chose virtuelle en C ++ est abstraite dans d'autres langages comme Java ou PHP. En C ++, vous gagnez juste un peu plus de contrôle pour certains cas rares (soyez conscient de l'héritage multiple ou de ce cas spécial du DDOD ). Mais pourquoi cette question est-elle publiée sur stackoverflow.com?
Edgar Alloro
Je pense que si vous jetez un œil à la liaison anticipée-à la liaison tardive et à VTABLE, ce serait plus raisonnable et logique. Il y a donc une bonne explication ( learncpp.com/cpp-tutorial/125-the-virtual-table ) ici.
ceyun

Réponses:

2730

Voici comment j'ai compris non seulement quelles virtualsont les fonctions, mais pourquoi elles sont requises:

Disons que vous avez ces deux classes:

class Animal
{
    public:
        void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

Dans votre fonction principale:

Animal *animal = new Animal;
Cat *cat = new Cat;

animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."

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):

// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }

Maintenant, notre fonction principale est:

Animal *animal = new Animal;
Cat *cat = new Cat;

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating generic food."

Euh oh ... nous avons croisé un chat func(), mais il ne mangera pas de rats. Devriez-vous surcharger func()donc il en faut un Cat*? Si vous devez dériver plus d'animaux d'Animal, ils auraient tous besoin du leur func().

La solution est de faire eat()de la Animalclasse une fonction virtuelle:

class Animal
{
    public:
        virtual void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

Principale:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."

Terminé.

M Perry
la source
165
Donc, si je comprends bien, virtual permet d'appeler la méthode de la sous-classe, même si l'objet est traité comme sa superclasse?
Kenny Worden
147
Au lieu d'expliquer la liaison tardive à travers l'exemple d'une fonction intermédiaire "func", voici une démonstration plus simple - Animal * animal = new Animal; // Cat * cat = nouveau Cat; Animal * chat = nouveau chat; animal-> manger (); // sorties: "Je mange des aliments génériques." chat-> manger (); // sorties: "Je mange des aliments génériques." Même si vous affectez l'objet sous-classé (Cat), la méthode invoquée est basée sur le type de pointeur (Animal) et non sur le type d'objet vers lequel il pointe. C'est pourquoi vous avez besoin de "virtuel".
rexbelia
37
Suis-je le seul à trouver ce comportement par défaut en C ++ juste bizarre? Je m'attendais à ce que le code sans "virtuel" fonctionne.
David 天宇 Wong
20
@David 天宇 Wong, je pense, virtualintroduit une liaison dynamique vs statique et oui, c'est bizarre si vous venez de langages comme Java.
peterchaula
32
Tout d'abord, les appels virtuels sont beaucoup, beaucoup plus chers que les appels de fonction réguliers. La philosophie C ++ est rapide par défaut, donc les appels virtuels par défaut sont un gros no-no. La deuxième raison est que les appels virtuels peuvent entraîner la rupture de votre code si vous héritez une classe d'une bibliothèque et que cela change son implémentation interne d'une méthode publique ou privée (qui appelle une méthode virtuelle en interne) sans changer le comportement de la classe de base.
saolof
672

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.

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()  {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 ()  {  std::cout << "Derived::Method2" << std::endl;  }
};

Base* obj = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

obj->Method1 ();  //  Prints "Base::Method1"
obj->Method2 ();  //  Prints "Derived::Method2"

EDIT - voir cette question .

Aussi - ce tutoriel couvre les liaisons précoces et tardives en C ++.

Steve314
la source
11
Excellent, et rentre chez lui rapidement et avec de meilleurs exemples. Ceci est cependant simpliste et le questionneur devrait vraiment lire la page parashift.com/c++-faq-lite/virtual-functions.html . D'autres personnes ont déjà souligné cette ressource dans les articles SO liés à ce fil, mais je pense que cela mérite d'être rappelé.
Sonny
36
Je ne sais pas si les liaisons précoces et tardives sont des termes spécifiquement utilisés dans la communauté c ++, mais les termes corrects sont les liaisons statiques (au moment de la compilation) et dynamiques (au moment de l'exécution).
Mike
31
@mike - "Le terme" liaison tardive "remonte au moins aux années 1960, où il peut être trouvé dans les communications de l'ACM." . Ce ne serait pas bien s'il y avait un mot correct pour chaque concept? Malheureusement, ce n'est pas le cas. Les termes «liaison anticipée» et «liaison tardive» sont antérieurs à C ++ et même à la programmation orientée objet, et sont tout aussi corrects que les termes que vous utilisez.
Steve314
4
@BJovke - cette réponse a été écrite avant la publication de C ++ 11. Même si, je viens compilé dans GCC 6.3.0 (en C ++ 14 par défaut) sans problème - emballage évidemment la déclaration variable et les appels en mainfonction 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 a dynamic_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.
Steve314
10
Notez que les compilateurs C ++ d'aujourd'hui peuvent souvent optimiser la liaison tardive au début - lorsqu'ils peuvent être certains de ce que sera la liaison. C'est ce que l'on appelle également la «dé-virtualisation».
einpoklum
83

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:

class Animal
{        
    public: 
      // turn the following virtual modifier on/off to see what happens
      //virtual   
      std::string Says() { return "?"; }  
};

class Dog: public Animal
{
    public: std::string Says() { return "Woof"; }
};

void test()
{
    Dog* d = new Dog();
    Animal* a = d;       // refer to Dog instance with Animal pointer

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}
Henk Holterman
la source
39
Votre exemple indique que la chaîne renvoyée dépend du fait que la fonction est virtuelle, mais elle ne dit pas quel résultat correspond à virtuel et lequel correspond à non virtuel. De plus, c'est un peu déroutant car vous n'utilisez pas la chaîne renvoyée.
Ross
7
Avec mot-clé virtuel: Woof . Sans mot - clé virtuel: ? .
Hesham Eraqi
@HeshamEraqi sans virtuel c'est une liaison anticipée et il affichera "?" de la classe de base
Ahmad
46

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 valueméthode comme virtual, et produit donc un «mauvais» résultat involontaire, à savoir 0:

#include <iostream>
using namespace std;

class Expression
{
public:
    auto value() const
        -> double
    { return 0.0; }         // This should never be invoked, really.
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const
        -> double
    { return number_; }     // This is OK.

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const
        -> double
    { return a_->value() + b_->value(); }       // Uhm, bad! Very bad!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

Dans la ligne commentée comme «mauvaise», la Expression::valueméthode est appelée, car le type connu statiquement (le type connu au moment de la compilation) l'est Expression, et la valueméthode n'est pas virtuelle.


Méthode virtuelle ⇒ liaison dynamique.

Déclarer valuecomme virtualdans le type statiquement connu Expressiongarantit que chaque appel vérifiera de quel type d'objet il s'agit et appellera l'implémentation appropriée de valuepour ce type dynamique :

#include <iostream>
using namespace std;

class Expression
{
public:
    virtual
    auto value() const -> double
        = 0;
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const -> double
        override
    { return number_; }

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const -> double
        override
    { return a_->value() + b_->value(); }    // Dynamic binding, OK!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

Ici, la sortie est 6.86telle 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ées override. Ils peuvent être marqués virtualmais ils sont automatiquement virtuels. Le overridemot-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, virtualil 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:

#include <iostream>
using namespace std;

class Expression
{
protected:
    typedef auto Value_func( Expression const* ) -> double;

    Value_func* value_func_;

public:
    auto value() const
        -> double
    { return value_func_( this ); }

    Expression(): value_func_( nullptr ) {}     // Like a pure virtual.
};

class Number
    : public Expression
{
private:
    double  number_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    { return static_cast<Number const*>( expr )->number_; }

public:
    Number( double const number )
        : Expression()
        , number_( number )
    { value_func_ = &Number::specific_value_func; }
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    {
        auto const p_self  = static_cast<Sum const*>( expr );
        return p_self->a_->value() + p_self->b_->value();
    }

public:
    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    { value_func_ = &Sum::specific_value_func; }
};


auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

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.

Santé et hth. - Alf
la source
40

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é virtualdans sa déclaration de classe de base. Par exemple,

     class Base
     {
        virtual void func();
     }
  • 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.

     class Derive : public Base
     {
        void func();
     }
  • 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.

  • Lorsque la fonction virtuelle est appelée à l'aide d'un pointeur de classe de base, le compilateur décide au moment de l'exécution quelle version de la fonction - c'est-à-dire la version de classe de base ou la version de classe dérivée substituée - doit être appelée. C'est ce qu'on appelle le polymorphisme d'exécution .
Yoon5oo
la source
34

Si la classe de base est Base, et une classe dérivée l'est Der, vous pouvez avoir un Base *ppointeur qui pointe réellement vers une instance de Der. Lorsque vous appelez p->foo();, if foon'est pas virtuel, alors sa Baseversion s'exécute, ignorant le fait qu'il ppointe réellement vers a Der. Si foo est virtuel, p->foo()exécute le remplacement "le plus à la feuille" de foo, 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.

Alex Martelli
la source
8
Je déteste vous contredire, mais le polymorphisme à la compilation est toujours du polymorphisme. Même la surcharge des fonctions non membres est une forme de polymorphisme - polymorphisme ad hoc utilisant la terminologie de votre lien. La différence ici est entre la liaison précoce et tardive.
Steve314
7
@ Steve314, vous êtes pédantiquement correct (en tant que camarade pédant, j'approuve cela ;-) - éditer la réponse pour ajouter l'adjectif manquant ;-).
Alex Martelli
26

Besoin de fonction virtuelle expliqué [Facile à comprendre]

#include<iostream>

using namespace std;

class A{
public: 
        void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
     void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B; // Create a base class pointer and assign address of derived object.
    a1->show();

}

La sortie sera:

Hello from Class A.

Mais avec fonction virtuelle:

#include<iostream>

using namespace std;

class A{
public:
    virtual void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
    virtual void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B;
    a1->show();

}

La sortie sera:

Hello from Class B.

Par conséquent, avec la fonction virtuelle, vous pouvez obtenir un polymorphisme d'exécution.

Ajay GU
la source
25

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.

class Animal {
    public:
    ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat() {
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

Production:

Deleting an Animal
class Animal {
    public:
    virtual ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat(){
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

Production:

Deleting an Animal name Cat
Deleting an Animal
Aryaman Gupta
la source
11
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 virtuel
underscore_d
21

Vous devez faire la distinction entre la surcharge et la surcharge. Sans le virtualmot - 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 base Baseet une classe dérivée Specializedqui implémentent toutes les deux void foo(). Vous avez maintenant un pointeur pour Basepointer vers une instance de Specialized. Lorsque vous l'appelez foo(), vous pouvez observer la différence qui virtualfait: Si la méthode est virtuelle, l'implémentation de Specializedsera utilisée, si elle est manquante, la version de Basesera 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.

h0b0
la source
3
Sans virtualvous ne surchargez pas. Vous observez . Si une classe de base Ba une ou plusieurs fonctions fooet que la classe dérivée Ddéfinit un foonom, cela foo masque tous ces foo-s dans B. Ils sont atteints en B::fooutilisant la résolution de portée. Pour promouvoir des B::foofonctions en Dsurcharge, vous devez utiliser using B::foo.
Kaz
20

Pourquoi avons-nous besoin de méthodes virtuelles en C ++?

Réponse rapide:

  1. Il nous fournit l'un des "ingrédients" 1 nécessaires à la programmation orientée objet .

Dans Bjarne Stroustrup C ++ Programming: Principles and Practice, (14.3):

La fonction virtuelle permet de définir une fonction dans une classe de base et d'avoir une fonction du même nom et de taper dans une classe dérivée appelée lorsqu'un utilisateur appelle la fonction de classe de base. Cela est souvent appelé polymorphisme au moment de l'exécution , répartition dynamique ou répartition au moment de l'exécution, car la fonction appelée est déterminée au moment de l'exécution en fonction du type de l'objet utilisé.

  1. C'est l'implémentation la plus rapide et la plus efficace si vous avez besoin d'un appel de fonction virtuelle 2 .

Pour gérer un appel virtuel, il faut une ou plusieurs données liées à l' objet dérivé 3 . La manière la plus courante consiste à ajouter l'adresse de la table des fonctions. Cette table est généralement appelée table virtuelle ou table de fonction virtuelle et son adresse est souvent appelée pointeur virtuel . Chaque fonction virtuelle obtient un emplacement dans la table virtuelle. Selon le type d'objet (dérivé) de l'appelant, la fonction virtuelle, à son tour, invoque le remplacement respectif.


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.

Ziezi
la source
15

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:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

// 2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

OK, bien que je pense que 1 est toujours meilleur que 2 , vous pouvez écrire 1 comme ceci:

//1:

Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();

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 ()):

double totalMonthBenefit = 0;    
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
     totalMonthBenefit += x -> getMonthBenefit();
}

Maintenant, essayez de réécrire ceci, sans maux de tête!

double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();

Et en fait, cela pourrait être un exemple artificiel non plus!

MJ
la source
2
le concept d'itération sur différents types de (sous-) objets en utilisant un seul (super-) type d'objet doit être mis en évidence, c'est un bon point que vous avez donné, merci
harshvchawla
14

Lorsque vous avez une fonction dans la classe de base, vous pouvez Redefineou Overrideelle 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 pasDynamic binding .

Substitution d'une méthode : Redefiningavirtual methodde la classe de base dans la classe dérivée. La méthode virtuelle facilite la liaison dynamique .

Alors, quand vous avez dit:

Mais plus tôt dans le livre, en apprenant l'héritage de base, j'ai pu remplacer les méthodes de base dans les classes dérivées sans utiliser «virtuel».

vous ne la surchargiez pas car la méthode dans la classe de base n'était pas virtuelle, vous la redéfinissiez plutôt

nitin_cherian
la source
11

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.

Kev
la source
8

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

rvkreddy
la source
7

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 .

Shape *shape = new Triangle(); 
cout << shape->getName();

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?

  1. C ++ est basé sur les principes de "Zero Overhead" et "Pay for what you use". Il n'essaie donc pas d'effectuer une répartition dynamique pour vous, sauf si vous en avez besoin.
  2. Pour fournir plus de contrôle à l'interface. En rendant une fonction non virtuelle, la classe interface / abstract peut contrôler le comportement dans toutes ses implémentations.
javaProgrammer
la source
4

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:

#include <iostream>
using namespace std;

class father
{
    public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

PRODUCTION:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years

Programme avec fonction virtuelle:

#include <iostream>
using namespace std;

class father
{
    public:
        virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

PRODUCTION:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

En analysant de près les deux sorties, on peut comprendre l'importance des fonctions virtuelles.

akshaypmurgod
la source
4

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

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~Animal(){};
};

class Cat : public Animal {
public:
    virtual void MakeTypicalNoise()
    {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
        cout << "Woof!" << endl;
    }
};

class Doberman : public Dog {
public:
    virtual void MakeTypicalNoise() {
        cout << "Woo, woo, woow!";
        cout << " ... ";
        Dog::MakeTypicalNoise();
    }
};

int main() {

    Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };

    const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);
    for ( int i = 0; i < cnAnimals; i++ ) {
        apObject[i]->MakeTypicalNoise();
    }
    for ( int i = 0; i < cnAnimals; i++ ) {
        delete apObject[i];
    }
    return 0;
}

Sortie de l'exemple de code

Meow!
Woof!
Woo, woo, woow! ... Woof!

Diagramme de classe UML d'un exemple de code

Diagramme de classe UML d'un exemple de code

Jörg «Wuwei» Brüggmann
la source
1
Prenez mon vote positif parce que vous montrez l'utilisation peut-être la plus importante du polymorphisme: qu'une classe de base avec des fonctions membres virtuelles spécifie une interface ou, en d'autres termes, une API. Le code utilisant un tel cadre de classe (ici: votre fonction principale) peut traiter tous les éléments d'une collection (ici: votre tableau) de manière uniforme et n'a pas besoin de, ne veut pas et, en fait, ne peut souvent pas savoir quelle implémentation concrète sera invoquée au moment de l'exécution, par exemple parce qu'il n'existe pas encore. C'est l'un des fondements de la création de relations abstraites entre les objets et les gestionnaires.
Peter - Réintègre Monica le
2

Voici un exemple complet qui illustre pourquoi la méthode virtuelle est utilisée.

#include <iostream>

using namespace std;

class Basic
{
    public:
    virtual void Test1()
    {
        cout << "Test1 from Basic." << endl;
    }
    virtual ~Basic(){};
};
class VariantA : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantA." << endl;
    }
};
class VariantB : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantB." << endl;
    }
};

int main()
{
    Basic *object;
    VariantA *vobjectA = new VariantA();
    VariantB *vobjectB = new VariantB();

    object=(Basic *) vobjectA;
    object->Test1();

    object=(Basic *) vobjectB;
    object->Test1();

    delete vobjectA;
    delete vobjectB;
    return 0;
}
user3371350
la source
1

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]

Duc
la source
2
La liaison tardive ne fait pas que ralentir l'appel de fonction, elle rend la fonction appelée inconnue jusqu'au moment de l'exécution, de sorte que les optimisations sur l'appel de fonction ne peuvent pas être appliquées. Cela peut tout changer f.ex. dans les cas où la propagation de valeur supprime beaucoup de code (pensez if(param1>param2) return cst;où le compilateur peut réduire l'appel de fonction entier à une constante dans certains cas).
curiousguy
1

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:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

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 implementedComme ils sont purement virtuels, il n'y a aucun moyen de créer une instance de IUnknown, 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 #definesa macro avec le mot interface, probablement parce que leurs utilisateurs ne peuvent pas simplement (A) voir le préfixe Idans le nom ou (B) regarder la classe pour voir que c'est une interface. Ugh
underscore_d
1

Je 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.

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

Si vous n'utilisez pas «virtuel» dans la déclaration de foo de Base, alors le foo de Derived ne ferait que l'observer.

edwinc
la source
1

Voici une version fusionnée du code C ++ pour les deux premières réponses.

#include        <iostream>
#include        <string>

using   namespace       std;

class   Animal
{
        public:
#ifdef  VIRTUAL
                virtual string  says()  {       return  "??";   }
#else
                string  says()  {       return  "??";   }
#endif
};

class   Dog:    public Animal
{
        public:
                string  says()  {       return  "woof"; }
};

string  func(Animal *a)
{
        return  a->says();
}

int     main()
{
        Animal  *a = new Animal();
        Dog     *d = new Dog();
        Animal  *ad = d;

        cout << "Animal a says\t\t" << a->says() << endl;
        cout << "Dog d says\t\t" << d->says() << endl;
        cout << "Animal dog ad says\t" << ad->says() << endl;

        cout << "func(a) :\t\t" <<      func(a) <<      endl;
        cout << "func(d) :\t\t" <<      func(d) <<      endl;
        cout << "func(ad):\t\t" <<      func(ad)<<      endl;
}

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 ().

$ g++ virtual.cpp -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  ??
func(a) :       ??
func(d) :       ??
func(ad):       ??

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 ()].

$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

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:

class   Animal:
        def     says(self):
                return  "??"

class   Dog(Animal):
        def     says(self):
                return  "woof"

def     func(a):
        return  a.says()

if      __name__ == "__main__":

        a = Animal()
        d = Dog()
        ad = d  #       dynamic typing by assignment

        print("Animal a says\t\t{}".format(a.says()))
        print("Dog d says\t\t{}".format(d.says()))
        print("Animal dog ad says\t{}".format(ad.says()))

        print("func(a) :\t\t{}".format(func(a)))
        print("func(d) :\t\t{}".format(func(d)))
        print("func(ad):\t\t{}".format(func(ad)))

La sortie est:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

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>.

Leon Chang
la source
1

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!"

user2445507
la source
0

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.

rashedcs
la source
-1

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:

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

D'accord, c'est ce que nous savons. Essayons maintenant de le faire avec des pointeurs de fonction membre:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

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) .

pêcheur
la source