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


1311

我正在学习C ++,并且刚开始使用虚函数。

从我阅读的内容(在书中和在线上)中,虚函数是基类中的函数,您可以在派生类中重写这些函数。

但是在本书的前面,当学习基本继承时,我无需使用即可在派生类中覆盖基本函数virtual

那我在这里想念什么?我知道虚函数还有很多,这似乎很重要,所以我想弄清楚到底是什么。我只是无法在网上找到直接的答案。


13
我创建了虚拟功能的实际解释一下:nrecursions.blogspot.in/2015/06/...
导航

4
这也许是虚函数的最大好处-以这样一种方式来构造代码的能力,即新派生的类无需修改就可以自动与旧代码一起使用!
user3530616 '17

tbh,虚拟功能是OOP的主要功能,用于类型擦除。我认为,非虚拟方法使Object Pascal和C ++变得与众不同,是对不必要的大vtable的优化,并允许与POD兼容的类。许多OOP语言期望每种方法都可以被覆盖。
斯威夫特-星期五派

这是一个很好的问题。实际上,C ++中的这种虚拟事物已被其他语言(如Java或PHP)抽象化了。在C ++中,您仅在某些罕见情况下获得更多控制权(请注意多重继承或DDOD的特殊情况)。但是,为什么这个问题发布在stackoverflow.com上?
埃德加·阿罗洛

我认为,如果您看一下早期的绑定-后期绑定和VTABLE,它将更加合理并且有意义。因此,这里有一个很好的解释(learningcpp.com/cpp-tutorial/125-the-virtual-table)。
ceyun

Answers:


2728

这是我不仅了解什么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*?如果您必须从Animal衍生出更多动物,则它们都需要它们自己的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."

做完了


164
因此,如果我正确地理解了这一点,即使对象被视为其超类,virtual也允许调用子类方法?
肯尼·沃登

146
这里没有通过中间函数“ func”的示例来解释后期绑定,而是一个更直接的演示 -Animal * animal = new Animal; // Cat * cat = new Cat; 动物* cat =新猫; 动物-> eat(); //输出:“我在吃普通食品。” cat-> eat(); //输出:“我在吃普通食品。” 即使您要分配子类对象(Cat),调用的方法也基于指针类型(Animal),而不是其指向的对象类型。这就是为什么您需要“虚拟”的原因。
rexbelia '16

37
我是唯一发现C ++中这种默认行为的人吗?我希望没有“虚拟”功能的代码也能正常工作。
David天宇Wong

20
@David天宇Wong我认为virtual引入了动态绑定与静态绑定,是的,如果您来自Java之类的语言,这很奇怪。
peterchaula

32
首先,虚拟调用比常规函数调用昂贵得多。默认情况下,C ++哲学是快速的,因此默认情况下,虚拟调用是一个很大的禁忌。第二个原因是,如果您从库继承类,则虚拟调用可能会导致代码中断,并且在不更改基类行为的情况下,它会更改其对公共或私有方法(内部调用虚拟方法)的内部实现。
saolof

671

没有“虚拟”,您将获得“早期约束力”。使用哪种方法的实现是在编译时根据调用的指针类型决定的。

使用“虚拟”,您将获得“后期绑定”。在运行时,将根据所指向对象的类型(最初将其构造为何种对象)来决定使用哪种方法。根据指向该对象的指针的类型,这不一定是您想要的。

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 ++中的早期和晚期绑定。


11
优秀,并且可以使用更好的示例快速回到家中。但是,这太简单了,发问者实际上应该只阅读parashift.com/c++-faq-lite/virtual-functions.html页面。其他人已经在与该线程链接的SO文章中指出了该资源,但是我认为值得再次提及。
桑尼2012年

36
我不知道早期绑定和后期绑定是否是c ++社区中专门使用的术语,但是正确的术语是静态(在编译时)和动态(在运行时)绑定。
mike 2015年

31
@mike- “术语后期绑定”至少可以追溯到1960年代,在ACM通讯中可以找到。。如果每个概念都有一个正确的词,那会很好吗?不幸的是,事实并非如此。术语“早期绑定”和“后期绑定”早于C ++甚至面向对象的编程,并且与您使用的术语一样正确。
Steve314,2015年

4
@BJovke-此答案是在C ++ 11发布之前编写的。即便如此,我刚刚编译它在GCC 6.3.0(使用C ++ 14默认情况下),没有任何问题-显然包裹的变量声明,并呼吁在一个main函数指针等对衍生隐式强制转换为指针到基地(更专业的内容隐式转换为更一般的内容)。反之亦然,您需要一个明确的强制转换,通常是dynamic_cast。其他-非常容易发生不确定的行为,因此请确保您知道自己在做什么。据我所知,这甚至在C ++ 98之前都没有改变。
Steve314 '17

