为什么我们需要C ++中的虚函数?

时间:2010-03-06 07:10:36

标签: c++ virtual-functions

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

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

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

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

26 个答案:

答案 0 :(得分:2506)

以下是我不仅了解virtual函数的含义,还解释了为什么需要它们:

假设你有这两个类:

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

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

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 :(得分:600)

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

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

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"

编辑 - 请参阅this question

此外 - this tutorial涵盖了C ++中的早期和晚期绑定。

答案 2 :(得分:79)

您需要至少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 :(得分:40)

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

这就是虚拟方法的作用:它们安全地向下转换,具有明显简单和简洁的代码,避免使用更复杂和冗长的代码中的不安全手动转换。

<小时/>

非虚方法⇒静态绑定

以下代码故意“不正确”。它没有将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;
}

在注释为“bad”的行中调用Expression::value方法,因为静态已知类型(编译时已知的类型)为Expression,并且value方法不是虚拟的。

<小时/>

虚拟方法⇒动态绑定。

在静态已知类型value中将virtual声明为 Expression ,可确保每次调用都会检查此对象的实际类型,并调用该动态类型的相关实施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关键字可确保在某些基类中存在 not 这样的虚方法时,您将收到错误(这是可取的)。

<小时/>

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

如果没有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 :(得分:35)

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

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

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

     class Base
     {
        virtual void func();
     }
    
  • 基类具有虚拟成员函数时,任何继承自基类的类都可以重新定义该函数与完全相同的原型即只能重新定义功能,而不能重新定义功能的界面。

     class Derive : public Base
     {
        void func();
     }
    
  • 基类指针可用于指向基类对象以及派生类对象。

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

答案 5 :(得分:33)

如果基类是Base,派生类是Der,则可以使用Base *p指针实际指向Der的实例。当您致电p->foo();时,如果foo 不是虚拟,那么Base的版本会执行,忽略p实际指向的事实到Der。如果foo 是虚拟的,p->foo()会执行foo的“最左边”覆盖,完全考虑指向项的实际类。因此,虚拟和非虚拟之间的区别实际上非常重要:前者允许运行时polymorphism,OO编程的核心概念,而后者则不然。

答案 6 :(得分:26)

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

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

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

答案 7 :(得分:22)

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

答案 8 :(得分:21)

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

虚拟解剖器

考虑下面的这个程序,而不将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

答案 9 :(得分:18)

  

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

快速回答:

  1. 它为我们提供了面向对象编程所需的&#34;成分&#34; 1 之一。
  2. 在Bjarne Stroustrup C ++编程:原理与实践,(14.3):

      

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

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

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

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

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

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

答案 10 :(得分:14)

如果在基类中有函数,则可以在派生类中RedefineOverride

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

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

所以当你说:

  

但是在书的前面,当我学习基本的继承时,我就是   能够在不使用的情况下覆盖派生类中的基本方法   '虚拟'。

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

答案 11 :(得分:14)

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

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

因为多态性。

什么是多态?

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

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

好吧,通过早期绑定

什么是早期绑定?

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

所以...?

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

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

因为我们需要多态性!

那么多态性的好处是什么?

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

我仍然不知道哪些虚拟功能对...有用!这是我的第一个问题!

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

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

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

简短的回答是&#34;基础&#34;中的虚拟成员函数,更长的答案是,#34;在这一步,如果程序在基类中看到虚函数,它知道(意识到)你正试图使用​​多态性&#34;所以去派生类(使用v-table,一种后期绑定的形式)来找到具有相同标题的另一种方法,但是 - 期望 - 一种不同的实现。

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

你指关节!去阅读good book

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

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

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

答案 12 :(得分:11)

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

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

答案 13 :(得分:9)

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

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

答案 14 :(得分:6)

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

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

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

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

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

  1. C ++基于“零开销”和“为您使用的付费”的原则。因此,它不会尝试为您执行动态调度,除非您需要它。
  2. 为界面提供更多控制。通过使函数非虚拟,接口/抽象类可以控制其所有实现中的行为。

