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


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

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

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

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


968
2018-03-06 07:10


起源


我在这里为虚函数创建了一个实用的解释: nrecursions.blogspot.in/2015/06/... - Nav
这可能是虚函数的最大好处 - 能够以这样的方式构造代码:新派生的类将自动使用旧代码而无需修改! - user3530616
tbh,虚函数是OOP的主要特征,用于类型擦除。我认为,非虚拟方法正在使Object Pascal和C ++变得特别,正在优化不必要的大vtable并允许POD兼容的类。许多OOP语言都希望如此 一切 方法可以覆盖。 - Swift - Friday Pie
这是一个很好的问题。事实上,C ++中的这个虚拟事物在Java或PHP等其他语言中被抽象出来。在C ++中,您可以获得对某些罕见情况的更多控制(请注意多重继承或特殊情况 DDOD)。但为什么这个问题发布在stackoverflow.com上? - Edgar Alloro


答案:


这是我不仅理解的原因 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一个虚函数:

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

完成。


2290
2018-03-06 13:54



因此,如果我正确理解这一点,虚拟允许调用子类方法,即使该对象被视为其超类? - Kenneth Worden
我会在维基百科中指出类似的解释 en.wikipedia.org/wiki/Virtual_function 。无论如何,真的很好解释。 - David Sánchez
不是通过中间函数“func”的例子来解释后期绑定,而是一个更简单的演示 - 动物*动物=新动物;  //猫*猫=新猫;  动物*猫=新猫;  动物 - >食();  //输出:“我正在吃普通食物。” CAT->吃();  //输出:“我正在吃普通食物。”即使您正在分配子类对象(Cat),被调用的方法也是基于指针类型(Animal)而不是它指向的对象类型。这就是你需要“虚拟”的原因。 - rexbelia
我是唯一一个在C ++中发现这种默认行为的人吗?我本以为没有“虚拟”的代码可以工作。 - David 天宇 Wong
你想不想使用虚拟吗?如果某人从您的班级继承,您似乎会(可能)出现不可预测的行为。 - Brad Richardson


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

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

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


534
2018-03-06 07:56



非常好,快速回家并使用更好的例子。然而,这是简单的,提问者应该只是阅读页面 parashift.com/c++-faq-lite/virtual-functions.html。其他人已经从这个帖子链接的SO文章中指出了这个资源,但我相信这值得再次提及。 - Sonny
我不知道是否 早 和 晚了 绑定是c ++社区中特别使用的术语,但正确的术语是 静态的 (在编译时)和 动态 (在运行时)绑定。 - mike
@mike - “术语”后期约束“至少可以追溯到20世纪60年代,它可以在ACM的通讯中找到。”。如果每个概念都有一个正确的词,那不是很好吗?不幸的是,事实并非如此。术语“早期绑定”和“后期绑定”早于C ++甚至是面向对象的编程,并且与您使用的术语一样正确。 - Steve314
请注意,今天的C ++编译器通常可以优化到早期绑定 - 当他们可以确定绑定将是什么时。这也称为“去虚拟化”。 - einpoklum
@ Steve314:我的意思是,例如,你为一个基类指针调用虚函数的情况,但编译器可以告诉指向对象实际上总是具有该方法的相同实现。 - einpoklum


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

72
2018-03-06 07:26



您的示例说明返回的字符串取决于函数是否为虚函数,但它不会说明哪个结果对应虚拟,哪个结果对应非虚拟。此外,由于您没有使用返回的字符串,因此有点混乱。 - Ross
使用Virtual关键字: 纬。没有Virtual关键字: ?。 - Hesham Eraqi
@HeshamEraqi没有虚拟它是早期绑定它会显示“?”基类 - Ahmad


你需要虚拟方法 安全的转发简单 和 简明

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


非虚方法⇒静态绑定

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


虚方法⇒动态绑定。

声明 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 关键字确保如果有  在某个基类中这样的虚方法,那么你会得到一个错误(这是可取的)。


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

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

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


28
2017-11-24 07:24





如果基类是 Base,派生类是 Der,你可以有一个 Base *p 实际指向实例的指针 Der。你打电话时 p->foo();如果 foo 是  虚拟,然后 Base它的版本执行,忽略了这个事实 p 实际上指向了一个 Der。如果foo  虚拟, p->foo() 执行“最叶子”覆盖 foo,充分考虑指向项目的实际类别。因此,虚拟和非虚拟之间的区别实际上非常重要:前者允许运行时 多态性,OO编程的核心概念,而后者则没有。


27
2018-03-06 07:27



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


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

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

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


24
2017-12-12 11:56





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

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

  • 您可以通过在关键字前面创建一个虚拟函数 virtual 在其基类声明中。例如,

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

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

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

23
2017-10-12 09:41