c++虛表以及data member的布局

注: 这篇文章写得有些杂乱,主题没有清晰的表述,我自己再隔了很久回头看的时候也感觉有些乱,我以后会改一改这样写文章的风格,做到每篇文章讲述一个主题,简洁清晰,多余的东西不写进来.现在在看之前记住主题呢就是这篇文章讲述了类的成员对象在内存中的布局,由于只要类包含虚函数(当然包括其继承下来的),其第一个成员一定是这个类的虚表的地址(继承层次下的每一个类都会有自己的虚表),所有我又写测试程序研究了下虚表的布局.而在写测试程序研究虚表布局的时候又用到就顺便提了些c++语言层面的知识,如函数指针等.

 

前段時間在看Inside c++ object model的时候,写测试程序看看有虚函数的类的对象在内存中是怎样布局的,发现在g++实现的时候,每个如此的对象都会有一个vtPtr,即虚表指针,而且是存在于对象的开始地址,然后才是其它数据成员,按照其在类中声明的顺序。后来我想查看虚表的地址,一直没找到方法,直到看到陈皓的这篇文章:http://blog.csdn.net/haoel/article/details/1948051。这篇文章讲得很精彩,但是有些问题没有涵盖,比如virtual destructor, 然后因为我自己的机子是x86_64架构的,指针是8个字节,有些地方不一样,而且可能随着时间的推移,编译器的一些实现可能也会有改变。

