浅谈C++对象内存布局

  好吧,我承认之前的标题“浅度探索C++对象模型”是用来搞笑的,因为内容还谈不上有多少深度可言,仅仅只是介绍下内存布局而已。

  如果真要谈对象模型的话,我认为对象的内存布局仅仅只是其中一部分,还有诸如对象的构造、拷贝、析构,以及数据成员的存取和成员函数的调用等内容需要和内存布局结合起来考虑,也许我会在后续的博文中慢慢整理出来。

  事先需要声明的一点是,C++对象模型并非属于C++语言标准规定的东西,因此不同的编译器会在标准规定的范畴内给出有差异的实现,以下程序运行环境均是32位WIN7系统 + Visual Studio 2010,如果换GCC运行得到不同的结果是完全正常的。个人对《深度探索C++对象模型》一书的态度是着其而不可着其,因为有些底层的内容会随着计算机的发展而更新,书里面的有些内容就会过时,就比如我后面想写的NRV优化。投入个人宝贵的时间去和一些细节纠缠是不值得的,领会精神、观其大略即可。

  下面进入正题,这次博客计划讨论如下三种情形下的C++对象内存布局:

1.带覆盖的单继承;

2.带重复继承带覆盖的多重继承;

3.带重复继承带覆盖的多重虚拟继承

之所以只给出这三种情况是因为个人认为其余的情形是平凡的,被以上三种所蕴含;其次这几种情况都是包含成员变量的,这也是和上篇博客的区别所在;最后就是这篇决定调整一下叙述方式:先给出预期结果,然后通过运行结果来验证我们的想法。

一、带覆盖的单继承:

  先上代码:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 //单继承带覆盖有数据成员和成员函数的情况下内存布局
 5 class Base{
 6 
 7 public:
 8     Base():_base(10){}
 9     virtual void f(){ cout<<"Base::f()"<<endl; }
10 private:
11     int _base;
12 };
13 
14 class Derive: public Base{
15 
16 public:
17     Derive():_derive(100){}
18     virtual void f(){ cout<<"Derive::f()"<<endl;}
19     virtual void g(){ cout<<"Derive::g()"<<endl;}
20 private:
21     int _derive;
22 };
23 
24 class DDerive: public Derive{
25 
26 public:
27     DDerive():_dderive(1000){}
28     virtual void f(){ cout<<"DDerive::f()"<<endl;}
29     virtual void g(){ cout<<"DDerive::g()"<<endl;}
30     virtual void h(){ cout<<"DDerive::h()"<<endl;}
31 private:
32     int _dderive;
33 };
34 
35 int main(){
36 
37     DDerive d;
38     typedef void (*Fun)();
39     Fun pFun = NULL;
40     //首先通过函数指针调用成员函数
41     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 0);
42     pFun();
43     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 1);
44     pFun();
45     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 2);
46     pFun();
47     //再通过函数指针访问派生类中的数据成员:
48     cout<<"Base::_base = "        <<*(int*)((int*)(&d) + 1)<<endl;
49     cout<<"Derive::_derive = "    <<*(int*)((int*)(&d) + 2)<<endl;
50     cout<<"DDerive::_dderive = "<<*(int*)((int*)(&d) + 3)<<endl;
51 
52     getchar();
53     return 0;
54 }
View Code

  预期内存布局图示如下:

  运行结果为:

   

可以看到,运行结果和预期完全一致。

