C++内存分布之虚函数和虚表

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/armwind/article/details/51893419

       虚函数:就是在类中被关键字Virtual修饰的成员函数。虚函数的作用就是实现多态,即多态性是将接口与实现进行分离,简单就是说允许将子类类型的指针赋值给父类类型的指针,那么指向基类的指针在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数,这个函数就是虚函数。博文中如果有错误的地方,欢迎大家指正,我们共同进步。

      这次这这篇博文,主要有下面几个探索点。

1.探究类在虚继承空类前后的对象大小有无变化。

2.探究虚函数的存放顺序,以及带有虚继承时虚表信息

3.探究虚表是什么东东,它具体放在什么地方,如何通过虚表查找到虚函数的

针对上面几条探索点,来开启探索之旅

一、源代码

       下面这个例子依然很简单。test1和test2类用来探索虚继承对带有虚函数类的影响,test2和test3用来探索虚表和虚函数的存放。

#include<iostream>

using namespace std;

class base{};

class test1:virtual public base{

public:
int a;
virtual int hehe(){
cout<<"hello"<<endl;
}

virtual int hehe1(){
cout<<"hello1"<<endl;
}
};

class test2:public base{
public:
int a;
virtual int hehe(){
cout<<"hello"<<endl;
}

virtual int hehe1(){
cout<<"hello1"<<endl;
}

};

class test3{
public:
int a;
virtual int hehe(){
cout<<"hello"<<endl;
}

virtual int hehe1(){
cout<<"hello1"<<endl;

}

};

int main(){
test1 *t1 = new test1();
test2 *t2 = new test2();
test3 *t3 = new test3();
cout<<"t1:"<<*((int*)t1)<<endl;
cout<<"t2:"<<*((int*)t2)<<endl;
cout<<"t3:"<<*((int*)t3)<<endl;
 
cout<<"t1->a:"<<&t1->a<<endl;
cout<<"t2->a:"<<&t2->a<<endl;
cout<<"t3->a:"<<&t3->a<<endl;
 
cout<<"sizeof(test1):"<<sizeof(test1)<<endl;
cout<<"sizeof(test2):"<<sizeof(test2)<<endl;
cout<<"sizeof(test3):"<<sizeof(test3)<<endl;

}

输出结果是:

t1:4198424  =0x401018

t2:4198384 =0x400FF0

t3:4198352  =0x400FD0

t1->a:0x1dd3018

t2->a:0x1dd3038

t3->a:0x1dd3058

sizeof(test1):16

sizeof(test2):16

sizeof(test3):16

 探索发现:上面的打印结果,我有以下几点发现

1.他们类中包含了同样的内容,为什么虚表大小不一样.在之前的菱形继承中,我们知道函数的局部变量都保存在栈中,先定义的局部变量,地址比后面定义的局部变量地址大。如下就可以看到这个现象。

t1:4198424  =0x401018 (t1虚函数表地址)

t2:4198384 =0x400FF0 (t2虚函数表地址)

t3:4198352  =0x400FD0 (t3虚函数表地址)

(*t1-*t2)0x401018-0x400FF0=0x28

(*t2-*t3)0x400FF0-0x400FD0=0x20

上面经过相减我们可以看出,他们虚表之间还是有间隙的。如果是连续的话,它们差值应该是一样的。

2.他们的大小都是16字节(8字节对齐),第一个数据域是虚函数表指针,第二个是数据域a。我示意的画出他们的对象内存和虚表分布图。由下图可以发现,对象的大小是一样的。我们在上一篇博文了解到,这里为了避免菱形继承导致数据冗余,记录了base的数据域偏移量。由此我们知道了,在类实例开始处存放的其实不是该类真实的虚表地址,而是指向虚函数的首地址(但是这里发现虚表中还存放其它东东)

 

二、根据虚表找到真实虚函数地址

       上面我们看到了运行结果。我们很有必要亲手调试一下。下面我们列出上述例子生成的3张虚表信息,其实他们在内存上是不连续的。同样使用下面这条命令生成test.cpp的虚表(这在上一篇都写过),命令执行完之后会生成很多虚表,这里我们只拿出我们test.cpp中类的虚表。

g++  -fdump-class-hierarchy test.cpp 

Class base

   size=1 align=1
   base size=0 base align=1
base (0x7f5079c33690) 0 empty

Vtable for test1
test1::_ZTV5test1: 5u entries
0     0u   “base数据的偏移,这里base是空类,所以为0
8     (int (*)(...))0
16    (int (*)(...))(& _ZTI5test1)
24    test1::hehe
32    test1::hehe1
 
VTT for test1
test1::_ZTT5test1: 1u entries
0     ((& test1::_ZTV5test1) + 24u)

Class test1
   size=16 align=8
   base size=12 base align=8
test1 (0x7f5079c33700) 0
    vptridx=0u vptr=((& test1::_ZTV5test1) + 24u)
  base (0x7f5079c33770) 0 empty virtual
      vbaseoffset=-0x00000000000000018

