C++ 对象的内存布局


C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 前言 07 年12 月,我写了一篇《 C++虚函数表解析 》的文章,引起了大家的兴趣。有很 多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的。我在 这里一并对大家的留言表示感谢。这也是我为什么再写一篇续言的原因。因为, 在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的 问题 ,也是为了图一些表达上方便和简单 。不想 ,这篇文章成为了打开 C++对象模 型内存布局的一个引子 ,引发了大家对 C++对象的更深层次的讨论 。当然 ,我之前 的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数表里,至少有 以下这些内容没有涉及: 1)有成员变量的情况。 2)有重复继承的情况。 3)有虚拟继承的情况。 4)有钻石型虚拟继承的情况。 这些都是我本篇文章需要向大家说明的东西 。所以 ,这篇文章将会是 《C++虚函数 表解析 》的一个续篇,也是一篇高级进阶的文章。我希望大家在读这篇文章之前 对C++有一定的基础和了解 ,并能先读我的上一篇文章 。因为这篇文章的深度可能 会比较深,而且会比较杂乱,我希望你在读本篇文章时不会有大脑思维紊乱导致 大脑死机的情况。 ;-) 对象 的影响因 素 简而言之,我们一个类可能会有如下的影响因素: 1)成员变量 2)虚函数(产生虚函数表) 3)单一继承(只继承于一个类) 4)多重继承(继承多个类) 5)重复继承(继承的多个父类中其父类有相同的超类) 6)虚拟继承 (使用 virtual 方式继承 ,为了保证继承后父类的内存布局只会存 在一份) 上述的东西通常是 C++这门语言在语义方面对对象内部的影响因素 ,当然 ,还会有 编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们 只讨论 C++语言上的影响。 本篇文章着重讨论下述几个情况下的 C++对象的内存布局情况。 1)单一的一般继承 (带成员变量、虚函数、虚函数覆盖) 2)单一的虚拟继承 (带成员变量、虚函数、虚函数覆盖) 3)多重继承 (带成员变量、虚函数、虚函数覆盖) 4)重复多重继承 (带成员变量、虚函数、虚函数覆盖) 5)钻石型的虚拟多重继承 (带成员变量、虚函数、虚函数覆盖) 我们的目标就是,让事情越来越复杂。 知识 复习 我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如: typedef void(*Fun)(void); Base b; Fun pFun = NULL; cout << "虚函数表地址:" << (int*)(&b) << endl; cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl; // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b)); pFun(); 我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存 中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内 存对象的布局。 本篇文章中的例程或内存布局主要使用如下编译器和系统: 1)Windows XP 和VC++ 2003 2)Cygwin 和G++ 3.4.4 单一 的一般继 承 下面,我们假设有如下所示的一个继承关系: 请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而 了类覆盖了父类的 f()方法,孙子类覆盖了子类的 g_child()及其超类的 f()。 我们的源程序如下所示: 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** pVtab 来作 为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有 的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感 到奇怪,) 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++){ pFun = (Fun)pVtab[0][i]; cout << "["<" << endl; pFun = (Fun)pVtab[0][0]; cout << "[0] "; pFun(); pFun = (Fun)pVtab[0][1]; cout << "[1] ";pFun(); pFun = (Fun)pVtab[0][2]; cout << "[2] ";pFun(); pFun = (Fun)pVtab[0][3]; cout << "[3] "; pFun(); pFun = (Fun)pVtab[0][4]; cout << "[4] "; cout<"<"<" << endl; pFun = (Fun)pVtab[0][0]; cout << "[0] "; pFun(); pFun = (Fun)pVtab[0][1]; cout << "[1] "; pFun(); pFun = (Fun)pVtab[0][2]; cout << "[2] "; pFun(); pFun = (Fun)pVtab[0][3]; cout << "[3] "; pFun(); pFun = (Fun)pVtab[0][4]; cout << "[4] "; pFun(); pFun = (Fun)pVtab[0][5]; cout << "[5] 0x" << pFun << endl; cout << "[1] B::ib = " << (int)pVtab[1] << endl; cout << "[2] B::cb = " << (char)pVtab[2] << endl; cout << "[3] B1::ib1 = " << (int)pVtab[3] << endl; cout << "[4] B1::cb1 = " << (char)pVtab[4] << endl; cout << "[5] D::B2::_vptr->" << endl; pFun = (Fun)pVtab[5][0]; cout << "[0] "; pFun(); pFun = (Fun)pVtab[5][1]; cout << "[1] "; pFun(); pFun = (Fun)pVtab[5][2]; cout << "[2] "; pFun(); pFun = (Fun)pVtab[5][3]; cout << "[3] "; pFun(); pFun = (Fun)pVtab[5][4]; cout << "[4] 0x" << pFun << endl; cout << "[6] B::ib = " << (int)pVtab[6] << endl; cout << "[7] B::cb = " << (char)pVtab[7] << endl; cout << "[8] B2::ib2 = " << (int)pVtab[8] << endl; cout << "[9] B2::cb2 = " << (char)pVtab[9] << endl; cout << "[10] D::id = " << (int)pVtab[10] << endl; cout << "[11] D::cd = " << (char)pVtab[11] << endl; 程序运行结果如下: GCCGCCGCCGCC 3.4.43.4.43.4.43.4.4 VC++VC++VC++VC++ 2003200320032003 [0][0][0][0]D::B1::_vptr->D::B1::_vptr->D::B1::_vptr->D::B1::_vptr-> [0][0][0][0]D::f()D::f()D::f()D::f() [1][1][1][1]B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]D::f1()D::f1()D::f1()D::f1() [3][3][3][3]B1::Bf1()B1::Bf1()B1::Bf1()B1::Bf1() [4][4][4][4]D::f2()D::f2()D::f2()D::f2() [5][5][5][5]0x10x10x10x1 [1][1][1][1]B::ibB::ibB::ibB::ib====0000 [2][2][2][2]B::cbB::cbB::cbB::cb====BBBB [3][3][3][3]B1::ib1B1::ib1B1::ib1B1::ib1====11111111 [4][4][4][4]B1::cb1B1::cb1B1::cb1B1::cb1====1111 [5][5][5][5]D::B2::_vptr->D::B2::_vptr->D::B2::_vptr->D::B2::_vptr-> [0][0][0][0]D::f()D::f()D::f()D::f() [0][0][0][0]D::B1::_vptr->D::B1::_vptr->D::B1::_vptr->D::B1::_vptr-> [0][0][0][0]D::f()D::f()D::f()D::f() [1][1][1][1]B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]D::f1()D::f1()D::f1()D::f1() [3][3][3][3]B1::Bf1()B1::Bf1()B1::Bf1()B1::Bf1() [4][4][4][4]D::Df()D::Df()D::Df()D::Df() [5][5][5][5]0x000000000x000000000x000000000x00000000 [1][1][1][1]B::ibB::ibB::ibB::ib====0000 [2][2][2][2]B::cbB::cbB::cbB::cb====BBBB [3][3][3][3]B1::ib1B1::ib1B1::ib1B1::ib1====11111111 [4][4][4][4]B1::cb1B1::cb1B1::cb1B1::cb1====1111 [5][5][5][5]D::B2::_vptr->D::B2::_vptr->D::B2::_vptr->D::B2::_vptr-> [0][0][0][0]D::f()D::f()D::f()D::f() [1][1][1][1]B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]D::f2()D::f2()D::f2()D::f2() [3][3][3][3]B2::Bf2()B2::Bf2()B2::Bf2()B2::Bf2() [4][4][4][4]0x00x00x00x0 [6][6][6][6]B::ibB::ibB::ibB::ib====0000 [7][7][7][7]B::cbB::cbB::cbB::cb====BBBB [8][8][8][8]B2::ib2B2::ib2B2::ib2B2::ib2====12121212 [9][9][9][9]B2::cb2B2::cb2B2::cb2B2::cb2====2222 [10][10][10][10]D::idD::idD::idD::id====100100100100 [11][11][11][11]D::cdD::cdD::cdD::cd====DDDD [1][1][1][1]B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]D::f2()D::f2()D::f2()D::f2() [3][3][3][3]B2::Bf2()B2::Bf2()B2::Bf2()B2::Bf2() [4][4][4][4]0x000000000x000000000x000000000x00000000 [6][6][6][6]B::ibB::ibB::ibB::ib====0000 [7][7][7][7]B::cbB::cbB::cbB::cb====BBBB [8][8][8][8]B2::ib2B2::ib2B2::ib2B2::ib2====12121212 [9][9][9][9]B2::cb2B2::cb2B2::cb2B2::cb2====2222 [10][10][10][10]D::idD::idD::idD::id====100100100100 [11][11][11][11]D::cdD::cdD::cdD::cd====DDDD 下面是对于子类实例中的虚函数表的图: 我们可以看见 ,最顶端的父类 B其成员变量存在于 B1和B2中,并被 D给继承下 去了。而在 D中,其有 B1和B2的实例,于是 B的成员在 D的实例中存在两份 , 一份是 B1继承而来的 ,另一份是 B2继承而来的 。所以 ,如果我们使用以下语句 , 则会产生二义性编译错误: D d; d.ib = 0; //二义性错误 d.B1::ib = 1; //正确 d.B2::ib = 2; //正确 注意,上面例程中的最后两条语句存取的是两个变量。虽然我们消除了二义性的 编译错误,但 B类在 D中还是有两个实例,这种继承造成了数据的重复,我们叫 这种继承为重复继承 。重复的基类数据成员可能并不是我们想要的 。所以 ,C++引 入了虚基类的概念。 钻石 型多重虚 拟继承 虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的。钻石型的结构 是其最经典的结构。也是我们在这里要讨论的结构: 上述的 “重复继承 ”只需要把 B1和B2继承 B的语法中加上 virtual 关键 ,就成了 虚拟继承,其继承图如下所示: 上图和前面的 “重复继承 ”中的类的内部数据和接口都是完全一样的,只是我们 采用了虚拟继承:其省略后的源码如下所示: class B{……}; class B1 : virtualvirtualvirtualvirtual public B{……}; class B2: virtualvirtualvirtualvirtual public B{……}; class D: public B1, public B2{ ……}; 在查看 D之前,我们先看一看 单一虚拟继承 的情况。下面是一段在 VC++2003 下 的测试程序:(因为 VC++和GCC 的内存而局上有一些细节上的不同,所以这里 只给出 VC++的程序 ,GCC 下的程序大家可以根据我给出的程序自己仿照着写一个 去试一试): int** pVtab = NULL; Fun pFun = NULL; B1 bb1; pVtab = (int**)&bb1; cout << "[0] B1::_vptr->" << endl; pFun = (Fun)pVtab[0][0]; cout << "[0] "; pFun(); //B1::f1(); cout << "[1] "; pFun = (Fun)pVtab[0][1]; pFun(); //B1::bf1(); cout << "[2] "; cout << pVtab[0][2] << endl; cout << "[1] = 0x"; cout << (int*)*((int*)(&bb1)+1) <" << endl; pFun = (Fun)pVtab[5][0]; cout << "[0] "; pFun(); //B1::f(); pFun = (Fun)pVtab[5][1]; cout << "[1] "; pFun(); //B::Bf(); cout << "[2] "; cout << "0x" << (Fun)pVtab[5][2] << endl; cout << "[6] B::ib = "; cout << (int)*((int*)(&bb1)+6) <" << endl; pFun = (Fun)pVtab[0][0]; cout << "[0] "; pFun(); //D::f1(); pFun = (Fun)pVtab[0][1]; cout << "[1] "; pFun(); //B1::Bf1(); pFun = (Fun)pVtab[0][2]; cout << "[2] "; pFun(); //D::Df(); pFun = (Fun)pVtab[0][3]; cout << "[3] "; cout << pFun << endl; //cout << pVtab[4][2] << endl; cout << "[1] = 0x"; cout << (int*)((&dd)+1) <" << endl; pFun = (Fun)pVtab[4][0]; cout << "[0] "; pFun(); //D::f2(); pFun = (Fun)pVtab[4][1]; cout << "[1] "; pFun(); //B2::Bf2(); pFun = (Fun)pVtab[4][2]; cout << "[2] "; cout << pFun << endl; cout << "[5] = 0x"; cout << *((int*)(&dd)+5) << endl; //??? cout << "[6] B2::ib2 = "; cout << (int)*((int*)(&dd)+6) <" << endl; pFun = (Fun)pVtab[11][0]; cout << "[0] "; pFun(); //D::f(); pFun = (Fun)pVtab[11][1]; cout << "[1] "; pFun(); //B::Bf(); pFun = (Fun)pVtab[11][2]; cout << "[2] "; cout << pFun << endl; cout << "[12] B::ib = "; cout << *((int*)(&dd)+12) << endl; //B::ib cout << "[13] B::cb = "; cout << (char)*((int*)(&dd)+13) <->->-> [0][0][0][0]::::D::f()D::f()D::f()D::f() [1][1][1][1]::::D::f1()D::f1()D::f1()D::f1() [2][2][2][2]::::B1::Bf1()B1::Bf1()B1::Bf1()B1::Bf1() [3][3][3][3]::::D::f2()D::f2()D::f2()D::f2() [4][4][4][4]::::D::Df()D::Df()D::Df()D::Df() [5][5][5][5]::::1111 [1][1][1][1]B1::ib1B1::ib1B1::ib1B1::ib1::::11111111 [2][2][2][2]B1::cb1B1::cb1B1::cb1B1::cb1::::1111 [3][3][3][3]B2::_vptrB2::_vptrB2::_vptrB2::_vptr->->->-> [0][0][0][0]D::B1::_vptr->D::B1::_vptr->D::B1::_vptr->D::B1::_vptr-> [0][0][0][0]D::f1()D::f1()D::f1()D::f1() [1][1][1][1]B1::Bf1()B1::Bf1()B1::Bf1()B1::Bf1() [2][2][2][2]D::Df()D::Df()D::Df()D::Df() [3][3][3][3]00000000000000000000000000000000 [1][1][1][1]====0x0013FDC40x0013FDC40x0013FDC40x0013FDC4���� 该地址 取值后是 -4-4-4-4 [2][2][2][2]B1::ib1B1::ib1B1::ib1B1::ib1====11111111 [3][3][3][3]B1::cb1B1::cb1B1::cb1B1::cb1====1111 [4][4][4][4]D::B2::_vptr->D::B2::_vptr->D::B2::_vptr->D::B2::_vptr-> [0][0][0][0]D::f2()D::f2()D::f2()D::f2() [0][0][0][0]::::D::f()D::f()D::f()D::f() [1][1][1][1]::::D::f2()D::f2()D::f2()D::f2() [2][2][2][2]::::B2::Bf2()B2::Bf2()B2::Bf2()B2::Bf2() [3][3][3][3]::::0000 [4][4][4][4]B2::ib2B2::ib2B2::ib2B2::ib2::::12121212 [5][5][5][5]B2::cb2B2::cb2B2::cb2B2::cb2::::2222 [6][6][6][6]D::idD::idD::idD::id::::100100100100 [7][7][7][7]D::cdD::cdD::cdD::cd::::DDDD [8][8][8][8]B::_vptrB::_vptrB::_vptrB::_vptr->->->-> [0][0][0][0]::::D::f()D::f()D::f()D::f() [1][1][1][1]::::B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]::::0000 [9][9][9][9]B::ibB::ibB::ibB::ib::::0000 [10][10][10][10]B::cbB::cbB::cbB::cb::::BBBB [11][11][11][11]NULLNULLNULLNULL::::0000 [1][1][1][1]B2::Bf2()B2::Bf2()B2::Bf2()B2::Bf2() [2][2][2][2]00000000000000000000000000000000 [5][5][5][5]====0x45392600x45392600x45392600x4539260���� 该地址 取值后是 -4-4-4-4 [6][6][6][6]B2::ib2B2::ib2B2::ib2B2::ib2====12121212 [7][7][7][7]B2::cb2B2::cb2B2::cb2B2::cb2====2222 [8][8][8][8]D::idD::idD::idD::id====100100100100 [9][9][9][9]D::cdD::cdD::cdD::cd====DDDD [10][10][10][10]====0x000000000x000000000x000000000x00000000 [11][11][11][11]D::B::_vptr->D::B::_vptr->D::B::_vptr->D::B::_vptr-> [0][0][0][0]D::f()D::f()D::f()D::f() [1][1][1][1]B::Bf()B::Bf()B::Bf()B::Bf() [2][2][2][2]00000000000000000000000000000000 [12][12][12][12]B::ibB::ibB::ibB::ib====0000 [13][13][13][13]B::cbB::cbB::cbB::cb====BBBB 关于虚拟继承的运行结果我就不画图了(前面的作图已经让我产生了很严重的厌 倦感,所以就偷个懒了,大家见谅了) 在上面的输出结果中 ,我用不同的颜色做了一些标明 。我们可以看到如下的几点 : 1)无论是 GCC 还是 VC++,除了一些细节上的不同 ,其大体上的对象布局是 一样的 。也就是说 ,先是 B1(黄色 ),然后是 B2(绿色 ),接着是 D(灰 色),而 B这个超类(青蓝色)的实例都放在最后的位置。 2)关于虚函数表,尤其是第一个虚表, GCC 和VC++有很重大的不一样。但 仔细看下来,还是 VC++的虚表比较清晰和有逻辑性。 3)VC++和GCC 都把 B这个超类放到了最后 ,而VC++有一个 NULL 分隔符 把 B和B1和B2的布局分开。 GCC 则没有。 4)VC++中的内存布局有两个地址我有些不是很明白 ,在其中我用红色标出了 。 取其内容是 -4。接道理来说 ,这个指针应该是指向 B类实例的内存地址 (这 个做法就是为了保证重复的父类只有一个实例的技术 )。但取值后却不是 。 这点我目前还并不太清楚,还向大家请教。 5)GCC 的内存布局中在 B1和B2中则没有指向 B的指针。这点可以理解, 编译器可以通过计算 B1和B2的size 而得出 B的偏移量。 结束 语 C++这门语言是一门比较复杂的语言 ,对于程序员来说 ,我们似乎永远摸不清楚这 门语言背着我们在干了什么 。需要熟悉这门语言 ,我们就必需要了解 C++里面的那 些东西 ,需要我们去了解他后面的内存对象 。这样我们才能真正的了解 C++,从而 能够更好的使用 C++这门最难的编程语言。 在文章束之前还是介绍一下自己吧。我从事软件研发有十个年头了,目前是软件 开发技术主管,技术方面,主攻 Unix/C/C++,比较喜欢网络上的技术,比如分布 式计算 ,网格计算 ,P2P,Ajax 等一切和互联网相关的东西 。管理方面比较擅长于 团队建设 ,技术趋势分析 ,项目管理 。欢迎大家和我交流 ,我的 MSN 和Email 是: haoel@hotmail.com
还剩31页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 2 人已下载

下载pdf

pdf贡献者

zengqi2418

贡献于2011-10-16

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf