C++虚函数表的分析

转载出处:

https://www.cnblogs.com/hushpa/p/5707475.html

https://blog.csdn.net/lihao21/article/details/50688337


先看代码:

复制代码
#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;}
};

class Derive : public Base{
public:
    void g() {cout<<"derive::g"<<endl;}
};

//可以稍后再看
int main () {
    cout<<"size of Base: "<<sizeof(Base)<<endl;

    typedef void(*Func)(void);
    Base b;
    Base *d = new Derive();

    long* pvptr = (long*)d;
    long* vptr = (long*)*pvptr;
    Func f = (Func)vptr[0];
    Func g = (Func)vptr[1];
    Func h = (Func)vptr[2];

    f();
    g();
    h();

    return 0;
}
复制代码

 

    都知道C++中的多态是用虚函数实现的: 子类覆盖父类的虚函数, 然后声明一个指向子类对象的父类指针, 如Base *b = new Derive();
当调用b->f()时, 调用的是子类的Derive::f()。 
这种机制内部由虚函数表实现,下面对虚函数表结构进行分析,并且用GDB验证。
 
1. 基础知识:
(1) 32位os 指针长度为4字节, 64位os 指针长度为8字节, 下面的分析环境为64位 linux & g++ 4.8.4.
(2) new一个对象时, 只为类中成员变量分配空间, 对象之间共享成员函数。

2. _vptr
    运行下上面的代码发现sizeof(Base) = 8, 说明编译器在类中自动添加了一个8字节的成员变量, 这个变量就是_vptr, 指向虚函数表的指针。
_vptr有些文章里说gcc是把它放在对象内存的末尾,VC是放在开始, 我编译是用的g++,验证了下是放在开始的:

验证代码:取对象a的地址与a第一个成员变量n的地址比较,如果不等,说明对象地址开始放的是_vptr. 也可以用gdb直接print a 会发现_vptr在开始

复制代码
class A
{
public:
      int n;
      virtual void Foo(void){}
};

int main()
{
     A a;
     char *p1 = reinterpret_cast<char*>(&a);
     char *p2 = reinterpret_cast<char*>(&a.n);
     if(p1 == p2)
     {
        cout<<"vPtr is in the end of class instance!"<<endl;
     }else
     {
        cout<<"vPtr is in the head of class instance!"<<endl;
     }
     return 1;
}
复制代码

(3) 虚函数表
    包含虚函数的类才会有虚函数表, 同属于一个类的对象共享虚函数表, 但是有各自的_vptr.
    虚函数表实质是一个指针数组,里面存的是虚函数的函数指针。

Base中虚函数表结构:

Derive中虚函数表结构:

 

(4)验证
运行上面代码结果:
    size of Base: 8
    base::f
    derive::g
    base::h

说明Derive的虚函数表结构跟上面分析的是一致的:
    d对象的首地址就是vptr指针的地址-pvptr,
    取pvptr的值就是vptr-虚函数表的地址
    取vptr中[0][1][2]的值就是这三个函数的地址
    通过函数地址就直接可以运行三个虚函数了。
    函数表中Base::g()函数指针被Derive中的Derive::g()函数指针覆盖, 所以执行的时候是调用的Derive::g()

 

(5)多继承

 

 

复制代码
附 GDB调试:

(1) #生成带有调试信息的可执行文件
g++ test.cpp -g -o test    

(2) #载入test
gdb test

(3) #列出Base类代码
(gdb) list Base
1       #include <iostream>
2
3       using namespace std;
4
5       class Base {
6       public:
7           virtual void f() {cout<<"base::f"<<endl;}
8           virtual void g() {cout<<"base::g"<<endl;}
9           virtual void h() {cout<<"base::h"<<endl;}
10      };

4) #查看Base函数地址
(gdb) info line 7 
Line 7 of "test.cpp" starts at address 0x400ac8 <Base::f()> and ends at 0x400ad4 <Base::f()+12>.
(gdb) info line 8
Line 8 of "test.cpp" starts at address 0x400af2 <Base::g()> and ends at 0x400afe <Base::g()+12>.
(gdb) info line 9
Line 9 of "test.cpp" starts at address 0x400b1c <Base::h()> and ends at 0x400b28 <Base::h()+12>.

5)#列出Derive代码
(gdb) list Derive 
7           virtual void f() {cout<<"base::f"<<endl;}
8           virtual void g() {cout<<"base::g"<<endl;}
9           virtual void h() {cout<<"base::h"<<endl;}
10      };
11
12      class Derive : public Base{
13      public:
14          void g() {cout<<"derive::g"<<endl;}
15      };

6)#查看Derive函数地址
(gdb) info line 14
Line 14 of "test.cpp" starts at address 0x400b46 <Derive::g()> and ends at 0x400b52 <Derive::g()+12>.

7)#start执行程序,n单步执行
(gdb) start
Temporary breakpoint 1, main () at test.cpp:19
19          cout<<"size of Base: "<<sizeof(Base)<<endl;
(gdb) n
size of Base: 8
22          Base b;
(gdb)
23          Base *d = new Derive();
(gdb)
25          long* pvptr = (long*)d;
(gdb)
26          long* vptr = (long*)*pvptr;
(gdb)
27          Func f = (Func)vptr[0];
(gdb)
28          Func g = (Func)vptr[1];
(gdb)
29          Func h = (Func)vptr[2];
(gdb)
31          f();
(gdb)

8) #print d对象, 0x400c90为成员变量_vptr的值,也就是函数表的地址
(gdb) p *d 
$4 = {_vptr.Base = 0x400c90 <vtable for Derive+16>}
(gdb) p vptr
$6 = (long *) 0x400c90 <vtable for Derive+16>9) #查看函数表值,与之前查看函数地址一致
(gdb) p (long*)vptr[0]
$9 = (long *) 0x400ac8 <Base::f()>
(gdb) p (long*)vptr[1]
$10 = (long *) 0x400b46 <Derive::g()>
(gdb) p (long*)vptr[2]
$11 = (long *) 0x400b1c <Base::h()>
复制代码

 

另vptr. vtable内存位置, refer  http://www.tuicool.com/articles/iUB3Ebi



补充:

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

这里写图片描述
图2:对象与它的虚表

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

四、动态绑定

说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

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所示。

这里写图片描述
图3:类A,类B,类C的对象模型

由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类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看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值