10
请注意,当今的C ++编译器通常可以确定绑定的内容,从而可以在后期进行优化,以提早进行绑定。这也称为“虚拟化”。
einpoklum

83

您需要至少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

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}

39
您的示例说返回的字符串取决于该函数是否为虚函数,但没有说明哪个结果对应于虚函数,哪个结果对应于非虚函数。此外,由于您没有使用要返回的字符串,这有点令人困惑。
罗斯2010年

7
使用虚拟关键字:Woof。如果没有虚拟关键字:
Hesham Eraqi

没有虚拟的@HeshamEraqi,它是早期绑定,它将显示“?” 基础课的内容
艾哈迈德

46

您需要使用虚拟方法来进行安全向下转换简化简洁

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


非虚拟方法⇒静态绑定

以下代码是有意的“不正确”。它没有将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方法不是虚拟的。


虚拟方法⇒动态绑定。

声明valuevirtual静态已知类型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关键字确保如果有没有在一些基础类这样的虚拟方法,那么你会得到一个错误(这是可取的)。


在没有虚拟方法的情况下这样做的丑陋

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

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


40

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

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

  • 您可以通过virtual在基类声明中的关键字前面使函数虚拟化。例如,

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

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

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

34

如果基类是Base,派生类是Der,您可以拥有一个Base *p实际指向实例的指针Der。当你打电话p->foo();,如果foo虚,那么Base的的版本,它执行,无视事实,p实际上指向Der。如果foo 虚拟的,则p->foo()foo充分考虑指向项的实际类的情况下执行的“最末尾”覆盖。因此,虚拟与非虚拟之间的区别实际上非常关键:前者允许运行时多态,即OO编程的核心概念,而后者则不允许。


8
我讨厌与您矛盾,但是编译时多态性仍然是多态性。甚至重载非成员函数也是多态的一种形式-使用链接中的术语的即席多态。此处的区别在于早期绑定和晚期绑定之间。
2010年

7
@ Steve314,您在书呆子方面是正确的(作为同伴,我同意;-)-编辑答案以添加缺少的形容词;-)。
Alex Martelli'3

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.

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


25

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

虚拟销毁器

考虑下面的程序,而不将基类的析构函数声明为虚函数;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

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.比那更糟。通过基本指针/引用删除派生对象是纯粹的未定义行为。因此,不仅仅是一些内存可能泄漏。而是,程序的格式不正确,因此编译器可能会将其转换为任何内容:碰巧可以正常工作,什么都不做的机器代码,从鼻子上召唤恶魔的机器等等。这就是为什么如果程序设计成这样一种某些用户可以通过基本引用删除派生实例的方式,该基本对象必须具有虚拟析构函数
underscore_d

21

您必须区分重载和重载。如果没有virtual关键字,则只能重载基类的方法。这只不过意味着躲藏。假设您有两个都实现的基类Base和派生类。现在,您有一个指向的实例的指针。调用它时,您可以观察到不同之处:如果该方法是虚拟的,则将使用的实现,如果缺少该方法,则将选择from的版本。最佳实践是永远不要从基类重载方法。使方法成为非虚拟方法是其作者告诉您的方法,即不打算在子类中对其进行扩展。Specializedvoid foo()BaseSpecializedfoo()virtualSpecializedBase


3
没有virtual你就不会超载。你在影子。如果基类B具有一个或多个函数foo,并且派生类D定义了一个foo名称,则将所有这些-s foo 隐藏foo在中B。它们是B::foo使用范围解析来达到的。要将B::foo功能提升D为重载,必须使用using B::foo
卡兹(Kaz)

20

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

快速回答:

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

在Bjarne Stroustrup C ++编程:原理和实践(14.3)中:

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

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

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


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

2.在运行时,您不能使用其他语言功能对功能进行编码,使其速度更快或占用更少的内存。Bjarne Stroustrup C ++编程:原理和实践。(14.3.1)

3.当我们调用包含虚函数的基类时,可以说出哪个函数真正被调用。


15

我以对话的形式得到了更好的阅读答案:


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

由于多态性。

什么是多态?

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

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

好吧,通过早期绑定

什么是早期绑定?

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

所以...?

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

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

因为我们需要多态!

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

