(转载)【C++拾遗】 C++虚函数实现原理

我们知道,与C语言相比,C++在布局和存取时间上的额外开销主要是由虚函数(virtual function)机制和虚继承(virtual base class)机制引起的。在前面一篇文章中,我们从内存布局的角度入手,分析了虚继承的实现原理,传送门:从内存布局看C++虚继承的实现原理

今天,我们来分析C++的虚函数机制。


初探virtual function

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

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

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

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

代码1:

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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

输出如下:

这里写图片描述

我们看到,同样是基类指针指向子类对象,对于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的类的内存布局来证实一下。关于如何查看C++类的内存布局,可以参考我之前写的一篇文章传送门

我们先定义一个类Base:

代码2:

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;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

使用Visual Studio的命令行选项,我们可以看到类Base的内存布局如下所示:

这里写图片描述

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

我们用一幅图来展示一下Base类的内存布局:

这里写图片描述

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

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

代码3:

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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

在Visual Studio2013 + Window10中运行结果如下:

这里写图片描述

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

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

单继承环境下的虚函数

我们先来看看简单的单继承情况。假设存在下面的两个类Base和A,A类继承自Base类:

代码4:

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; }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

我们继续利用Visual Studio提供的命令行工具查看一下这两个类的内存布局。Base类已经在上面展示过,为了便于比较,这里再贴一次图片:

  1. 类Base的内存布局图:

    这里写图片描述

  2. 类A的内存布局图:

    这里写图片描述

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

  • 在单继承中,A类覆盖了Base类中的同名虚函数,在虚函数表中体现为对应位置被A类中的新函数替换,而没有被覆盖的函数则没有发生变化。
  • 对于子类自己的虚函数,直接添加到虚函数表后面。

另外,我们注意到,类A和类Base中都只有一个vfptr指针,前面我们说过,该指针指向虚函数表,我们分别输出类A和类Base的vfptr:

代码5:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

输出信息如下:

这里写图片描述

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

最后,我们用一幅图来表示单继承下的虚函数实现:

这里写图片描述

多继承环境下的虚函数

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

代码6:

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; }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

在代码6中,类A和类B分别继承自类Base,类C继承了类B和类A,我们查看一下类C的内存布局:

这里写图片描述

我们可以看到,类C中拥有两个虚函数表指针vptr。类C中覆盖了类A的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数;类C中覆盖了类B中的两个同名函数,在虚函数表中体现为对应位置替换为C中新函数(注意,这里使用跳转语句,而不是重复定义)。

类C的内存布局可以归纳为下图:

这里写图片描述

原文地址:http://blog.csdn.net/xiejingfa/article/details/50454819

原文地址:https://www.cnblogs.com/wodehao0808/p/8252002.html