首先看一个测试程序,写得稍微有点长,问题都集中测试了

 1 #include<cstdio>
 2 #include<iostream>
 3 using namespace std;
 4 class Point{
 5 public :
 6     Point(){
 7         std::cout << "Point constructor" << std::endl;
10 } 11 virtual ~Point(){ 12 std::cout << "Point destructor" << std::endl; 13 //printf("the address of ~Point: %p\n", &Point::~Point); 14 } 15 virtual void func_hs(){ 16 std::cout << "Point::func_hs" << std::endl; 17 printf("the address of this virtual func - func_hs : %p\n", &Point::func_hs); 18 } 19 virtual void func_zzy(){ 20 std::cout << "Point::func_zzy" << std::endl; 21 printf("the address of this virtual func - func_zzy : %p\n", &Point::func_zzy); 22 } 23 //virtual 函数在虚表中的顺序完全和声明的顺序一样 24 static void print(){ 25 printf("&Point::x = %p\n&Point::y = %p\n&Point::z = %p\n", &Point::x, &Point::y, &Point::z); // float Point::* 26 } 27 void printThis(){ 28 printf("&this->x = %p\n&this->y = %p\n&this->z = %p\n", &this->x, &this->y, &this->z); // float * 29 } 30 void printVt(){ 31 printf("the address of object,this:%p\nthe address of vt :%p\n", this, (void*)*(long*)this); 32 } 33 void callVtFuncs(int num=2){ 34 typedef void (*Funp)(void); 35 for(int i=0; i<num; i++){ 36 Funp funp = (Funp)*((long*)*(long*)this + i); //前一个long如果像上面用成void就编不过 37 printf("Point::callVtFuncs => address of this fun : %p\n", funp); 38 if(i < 2){ 39 continue; 40 } 41 funp(); 42 } 43 } 44 void printVirtualFuncAddress(){ 45 printf("func_hs : %p\nfunc_zzy : %p\n", &Point::func_hs, &Point::func_zzy); 46 } 47 protected :
49 float x,y,z; 50 }; 51 class Point2 : public Point{ 52 public : 53 Point2(){ 54 std::cout << "Point2 constructor" << std::endl; 55 } 56 ~Point2(){ 57 std::cout << "Point2 destructor" << std::endl; 58 } 59 void func_hs(){ 60 std::cout << "Point2::func_hs" << std::endl; 61 } 62 void printVirtualFuncAddress(){ 63 printf("func_hs : %p\nfunc_zzy : %p\nfunc_ss : %p\n", &Point2::func_hs, &Point::func_zzy, &Point2::func_ss); 64 } 65 private : 66 virtual void func_ss(){ 67 cout << "Point2::func_ss" << endl; 68 } 70 }; 71 int main(){ 72 Point point; 73 Point::print(); 74 point.printThis(); 75 point.printVt(); 76 point.callVtFuncs(4); 77 point.printVirtualFuncAddress(); 78 79 Point2 point2; 80 point2.printVt(); 81 point2.callVtFuncs(5); 82 point2.printVirtualFuncAddress(); 83 }

上面的代码定义了两个类,Point和Point2, 主要分为两部份,一个是父类和子类中定义的虚函数 ,按顺序是~Point(), func_ys(), func_zzy, func_ss(),另一个是子类中定义的几个member function用于打出虚表及成员的地址,还有直接取虚函数的地址,以及还有一个函数用我们取到的地址去依次调这个类的所有虚函数。全部的输出结果比较长,放到最后

 

1.对象在内存中的布局

73,74,75行运行的输如结果如下

&Point::x = 0x8
&Point::y = 0xc
&Point::z = 0x10
&this->x = 0x7fffddb48898
&this->y = 0x7fffddb4889c
&this->z = 0x7fffddb488a0
the address of object,this:0x7fffddb48890
the address of vt :0x4013b0

&Point::x的类型是 float Point::* ,是pointer to member类型,而 .* 和 ->*是 member selection operator用来解引用pointer to member的。可以看到pointer to data member的值是一个相对位置,相对于对象的起始地址,而虚表指针就在对象的起始地址处,然后依次是三个成员x,y,z。指针的值就是虚表的地址,也就是上面的0x4013b0,这个地址值是上面31行的代码打出来的

31         printf("the address of object,this:%p\nthe address of vt :%p\n", this, (void*)*(long*)this);

(void*)*(long*)this 是把this这种类型的指针转化成 long* 型,然后把这个值解引用再取指针了,也就是可以这么干的, 这个值也可以这么打出来,用%x(hex)

1          printf("the address of object,this:%p\nthe address of vt :0x%x\n", this, *(long*)this);

 

2 . 取虚表中的函数指针并调用

76行代码调用的函数依次打印了了这个对象的虚表中实际指向的函数的地址并调用了这些函数。看看这两行代码

34  typedef void (*Funp)(void);
36  Funp funp = (Funp)*((long*)*(long*)this + i); 

首先定义了一个type,注意Funp是一个类型,函数指针类型。第二行的代码其实应该这样写比较清晰

Funp funp = (Funp)(*(((long*)*(long*)this) + i));

就是通过this得到指向虚表成员的指针,然后取出虚表成员指针的值,把它转化为Funp类型的函数指针。这里可能稍微有点乱,首先实际情况是这样的,this指向了这个对象,而这个对象的开头存放的是虚表的地址,虚表其实就是一个函数指针数组,指针在x86_64系统上占用8个字节,和long一样,我现在呢要得到这些函数指针的值。所以我先把this转化为指向一个long型(long*)this, 然后得到这个long的值,也就是虚表的地址 *(long*)this,  这个值的类型到目前是long,我把它再转化为(long*),也就是(long*)*(long*)this, 这样我就得到一个long*,并且指向了虚表的开头,然后我就对这个值进行 + i操作,让它指向虚表中第 i个元素,然后再取第i个元素的值, *(((long*)(*(long*)this))+i), 这样我就得到了,但到目前它是long型的,它实际上是个函数指针,我再把它转化为Funp类型的就好了。  可以看到我们可以把一个long,也就是整型去转换为指针(本来在机器层面,整型和指针都是在整数寄存器中存放,不区分),另外转换为函数指针时Funp就是一个指针类型了,像(long*)一样。

 

3虚表中的虚析构函数

我们已经知道,一个基类的destructor应该是virtual的。上面的代码中~Point()也是被声明为virtual, 13行被注掉的代码本来想去取~Point的地址,但是编译通不过,去查了一下,在c++ standard 12.4.2中提到 The address of a destructor shall not be taken ,不能去取destructor的地址,不管它是不是virtual的,去google上搜了一下,一篇stackoverflow的问答中看到有回答,说是编译器在调用destructor时会传入额处隐含的参数,如果获取的destructor的地址,编译器就会失去这样的控制。

在执行point和point2的虚表中指向的函数时,我略过了第一个和第二个(38~40行代码),这是因为这两个对象中虚表中的前两个函数指针都指向了~Point(), 而在运行时通过funp这个函数指针去调~Point()会报Segmentation fault。通过gdb很明显的看到了这个情况。使用的gdb命令是 x/5ag , a表示address, 即查看的内存地址放的是address。

可以看到虚表中的前两个成员其实指向不同的地方,但这两个地方都是Point::~Point(),对point2的验证也有相同的情况

还可以看到point2的虚表中第四个成员是指向基类的func_zz(),因为子类并没有重写这个虚函数。

 

4。 安全性

可以看到在上面的Point2::func_ss是被声明为private的virtual的函数,但是81行代码运行的时候毫无阻碍的调用了它,而且callVtFuncs是一个基类的函数。这用再一次说明了c++可以在语言层面做很多事,c的指针的强大和危险。而且我们也可以通过父类的指针去调用只属于子类的虚函数,在上面的main函数中加入以下两行代码就验证了

1 Point * p = new Point2();
2 p->callVtFuncs(5);

输出结果和用point2去调用完全一样。

 

5。多重继承下的虚表

首先我们有如下的类层次结构

三个基类中的f(),g()都是虚函数, destructor也是虚函数,子类Derived重写了f() , 但定义了新的虚函数g1()。

在inside object model书中,对多重继承下的内存布局及虚表有详细的讨论以及对象布局本身和虚表布局的关系,在这里我先不深入挖掘了,这里就展现一个多重继承下虚表的样子就好了, 这也应该是普遍的实现方式,Derived的虚表应该是这样的结构。

 

我们用gdb来验证一下虚表的实际结构和内容是怎样的

上面的截图看起来会有一点乱,因为其中有很多名字为什么是那样的我也不理解,从左边可以看到地址是从上到下是增长的,先前的几行,从0x4010d0到0x401180是Derived的虚表,可以看到从低地址到高地址依次是从Base1派生的虚表,从Base2派生的虚表,从Base3派生的虚表。从Base1派生的那个虚表也就是Derived对象会使用的虚表,可以注意到只有它有 Derived::g1()。

1 Base1 * p1 = new Derived();
2 Base2 * p2 = new Derived();
3 Base3 * p3 = new Derived();
4 Derived d;

对如上的例子,我自己去验证过,p1, p2, p3所指向的对象使用的虚表分别就是上面我说的从Base1, Base2, Base3派生而得到的那个虚表, 而d使用的也是从Base1派生的那个虚表。比如p2所指向的对象中虚表的地址就是上图的 0x401108。

我们还可以注意到,p2所指向的虚表开始的那三个函数名字都很奇怪,我自己用代码去控制调用,像前面的例子中做的那样,发现这三个函数其实分别就是 Derived::~Derived, Derived::~Derived, Derived::f,至少我去调用调到了Derived::f。另外这三个虚表之前都间隔了两个8字节,前个8字节是那个数0xfffffffffffffff8, 另一个则也是一个奇怪的函数名,我也不知道用来干嘛,但是可以肯定的是它不是虚表中的函数。这三个属于Derived的子虚表的顺序一定是按照Derived定义时基类的声明顺序来的。    从上面这图还可以看到从Derived的虚表再往高地址走接着就是Base3, Base2, Base1的虚表了。 而总体各个类的虚表的顺序从低地址到高地址则与类定义的顺序相反,通过代码验证了,如这里的定义顺序是 Base1, Base2, Base3, Derived。

关于多重继承,先暂时就到这个程度。

 

 

附录


5小结所使用的测试代码

 1 #include<iostream>
 2 #include<stdio.h>
 3 using namespace std;
 4 class Base2{
 5 public :
 6     virtual ~Base2(){}
 7     virtual void f(){
 8         cout << "Base2::f" << endl;
 9     }
10     void checkVt(){
11         typedef void (*Funp)();
12         void * vtAddr = (long*)*(long*)this;
13         printf("Derived::checkVt => funp = %p\n", vtAddr);
14         
15         for(int i=0; i<6; i++){
16             Funp funp = (Funp)(*(((long*)*(long*)this) + i)); //前一个long如果像上面用成void就编不过
17             printf("Point::callVtFuncs => address of this fun : %p\n", funp);
18             if(i < 2 || i == 4){
19                 continue;    
20             }
21             funp();
22         }
23     }
24     virtual void g(){
25         cout << "Base2::g" << endl;
26     }
27 };
28 class Base1{
29 public :
30     virtual ~Base1(){}    
31     virtual void f(){
32         cout << "Base1::f" << endl;
33     }
34     virtual void g(){
35         cout << "Base1::g" << endl;
36     }
37 };
38 class Base3{
39 public :
40     virtual ~Base3(){}
41     virtual void f(){
42         cout << "Base3::f" << endl;
43     }
44     virtual void g(){
45         cout << "Base3::g" << endl;
46     }
47 };
48 class Derived : public Base1, public Base2, public Base3{
49 public :
50     ~Derived(){}
51     virtual void f(){
52         cout << "Derived::f" << endl;
53     }
54     virtual void g1(){
55         cout << "Derived:g1" << endl;    
56     }
57     void checkVt(){
58         typedef void (*Funp)();
59         void * vtAddr = (long*)*(long*)this;
60         printf("Derived::checkVt => funp = %p\n", vtAddr);
61 
62         for(int i=0; i<4; i++){
63             Funp funp = (Funp)(*(((long*)*(long*)this) + i)); 
64             printf("Point::callVtFuncs => address of this fun : %p\n", funp);
65             if(i < 2){
66                 continue;    
67             }
68             funp();
69         }
70     }
71 };
72 int main(){
73     Derived d;
74     d.checkVt();
75     Base2 * p = new Derived();
76     p->checkVt();
77     p->f();
78 }

 

1小结代码的运行结果

Point constructor
&Point::x = 0x8
&Point::y = 0xc
&Point::z = 0x10
&this->x = 0x7fff6cb97518
&this->y = 0x7fff6cb9751c
&this->z = 0x7fff6cb97520
the address of object,this:0x7fff6cb97510
the address of vt :0x4013b0
Point::callVtFuncs => address of this fun : 0x400b6e
Point::callVtFuncs => address of this fun : 0x400bbc
Point::callVtFuncs => address of this fun : 0x400be2
Point::func_hs
the address of this virtual func - func_hs : 0x11
Point::callVtFuncs => address of this fun : 0x400c3a
Point::func_zzy
the address of this virtual func - func_zzy : 0x19
func_hs : 0x11
func_zzy : (nil)
Point constructor
Point2 constructor
&this->x = 0x7fff6cb974f8
&this->y = 0x7fff6cb974fc
&this->z = 0x7fff6cb97500
&this->a = 0x7fff6cb97508
the address of object,this:0x7fff6cb974f0
the address of vt :0x401370
Point::callVtFuncs => address of this fun : 0x400e44
Point::callVtFuncs => address of this fun : 0x400ec0
Point::callVtFuncs => address of this fun : 0x400ee6
Point2::func_hs
Point::callVtFuncs => address of this fun : 0x400c3a
Point::func_zzy
the address of this virtual func - func_zzy : 0x19
Point::callVtFuncs => address of this fun : 0x400fcc
Point2::func_ss
func_hs : 0x11
func_zzy : (nil)
func_ss : 0x19
Point2 destructor
Point destructor
Point destructor

 

posted on 2012-08-03 18:37  小宇2  阅读(2305)  评论(0编辑  收藏  举报

导航