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

Jak*_*son 1223 c++ virtual-functions

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

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

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

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

M P*_*rry 2618

以下是我不仅了解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."; }
};
Run Code Online (Sandbox Code Playgroud)

在你的主要功能:

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

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

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

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

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

现在我们的主要功能是:

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

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating generic food."
Run Code Online (Sandbox Code Playgroud)

哦......我们把猫送进了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."; }
};
Run Code Online (Sandbox Code Playgroud)

主要:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."
Run Code Online (Sandbox Code Playgroud)

完成.

  • 因此,如果我正确理解这一点,虚拟允许调用子类方法,即使该对象被视为其超类? (143认同)
  • 而不是通过中间函数"func"的例子来解释后期绑定,这里是一个更直接的演示 - __Animal*animal = new Animal; __ __ // Cat*cat = new Cat; __ __Animal*cat = new Cat; __ __animal-> eat(); __ //输出:"我正在吃普通食物." __cat-> eat(); __ //输出:"我正在吃普通食物." 即使您正在分配子类对象(Cat),被调用的方法也是基于指针类型(Animal)而不是它指向的对象类型.这就是你需要"虚拟"的原因. (129认同)
  • 我是唯一一个在C++中发现这种默认行为的人吗?我本以为没有"虚拟"的代码可以工作. (30认同)
  • 首先,虚拟调用比常规函数调用要昂贵得多.默认情况下,C++理念很快,因此默认情况下虚拟调用是一个很大的禁忌.第二个原因是,如果从库继承类并且它更改其公共或私有方法(在内部调用虚方法)的内部实现而不更改基类行为,则虚拟调用可能导致代码中断. (27认同)
  • @David天宇黄我认为`virtual`引入了一些动态绑定和静态,是的,如果你来自像Java这样的语言,这很奇怪. (15认同)
  • @BJovke:这不是强制转换,隐式或显式.这是一种替代.参见[Liskov替代原则](https://en.wikipedia.org/wiki/Liskov_substitution_principle). (9认同)
  • 在C++ 11中,您可以添加`override`关键字:`void eat()override {std :: cout <<"我正在吃一只老鼠."; 因此,您知道您正在覆盖基类的现有方法(并使编译器意识到它). (8认同)
  • @BJovke:为什么在世界上你会期望这是一个编译错误,甚至是不好的做法? (7认同)
  • 你想不想使用虚拟吗?如果某人从您的班级继承,您似乎会(可能)有不可预测的行为. (4认同)
  • @Brad有些学校会说"你不会",因此"让一切变得虚拟".但是,运行时多态性的成本,无论是在性能方面还是在能够准确和数学上保证程序的行为和正确性方面.所以,如果你不需要它来实现目标,通常的智慧是避免它.像任何东西:利弊,使用正确的工具来完成工作. (4认同)
  • @hounded:与问题无关。“最佳”方法是`std :: unique_ptr &lt;Animal&gt; animal = std :: make_unique &lt;Cat&gt;();`,它具有完全相同的问题。 (4认同)
  • @KennyWorden更具体地说,该关键字允许覆盖基类的方法.因为Cat源自Animal,所以func()接受Cat.但是,由于`*xyz`是一个Animal指针,并且`Animal-> eat()`没有被覆盖,所以当你不将`eat()`标记为虚函数时,它会假定使用该函数. (3认同)
  • 我会在维基百科https://en.wikipedia.org/wiki/Virtual_function中指出类似的解释.无论如何,真的很好解释. (2认同)
  • 这到底是什么“虚拟”? (2认同)
  • `void func(Animal*xyz){xyz-> eat(); 给这个函数一个指向`Cat`作为参数的指针将在现代C++中产生编译器错误.这不是一般在C++中应该完成的方式.虚函数的目的是改变基类的行为,类`Animal`调用成员方法`eat()`的方法将调用`Animal`的`eat()`. (2认同)
  • @BradRichardson非虚拟调用是100%可预测的.虚拟引入了"不可预测的"行为. (2认同)

Ste*_*314 629

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

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

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"
Run Code Online (Sandbox Code Playgroud)

编辑 - 看到这个问题.

此外 - 本教程介绍了C++中的早期和晚期绑定.

  • 我不知道**早期**和**晚**绑定是否是c ++社区中特定使用的术语,但正确的术语是**静态**(在编译时)和**动态**(在运行时)绑定. (30认同)
  • @mike - ["术语"后期约束"至少可以追溯到20世纪60年代,它可以在ACM的通讯中找到."](https://en.wikipedia.org/wiki/Late_binding#History).如果每个概念都有一个正确的词,那不是很好吗?不幸的是,事实并非如此.术语"早期绑定"和"后期绑定"早于C++甚至是面向对象的编程,并且与您使用的术语一样正确. (25认同)
  • 非常好,快速回家并使用更好的例子.然而,这是简单的,提问者应该只阅读页面http://www.parashift.com/c++-faq-lite/virtual-functions.html.其他人已经从这个帖子链接的SO文章中指出了这个资源,但我相信这值得再次提及. (8认同)
  • 请注意,今天的C++编译器通常可以优化到早期绑定 - 当他们可以确定绑定将是什么时.这也称为"去虚拟化". (8认同)
  • @Steve314:对于这种情况,`std::unique_ptr` 实际上是隐式可转换的,专门用于处理这种向上转换,这是完全正常的。它不违反唯一性,因为它是一个移动构造函数,而不是一个复制构造函数。 (3认同)
  • @BJovke-此答案是在C ++ 11发布之前编写的。即便如此,我也只是在GCC 6.3.0中编译了它(默认情况下使用C ++ 14),没有任何问题-显然将变量声明和调用包装在`main`函数中。指向指针的*隐式*强制转换为指向基础的指针(更专业的隐式转换为更通用的指针)。反之亦然,您需要一个显式的强制转换,通常是一个“ dynamic_cast”。其他-极易发生不确定的行为,因此请确保您知道自己在做什么。据我所知,这甚至在C ++ 98之前都没有改变。 (2认同)
  • @ Steve314:我的意思是,例如,你为一个基类指针调用虚函数的情况,但是编译器可以告诉指向对象实际上总是具有该方法的相同实现. (2认同)

Hen*_*man 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

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

  • 您的示例表示返回的字符串取决于函数是否为虚拟,但它不会说明哪个结果对应虚拟,哪个结果对应非虚拟.此外,由于您没有使用返回的字符串,因此有点混乱. (35认同)
  • 使用Virtual关键字:_Woof_.没有Virtual关键字:_?_. (7认同)

Che*_*Alf 43

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

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


非虚方法⇒静态绑定

以下代码故意"不正确".它没有将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;
}
Run Code Online (Sandbox Code Playgroud)

在注释为"bad"的行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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

请注意,此处派生类中的方法实现未标记virtual,而是标记override.它们可以被标记,virtual但它们是自动虚拟的.该override关键字确保如果有没有在一些基础类这样的虚拟方法,那么你会得到一个错误(这是可取的).


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

如果没有virtual人必须实现一些动态绑定的Do It Yourself版本.这通常涉及不安全的手动向下转换,复杂性和冗长.

对于单个函数的情况,就像这里一样,只需将函数指针存储在对象中并通过该函数指针调用即可,但即使这样,它也会涉及一些不安全的向下转换,复杂性和冗长,即:

#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;
}
Run Code Online (Sandbox Code Playgroud)

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


小智 39

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

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

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

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

     class Derive : public Base
     {
        void func();
     }
    
    Run Code Online (Sandbox Code Playgroud)
  • Base类指针可用于指向Base类对象以及Derived类对象.

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


Ale*_*lli 34

如果基类是Base,并且派生类是Der,则可以有一个Base *p实际指向实例的指针Der.当你打电话p->foo();,如果foo虚,那么Base的的版本,它执行时,忽略了一个事实p实际上指向Der.如果foo 虚拟的,则p->foo()执行"最左边"覆盖foo,完全考虑指向项的实际类.因此,虚拟和非虚拟之间的区别实际上非常重要:前者允许运行时多态性,OO编程的核心概念,而后者则不然.

  • 我讨厌与你发生矛盾,但编译时多态仍然是多态的.即使重载非成员函数也是一种多态性 - 使用链接中的术语进行ad-hoc多态.这里的区别在于早期和晚期绑定. (7认同)
  • @ Steve314,你迂腐正确(作为同伴,我赞成;-) - 编辑答案添加缺少的形容词;-). (6认同)