二、带重复继承带覆盖的多重继承:

   照例先上代码:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 //多继承带覆盖
 5 class Base{
 6 
 7 public:
 8     Base():_nb(10),_mb(20){}
 9     virtual void f()   { cout<<"Base::f()" <<endl; }
10     virtual void bf()  { cout<<"Base::bf()"<<endl; }
11 private:
12     int _nb;
13     int _mb;
14 };
15 
16 class Base1:public Base{
17 
18 public:
19     Base1():_nb1(100),_mb1(200){}
20     virtual void f()   { cout<<"Base1::f()"  <<endl; }
21     virtual void f1()  { cout<<"Base1::f1()" <<endl; }
22     virtual void bf1() { cout<<"Base1::bf1()"<<endl; }
23 private:
24     int _nb1;
25     int _mb1;
26 };
27 
28 class Base2:public Base{
29 
30 public:
31     Base2():_nb2(150),_mb2(250){}
32     virtual void f()   { cout<<"Base2::f()"  <<endl; }
33     virtual void f2()  { cout<<"Base2::f2()" <<endl; }
34     virtual void bf2() { cout<<"Base2::bf2()"<<endl; }
35 private:
36     int _nb2;
37     int _mb2;
38 };
39 
40 class Derive:public Base1,public Base2{
41 
42 public:
43     Derive():_nd(1000),_md(2000){}
44     virtual void f()   { cout<<"Derive::f()" <<endl; }
45     virtual void f1()  { cout<<"Derive::f1()"<<endl; }
46     virtual void f2()  { cout<<"Derive::f2()"<<endl; }
47     virtual void df()  { cout<<"Derive::df()"<<endl; }
48 private:
49     int _nd;
50     int _md;
51 };
52 
53 int main(){
54 
55     Derive d;
56     typedef void(*Fun)();
57     Fun pFun = NULL;
58 
59     //首先通过函数指针调用成员函数
60     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 0);
61     pFun();
62     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 1);
63     pFun();
64     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 2);
65     pFun();
66     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 3);
67     pFun();
68     pFun = (Fun)*((int*)*((int*)(&d) + 0) + 4);
69     pFun();
70 
71     pFun = (Fun)*((int*)*((int*)(&d) + 5) + 0);
72     pFun();
73     pFun = (Fun)*((int*)*((int*)(&d) + 5) + 1);
74     pFun();
75     pFun = (Fun)*((int*)*((int*)(&d) + 5) + 2);
76     pFun();
77     pFun = (Fun)*((int*)*((int*)(&d) + 5) + 3);
78     pFun();
79 
80     //再利用对象内存布局访问派生类中的数据成员:
81     cout<<*(int*)((int*)(&d) + 1)<<endl;
82     cout<<*(int*)((int*)(&d) + 2)<<endl;
83     cout<<*(int*)((int*)(&d) + 3)<<endl;
84     cout<<*(int*)((int*)(&d) + 4)<<endl;
85     cout<<*(int*)((int*)(&d) + 6)<<endl;
86     cout<<*(int*)((int*)(&d) + 7)<<endl;
87     cout<<*(int*)((int*)(&d) + 8)<<endl;
88     cout<<*(int*)((int*)(&d) + 9)<<endl;
89     cout<<*(int*)((int*)(&d) + 10)<<endl;
90     cout<<*(int*)((int*)(&d) + 11)<<endl;
91     
92     getchar();
93     return 0;
94 }
View Code

  之前用笔在纸上画了画,觉得只要把握住如下几点就可以很轻松的推导出来

1.每个对象只需关注其直接基类,无需关注其祖先基类;

2.派生类所独有的虚函数会链在按继承声明顺序排第一的那个基类的虚函数表的最后;

3.派生类覆盖基类的虚函数地址会在虚函数表中替换原先基类的虚函数地址;

4.继承自几个基类在派生类对象中就会有几个虚表指针以及相应的虚函数表;

