提问



我正在学习C ++而我正在进入虚拟功能。


根据我的阅读(在书中和在线),虚函数是基类中的函数,您可以在派生类中重写它们。


但是在本书前面,当学习基本继承时,我能够在不使用virtual的情况下覆盖派生类中的基函数。


那我在这里错过了什么?我知道虚拟功能还有更多功能,它似乎很重要,所以我想清楚它究竟是什么。我无法在网上找到一个直接的答案。

最佳参考


以下是我不仅理解virtual函数是什么,而且还需要它们的原因:[89]


我们假设你有这两个类:


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


在你的主要功能:


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

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


到目前为止一切都那么好吧?动物吃普通食物,猫吃老鼠,都没有virtual


让我们现在改变一下,以便通过中间函数调用eat()(这个例子只是一个简单的函数):


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


现在我们的主要功能是:


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

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


哦......我们把猫送进func(),但它不会吃老鼠。你应该超负荷func()所以需要Cat*吗?如果你需要从中获取更多的动物动物他们都需要自己的func()


解决方案是使Animal类的eat()成为一个虚函数:


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


主要:


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


完成。

其它参考1


如果没有虚拟,你就会得到早期绑定。根据您调用的指针类型,在编译时决定使用该方法的哪个实现。


使用虚拟,您将获得后期绑定。使用该方法的哪个实现在运行时根据指向对象的类型决定 - 它最初构造为什么。这不一定是你根据指向该对象的指针类型的想法。


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"


编辑 - 请参阅此问题。


此外 - 本教程涵盖了C ++中的早期和晚期绑定。[91]

其它参考2


您需要至少1级继承和向下转换来演示它。这是一个非常简单的例子:


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
}

其它参考3


您需要虚拟方法来安全向下转换简单简洁


这就是虚拟方法的作用:它们安全地向下转换,显然简单明了的代码,避免了不安全的手动转换,而不是你本来会遇到的更复杂和冗长的代码。






非虚方法⇒静态绑定



以下代码是故意不正确的。它没有将value方法声明为virtual,因此产生了一个意想不到的错误。结果,即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;
}


在该行评论为坏”调用Expression::value方法,因为静态已知类型(编译时已知的类型)是Expression,而value方法不是虚拟的。






虚拟方法⇒动态绑定。



在静态已知类型Expression中将value声明为 virtual 可确保每个调用将检查这是什么实际类型的对象,并调用相关的实现value动态类型:


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


这里的输出应该是6.86,因为虚拟方法虚拟地称为。这也称为调用的动态绑定。执行一点检查,查找实际动态类型的对象,并调用该动态类型的相关方法实现。


相关实现是最具体(最派生)类中的实现。


请注意,此处派生类中的方法实现未标记为virtual,而是标记为 override 。它们可以标记virtual,但它们会自动虚拟。 override关键字确保如果某个基类中存在不这样的虚方法,那么您将收到错误(这是可取的)。






没有虚拟方法这样做的丑陋



没有virtual,就必须实现动态绑定的一些自己动手版本。它通常涉及不安全的手动向下转换,复杂性和冗长。


对于单个函数的情况,如此处,将函数指针存储在对象中并通过该函数指针进行调用就足够了,但即使这样,它也会涉及一些不安全的向下转换,复杂性和冗长,例如:


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


一种看待这种情况的积极方式是,如果您遇到如上所述的不安全的向下转换,复杂性和冗长,那么通常虚拟方法或方法确实可以提供帮助。

其它参考4


如果基类是Base,并且派生类是Der,则可以使用Base *p指针实际指向Der的实例。当你p->foo();调用时,如果foo而不是虚拟,则执行Base的版本,忽略p实际指向的事实到Der。如果foo 虚拟,p->foo()执行foo的最底层覆盖,完全考虑到指向的实际类别因此,虚拟和非虚拟之间的区别实际上非常重要:前者允许运行时多态性,OO编程的核心概念,而后者不是。[92]

其它参考5


需要虚拟功能解释[[易于理解]]


#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();

}


输出将是:


Hello from Class A.


但是有了虚函数:


#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();

}


输出将是:


Hello from Class B.


因此,使用虚函数,您可以实现运行时多态性。

其它参考6


虚函数用于支持运行时多态性


也就是说,虚拟关键字告诉编译器不要在编译时做出(函数绑定)决定,而是推迟运行时



  • 您可以通过在其基类声明中添加关键字virtual来使函数成为虚拟。例如,


     class Base
     {
        virtual void func();
     }
    

  • 基类具有虚拟成员函数时,任何继承自基类的类都可以重新定义该函数完全相同的原型即只能重新定义功能,而不是功能的界面。


     class Derive : public Base
     {
        void func();
     }
    

  • 基类指针可用于指向基类对象以及派生类对象。

  • 当使用Base类指针调用虚函数时,编译器在运行时决定调用哪个版本的函数 - 即Base类版本或重写的Derived类版本。这称为运行时多态性


其它参考7


您必须区分覆盖和重载。如果没有virtual关键字,则只会重载基类的方法。这意味着隐藏。
假设你有一个基类Base和一个派生类Specialized,它们都实现了void foo()。现在你有一个指向Base的指针指向void foo()的实例。当你在foo()上调用foo()时,你可以观察到virtual产生的差异:如果方法是虚拟的,将使用Specialized的实现,如果它缺失,将选择Base的版本。
最好不要从基类重载方法。使一个方法非虚拟是作者的方式告诉你它在子类中的扩展是不打算的。

其它参考8



  为什么我们需要C ++中的虚方法?



快速回答:




  1. 它为我们提供了面向对象编程所需的成分 1 之一。



在Bjarne Stroustrup C ++编程:原理与实践,(14.3):



  虚函数提供了在基类中定义函数的能力,并且在用户调用基类函数时调用的派生类中具有相同名称和类型的函数。这通常称为运行时多态,动态调度或运行时调度,因为调用的函数是在运行时基于所用对象的类型。




  1. 如果您需要虚拟函数调用 2 ,这是最快速,更高效的实现。




  要处理虚拟调用,需要一个或多个与派生对象 3 相关的数据。通常的方法是添加函数表的地址。此表通常称为虚拟表或虚拟功能表,其地址通常称为虚拟指针。每个虚拟函数都获得虚拟表中的一个插槽。根据调用者的对象(派生)类型,虚函数依次调用相应的覆盖。






1.继承,运行时多态和封装的使用是面向对象编程的最常见定义。


2。您不能使用其他语言功能更快地编写功能代码或使用更少的内存在运行时选择其他选项.Bjarne Stroustrup C ++编程:原理与实践。(14.3.1)


3。当我们调用包含虚函数的基类时,要告诉哪个函数真正被调用。

其它参考9


我想添加虚拟功能的另一个用途,虽然它使用与上述答案相同的概念,但我想它值得一提。


VIRTUAL DESTRUCTOR


考虑下面的这个程序,而不将Base类析构函数声明为虚拟; Cat的内存可能无法清除。


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


输出:



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


输出:



Deleting an Animal name Cat
Deleting an Animal


其它参考10


当你在基类中有一个函数时,你可以在派生类中RedefineOverride它。


重新定义方法 :
派生类中给出了基类方法的新实现。 促进Dynamic binding


覆盖方法 :
Redefining派生类中基类的virtual method。虚方法有助于动态绑定


所以当你说:



  但是在本书前面,当我了解基本遗传时,我就是
  能够在不使用的情况下覆盖派生类中的基本方法
  虚拟。



你没有覆盖它,因为基类中的方法不是虚拟的,而是你重新定义它

其它参考11


如果您了解基础机制,它会有所帮助。 C ++规范了C程序员使用的一些编码技术,使用覆盖替换类 - 具有公共头部分的结构将用于处理不同类型但具有一些共同数据或操作的对象。通常,覆盖的基础结构(公共部分)具有指向功能表的指针,该功能表指向每种对象类型的不同例程集。 C ++做了同样的事情,但隐藏了机制,即C ++ ptr->func(...),其中func是虚拟的,因为C将是(*ptr->func_table[func_num])(ptr,...),其中派生类之间的变化是func_table内容。 [[非虚方法ptr-> func()只是转换为mangled_func(ptr,..)。


结果就是你只需要理解基类就可以调用派生类的方法,即如果例程理解了类A,你可以传递一个派生类B指针,然后调用的虚方法将是那些因为你通过功能表B指向B而不是A.

其它参考12


我以对话的形式回答我的答案:





为什么我们需要虚拟功能?


因为多态性。


什么是多态?


基指针也可以指向派生类型对象的事实。


多态性的定义如何导致对虚函数的需求?


好吧,通过早期绑定。


什么是早期绑定?


C ++中的早期绑定(编译时绑定)意味着在执行程序之前修复了函数调用。


所以...?


因此,如果使用基类型作为函数的参数,编译器将只识别基接口,如果使用派生类中的任何参数调用该函数,它将被切掉,这不是您想要发生的。


如果不是我们想要发生的事情,为什么允许这样做?


因为我们需要多态性!


那么多态性有什么好处呢?


您可以使用基类型指针作为单个函数的参数,然后在程序的运行时,您可以使用该单个的解除引用来访问每个派生类型接口(例如其成员函数)而不会出现任何问题基指针。


我仍然不知道虚拟功能对...有什么好处!这是我的第一个问题!


好吧,这是因为你太快问了你的问题!


为什么我们需要虚拟功能?


假设您使用基指针调用了一个函数,该指针具有来自其派生类之一的对象的地址。正如我们上面讨论的那样,在运行时,这个指针被解除引用,到目前为止一直很好,但是,我们期望一个方法(==一个成员函数)从我们的派生类执行!但是,在基类中已经定义了一个相同的方法(一个具有相同标题的方法),那么为什么你的程序会选择另一种方法呢?换句话说,我的意思是,你怎么能从我们以前看到的这个方案中说出来通常发生在之前?


简短的回答是基础中的虚拟成员函数,更长的答案是,在此步骤,如果程序在基类中看到虚函数,它知道(意识到)您正在尝试使用多态性以及派生类(使用v-table,一种后期绑定形式)来查找具有相同标头的另一种方法,但是 - 期望 - 一种不同的实现。[93]]]


