web-dev-qa-db-fra.com

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

J'apprends le C++ et j'entre 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 des classes dérivées.

Mais plus tôt dans le livre, lors de l'apprentissage de l'héritage de base, j'étais capable de remplacer les fonctions de base dans les classes dérivées sans utiliser virtual.

Alors qu'est-ce qui me manque ici? Je sais qu'il y a plus que des fonctions virtuelles, et cela semble être important, je veux donc être clair sur ce que c'est exactement. Je n'arrive pas à trouver une réponse directe en ligne.

1169
Jake Wilson

Voici comment j’ai compris non seulement ce que sont les fonctions virtual , mais pourquoi elles sont nécessaires:

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.

Changeons-le un peu maintenant pour que 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 passé un chat dans func(), mais il ne mangera pas de rats. Si vous surchargez func(), il faut donc un Cat*? Si vous devez extraire plus d'animaux d'Animal, ils auront tous besoin de leur propre func().

La solution consiste à faire de eat() de la classe Animal 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é.

2551
M Perry

Sans "virtuel", vous obtenez une "liaison anticipée". La mise en œuvre de la méthode utilisée est décidée au moment de la compilation en fonction du type de pointeur utilisé.

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 d'objet pointé - de quoi il a été construit à l'origine. Ce n'est pas nécessairement ce que vous penseriez 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++.

612
Steve314

Vous avez besoin d'au moins 1 niveau d'héritage et d'un downcast 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

    cout << d->Says();   // always Woof
    cout << a->Says();   // Woof or ?, depends on virtual
}
79
Henk Holterman

Vous avez besoin de méthodes virtuelles pour downcasting sécurisé, simplicité et concision.

C’est ce que font les méthodes virtuelles: elles descendent en toute sécurité, avec un code apparemment simple et concis, évitant ainsi les conversions manuelles peu sûres du code plus complexe et plus détaillé que vous auriez autrement.


Méthode non virtuelle ⇒ Liaison statique

Le code suivant est intentionnellement "incorrect". Elle ne déclare pas la méthode value en tant que virtual et génère par conséquent un résultat "erroné" inattendu, à 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 méthode Expression::value est appelée, car le type connu de manière statique (le type connu lors de la compilation) est Expression et la méthode value n'est pas virtuelle.


Méthode virtuelle ⇒ liaison dynamique.

Déclarer value comme virtual dans le type connu de manière statique Expression garantit que chaque appel vérifie le type réel de l'objet et appelle l'implémentation pertinente de value pour celui-ci 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, le résultat est 6.86 comme il se doit, puisque 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'appel de la méthode appropriée pour ce type dynamique.

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éthodes dans les classes dérivées ne sont pas marquées ici virtual, mais plutôt override. Ils peuvent être marqués virtual mais ils sont automatiquement virtuels. Le mot clé override garantit que s'il existe pas une telle méthode virtuelle dans une classe de base, vous obtiendrez une erreur (ce qui est souhaitable).


La laideur de faire cela sans méthodes virtuelles

Sans virtual, il faudrait implémenter une version à faire soi-même de la liaison dynamique. C’est ce qui implique généralement un downcasting manuel dangereux, de la complexité et de la verbosité.

Dans 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 cela implique néanmoins des rétrogradations peu sûres, de complexité et de 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 cela est que, si vous rencontrez un downcasting, une complexité et une verbosité non sécurisés comme ci-dessus, une méthode ou des méthodes virtuelles peuvent souvent vraiment aider.

41

Les fonctions virtuelles sont utilisées pour prendre en charge polymorphisme à l'exécution.

C’est-à-dire que le mot-clé virtuel indique au compilateur ne pas prendre la décision (de la liaison de la fonction) au moment de la compilation, mais plutôt le reporter au moment de 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,

     class Base
     {
        virtual void func();
     }
    
  • Quand une classe de base a une fonction de membre virtuel, toute classe qui hérite de la classe de base peut redéfinir la fonction avec exactement le même prototype c'est-à-dire uniquement la fonctionnalité peut être redéfinie, pas l'interface de la fonction.

     class Derive : public Base
     {
        void func();
     }
    
  • Un pointeur de classe Base peut être utilisé pour pointer sur un objet de la classe Base ainsi que sur un objet de la classe dérivée.

  • Lorsque la fonction virtuelle est appelée à l'aide d'un pointeur de classe Base, le compilateur décide au moment de l'exécution quelle version de la fonction - c'est-à-dire la version de la classe Base ou la version dérivée de la classe Derived - doit être appelée. Ceci s'appelle polymorphisme d'exécution.
