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


965
2018-03-06 07:10


origine


Réponses:


Voici comment j'ai compris non seulement ce que virtual fonctions sont, mais pourquoi ils 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() est 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."

Oh oh ... nous avons passé un chat dans func()mais il ne mange pas de rats. Si vous surchargez func() il faut donc un Cat*? Si vous devez dériver plus d'animaux d'Animal, ils auront tous besoin de leur propre func().

La solution est de faire eat() du Animal classer 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é.


2286
2018-03-06 13:54



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"

MODIFIER - voir cette question.

Aussi - ce tutoriel couvre la liaison précoce et tardive en C ++.


533
2018-03-06 07:56



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
}

72
2018-03-06 07:26



Vous avez besoin de méthodes virtuelles pour descente en toute sécurité, simplicité et 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 value mé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 a commenté comme "mauvais" le Expression::value méthode est appelée, parce que le type statiquement connu (le type connu au moment de la compilation) est Expression, et le value la méthode n'est pas virtuelle.


Méthode virtuelle ⇒ liaison dynamique.

Déclaration value comme virtual dans le type statiquement connu Expression s'assure que chaque appel vérifie le type d'objet réel et appelle la mise en œuvre valuepour ça 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é 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éthodes dans les classes dérivées ne sont pas marquées ici virtual, mais sont marqués override. Ils pourraient être marqués virtual mais ils sont automatiquement virtuels. le override mot-clé assure que s'il y a ne 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 pour autant virtual il faudrait mettre en œuvre certains Fais le toi-même version 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.


28
2017-11-24 07:24



Si la classe de base est Base, et une classe dérivée est Der, vous pouvez avoir un Base *p pointeur qui pointe vers une instance de Der. Quand vous appelez p->foo();, si foo est ne pas virtuel, alors Basela version de celui-ci s'exécute, en ignorant le fait que p pointe en fait à un Der. Si foo est virtuel, p->foo() exécute le remplacement "leafmost" de foo, en tenant pleinement compte de la classe réelle de l'élément pointé. La différence entre virtuel et non-virtuel est donc très importante: les premiers permettent l'exécution polymorphisme, le concept de base de la programmation OO, alors que ce dernier ne le fait pas.


27
2018-03-06 07:27



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

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


24
2017-12-12 11:56



Les fonctions virtuelles sont utilisées pour prendre en charge Polymorphisme d'exécution.

C'est, virtuel mot-clé dit au compilateur ne pas prendre la décision (de la liaison de fonction) à la compilation, plutôt la reporter pour l'exécution ".

  • Vous pouvez rendre une fonction virtuelle en précédant le mot-clé virtual dans sa déclaration de classe de base. Par exemple,

     class Base
     {
        virtual void func();
     }
    
  • Lorsqu'un 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 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 sur un objet de classe de base ainsi que sur un objet de classe dérivé.

  • 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, à savoir la version de classe de base ou la version de classe dérivée dérivée, doit être appelée. C'est appelé Polymorphisme d'exécution.

23
2017-10-12 09:41