答案 15 :(得分:4)

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

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

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

没有虚拟功能的程序:

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

输出:

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

输出:

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

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

答案 16 :(得分:2)

关于效率,虚函数的效率略低于早期绑定功能。

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

答案 17 :(得分:2)

OOP答案:亚型多态性

在C ++中,如果应用Wikipedia中的定义,则需要虚拟方法来实现多态性,更确切地说,是 subtyping subtype多态性。< / p>

维基百科,子类型化,2019年1月9日: 在编程语言理论中,子类型化(也是子类型多态性或包含性多态性)是类型多态性的一种形式,其中子类型是通过某种可替换性概念与另一个数据类型(超类型)相关的数据类型。这意味着程序元素通常是子例程或功能,写成可在父类型的元素上操作的对象,也可以对子类型的元素进行操作。

注意:子类型表示基类,子类型表示继承的类。

关于亚型多态性

的进一步阅读

技术解答:动态调度

如果您有一个指向基类的指针,则该方法的调用(被声明为virtual)将被分派到所创建对象的实际类的方法中。这就是C ++实现子类型多态性的方式。

C ++和动态调度

中的进一步阅读多态性

实施答案:创建 vtable条目

对于方法上的每个“虚拟”修饰符,C ++编译器通常在声明该方法的类的vtable中创建一个条目。这就是普通的C ++编译器如何实现动态调度

进一步阅读vtables


示例代码

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

示例代码的输出

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

UML类代码示例图

UML class diagram of code example

答案 18 :(得分:2)

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

#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 :(得分:2)

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

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

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

答案 20 :(得分:1)

我认为一旦方法被声明为虚拟方法,您就无需再在覆盖中使用'virtual'关键字了。

class Base { virtual void foo(); };

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

如果您在Base的foo声明中不使用'virtual',那么Derived的foo就会对其进行遮蔽。

答案 21 :(得分:1)

这里是前两个答案的C ++代码的合并版本。

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

两个不同的结果是:

没有#define virtual ,它在编译时绑定。 Animal * ad和func(Animal *)都指向Animal的say()方法。

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

使用#define virtual ,它在运行时绑定。 Dog * d,Animal * ad和func(Animal *)指向/引用Dog的say()方法,因为Dog是它们的对象类型。除非未定义[Dog's say()“ woof”]方法,否则它将是在类树中首先搜索的方法,即派生类可能会覆盖其基础类[Animal's say()]的方法。

$ 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

有趣的是注意到Python are effectively virtual中的所有类属性(数据和方法)。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字virtual。下面是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)))

输出为:

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

与C ++的虚拟定义相同。请注意, d ad 是引用/指向同一Dog实例的两个不同的指针变量。表达式(ad是d)返回True,并且它们的值是相同的。位于0xb79f72cc>的狗对象。

答案 22 :(得分:1)

解释虚拟函数的问题在于,它们没有解释在实践中如何使用它,以及它如何有助于可维护性。我创建了一个虚拟功能教程,人们已经发现它非常有用。另外,它基于战场前提,这使它更加令人兴奋:https://nrecursions.blogspot.com/2015/06/so-why-do-we-need-virtual-functions.html

考虑此战场应用程序:
enter image description here

#include "iostream"

//This class is created by Gun1's company
class Gun1 {public: void fire() {std::cout<<"gun1 firing now\n";}};
//This class is created by Gun2's company
class Gun2 {public: void shoot() {std::cout<<"gun2 shooting now\n";}};

//We create an abstract class to interface with WeaponController
class WeaponsInterface {
 public:
 virtual void shootTarget() = 0;
};

//A wrapper class to encapsulate Gun1's shooting function
class WeaponGun1 : public WeaponsInterface {
 private:
 Gun1* g;

 public:
 WeaponGun1(): g(new Gun1()) {}
 ~WeaponGun1() { delete g;}
 virtual void shootTarget() { g->fire(); }
};