39
user6359267

Si la classe de base est Base et qu'une classe dérivée est Der, vous pouvez avoir un pointeur Base *p qui pointe en fait sur une instance de Der. Lorsque vous appelez p->foo();, si foo est non virtuel, la version de Base s’exécute, en ignorant la fait que p pointe en fait sur un Der. Si foo est virtuel, p->foo() exécute le remplacement "Leafmost" de foo, en tenant pleinement compte de la classe réelle du article pointé. Donc, la différence entre virtuel et non virtuel est en fait assez cruciale: la première permet l'exécution polymorphism , le concept de base de la programmation OO, contrairement à la dernière.

33
Alex Martelli

Le 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 à l'exécution.

26
Ajay GU

Je voudrais ajouter une autre utilisation de la fonction virtuelle, bien qu’elle utilise le même concept que celui indiqué ci-dessus, mais je suppose que cela vaut la peine d’être mentionné.

DESTRUCTOR VIRTUEL

Considérez ce programme ci-dessous, sans déclarer le destructeur de la classe de base comme virtuel; la mémoire pour 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;
}

Sortie:

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;
}

Sortie:

Deleting an Animal name Cat
Deleting an Animal
24
Aryaman Gupta

Vous devez faire la distinction entre neutraliser et surcharger. Sans le mot clé virtual, vous ne surchargez qu'une méthode d'une classe de base. Cela ne veut rien dire que se cacher. Supposons que vous avez une classe de base Base et une classe dérivée Specialized qui implémentent toutes les deux void foo(). Vous avez maintenant un pointeur sur Base pointant vers une instance de Specialized. Lorsque vous appelez foo(), vous pouvez observer la différence que fait virtual: Si la méthode est virtuelle, l'implémentation de Specialized sera utilisée, si elle est manquante, la version de Base sera choisi. Il est recommandé de ne jamais surcharger les méthodes d'une classe de base. Rendre une méthode non virtuelle permet à son auteur de vous dire que son extension dans les sous-classes n'est pas destinée.

22
h0b0

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

Réponse rapide:

  1. Il nous fournit l'un des "ingrédients" nécessaires1 pour programmation orientée objet.

Dans Bjarne Stroustrup Programmation C++: principes et pratique, (14.3):

La fonction virtuelle offre la possibilité de définir une fonction dans une classe de base et d'avoir une fonction du même nom et du même type dans une classe dérivée appelée lorsqu'un utilisateur appelle la fonction de classe de base. Cela s'appelle souvent polymorphisme au moment de l'exécution, dispatch dynamique, ou dispatch 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 la mise en oeuvre la plus rapide et la plus efficace si vous avez besoin d'un appel de fonction virtuelle 2.

Pour traiter un appel virtuel, il faut une ou plusieurs données relatives au objet dérivé 3. La manière généralement utilisée consiste à ajouter l'adresse de la table des fonctions. Cette table est généralement appelée table virtuelle ou table de fonctions virtuelles et son adresse est souvent appelée le 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 appelle à son tour le remplacement respectif.


1.L'utilisation de l'héritage, du polymorphisme au moment de l'exécution et de l'encapsulation est la définition la plus courante de programmation orientée objet.

2. Vous ne pouvez pas coder la fonctionnalité pour qu'elle soit plus rapide ou pour utiliser moins de mémoire en utilisant d'autres fonctionnalités de langage pour sélectionner des alternatives au moment de l'exécution. Bjarne Stroustrup Programmation C++: principes et pratique (14.3.1).

3. Quelque chose pour dire quelle fonction est réellement appelée lorsque nous appelons la classe de base contenant la fonction virtuelle.

19
Ziezi

Lorsque vous avez une fonction dans la classe de base, vous pouvez Redefine ou Override 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 le fait pas faciliter Dynamic binding.

Substitution d'une méthode: Redefining un virtual method de la classe de base dans la classe dérivée. Méthode virtuelle facilite la liaison dynamique.

Alors quand tu as dit:

Mais plus tôt dans le livre, en apprenant l’héritage de base, j’étais capable de remplacer les méthodes de base dans les classes dérivées sans utiliser "virtuel".

vous ne l'utilisiez pas car la méthode de la classe de base n'était pas virtuelle, mais vous la redéfinissiez

