virtuelle - virtualisation c++




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

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

Réponse rapide:

  1. Il nous fournit l'un des «ingrédients» nécessaires 1 pour la programmation orientée objet .

Dans la programmation C ++ de Bjarne Stroustrup: Principes et Pratique, (14.3):

La fonction virtuelle permet de définir une fonction dans une classe de base et possède 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. C'est ce que l'on appelle souvent le polymorphisme d'exécution , la répartition dynamique ou l' envoi 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 un ou plusieurs éléments de données liés à l' objet dérivé 3 . La manière habituelle 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. En fonction du type d'objet (dérivé) de l'appelant, la fonction virtuelle appelle à son tour le remplacement correspondant.

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 qu'elle soit plus rapide ou pour utiliser moins de mémoire en utilisant d'autres fonctionnalités de langue pour choisir parmi les alternatives au moment de l'exécution. Programmation C ++ de Bjarne Stroustrup: Principes et Pratique (14.3.1) .

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

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 les classes dérivées.

Mais plus tôt dans le livre, en apprenant l'héritage de base, j'ai été 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 de fonctions virtuelles, et cela semble important, donc je veux être clair sur ce que c'est exactement. Je ne trouve tout simplement pas de réponse directe en ligne.


A propos de 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 une limite de 25%), son espace supplémentaire étant un pointeur dans chaque objet d'une classe avec des fonctions virtuelles plus un vtbl pour chaque classe. tour de C ++ par Bjarne Stroustrup]


Cela aide si vous connaissez les mécanismes sous-jacents. C ++ formalise certaines techniques de codage utilisées par les programmeurs en C, les «classes» remplacées par des «overlays» - les structs avec des sections d'en-tête communes seraient utilisés pour gérer des objets de types différents 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.-à-d. 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 juste 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 si une routine comprend la classe A, vous pouvez lui passer un pointeur de classe B dérivé, puis les méthodes virtuelles seront celles de B plutôt que A puisque vous passez par la table de fonctions B points à.


J'ai ma réponse sous forme de conversation pour mieux la lire:

Pourquoi avons-nous besoin de fonctions virtuelles?

A cause du polymorphisme.

Qu'est-ce que le polymorphisme?

Le fait qu'un pointeur de base peut é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 précoce .

Qu'est-ce que la liaison anticipée?

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

Alors...?

Donc, si vous utilisez un type de base comme paramètre d'une fonction, le compilateur ne reconnaîtra que l'interface de base, et si vous appelez cette fonction avec des arguments provenant de classes dérivées, il sera tronqué, ce qui n'est pas ce que vous voulez.

Si ce n'est pas ce que nous voulons, pourquoi cela est-il permis?

Parce que nous avons besoin de polymorphisme!

Quel est l'avantage du polymorphisme alors?

Vous pouvez utiliser un pointeur de type base comme paramètre d'une seule fonction, puis dans l'exécution de votre programme, vous pouvez accéder à chacune des interfaces de type dérivées (par exemple leurs fonctions membres) sans problème, en utilisant la 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 avez 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 l'avons vu plus haut, au moment de l'exécution, ce pointeur est déréférencé, mais pour l'instant nous attendons une méthode (== une fonction membre) "de notre classe dérivée" à exécuter! Cependant, une même méthode (celle qui a un même en-tête) est déjà définie dans la classe de base, alors pourquoi votre programme devrait-il choisir l'autre méthode? En d'autres termes, je veux dire, comment pouvez-vous dire ce scénario hors de ce que nous avions l'habitude de voir se produire normalement avant?

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 polymorphisme "et donc va aux classes dérivées (en utilisant v-table , une forme de liaison tardive) pour trouver une autre méthode avec le même en-tête, mais avec -de façon inattendue- une implémentation différente.

Pourquoi une implémentation différente?

Tête-de-poing! Va lire un bon livre !

OK, attendez wait wait, pourquoi voudrait-on utiliser des pointeurs de base, alors qu'il pourrait simplement utiliser des pointeurs de type dérivé? Tu es 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 mieux que 2 , vous pouvez écrire 1 comme ceci soit:

//1:

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

