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

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

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

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

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

答案

这是我不仅了解什么是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()调用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()

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

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

做完了

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

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

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 级继承和一个低级继承来演示它。这是一个非常简单的示例:

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
}

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

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


非虚拟方法⇒静态绑定

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


虚拟方法⇒动态绑定。

在静态已知的类型Expression中将value声明为virtual可以确保每个调用都将检查该对象的实际类型,并为该动态类型调用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;
}

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

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

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

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

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

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

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

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

需要虚拟功能的说明 [易于理解]

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

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

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

虚拟销毁器

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

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

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

快速回答:

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

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

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

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

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


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

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

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