【C++ Primer | 15】虚函数表剖析(一)

一、虚函数

1. 概念

多态指当不同的对象收到相同的消息时,产生不同的动作

  • 编译时多态(静态绑定),函数重载,运算符重载,模板。
  • 运行时多态(动态绑定),虚函数机制。

为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。

C++多态实现的原理:

  •  当类中声明虚函数时,编译器会在类中生成一个虚函数表
  • 虚函数表是一个存储成员函数地址的数据结构
  • 虚函数表是由编译器自动生成与维护的
  •  virtual成员函数会被编译器放入虚函数表中
  • 存在虚函数表时,每个对象中都有一个指向虚函数表的指针

 

2. 类的虚表

每个包含了虚函数的类都包含一个虚表。 
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

 

3. 虚表指针

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 
为了指定对象的虚表,每个对象的内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚

4. 动态绑定

class A 
{
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A 
{
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};

class C: public B 
{
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图3所示。

由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。 

  • 类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。 
  • 类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。 
  • 类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。 

虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。[非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

【注意】非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

下面以代码说明,代码如下:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};

typedef void(*Fun)(void);  //函数指针
int main()
{
    Base b;
    //  这里指针操作比较混乱,在此稍微解析下:

    //  *****printf("虚表地址:%p\n", *(int *)&b); 解析*****:
    //  1.&b代表对象b的起始地址
    //  2.(int *)&b 强转成int *类型,为了后面取b对象的前四个字节,前四个字节是虚表指针
    //  3.*(int *)&b 取前四个字节,即vptr虚表地址
    //

    //  *****printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);*****:
    //  根据上面的解析我们知道*(int *)&b是vptr,即虚表指针.并且虚表是存放虚函数指针的
    //  所以虚表中每个元素(虚函数指针)在32位编译器下是4个字节,因此(int *)*(int *)&b
    //  这样强转后为了后面的取四个字节.所以*(int *)*(int *)&b就是虚表的第一个元素.
    //  即f()的地址.
    //  那么接下来的取第二个虚函数地址也就依次类推.  始终记着vptr指向的是一块内存,
    //  这块内存存放着虚函数地址,这块内存就是我们所说的虚表.
    //
    printf("虚表地址:%p\n", *(int *)&b);
    printf("第一个虚函数地址:%p\n", *(int *)*(int *)&b);
    printf("第二个虚函数地址:%p\n", *((int *)*(int *)(&b) + 1));

    Fun pfun = (Fun)*((int *)*(int *)(&b));  //vitural f();
    printf("f():%p\n", pfun);
    pfun();

    pfun = (Fun)(*((int *)*(int *)(&b) + 1));  //vitural g();
    printf("g():%p\n", pfun);
    pfun();
}

输出结果:

通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下

(Fun)*((int*)*(int*)(&b)+0);  // Base::f()
(Fun)*((int*)*(int*)(&b)+1);  // Base::g()
(Fun)*((int*)*(int*)(&b)+2);  // Base::h()

 

二、一般继承 

下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:

class Base
{
public:
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
};

class Derive : public Base
{
public:
    virtual void f1() { cout << "Base::f1()" << endl; }
    virtual void g1() { cout << "Base::g1()" << endl; }
    virtual void h1() { cout << "Base::h1()" << endl; }
};

 

 请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:

对于实例:Derive d; 的虚函数表如下:

我们可以看到下面几点:

  • 虚函数按照其声明顺序放于表中。
  • 父类的虚函数在子类的虚函数前面。

 

三、一般继承(有虚函数覆盖) 

覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。

class Base
{
public:
    virtual void f() { cout << "Base::f()" << endl; }
    virtual void g() { cout << "Base::g()" << endl; }
    virtual void h() { cout << "Base::h()" << endl; }
};

class Derive : public Base
{
public:
    virtual void f() { cout << "Base::f1()" << endl; }
    virtual void g1() { cout << "Base::g1()" << endl; }
    virtual void h1() { cout << "Base::h1()" << endl; }
};

为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:

我们从表中可以看到下面几点,

  • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
  • 没有被覆盖的函数依旧。

这样,我们就可以看到对于下面这样的程序:

Base *b = new Derive();
b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

 

四、单一的一般继承

 下面,我们假设有如下所示的一个继承关系:

注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。

测试代码:

#include<iostream>
using namespace std;

class Parent {
public:
    int iparent;
    Parent(): iparent(10) {}
    virtual void f() { cout << " Parent::f()" << endl; }
    virtual void g() { cout << " Parent::g()" << endl; }
    virtual void h() { cout << " Parent::h()" << endl; }
};

class Child : public Parent {
public:
    int ichild;
    Child(): ichild(100) {}
    virtual void f() { cout << "Child::f()" << endl; }
    virtual void g_child() { cout << "Child::g_child()" << endl; }
    virtual void h_child() { cout << "Child::h_child()" << endl; }
};

class GrandChild : public Child {
public:
    int igrandchild;
    GrandChild(): igrandchild(1000) {}
    virtual void f() { cout << "GrandChild::f()" << endl; }
    virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
    virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};

int main()
{
    typedef void(*Fun)(void);

    GrandChild gc;

    int** pVtab = (int**)&gc;

    cout << "[0] GrandChild::_vptr->" << endl;
    for (int i = 0; (Fun)pVtab[0][i] != NULL; i++) 
    {
        Fun pFun = (Fun)pVtab[0][i];
        cout << "    [" << i << "] ";
        pFun();
    }
    cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;
    cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;
    cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;
}

输出结果:

使用图片表示如下:

可见以下几个方面:

  • 虚函数表在最前面的位置。
  • 成员变量根据其继承和声明顺序依次放在后面。
  • 在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

 

参考资料

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值