et de plus, vous devriez être conscient que c'est encore juste un usage artificiel de toutes les choses 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 cela, 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 soit!


Le mot-clé virtual 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 force 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 conserve une trace des différentes implémentations de la méthode virtuelle des sous-classes. Ceci est également appelé répartition dynamique, et il y a 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'essaie 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.

Le mot-clé virtual indique au compilateur qu'il ne doit pas effectuer de liaison anticipée. Au lieu de cela, il devrait automatiquement installer tous les mécanismes nécessaires pour effectuer une liaison tardive. Pour ce faire, le compilateur1 type 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é le 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 récupérer 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


Les méthodes virtuelles sont utilisées dans la conception d'interface. Par exemple dans Windows il y a 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 des trois méthodes et s'attend à ce qu'elles soient implémentées quand elles les appellent. Donc, dans un sens, ils agissent comme un contrat entre l'objet lui-même et tout ce qui utilise cet objet.


Lorsque vous avez une fonction dans la classe de base, vous pouvez la Redefine ou la Override dans la classe dérivée.

Redéfinir une méthode : Une nouvelle implémentation pour la méthode de la classe de base est donnée dans la classe dérivée. Ne facilite pas la Dynamic binding .

Substitution d'une méthode : Redefining une virtual 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:

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

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


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é - ce à 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 la liaison précoce et tardive en C ++.


Si la classe de base est Base et une classe dérivée est Der , vous pouvez avoir un pointeur Base *p qui pointe vers une instance de Der . Lorsque vous appelez p->foo(); , si foo n'est pas virtuel, la version de Base s'exécute, ignorant le fait que p pointe vers un Der . Si foo est virtuel, p->foo() exécute le "override" de foo , en prenant en compte la classe réelle de l'élément pointé. Ainsi, la différence entre virtuel et non virtuel est en fait assez cruciale: les premiers permettent le polymorphism exécution, le concept de base de la programmation OO, tandis que les seconds ne le font pas.


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

Vous avez besoin de méthodes virtuelles pour assurer la diffusion en toute sécurité , la simplicité et la concision .

C'est ce que font les méthodes virtuelles: elles sont diffusées en toute sécurité, avec un code apparemment simple et concis, évitant les distributions manuelles dangereuses dans le code plus complexe et plus verbeux que vous auriez autrement.

Méthode non virtuelle ⇒ liaison statique

Le code suivant est intentionnellement "incorrect". Il ne déclare pas la méthode de value comme virtual , et produit donc un résultat "faux" 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 méthode Expression::value est appelée, car le type connu statiquement (le type connu à la compilation) est Expression , et la méthode value n'est pas virtuelle.

Méthode virtuelle ⇒ liaison dynamique.

Déclaration de value tant que type virtual dans l' Expression type statique L' Expression garantit que chaque appel vérifie le type d'objet réel et appelle l'implémentation de value appropriée pour 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.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 dynamique réel de l'objet et l'implémentation de la méthode appropriée pour ce type dynamique est appelée.

La mise en œuvre 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 remplacées à la place. Ils pourraient être marqués virtual mais ils sont automatiquement virtuels. Le mot-clé override garantit que s'il n'y a pas une telle méthode virtuelle dans une classe de base, alors vous obtiendrez une erreur (ce qui est souhaitable).

La laideur de le faire sans méthodes virtuelles

Sans virtual il faudrait implémenter une version Do It Yourself de la liaison dynamique. C'est ce qui implique généralement la descente manuelle dangereuse, la complexité et la verbosité.

Pour le cas d'une seule fonction, comme ici, il suffit de stocker un pointeur de fonction dans l'objet et d'appeler via ce pointeur de fonction, mais cela implique quand même des downcasts, de la complexité et de la verbosité dangereux, à 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 manière positive de voir cela est, si vous rencontrez la descente dangereuse, la complexité et la verbosité comme ci-dessus, alors souvent une méthode virtuelle ou des méthodes peuvent vraiment aider.


Here is complete example that illustrates why virtual method is used.

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

We need virtual methods for supporting "Run time Polymorphism". When you refer to a derived class object using a pointer or a reference to the base class, you can call a virtual function for that object and execute the derived class's version of the function.







virtual-functions