//A wrapper class to encapsulate Gun2's shooting function
class WeaponGun2 : public WeaponsInterface {
 private:
 Gun2* g;

 public:
 WeaponGun2(): g(new Gun2()) {}
 ~WeaponGun2() { delete g;}
 virtual void shootTarget() { g->shoot(); }
};

class WeaponController {
 private:
 WeaponsInterface* w;
 WeaponGun1* g1;
 WeaponGun2* g2;
 public:
 WeaponController() {g1 = new WeaponGun1(); g2 = new WeaponGun2(); w = g1;}
 ~WeaponController() {delete g1; delete g2;}
 void shootTarget() { w->shootTarget();}
 void changeGunTo(int gunNumber) {//Virtual functions makes it easy to change guns dynamically
   switch(gunNumber) {
     case 1: w = g1; break;
     case 2: w = g2; break;
   }
 }
};


class BattlefieldSoftware {
 private:
 WeaponController* wc;
 public:
 BattlefieldSoftware() : wc(new WeaponController()) {}
 ~BattlefieldSoftware() { delete wc; }

 void shootTarget() { wc->shootTarget(); }
 void changeGunTo(int gunNumber) {wc->changeGunTo(gunNumber); }
};


int main() {
 BattlefieldSoftware* bf = new BattlefieldSoftware();
 bf->shootTarget();
 for(int i = 2; i > 0; i--) {
     bf->changeGunTo(i);
     bf->shootTarget();
 }
 delete bf;
}

我鼓励您首先阅读博客上的文章,以了解创建包装器类的原因。

在图像中可见,可以将各种枪支/导弹与战场软件连接,并且可以向这些武器发出命令,进行射击或重新校准等。这里的挑战是要能够改变/更换枪支/导弹,而无需更改蓝色战场软件,并且能够在运行时在武器之间进行切换,而无需更改代码并重新编译。

上面的代码显示了如何解决此问题,以及具有精心设计的包装器类的虚函数如何封装函数并有助于在运行时分配派生类指针。类WeaponGun1的创建可确保您将Gun1的处理完全分离到该类中。无论您对Gun1进行什么更改,都只需在WeaponGun1中进行更改,并确信没有其他班级受到影响。

由于有了WeaponsInterface类,您现在可以将任何派生类分配给基类指针WeaponsInterface,并且由于它的功能是虚拟的,因此当您调用WeaponsInterface的{​​{1} },派生类shootTarget被调用。

最好的是,您可以在运行时(shootTargetw=g1)更换枪支。这是虚拟功能的主要优点,这就是为什么我们需要虚拟功能。

因此,在更换枪支时,不再需要在各个地方注释掉代码。现在,这是一个简单而干净的过程,添加更多的枪支类别也更加容易,因为我们只需要创建一个新的w=g2WeaponGun3类,我们就可以确信不会搞乱{{ 1}}的代码或WeaponGun4 / BattlefieldSoftware的代码。

答案 23 :(得分:0)

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

答案 24 :(得分:0)

您熟悉函数指针吗?虚拟函数与之类似,只是您可以轻松地将数据绑定到虚拟函数(作为类成员)。将数据绑定到函数指针并不容易。对我来说,这是主要的概念区别。这里的许多其他答案只是说“因为...多态!”

答案 25 :(得分:0)

最重要的是,虚函数使生活更轻松。让我们使用M Perry的一些想法,并描述如果我们没有虚拟函数而只能使用成员函数指针,将会发生什么情况。在正常估计中,我们没有虚函数:

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

好的,这就是我们所知道的。现在,让我们尝试使用成员函数指针做到这一点:

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

虽然我们可以使用成员函数指针来做一些事情,但是它们不像虚函数那样灵活。在类中使用成员函数指针是很棘手的。至少在我的实践中,几乎总是必须在主函数中或从成员函数内部调用成员函数指针,如上例所示。

另一方面,虚拟函数虽然可能会有一些函数指针开销,但确实可以大大简化事情。

编辑:还有另一种与eddietree类似的方法:c++ virtual function vs member function pointer (performance comparison)