Aja*_* GU 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();

}
Run Code Online (Sandbox Code Playgroud)

输出将是:

Hello from Class A.
Run Code Online (Sandbox Code Playgroud)

但是有了虚函数:

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

}
Run Code Online (Sandbox Code Playgroud)

输出将是:

Hello from Class B.
Run Code Online (Sandbox Code Playgroud)

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


Ary*_*pta 25

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

虚拟的解构器

考虑下面的这个程序,而不将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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Deleting an Animal
Run Code Online (Sandbox Code Playgroud)
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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Deleting an Animal name Cat
Deleting an Animal
Run Code Online (Sandbox Code Playgroud)

  • `没有将基类析构函数声明为虚拟; 猫的记忆可能无法清理.它比这更糟糕.通过基指针/引用删除派生对象是纯未定义的行为.所以,不仅仅是某些内存可能会泄漏.相反,程序是不正确的,因此编译器可以将其转换为任何东西:_happens_工作正常,或什么都不做,或从你的鼻子召唤恶魔等的机器代码.这就是为什么,如果一个程序是这样设计的一些用户_might_通过基本引用删除派生实例的方式,基_must_有一个虚拟析构函数 (10认同)

h0b*_*0b0 22

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

  • 如果没有"虚拟",你就不会超载.你是*影子*.如果基类"B"具有一个或多个函数`foo`,并且派生类`D`定义了一个`foo`名称,则`foo`*隐藏*所有那些`foo`-s在`B`中.使用范围解析将它们作为"B :: foo"到达.要将`B :: foo`函数提升为`D`进行重载,你必须使用`using B :: foo`. (2认同)

Zie*_*ezi 20

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

快速回答:

  1. 它为我们提供了所需的"原料"的一个1面向对象的编程.

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

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

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

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


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

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

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


nit*_*ian 14

当你在基类中有一个功能,你可以Redefine或者Override它的派生类.

重新定义方法:派生类中给出了基类方法的新实现.便利Dynamic binding.

重写的方法: Redefining一个virtual method基类的派生类.虚方法有助于动态绑定.

所以当你说:

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

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


M-J*_*M-J 14

我的回答形式是一个更好的阅读:


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

因为多态性.

什么是多态?

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

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

好吧,通过早期绑定.

什么是早期绑定?

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

所以...?

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

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

因为我们需要多态性!

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

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

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

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

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

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

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

为什么不同的实施?

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

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

// 1:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();
Run Code Online (Sandbox Code Playgroud)

// 2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();
Run Code Online (Sandbox Code Playgroud)

好吧,虽然我认为1仍然优于2,你可以这样写1:

// 1:

Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();
Run Code Online (Sandbox Code Playgroud)

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

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

现在,尝试重写这个,没有任何头痛!

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();
Run Code Online (Sandbox Code Playgroud)

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

  • 应该突出显示使用单个(超)对象类型迭代不同类型(子)对象的概念,这是您提出的一个好观点,谢谢 (2认同)

小智 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.


rvk*_*ddy 9

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

更多详细信息,请参见此链接 http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


jav*_*mer 7

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

Shape *shape = new Triangle(); 
cout << shape->getName();
Run Code Online (Sandbox Code Playgroud)

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

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

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

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


Jör*_*ann 7

OOP 答案:子类型多态性

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

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

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

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

技术答案:动态调度

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

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

实现答案:创建vtable 条目

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

进一步阅读 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;
}
Run Code Online (Sandbox Code Playgroud)

示例代码的输出

Meow!
Woof!
Woo, woo, woow! ... Woof!
Run Code Online (Sandbox Code Playgroud)

代码示例的UML类图

代码示例的UML类图

  • 接受我的支持,因为您展示了多态性也许是最重要的用途:具有虚拟成员函数的基类指定一个*接口*,或者换句话说,一个*API。*使用这样的类框架的代码(这里:您的主要函数)可以统一处理集合中的所有项目(此处:您的数组),并且不需要、不想、而且实际上经常“无法”知道在运行时将调用哪个具体实现,例如因为它不但仍然存在。这是在对象和处理程序之间建立抽象关系的基础之一。 (2认同)

小智 5

为什么我们需要虚函数?

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

让我们比较下面两个简单的程序来理解虚函数的重要性:

没有虚函数的程序:

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years
Run Code Online (Sandbox Code Playgroud)

具有虚函数的程序:

#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;
}
Run Code Online (Sandbox Code Playgroud)

输出:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years
Run Code Online (Sandbox Code Playgroud)

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


Nav*_*Nav 5

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

考虑这个战场应用:
在此处输入图片说明

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

由于WeaponsInterfaceclass,您现在可以将任何派生类分配给基类指针,WeaponsInterface并且因为它的函数是虚拟的,当您调用WeaponsInterface's 时shootTarget,派生类shootTarget被调用。

最好的部分是,您可以在运行时更换枪支(w=g1w=g2)。这是虚函数的主要优点,也是我们需要虚函数的原因。

所以在换枪的时候再也不用在各个地方注释掉代码了。它现在是一个简单而干净的过程,添加更多的枪类也更容易,因为我们只需要创建一个新的WeaponGun3orWeaponGun4类,我们可以确信它不会弄乱BattlefieldSoftware的代码或WeaponGun1/WeaponGun2的代码。

  • 我也最喜欢这个了。当我看到动物和狗的例子时,我有完全相同的想法,比如你为什么要这么做?这是理解虚函数的实用方法的一个更好的例子。我想提一下这一点。原始博客文章中的第一个示例(动物和狗类)并不完整。如果我之前没有读过其他例子,我就无法理解它。 (2认同)