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

Why do we need virtual functions in C++?

我正在学习C++,我正在学习虚拟函数。

根据我(在书中和在线上)所读到的,虚拟函数是基类中的函数,您可以在派生类中重写这些函数。

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

那么我在这里错过了什么?我知道虚拟函数还有很多,它似乎很重要,所以我想清楚它到底是什么。我只是在网上找不到一个直接的答案。


以下是我如何理解的不仅是virtual函数是什么,还有为什么需要它们:

假设您有这两个类:

1
2
3
4
5
6
7
8
9
10
11
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."; }
};

在您的主要功能中:

1
2
3
4
5
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()

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

现在我们的主要功能是:

1
2
3
4
5
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()超负荷,所以需要一个Cat*?如果你必须从动物身上获得更多的动物,它们都需要自己的func()

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

1
2
3
4
5
6
7
8
9
10
11
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."; }
};

主营:

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

完成。


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

使用"虚拟"可以获得"后期绑定"。在运行时,根据指向对象的类型(它最初的构造方式)来决定使用哪种方法实现。这不一定是您根据指向该对象的指针类型所想的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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++中的早期绑定和后期绑定。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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
}


您需要虚拟方法来实现安全的向下转换、简单和简洁。

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

非虚拟方法&rarr;静态绑定

以下代码故意"不正确"。它不会将value方法声明为virtual,因此会产生一个意外的"错误"结果,即0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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方法不是虚拟的。

虚拟方法&rarr;动态绑定。

在静态已知的Expression类型中将value声明为virtual可确保每次调用都会检查对象的实际类型,并为该动态类型调用value的相关实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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,就必须实现一些自己动手的动态绑定版本。这通常涉及不安全的手动下压、复杂性和冗长性。

对于单个函数的情况,如这里所示,只需在对象中存储一个函数指针并通过该函数指针调用,但即使如此,它仍然涉及一些不安全的向下转换、复杂性和冗长性,也就是说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#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;
}

看待这一点的一个积极的方法是,如果您遇到不安全的向下转换、复杂性和冗长的内容,那么通常一个或多个虚拟方法可以真正起到帮助。


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

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

  • 您可以通过在函数的基类声明中在关键字virtual之前将其设置为虚函数。例如,

    1
    2
    3
    4
     class Base
     {
        virtual void func();
     }
  • 当基类具有虚拟成员函数时,从基类继承的任何类都可以使用完全相同的原型重新定义函数,即只能重新定义功能,而不能重新定义函数的接口。

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

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

如果基类是Base,派生类是Der,则可以有一个Base *p指针,它实际上指向Der的一个实例。当您调用p->foo();时,如果foo不是虚拟的,则执行Base的版本,忽略p实际上指向Der的事实。如果foo是虚拟的,那么p->foo()执行foo的"leafmost"覆盖,充分考虑到指向项的实际类。因此,虚拟和非虚拟之间的区别实际上是非常关键的:前者允许运行时多态性(OO编程的核心概念),而后者则不允许。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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();

}

输出将是:

1
Hello from Class A.

但有了虚拟功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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();

}

输出将是:

1
Hello from Class B.

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


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


我想添加虚拟函数的另一种用法,尽管它使用了与上述答案相同的概念,但我想值得一提。

虚拟析构函数

请考虑下面的程序,不要将基类析构函数声明为虚拟的;cat的内存可能不会被清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

输出:

1
Deleting an Animal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

输出:

1
2
Deleting an Animal name Cat
Deleting an Animal


Why do we need Virtual Methods in C++?