14
nitin_cherian

J'ai ma réponse sous la forme d'une conversation pour être 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 sur des objets de type dérivé.

Comment cette définition du polymorphisme conduit-elle à la nécessité de fonctions virtuelles?

Eh bien, par le biais de liaison anticipée .

Qu'est-ce qu'une liaison anticipée?

La liaison précoce (liaison à la compilation) en C++ signifie qu'un appel de fonction est fixé avant l'exécution du programme.

Alors ...?

Ainsi, si vous utilisez un type de base en tant que paramètre d'une fonction, le compilateur ne reconnaîtra que l'interface de base. Si vous appelez cette fonction avec des arguments de classes dérivées, elle sera découpée en tranches, ce qui n'est pas ce que vous souhaitez.

Si ce n'est pas ce que nous voulons, 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 fonction unique, 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 problème, en utilisant le déréférencement de cette unique. 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 contenait l'adresse d'un objet de l'une de ses classes dérivées. Comme nous en avons parlé ci-dessus, au moment de l'exécution, ce pointeur est déréférencé, mais 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 (celle qui a le même en-tête) est déjà définie dans la classe de base, alors pourquoi votre programme devrait-il se donner 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 normalement 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 (réalise) que vous essayez d'utiliser polymorphism "et va donc aux classes dérivées (en utilisant v-table , une forme de liaison tardive) pour trouver que une autre méthode avec le même en-tête, mais avec, de manière attendue, une implémentation différente.

Pourquoi une implémentation différente?

Vous vous faites la tête! Allez lire un bon livre !

OK, attendez, attendez, pourquoi s'embarrasserait-il d'utiliser des pointeurs de base, alors qu'il/elle pourrait simplement utiliser des pointeurs de type dérivé? Sois le juge, est-ce que tout ce mal de tête en vaut 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 pourrait écrire 1 comme suit:

//1:

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

de plus, sachez qu'il ne s'agit encore que d'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 avez eu une fonction dans votre programme qui utilisait 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 aucun mal 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 encore un exemple artificiel non plus!

14
M-J

Cela aide si vous connaissez les mécanismes sous-jacents. C++ formalise certaines techniques de codage utilisées par les programmeurs C, les "classes" étant remplacées par "overlays" - les structures avec des sections d'en-tête communes seraient utilisées pour gérer des objets de types différents mais avec certaines données ou opérations communes. Normalement, la structure de base de la superposition (la partie commune) a un pointeur sur 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, à savoir le C++ ptr->func(...) où func est virtuel comme C serait (*ptr->func_table[func_num])(ptr,...), où ce qui change entre les classes dérivées est le contenu de func_table. [Une méthode non virtuelle ptr-> func () se traduit simplement par mangled_func (ptr, ..).]

Le résultat est que vous devez seulement 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 de classe B dérivée, puis les méthodes virtuelles appelées sont celles de B plutôt que de A puisque vous passez par la table de fonctions B.

11
Kev

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 compilateur1 typique crée une seule table (appelée VTABLE) pour chaque classe contenant des fonctions virtuelles. Le compilateur place les adresses des fonctions virtuelles de cette classe dans le VTABLE. Dans chaque classe avec des fonctions virtuelles, il place secrètement un pointeur, appelé vpointer (en abrégé VPTR), qui pointe vers la table VTABLE pour cet objet. Lorsque vous effectuez un appel de fonction virtuelle via un pointeur de classe de base, le compilateur insère discrètement du code pour extraire le VPTR et rechercher l'adresse de la fonction dans le 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

9
rvkreddy

Le mot clé virtuel oblige 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 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 garde une trace des différentes implémentations de méthodes virtuelles des sous-classes. Cela s'appelle également la répartition dynamique, et il y a est une surcharge associée.

Enfin, pourquoi le virtuel est-il 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 "Payer pour ce que vous utilisez". Donc, il n’essaye 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/abstraite peut contrôler le comportement dans toutes ses implémentations.
7
javaProgrammer

Pourquoi avons-nous besoin de fonctions virtuelles?

Les fonctions virtuelles évitent les problèmes de conversion de typage inutiles, et certains d'entre nous peuvent débattre de la raison pour laquelle nous avons besoin de fonctions virtuelles lorsque nous pouvons utiliser un pointeur de classe dérivée pour appeler la fonction spécifique dans la classe dérivée! développement, où avoir un seul objet de classe de base de pointeur 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;
}