为什么采用不同的实施方式?


你指关节!去读一本好书!


好的,等待等待,当他/她可以简单地使用派生类型指针时,为什么还要费心去使用基本指针?你是判断者,难道这一切都值得吗?看看这两个片段:


//1:


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


//2:


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


好的,虽然我认为 1 仍然优于 2 ,但您可以像这样编写 1 :


//1:


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


而且,你应该知道,这只是对我迄今为止向你解释过的所有事情的一种人为的使用。而不是这样,假设你的程序中有一个使用这些方法的函数的情况。分别来自每个派生类(getMonthBenefit()):


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


现在,尝试重写这个,没有任何麻烦!


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


实际上,这也可能是一个人为的例子!

其它参考13


关键字virtual告诉编译器它不应该执行早期绑定。相反,它应该自动安装执行后期绑定所需的所有机制。
为此,典型的compiler1为包含虚函数的每个类创建一个表(称为VTABLE)。编译器将该特定类的虚函数的地址放在VTABLE中。在每个具有虚函数的类中,它秘密地放置一个指针,称为vpointer(缩写为VPTR),指向该对象的VTABLE。
当您通过基类指针进行虚函数调用时,编译器会安静地插入代码以获取VPTR并在VTABLE中查找函数地址,从而调用正确的函数并导致后期绑定发生。


此链接中的更多详细信息
http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html[95]

其它参考14


虚拟关键字强制编译器选择对象的类中定义的方法实现,而不是指针的类。


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


在上面的示例中,默认情况下将调用Shape :: getName,除非在Base类Shape中将getName()定义为virtual。这会强制编译器在Triangle类中而不是在Shape类中查找getName()实现。


虚拟表是编译器跟踪子类的各种虚方法实现的机制。这也称为动态调度,是与之相关的一些开销。


最后,为什么在C ++中甚至需要虚拟化,为什么不将其作为Java中的默认行为?



  1. C ++基于零开销和为您使用的付费的原则。因此,除非您需要,否则它不会尝试为您执行动态调度。

  2. 为界面提供更多控制。通过使函数非虚拟,接口/抽象类可以控制其所有实现中的行为。


其它参考15


为什么我们需要虚拟功能?


虚函数避免了不必要的类型转换问题,当我们可以使用派生类指针调用派生类中特定的函数时,我们中的一些人可以辩论为什么我们需要虚函数!答案是 - 它使大系统中的继承的整个想法无效开发,非常需要具有单指针基类对象的开发。


让我们在下面的两个简单程序进行比较,以了解虚函数的重要性:


没有虚函数的程序:


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


OUTPUT:


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


具有虚函数的程序:


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


OUTPUT:


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


通过仔细分析两个输出,人们可以理解虚函数的重要性。

其它参考16


关于效率,虚函数与早期绑定函数相比效率稍差。


这个虚拟调用机制几乎和普通函数调用机制一样有效(在25%以内)。它的空间开销是一个具有虚函数的类的每个对象中的一个指针加上每个这样的类的一个vtbl[[< em> Bjarne Stroustrup的C ++之旅

其它参考17


虚拟方法用于界面设计。例如,在Windows中有一个名为IUnknown的接口,如下所示:


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


这些方法留给接口用户实现。它们对于必须继承IUnknown的某些对象的创建和销毁至关重要。在这种情况下,运行时知道这三种方法,并期望它们在调用它们时实现它们。因此从某种意义上说,它们充当对象本身与对象之间的契约。

其它参考18


这是完整的示例,说明了使用虚方法的原因。


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

其它参考19


我们需要虚拟方法来支持运行时多态性。
当您使用指针或对基类的引用引用派生类对象时,可以为该对象调用虚函数并执行派生类的函数版本。

其它参考20


我认为你指的是一旦将方法声明为虚拟,你就不需要在覆盖中使用virtual关键字。


class Base { virtual void foo(); };

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


如果你不使用虚拟in Base的foo声明,那么Derived的foo就会影响它。