c++虚函数详解

1.虚函数的简介

由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。

用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。

虚函数主要通过V-Table虚函数表来实现,该表主要包含一个类的虚函数的地址表,可解决继承、覆盖的问题。当我们使用一个父类的指针去操作一个子类时,虚函数表就像一个地图一样,可指明实际所应该调用的函数。(每一个virtual函数的class都有一个相应的vtbl,当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。)

防止多重派生时,使用指针调用同名函数时已基类函数(父类)为准。

2.虚函数的核心概念

某基类中声明为virtual并在一个或多个派生类中重新定义的成员函数叫做虚函数。

3.虚函数的核心作用

  • 实现动态联编,在函数运行阶段动态的选择合适的成员函数。
  • 在定义了虚函数后,可实现在派生类中对虚函数进行重写,从而实现统一的接口和不同的执行过程。

我们通过三个虚函数的应用实例来说明虚函数的主要作用。

?继承关系中非虚函数:

#include <iostream>
using namespace std;
class A{
public:
    A(){};
    ~A(){};
    void show(void){
        cout<<"I am A!"<<endl;
    }
};
class B:public A{
public:
    B(){};
    ~B(){};
    void show(void){
        cout<<"I am B!"<<endl;    
    }        
};
int main(){
    A atr, *ptr;
    B btr;
    ptr = &atr;
    ptr->show();
    ptr = &btr;
    ptr->show();
    return 0;
}

结果:

(PS: 即使ptr指向B对象后,第二个show()的结果也并不是"I am B!", 主要原因该程序使用的是静态联编,而静态联编选择函数是基于指向对象的指针类型,而ptr是指向A的指针,因此也会一直调用A的成员函数show()。)

?继承关系中虚函数:

#include <iostream>
using namespace std;
class A{
public:
    A(){};
    ~A(){};
    virtual void show(void){
        cout<<"I am A!"<<endl;
    }
};
class B:public A{
public:
    B(){};
    ~B(){};
    void show(void){
        cout<<"I am B!"<<endl;    
    }        
};
int main(){
    cout<<"hello world"<<endl;
    A atr, *ptr;
    B btr;
    ptr = &atr;
    ptr->show();
    ptr = &btr;
    ptr->show();
    system("pause>nul");
}

结果:

 (PS:在基类A中对show()这一成员函数变为virtual虚函数后,会采用动态编译,只有调用它时才用根据它的对象类型去匹配对应的函数体。)

?非继承关系中虚函数:

#include<iostream>
using namespace std;
class A{
public:
    A(){};
    ~A(){};
    virtual void show(void){
        cout<<"I am A!"<<endl;
    }
};
class B{
public:
    B(){};
    ~B(){};
    virtual void show(void){
        cout<<"I am B!"<<endl;    
    }        
};
int main(){
    cout<<"hello world"<<endl;
    A atr, *ptr;
    B btr;
    ptr = &atr;
    ptr->show();
    ptr = (A*)&btr;
    ptr->show();
    system("pause>nul");
}

结果: 

总结

1.在使用继承的方式实现运行时多态时,基类需要将与派生类相同函数名的函数加上virtual关键字,这样才可以在运行时精准识别出子类的虚函数。

2.如果要不通过继承关系也实现出运行时多态的效果,则需要将两个不同类的同名函数都加上virtual关键字;同时,需要将定义的指针指向其他对象时,要进行强制类型转换。(因为两个类已经没有继承关系了,不能通过赋值兼容规则进行自动转换,所以要强制转化。)

3.带有多态性质的基类均应该声明一个virtual析构函数。同时如果任一class带有任何virtual函数,它就应该拥有一个virtual析构函数。

4.当class的设计目的如果不是作为base class使用,或不是为了具备多态性,则就不该声明virtual析构函数。(因为如果class中含有virutal函数会使得该class的体积增加,因为添加一个vptr(virtual table pointer)会增加其class大小达50%-100%)

拓展问题:

1.为什么调用普通函数比调用虚函数的效率高?

  • 因为普通函数是静态联编的,而调用虚函数是动态联编的。
  • 联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。(所谓联编就是将函数名和函数体的程序连接到一起的过程)
  • 静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。

(静态联编本质是系统用实参与形参进行匹配,对于重名的重载函数根据参数上的差异进行区分,然后进行联编,从而实现编译时的多态。函数的选择基于指向对象的指针类型或者引用类型。)

  • 动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。

(动态联编本质上是运行阶段执行的联编,当程序调用某一个函数时,系统会根据当前的对象类型去寻找和连接其程序的代码。函数的选择基于对象的类型。)

2.为什么要用虚函数表(存函数指针的数组)?

  • 同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。同时,虚函数表本质是一个地图导航,可以清楚告诉一个想要操作子类的父类指针到底该使用哪个函数。

3.为什么要把基类的析构函数定义为虚函数?

  • 在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.

4.虚函数可以是内联的吗?

  • 要多态的时候不内联,不多态的时候(也就是非指针、引用,也就是传值)可以内联。

参考文献

c++中的虚函数_雪山上的小草的博客-CSDN博客_c++虚函数

C++ 虚函数_空山新雨⁣的博客-CSDN博客_c++虚函数

《Effective C++(第三版)》


版权声明:本文为weixin_45138932原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。