SORTIE:

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;
}

SORTIE:

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

En analysant de près les deux résultats, on peut comprendre l’importance des fonctions virtuelles.

4
akshaypmurgod

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 polymorphisme de sous-type si vous appliquez la définition de wikipedia.

Wikipedia, sous-typage, 2019-01-09: dans la théorie des langages de programmation, le sous-typage (également polymorphisme de sous-type ou polymorphisme d'inclusion) est une forme de polymorphisme de type dans laquelle un sous-type est un type de données lié à un autre type de données (le supertype) de substituabilité, ce qui signifie que les éléments de programme, généralement des sous-programmes ou des fonctions, écrits pour agir sur des éléments du supertype peuvent également agir sur des éléments du sous-type.

REMARQUE: Sous-type signifie classe de base et sous-type signifie classe héritée.

Lectures supplémentaires concernant Polymorphisme de sous-type

Réponse technique: Dispatch dynamique

Si vous avez un pointeur sur une classe de base, l'appel de la méthode (déclarée comme virtuelle) sera alors envoyé à la méthode de la classe réelle de l'objet créé. Voici comment le polymorphisme de sous-type est réalisé est C++.

Lectures complémentaires Polymorphisme en C++ et Dispatch dynamique

Réponse d'implémentation: crée l'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 vtable de la classe dans laquelle la méthode est déclarée. C’est ainsi que le compilateur C++ réalise couramment Dispatch dynamique .

Lectures supplé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 avec exemple de code

UML class diagram of code example

3

En ce qui concerne 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 normal "(dans la limite de 25%). Son encombrement d'espace est égal à un pointeur dans chaque objet d'une classe comportant des fonctions virtuelles plus un vtbl pour chacune de ces classes" [- ne visite de C++ par Bjarne Stroustrup]

2
Duke

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;
}
2
user3371350

Les méthodes virtuelles sont utilisées dans la conception d'interface. 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 à implémenter. Ils sont essentiels pour la création et la destruction de certains objets qui doivent hériter de IUnknown. Dans ce cas, l'exécution est consciente de ces trois méthodes et s'attend à ce 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 ce qui l'utilise.

2
user2074102

je pense que vous faites référence au fait qu'une fois une méthode déclarée virtuelle, vous n'avez pas besoin d'utiliser le mot clé 'virtual' dans les substitutions.

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 foo de la base, le foo de Derived le suivra.

1
edwinc

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 de say () de 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 say () du chien, car Dog est leur type d'objet. À moins que la méthode [Dog's ne fait pas "woof"] ne soit pas définie, ce sera la première recherche dans l'arbre de classes, c'est-à-dire que les classes dérivées peuvent écraser les méthodes de leurs classes de base [dit de Animal ()].

$ 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) dans Python sont effectivement virtuels . Tous les objets étant créés dynamiquement à l'exécution, il n'y a pas de déclaration de type ni de besoin du mot clé virtual. Ci-dessous, la version de code de Python:

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

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

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

if      __== "__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 à la même instance de chien. L'expression (ad is d) renvoie True et leurs valeurs sont identiques. Objet chien main . À 0xb79f72cc>.

1
Leon Chang

Êtes-vous familier avec 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 classe). Il n'est pas aussi facile de lier des données à des pointeurs de fonction. Pour moi, c'est la distinction conceptuelle principale. Beaucoup d'autres réponses ici ne font que dire "parce que ... le polymorphisme!"

0
user2445507

L'essentiel est que les fonctions virtuelles facilitent la vie. Utilisons certaines des idées de M. Perry et décrivons ce qui se produirait si nous n’avions pas de fonctions virtuelles mais que nous ne pouvions utiliser que des pointeurs de fonctions membres. Nous avons, 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!"

Ok, alors c'est ce que nous savons. Essayons maintenant de le faire avec les pointeurs de fonctions membres:

 #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 de fonction membre presque, du 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'autre part, les fonctions virtuelles, même si elles peuvent avoir une surcharge de pointeur de fonction, simplifient considérablement les choses.

EDIT: Il existe une autre méthode similaire à eddietree: c ++ fonction virtuelle vs pointeur de fonction membre (comparaison de performances) .

0
fishermanhat

Nous avons besoin de méthodes virtuelles pour prendre en charge le "polymorphisme à l'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 fonction de la classe dérivée.

0
rashedcs