快速回答:

  • 它为我们提供了面向对象编程所需的"成分"1之一。
  • Bjarne Stroustrup C++程序设计:原理与实践,(14.3):

    The virtual function provides the ability to define a function in a base class and have a function of the same name and type in a derived class called when a user calls the base class function. That is often called run-time polymorphism, dynamic dispatch, or run-time dispatch because the function called is determined at run time based on the type of the object used.

  • 如果需要虚拟函数调用2,它是最快、更有效的实现。
  • To handle a virtual call, one needs one or more pieces of data related to the derived object 3. The way that is usually done is to add the address of table of functions. This table is usually referred to as virtual table or virtual function table and its address is often called the virtual pointer. Each virtual function gets a slot in the virtual table. Depending of the caller's object (derived) type, the virtual function, in its turn, invokes the respective override.

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

    2。您不能在运行时使用其他语言功能在备选方案中进行选择,从而更快地编写功能代码,或者使用更少的内存。Bjarne Stroustrup C++程序设计:原理与实践(143.1)。

    三。当我们调用包含虚函数的基类时,用来判断哪个函数是真正被调用的。


    我的答案以对话的形式呈现出来,以便更好地阅读:

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

    因为多态性。

    什么是多态性?

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

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

    好吧,通过早期绑定。

    什么是早期绑定?

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

    那么……?

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

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

    因为我们需要多态性!

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

    可以使用基类型指针作为单个函数的参数,然后在程序的运行时,可以使用该单基指针的取消引用,无需任何问题即可访问每个派生类型接口(例如,它们的成员函数)。

    我仍然不知道什么样的虚拟功能对…!这是我的第一个问题!

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

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

    假设您使用一个基指针调用了一个函数,该基指针具有来自它的一个派生类的对象地址。正如我们在上面讨论过的,在运行时中,这个指针会被取消引用,但是到目前为止,我们希望从派生类中执行一个方法(=a成员函数)""。但是,已经在基类中定义了相同的方法(具有相同头的方法),那么为什么您的程序要费心选择另一个方法呢?换句话说,我的意思是,你怎么能从我们以前看到的正常情况中分辨出这种情况呢?

    简单的答案是"在基中有一个虚拟成员函数",而稍长一点的答案是,"在这个步骤中,如果程序在基类中看到一个虚拟函数,它就会知道(意识到)您正在尝试使用多态性",于是就转到派生类(使用v-table,一种后期绑定的形式)中,找到另一个具有相同头的方法,但是有一个不同的实现。

    为什么实施不同?

    你这个笨蛋!去读一本好书!

    好的,等等,等等,当他/她可以简单地使用派生类型指针时,为什么还要使用基指针呢?你是法官,所有这些头痛值得吗?看看这两个片段:

    / / 1:

    1
    2
    3
    4
    Parent* p1 = &boy;
    p1 -> task();
    Parent* p2 = &girl;
    p2 -> task();

    / / 2:

    1
    2
    3
    4
    Boy* p1 = &boy;
    p1 -> task();
    Girl* p2 = &girl;
    p2 -> task();

    好吧,尽管我认为1仍然比2好,但你也可以这样写1:

    / / 1:

    1
    2
    3
    4
    Parent* p1 = &boy;
    p1 -> task();
    p1 = &girl;
    p1 -> task();

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

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

    现在,试着重新写这个,不要头痛!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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();

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


    在基类中有函数时,可以在派生类中使用RedefineOverride

    重新定义方法:在派生类中给出了一个新的基类方法实现。不利于Dynamic binding

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

    所以当你说:

    But earlier in the book, when learning about basic inheritance, I was
    able to override base methods in derived classes without using
    'virtual'.

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


    如果你知道基本的机制,它会有所帮助。C++对C程序员使用的一些编码技术进行了形式化,"类"用"覆盖"来替换,具有公共头段的结构将被用来处理不同类型的对象,但是使用一些常见的数据或操作。通常,覆盖的基结构(公共部分)有一个指向函数表的指针,该函数表指向每个对象类型的一组不同的例程。C++做了同样的事情,但是隐藏了机制,即C++ +EDCOX1,0,其中FUNC是虚拟的,因为C是EDCOX1,1,所以派生类之间的变化是函数表的内容。[一个非虚拟方法ptr->func()只是转换为已损坏的_func(ptr,….)。]

    这样做的结果是,您只需要了解基类就可以调用派生类的方法,即,如果一个例程了解类A,您可以将它传递给派生类B指针,那么调用的虚拟方法将是B的方法,而不是A的方法,因为您通过函数表B指向处。


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

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


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

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

    在上面的示例中,默认情况下将调用shape::getname,除非getname()在基类形状中被定义为虚拟的。这迫使编译器在三角形类而不是形状类中查找getname()实现。

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

    最后,为什么在C++中甚至需要虚拟化,为什么不让它成为Java中的默认行为呢?

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

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

    虚拟函数避免了不必要的类型转换问题,我们中的一些人可能会争论,当我们可以使用派生类指针调用派生类中特定的函数时,为什么我们需要虚拟函数?答案是——它否定了大型系统开发中继承的全部思想,因为在大型系统开发中需要单指针基类对象。

    让我们比较下面两个简单的程序,了解虚拟功能的重要性:

    不带虚拟功能的程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #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;
    }

    输出:

    1
    2
    3
    Fathers age is 50 years
    Fathers age is 50 years
    son`s age is 26 years

    具有虚拟功能的程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #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;
    }

    输出:

    1
    2
    3
    Fathers age is 50 years
    son`s age is 26 years
    son`s age is 26 years

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


    在效率方面,虚函数的效率比早期的绑定函数稍低。

    这种虚拟调用机制的效率几乎可以与"正常函数调用"机制(25%以内)的效率相同。它的空间开销是一个类的每个对象中的一个指针,每个类的虚拟函数加上一个VTBL,用于"每个类"[ Bjarne Stroustrup的C++访问]


    下面是一个完整的例子,说明了为什么使用虚拟方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    #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;
    }

    在接口设计中采用了虚拟方法。例如,在Windows中有一个名为iunknown的接口,如下所示:

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

    这些方法留给界面用户来实现。它们对于某些必须继承知识的物体的创造和毁灭是必不可少的。在这种情况下,运行时知道这三个方法,并期望在调用它们时实现它们。所以在某种意义上,它们充当对象本身和使用该对象的任何对象之间的契约。


    OOP答案:亚型多态性

    在C++中,如果应用维基百科的定义,则需要使用虚拟方法来实现多态性,更准确地说是子类型或子类型多态性。

    维基百科,子类型,2019-01-09:在编程语言理论中,子类型(也称为子类型多态性或包含多态性)是类型多态性的一种形式,其中子类型是一种数据类型,它与另一种数据类型(父类型)相关,具有可替换性的概念,这意味着程序元素(通常是子程序或函数)被编写来操作父类型还可以对子类型的元素进行操作。

    注意:subtype表示基类,subtyp表示继承类。

    关于亚型多态性的进一步阅读

    • https://en.wikipedia.org/wiki/subtyping
    • https://en.wikipedia.org/wiki/多态性(计算机科学)子类型

    技术答案:动态调度

    如果您有指向基类的指针,那么方法(声明为虚拟)的调用将被调度到所创建对象的实际类的方法。这就是子类型多态是如何实现的C++。

    C++中多态性的进一步阅读与动态调度

    • http://www.cplusplus.com/doc/tutorial/多态性/
    • https://en.cppreference.com/w/cpp/language/virtual/虚拟

    实现答案:创建vtable条目

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

    进一步阅读vtables

    • https://en.wikipedia.org/wiki/virtual_method_表

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #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;
    }

    示例代码输出

    1
    2
    3
    Meow!
    Woof!
    Woo, woo, woow! ... Woof!

    代码示例的UML类图

    UML class diagram of code example


    我认为,一旦一个方法被声明为虚方法,您就不需要在重写中使用"virtual"关键字了。

    1
    2
    3
    4
    5
    6
    class Base { virtual void foo(); };

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

    如果在基的foo声明中不使用"virtual",那么派生的foo将只是隐藏它。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #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;
    }

    两种不同的结果是:

    如果没有定义virtual,它将在编译时绑定。animal*ad和func(animal*)都指向动物的says()方法。

    1
    2
    3
    4
    5
    6
    7
    8
    $ 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的says()方法,因为dog是它们的对象类型。除非没有定义[dog's says()"woof"]方法,否则它将是类树中首先搜索到的方法,即派生类可以覆盖其基类的方法[animal's says()]。

    1
    2
    3
    4
    5
    6
    7
    8
    $ 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中的所有类属性(数据和方法)都是有效虚拟的。因为所有对象都是在运行时动态创建的,所以不需要类型声明或关键字virtual。下面是python的代码版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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)))

    输出是:

    1
    2
    3
    4
    5
    6
    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 is d)返回true,并且它们的值与0xb79f72cc>处的


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