Vtable for test2
test2::_ZTV5test2: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5test2)
16    test2::hehe
24    test2::hehe1

Class test2
   size=16 align=8
   base size=12 base align=8
test2 (0x7f5079c33b60) 0
    vptr=((& test2::_ZTV5test2) + 16u)
  base (0x7f5079c33bd0) 0 empty
 
Vtable for test3
test3::_ZTV5test3: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5test3)
16    test3::hehe
24    test3::hehe1
 
Class test3
   size=16 align=8
   base size=12 base align=8
test3 (0x7f5079c33e70) 0
    vptr=((& test3::_ZTV5test3) + 16u)

       上面的虚表信息中清楚的记录了虚函数的偏移,以及虚表中存放了什么数据。唯一不同的是test1的虚表比其他test2和test3的虚表多了8个字节,前面我们也了解到。这是因为在虚表中记录了base数据的偏移信息。由于这里采用虚继承而且base基类是个空类,所以这里偏移是0.

每个虚表中都会含有如下的信息:

 

0     (int (*)(...))0
8     (int (*)(...))(& _ZTI5test3)

       针对这种情况,我也不知道具体原因,也许是编译器标配吧。^_^ ^_^ ^_^ ^_^!。我们也可以看到,vptr都是虚表指针加上偏移的,所以这里可以看到,虚表中存放不全是虚函数指针,还有虚继承的情况下,基类数据的偏移量。

 

 

vptr=((& test1::_ZTV5test1) + 24u)
vptr=((& test2::_ZTV5test2) + 16u)
vptr=((& test3::_ZTV5test3) + 16u)

 

        通过反汇编查看,虚函数指针到底是怎么存放的。虚表是放在.rodata段中的,这里我们列出test1,test2类的虚表,其它类的虚表和这个是一样的模式。在上面我们已经知道,其实虚表与虚表在内存中可以是连续的,也可以是不连续的,我们在刚开始的时候已经领略过。(觉得说了废话,虚表放在rodata段中,它们之间连不连续我们其实不用关心)。

0000000000401000 <_ZTV5test1>:
...
  401010:80 10 40             adcb   $0x40,(%rax)
  401013:00 00                add    %al,(%rax)
  401015:00 00                add    %al,(%rax)
  401017:00 f8                add    %bh,%al
  401019:0c 40                or     $0x40,%al  //看到了梦寐以求的0x400cf8
  40101b:00 00                add    %al,(%rax)
  40101d:00 00                add    %al,(%rax)
  40101f:00 22                add    %ah,(%rdx)
  401021:0d 40 00 00 00       or     $0x40,%eax //看到了梦寐以求的0x400d22

     在上面我们知道test1的虚函数表地址是0x401018,我们去这个地址看看情况。我们只能看到0x401017和0x401019,没有0x401018 这里由于反汇编对齐问题,显然我们这里8字节对齐,而且我的机器是小端对齐的。我们可以取到0x401017和0x401019的数据,即0x400cf8,同时我们可以看到后面的0x401021和0x40101f存放的是0x400d22.我们来到汇编代码中看看,如下所示,的确是和我们预期的是一样的。

0000000000400cf8 <_ZN5test14heheEv>: 

0000000000400d22 <_ZN5test15hehe1Ev>:

     照着上面的分析我们来看一下test2的信息,由之前的分析,我们了解到test2的虚函数表被安放在0x400ff0。同样我们在下面的信息看到了第一个虚函数的地址0x400d4c,第二个是0x400d76.

 

0000000000400fe0 <_ZTV5test2>:
...
  400fe8:60                   (bad)  
  400fe9:10 40 00             adc    %al,0x0(%rax)
  400fec:00 00                add    %al,(%rax)
  400fee:00 00                add    %al,(%rax)
  400ff0:4c 0d 40 00 00 00    rex.WR or $0x40,%rax  //梦寐以求的0x400d4c
  400ff6:00 00                add    %al,(%rax)
  400ff8:76 0d                jbe    401007 <_ZTV5test1+0x7>
  400ffa:40 00 00             add    %al,(%rax) //梦寐以求的0x400d76
  400ffd:00 00                add    %al,(%rax)

 

同样我们在汇编代码中看看它们对应的地址:结果和预期是一样的。

0000000000400d4c <_ZN5test24heheEv>:

0000000000400d76 <_ZN5test25hehe1Ev>:

三、GDB 调试过程

 

下面调试过程是我在本地试验过的,gdb很强大,建议大家自己跑跑例子。

armwind@armwind:/home/D-disk/test/cpp_obj$ gdb a.out 

GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04

Copyright (C) 2012 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "x86_64-linux-gnu".

For bug reporting instructions, please see:

<http://bugs.launchpad.net/gdb-linaro/>...

Reading symbols from /home/D-disk/test/cpp_obj/a.out...done.

