C++虚函数实现原理

来源:互联网 发布:ubuntu autotools 编辑:程序博客网 时间:2024/06/03 22:41

与C语言相比,C++在布局和存取时间上的额外开销主要是由虚函数(virtual function)机制和虚继承(virtual base class)机制引起的。

在C++中,存在着静态联编和动态联编的区别。简而言之,“静态联编”是指编译器在编译过程中就完成了联编或绑定(binding),比如函数重载,C++编译器根据传递给函数的参数和函数名称就可以判断具体要使用哪一个函数,这种在编译过程中进行的绑定就称作静态联编(static binding)。而“动态联编”是指在在程序运行时完成的联编。

C++通过虚函数(virtual function)机制来支持动态联编(dynamic binding),并实现了多态机制。多态是面向对象程序设计语言的基本特征之一。在C++中,多态就是利用基类指针指向子类实例,然后通过基类指针调用子类(虚)函数从而实现“一个接口,多种形态”的效果。

C++利用基类指针和虚函数来实现动态多态,虚函数的定义很简单,只要在成员函数原型前加上关键字virtual即可,并且,virtual关键字只要声明一次,其派生类中的相应函数仍为虚函数,可以省略virtual关键字。

下面,我们就通过一个简单的例子来展示一下虚函数如何实现多态:

class Base1 {public:    virtual void func1() { cout << "Base1::func1()" << endl; }    void func2() { cout << "Base1::func2()" << endl; }};class Base2 : public Base1{    void func1() { cout << "Base2::func1()" << endl; }};int main(){    cout << "virtual function testing:" << endl;    Base2 b;    Base1 *ptr = &b;    ptr->func1();    ptr->func2();}

这里写图片描述

我们看到,同样是基类指针指向子类对象,对于func1,调用了基类的实现版本,对于func2,却调用了子类的实现版本。由此可见

  1. 对于virtual函数,具体调用哪个版本的函数取决于指针所指向对象类型。
  2. 对于非virtual函数,具体调用哪个版本的函数取决于指针本身的类型,而和指针所指对象类型无关。

那么,这一功能是如何实现的呢?下面我们就来探讨一下虚函数的实现原理。

virtual函数的实现原理

C++中虚函数是如何实现的呢?不少资料中都提到过,C++通过虚函数表和虚函数表指针来实现virtual function机制,具体而言:

  • 对于一个class,产生一堆指向virtual functions的指针,这些指针被统一放在一个表格中。这个表格被称为虚函数表,英文又称做virtual table(vtbl)。
  • 每一个对象中都添加一个指针,指向相关的virtual table。通常这个指针被称作虚函数表指针(vptr)。出于效率的考虑,该指针通常放在对象实例最前面的位置(第一个slot处)。每一个class所关联的type_info信息也由virtual table指出(通常放在表格的最前面)。
    为了更加直观地了解上面描述的实现机制,我们通过查看带有virtual function的类的内存布局来证实一下。

我们先定义一个类Base:

class Base{public:    // 虚函数func1    virtual void func1() { cout << "Base::func1()" << endl; }    // 虚函数func2    virtual void func2() { cout << "Base::func2()" << endl; }    // 虚函数func3    virtual void func3() { cout << "Base::func3()" << endl; }    int a;};

这里写图片描述

我们可以看到在Base类的内存布局上,第一个位置上存放虚函数表指针,接下来才是Base的成员变量。另外,存在着虚函数表,该表里存放着Base类的所有virtual函数。

我们用一幅图来展示一下Base类的内存布局:
这里写图片描述

虚函数表中存在在一个“结束结点”,用以标识虚函数表的结束(具体实现与编译器有关)。

既然虚函数表指针通常放在对象实例的最前面的位置,那么我们应该可以通过代码来访问虚函数表:

int main(){    typedef void(*pFunc)(void);    cout << "virtual function testing:" << endl;    Base b;    cout << "虚函数表地址:" << (int *)(&b) << endl;    pFunc pfunc;    pfunc = (pFunc)*((int *)(*((int *)(&b))));    pfunc();    pfunc = (pFunc)*((int *)(*((int *)(&b))) + 1);    pfunc();    pfunc = (pFunc)*((int *)(*((int *)(&b))) + 2);    pfunc();}

这里写图片描述

我们把&b强转为int *,这样就取到了虚函数表的地址,然后,再次取址,得到了第一个虚函数地址。同样,我们通过对函数指针进行下标操作就可以进一步得到第二和第三个虚函数地址。

通过上面实例演示,我们了解了虚函数具体的实现机制。那C++又是如何利用基类指针和虚函数来实现多态的呢?这是,我们就需要探讨一下在继承环境下虚函数表示如何工作的。

单继承环境下的虚函数

假设存在下面的两个类Base和A,A类继承自Base类:

class Base{public:    // 虚函数func1    virtual void func1() { cout << "Base::func1()" << endl; }    // 虚函数func2    virtual void func2() { cout << "Base::func2()" << endl; }    // 虚函数func3    virtual void func3() { cout << "Base::func3()" << endl; }    int a;};class A : public Base{public:    // 重写父类虚函数func1    void func1() { cout << "A::func1()" << endl; }    void func2() { cout << "A::func2()" << endl; }    // 新增虚函数func4    virtual void func4() { cout << "A::func3()" << endl; }};

这里写图片描述

通过两幅图片的对比,我们可以看到:

  • 在单继承中,A类覆盖了Base类中的同名虚函数,在虚函数表中体现为对应位置被A类中的新函数替换,而没有被覆盖的函数则没有发生变化。
  • 对于子类自己的虚函数,直接添加到虚函数表后面。
    另外,我们注意到,类A和类Base中都只有一个vfptr指针,前面我们说过,该指针指向虚函数表,我们分别输出类A和类Base的vfptr:
int main(){    typedef void(*pFunc)(void);    cout << "virtual function testing:" << endl;    Base b;    cout << "Base虚函数表地址:" << (int *)(&b) << endl;    A a;    cout << "A类虚函数表地址:" << (int *)(&a) << endl;}

这里写图片描述

我们可以看到,类A和类B分别拥有自己的虚函数表指针vptr和虚函数表vtbl。到这里,你是否已经明白为什么指向子类实例的基类指针可以调用子类(虚)函数?每一个实例对象中都存在一个vptr指针,编译器会先取出vptr的值,这个值就是虚函数表vtbl的地址,再根据这个值来到vtbl中调用目标函数。所以,只要vptr不同,指向的虚函数表vtbl就不同,而不同的虚函数表中存放着对应类的虚函数地址,这样就实现了多态的”效果“。

最后,我们用一幅图来表示单继承下的虚函数实现:
这里写图片描述

多继承环境下的虚函数

假设存在下面这样的四个类:

class Base{public:    // 虚函数func1    virtual void func1() { cout << "Base::func1()" << endl; }    // 虚函数func2    virtual void func2() { cout << "Base::func2()" << endl; }    // 虚函数func3    virtual void func3() { cout << "Base::func3()" << endl; }};class A : public Base{public:    // 重写父类虚函数func1    void func1() { cout << "A::func1()" << endl; }    void func2() { cout << "A::func2()" << endl; }};class B : public Base{public:    void func1() { cout << "B::func1()" << endl; }    void func2() { cout << "B::func2()" << endl; }};class C : public A, public B{public:    void func1() { cout << "D::func1()" << endl; }    void func2() { cout << "D::func2()" << endl; }};

类A和类B分别继承自类Base,类C继承了类B和类A,我们查看一下类C的内存布局:
这里写图片描述
我们可以看到,类C中拥有两个虚函数表指针vptr。类C中覆盖了类A的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数;类C中覆盖了类B中的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数(注意,这里使用跳转语句,而不是重复定义)。

类C的内存布局可以归纳为下图:
这里写图片描述

原创粉丝点击