您可以将基本类型指针用作单个函数的参数,然后在程序的运行时,可以使用该单个引用取消访问每个派生类型接口(例如其成员函数)而没有任何问题。基本指针。

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

好吧,这是因为您过早提出了您的问题!

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

假设您使用基本指针调用了一个函数,该指针具有其派生类之一中对象的地址。正如我们在上面讨论的那样,在运行时,该指针已被取消引用,到目前为止,到目前为止,我们仍然希望执行“来自派生类”的方法(==成员函数)!但是,在基类中已经定义了相同的方法(一个具有相同的标头),那么为什么您的程序要费心选择另一个方法?换句话说,您如何将这种情况与我们以前通常看到的情况区分开来?

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

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

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

OK,等等,等等,为什么当他/她可以简单地使用派生类型指针时,为什么还要麻烦使用基本指针呢?您是法官,这一切值得头痛吗?查看以下两个片段:

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

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


2
应该强调使用单个(超级)对象类型在不同类型(子)对象上进行迭代的概念,这是您给出的一个好点,谢谢
harshvchawla

14

当您在基类中有一个函数时,可以在派生类中RedefineOverride它中进行操作。

重新定义方法:派生类中提供了基类方法的新实现。便利Dynamic binding

覆盖方法: 派生类中基类的Redefiningavirtual 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而不是A的值,因为您浏览了功能表B所指向的位置。


8

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

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


7

所述虚拟关键字强制编译器来接在所限定的方法实现对象的类,而不是在指针的类。

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

在上面的示例中,默认情况下将调用Shape :: getName,除非在基类Shape中将getName()定义为虚拟的。这迫使编译器在Triangle类而不是Shape类中寻找getName()实现。

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

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

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

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

通过仔细分析两个输出,可以了解虚拟功能的重要性。


3

OOP答案:亚型多态性

在C ++中,需要虚拟方法来实现多态,更准确地说是子类型化子类型多态如果应用Wikipedia的定义。

维基百科,子类型,2019年1月9日:在编程语言理论中,子类型(也称为子类型多态或包含多态)是类型多态的一种形式,其中子类型是与某种概念相关的另一数据类型(超类型)相关的数据类型。可替换性的含义是指被编写为对超类型的元素进行操作的程序元素(通常是子例程或函数)也可以对子类型的元素进行操作。

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

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

技术答案:动态调度

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

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

实现答案:创建vtable条目

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

进一步阅读vtable


范例程式码

#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类图的代码示例


1
赞成我,因为您展示了多态可能是最重要的用途:具有虚拟成员函数的基类指定接口API。使用这种类框架的代码(此处为您的主要功能)可以统一处理集合中的所有项目(此处为您的数组),并且不需要,不希望并且实际上常常知道将调用哪个具体实现在运行时,例如因为它尚不存在。这是在对象与处理程序之间建立抽象关系的基础之一。
彼得-恢复莫妮卡

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

1

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

“这种虚拟调用机制几乎可以与“常规函数调用”机制一样高效(25%之内。它的空间开销是具有虚拟功能的类的每个对象中的一个指针,每个此类类具有一个vtbl” [ A Bjarne Stroustrup 的C ++之行 ]


2
后期绑定不仅使函数调用变慢,而且使被调用函数直到运行时才未知,因此无法应用跨函数调用的优化。这可以改变一切。在值传播删除大量代码的情况下(请考虑if(param1>param2) return cst;在某些情况下编译器可以将整个函数调用减少为常量)。
curiousguy 2015年

1

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

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

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


the run-time is aware of the three methods and expects them to be implemented由于它们是纯虚拟的,因此无法创建的实例IUnknown,因此所有子类都必须实现所有此类方法才能进行编译。没有实现它们而不只是在运行时发现它们的危险(当然,显然可以错误地实现它们!)。哇,今天,我#define用单词来学习Windows sa宏interface,大概是因为他们的用户不能(A)看到I名称中的前缀或(B)看到类以查看它是一个接口。gh
underscore_d

1

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

class Base { virtual void foo(); };

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

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


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中的所有类属性(数据和方法)实际上都是虚拟的。由于所有对象都是在运行时动态创建的,因此不需要类型声明或关键字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 ++的虚拟定义相同。请注意,dad是引用/指向同一Dog实例的两个不同的指针变量。表达式(ad是d)返回True,并且它们的值是相同的< main .Dog object at 0xb79f72cc>。


0

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


0

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


-1

最重要的是,虚拟功能使生活更轻松。让我们使用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 ++虚拟函数与成员函数指针(性能比较)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.