c++ - 为什么要用虚函数




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

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

从我读过的(书中和在线中),虚函数是基类中的函数,您可以在派生类中重写。

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

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


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

快速回答:

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

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

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

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

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

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

2.无法使代码功能更快,或者在运行时使用其他语言功能在备选内容之间进行选择时使用更少的内存。 Bjarne Stroustrup C ++编程:原理与实践(14.3.1)

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


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

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

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

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

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

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

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

虚函数避免了不必要的类型转换问题,我们中的一些人可以辩论,为什么当我们可以使用派生类指针调用派生类中特定的函数时,我们需要虚函数!答案是 - 它使大系统中的继承开发,其中非常需要单指针基类对象。

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

没有虚拟功能的程序:

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

OUTPUT:

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

OUTPUT:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

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


以下是我如何理解不仅是什么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()

解决的办法是让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."

完成。


关于效率, 虚拟函数与早期绑定函数相比效率稍低。

“这种虚拟调用机制几乎和”正常函数调用“机制一样高效(在25%以内),其空间开销是一个类的每个对象中的一个指针,带有虚拟函数加上每个这样的类一个vtbl”[ A Bjarne Stroustrup 参观C ++ ]


如果你知道基本的机制,它会有所帮助。 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指向。


当你在基类中有一个函数时,你可以在派生类中重RedefineOverride它。

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

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

所以当你说:

但是在本书的前面,在学习基本继承时,我能够在不使用'虚拟'的情况下重写派生类中的基本方法。

你不是在压倒它,因为基类中的方法不是虚拟的,而是你正在重新定义它


您至少需要1级继承和downcast来展示它。 这是一个非常简单的例子:

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
}

我以对话的形式回答了一个更好的阅读:

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

由于多态性。

什么是多态性?

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

这种多态性的定义如何导致对虚拟功能的需求?

那么,通过早期的约束

什么是早期绑定?

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

所以...?

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

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

因为我们需要多态!

多晶现在的好处是什么?

您可以使用基本类型指针作为单个函数的参数,然后在程序运行时,您可以毫无问题地访问每个派生类型接口(例如,它们的成员函数),并使用该单一引用基指针。

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

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

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

假设你用一个基指针调用了一个函数,该基指针具有来自其派生类之一的对象的地址。 正如我们在上面讨论的那样,在运行时,这个指针被取消引用,但是,我们期望一个方法(==成员函数)“来自我们的派生类”被执行! 然而,一个相同的方法(具有相同头文件的方法)已经在基类中定义了,那么为什么你的程序会费心选择另一种方法呢? 换句话说,我的意思是,你怎么能把这个场景从我们之前看到的事情中解脱出来?

简短的回答是“基于虚拟成员函数”,稍微长一点的答案是,“在这一步,如果程序在基类中看到一个虚函数,它知道(意识到)你正在尝试使用多态性“,所以派生类(使用v-table ,一种后期绑定的形式)可以找到另一种具有相同头的方法 ,但预期具有不同的实现。

为什么有不同的实现?

你kn头! 去读一本好书

好吧,等待等待,当他/她可以简单地使用派生类型指针时,为什么还要费心使用基本指针? 你是法官,难道这就是头疼吗? 看看这两个片段:

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

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


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

虚拟破坏者

考虑下面的这个程序,没有声明基类析构函数为虚拟的; 猫的内存可能无法清理。

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之前创建一个虚函数。 例如,

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

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

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

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

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


需要虚拟功能解释[易懂]

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

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


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





virtual-functions