(gdb) l

34virtual int hehe(){

35cout<<"hello"<<endl;

36}

37virtual int hehe1(){

38cout<<"hello1"<<endl;

39}

40};

41

42int main(){

43test1 *t1 = new test1();

(gdb) b 43

Breakpoint 1 at 0x400a8d: file test.cpp, line 43.

(gdb) n

The program is not being run.

(gdb) r

Starting program: /home/D-disk/test/cpp_obj/a.out 

 

Breakpoint 1, main () at test.cpp:43

43test1 *t1 = new test1();

(gdb) n

44test2 *t2 = new test2();

(gdb) n

45test3 *t3 = new test3();

(gdb) n

46cout<<"t1:"<<*((int*)t1)<<endl;

(gdb) p *t1

$1 = {<base> = {<No data fields>}, _vptr.test1 = 0x401018, a = 0}   "test1虚表中虚函数起始地址

(gdb) p t1

$2 = (test1 *) 0x603010  t1实例内存中的线性地址

(gdb) p *t2

$3 = {<base> = {<No data fields>}, _vptr.test2 = 0x400ff0, a = 0}   "test2虚表中虚函数起始地址

(gdb) p t2

$4 = (test2 *) 0x603030  t2实例在内存中的现行地址

(gdb) p *t3

$5 = {_vptr.test3 = 0x400fd0, a = 0}  "test3虚表中虚函数起始地址

(gdb) p t3

$6 = (test3 *) 0x603050

(gdb) p  t1->hehe

$7 = {int (test1 * const)} 0x400cf8 <test1::hehe()>  "test1::hehe()虚函数地址

(gdb) p  t1->hehe1

$8 = {int (test1 * const)} 0x400d22 <test1::hehe1()>  "test1::hehe1()虚函数地址

(gdb) p  t2->hehe

$9 = {int (test2 * const)} 0x400d4c <test2::hehe()>  "test2::hehe()虚函数地址

(gdb) p  t2->hehe1

$10 = {int (test2 * const)} 0x400d76 <test2::hehe1()>  "test2::hehe1()虚函数地址

(gdb) p *((int*)*(int*)(t1)+0)  "由此可以发现,类对象首地址存放的不是虚表指针而是虚函数起始地址。

$11 = 4197624 (这个转换成十六进制就是0x400cf8)

(gdb) p *((int*)*(int*)(t1)+2)

$12 = 4197666 (这个转换成十六进制就是0x400d22)

(gdb) p *((int*)*(int*)(t2)+0)

$13 = 4197708 (这个转换成十六进制就是0x400d4c)

(gdb) p *((int*)*(int*)(t2)+2)

$14 = 4197750 (这个转换成十六进制就是0x400d76)

(gdb) n

t1:4198424

47cout<<"t2:"<<*((int*)t2)<<endl;

(gdb) n

t2:4198384  即:0x400ff0

48cout<<"t3:"<<*((int*)t3)<<endl;

(gdb) n

t3:4198352 即:0x400fd0

50cout<<"t1->a:"<<&t1->a<<endl;

(gdb) n

t1->a:0x603018

51cout<<"t2->a:"<<&t2->a<<endl;

(gdb) n

t2->a:0x603038

52cout<<"t3->a:"<<&t3->a<<endl;

(gdb) n

t3->a:0x603058

54cout<<"sizeof(test1):"<<sizeof(test1)<<endl;

(gdb) n

sizeof(test1):16

55cout<<"sizeof(test2):"<<sizeof(test2)<<endl;

(gdb) n

sizeof(test2):16

56cout<<"sizeof(test3):"<<sizeof(test3)<<endl;

(gdb) n

sizeof(test3):16

57}

由上面的打印结果,我画出了new出来三个类实例的内存分布,这和之前运行时的线性地址不一样,这里我们就不关心了。但是我们发现虚函数表的地址依然式样的。其实相同类实例的虚函数表都是一样的,即实例开始存放都是一样的内容

备注:上面图中的虚表,指的不是该类真实的虚表地址,而是该类有效虚函数的起始地址。

四、总结:

 

1.探究类在虚继承(空类)前后的对象大小有无变化。

答:由上面的我们已经清楚答案了吧。同一类虚继承某空类前后,类实例大小是不变的,只是虚表信息中多记录了基类数据域的偏移。

2.探究虚函数的存放顺序,以及带有虚继承时虚表信息

答:虚函数存放是按着我们在类中声明的顺序来安放在虚表中的。带有虚继承时,虚表开始处会记录基类数据偏移位置,这个在编译阶段就已经确定了。

3.探究虚表是什么东东,它具体放在什么地方,如何通过虚表查找到虚函数的

答:虚表是一张存放虚函数地址和基类数据偏移地址的表。通常它被放到rodata数据段。每个对象的首地址存放的是虚函数表地址,不是虚表地址。

 

展开阅读全文

没有更多推荐了,返回首页