5.在派生类中,基类子对象保持完整原样性,这就解释了为什么数据成员不是全部连在一起而是各自为阵,在内存布局中属于它所属的基类子对象部分。

  预期内存布局如下图所示:

  (注:下图之所以把Base1和Base2的图示也附上是为了方便演示推导过程,以上那几点都很符合直观,所以完全不用记忆,只要理解了就能手到擒来。PS:本人最不擅长的就是在不理解一件东西的情况下记住它了,这对本人简直是不可能完成的任务:-( )

  可以看到运行结果和预期完全一致:

   

三、带重复继承带覆盖的多重虚拟继承:

  虚拟继承的存在当然是为了解决问题的,而不是单纯的蛋疼。语法是为语义服务的,不从语义层面把握的话语法就会味同嚼蜡。具体而言,它的存在是为了解决在多重继承的情形下,派生类对象中会持有多个间接基类对象的副本这个问题。

  正如我们在上一张图看到的那样,Base类的数据成员_nb和_mb在Derive对象中的Base1和Base2子对象部分各有一份拷贝,内容完全一样。 这将至少会带来如下几个问题:1.存储空间的浪费;2.访问派生类对象中的基类数据成员将会产生二义性错误;3.破坏共享行为

  而虚拟继承解决这个问题的方式是共享基类,共享的基类称为虚基类,“在这种机制下,无论虚基类在继承体系中出现了多少次,在派生类中都只包含唯一一个共享的虚基类子对象。” ——摘自《C++ Primer》中文版 第5版 P717

  从我们之前的讨论可以看出,在没有多重继承的情形下,这句话就是一句废话,任何一个派生类当然只可能包含唯一一个基类子对象;

  即使在多重继承的前提下,这句话依然有可能是句正确的废话,假设整个继承体系中都没有一个公共的基类,那当然无需担心哪个派生类对象会包含某个基类子对象的多个副本了;

  所以我们现在可以缩小讨论范围,只有在多重继承且相应的继承链存在公共基类的情形下,才有虚继承的用武之地。

言归正传,先看代码:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 //多重虚拟继承带覆盖
 5 class Base{
 6 
 7 public:
 8     Base():_nb(10),_mb(20){}
 9     virtual void f()    { cout<<"Base::f()" <<endl;  }
10     virtual void bf()  { cout<<"Base::bf()"<<endl; }
11 private:
12     int _nb;
13     int _mb;
14 };
15 
16 class Base1: virtual public Base{
17 
18 public:
19     Base1():_nb1(100),_mb1(200){}
20     virtual void f()     { cout<<"Base1::f()"  <<endl;   }
21     virtual void f1()   { cout<<"Base1::f1()" <<endl;  }
22     virtual void bf1() { cout<<"Base1::bf1()"<<endl; }
23 private:
24     int _nb1;
25     int _mb1;
26 };
27 
28 class Base2: virtual public Base{
29 
30 public:
31     Base2():_nb2(150),_mb2(250){}
32     virtual void f()     { cout<<"Base2::f()"  <<endl;   }
33     virtual void f2()   { cout<<"Base2::f2()" <<endl;  }
34     virtual void bf2() { cout<<"Base2::bf2()"<<endl; }
35 private:
36     int _nb2;
37     int _mb2;
38 };
39 
40 class Derive:public Base1,public Base2{
41 
42 public:
43     Derive():_nd(1000),_md(2000){}
44     virtual void f()    { cout<<"Derive::f()" <<endl;  }
45     virtual void f1()  { cout<<"Derive::f1()"<<endl; }
46     virtual void f2()  { cout<<"Derive::f2()"<<endl; }
47     virtual void df()  { cout<<"Derive::df()"<<endl; }
48 private:
49     int _nd;
50     int _md;
51 };
52 
53 int main(){
54     
55     Derive d;
56     return 0;
57 }
View Code

  可以看到代码和上一块内容基本一样,无非是将继承方式改为了virtual public而已。

这  一次我将不再采用利用指针直接访问内存的方式去验证对象的内存布局,而是借助编译器来查看派生类的内存布局,在Visual Studio命令提示中输入:

cl main.cpp /d1reportSingleClassLayoutDerive 即可查看Derive的内存布局,具体如下图所示:

  从上图我们可以看出,虚拟继承和普通多重继承在内存布局上还是有差别的,体现在:

1.派生类对象中的直接基类子对象除了虚表指针之外,还多了一项vbptr,其实是虚基表指针,其指向的内容包含两项,第一项表示虚表指针和虚基表指针的相对偏移量;第二项表示的是虚基类子对象相对直接基类子对象的偏移量;

2.虚基类子对象不在有两个副本存于直接基类子对象中,而是从中独立出来,位于派生类对象的末尾,并且自身也包含一个虚表指针,指向它自身的虚函数表;

3.所有被派生类所覆盖的基类虚函数地址依然会在虚函数表中得到更新,指向相应的派生类虚函数,同时派生类独有的虚函数地址依然会附在第一个直接基类的虚函数表末尾;这一点和以前是一样的。

4.另外在派生类内存布局中出现了一个神秘的vtordisp for vbase class,占有四个字节,我查了一下msdn中是这样描述的:“The vtordisp pragma is applicable only to code that uses virtual bases. If a derived class overrides a virtual function that it inherits from a virtual base class, and if a constructor or destructor for the derived class calls that function using a pointer to the virtual base class, the compiler might introduce additional hidden vtordisp fields into classes with virtual bases.”

  这里让我困惑的是派生类确实覆盖了虚基类的虚函数,满足第一个条件,但是这里派生类的构造函数或者析构函数哪里有通过指向虚基类的指针来调用该函数呢?意即个人认为第二个条件并不满足。所以这一点暂且存疑,如果园子里有达人知道答案的烦请评论指点一二,感激不尽。

  写到这里就结束总有点意犹未尽,隔靴搔痒的感觉,但是考虑到篇幅还是欲知详情请听下回分解吧:-)

posted @ 2015-03-29 16:06  _江湖夜雨十年灯  阅读(277)  评论(0编辑  收藏  举报