上一篇博客《C++对象模型:g++实现(一)》 用我的理解总结了在无继承体系下g++实现的C++对象的内存布局,这篇就来总结一下在有继承情况下的C++对象的内存布局。
本文中所有代码均可从这里 拿到(百度网盘)。
每篇一首歌
1. 有继承情况下的C++对象的内存布局 C++是支持多继承的,而多继承可能就会出现继承的两个(或多个)类有公共的父类,为了防止在一个对象中出现多个公共父类的实体,C++就提出了虚继承。因此总的来说C++的继承有两种:无虚继承和有虚继承。
1.1 无虚继承 在无虚继承情况下,无论是public
继承还是protected
、private
继承,其内存布局都是一样的,因此这里只提供public
继承的例子。
1.1.1 所有的基类和派生类都没有虚函数 在这种情况下,其实就相当于构造了一个对象,其内部按基类的声明顺序添加了各个基类对象,最后再添加派生类自己的对象,其对齐要求和类作为(非静态)成员变量是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <cstdio> class Base1 {public : Base1 (int i, char c) :m_i (i), m_c (c) {} private : int m_i; char m_c; }; class Base2 {public : Base2 (short s, int i) : m_s (s), m_i (i) {} private : short m_s; int m_i; }; class Derived : public Base1, public Base2 {public : Derived (int i1, char c, short s, int i2, long l) : Base1 (i1, c), Base2 (s, i2), m_l (l) {} long getLong () const { return m_l; } private : long m_l; }; int main () { std::printf ("sizeof Derived: %d\n" , static_cast <int >(sizeof (Derived))); Derived d (1 , static_cast <char >(2 ), static_cast <short >(3 ), 4 , static_cast <long >(5 )) ; long l = d.getLong (); }
使用gdb调试,查看内存布局,如下: 如果不是按基类整体作为一个(非静态)成员变量的话,Base2::m_s
的offset应为6,但实际上其offset为8,因为Base2
的对齐要求为4。
1.1.2 基类无虚函数,派生类有虚函数 这个也很简单,只需要在整个类的头部安插一个虚表指针vptr
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <cstdio> class Base1 {public : Base1 (int i, char c) :m_i (i), m_c (c) {} private : int m_i; char m_c; }; class Base2 {public : Base2 (short s, int i) : m_s (s), m_i (i) {} private : short m_s; int m_i; }; class Derived : public Base1, public Base2 {public : Derived (int i1, char c, short s, int i2, long l) : Base1 (i1, c), Base2 (s, i2), m_l (l) {} virtual long getLong () const { return m_l; } private : long m_l; }; int main () { std::printf ("sizeof Derived: %d\n" , static_cast <int >(sizeof (Derived))); Derived d (1 , static_cast <char >(2 ), static_cast <short >(3 ), 4 , static_cast <long >(5 )) ; long l = d.getLong (); }
1.1.3 基类也有虚函数 在这种情况下,派生类会继承基类的虚函数,也会复用基类放置虚表指针的内存位置,无论基类有没有定义虚函数,有没有新增虚函数,都不会增加新的虚表指针,而是复用基类的虚表指针的内存位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <cstdio> class Base1 {public : Base1 (int i, char c) :m_i (i), m_c (c) {} virtual char getChar () const { return m_c; } int getIntOfBase1 () const { return m_i; } private : int m_i; char m_c; }; class Base2 {public : Base2 (short s, int i) : m_s (s), m_i (i) {} virtual short getShort () const { return m_s; } int getIntOfBase2 () const { return m_i; } private : short m_s; int m_i; }; class Derived : public Base1, public Base2 {public : Derived (int i1, char c, short s, int i2, long l) : Base1 (i1, c), Base2 (s, i2), m_l (l) {} virtual long getLong () const { return m_l; } virtual int getInt () { return getIntOfBase2 (); } private : long m_l; }; int main () { std::printf ("sizeof Derived: %d\n" , static_cast <int >(sizeof (Derived))); Derived d (1 , static_cast <char >(2 ), static_cast <short >(3 ), 4 , static_cast <long >(5 )) ; auto c = d.getChar (); auto s = d.getShort (); auto i = d.getInt (); }
使用gdb调试编译出来的程序,如下: 我们可以看到只有两个虚函数表,是继承自Base1
和Base2
的。 我们再看一下这两个虚函数表里面的内容: 可以看到Base1
的虚表里面,Base1::vptr[0]
为Base1::getChar()
的地址,Base1::vptr[2]
为Derived::getInt()
的地址;在Base2
的虚表里面,Base2::vptr[0]
为Base2::getShort()
的地址。 那Base1::vptr[1]
是什么地址?我们现在还没有看到Derived::getLong()
这个虚函数,是它的地址吗?查看该地址处的反汇编代码可知,确实如此。 至此,我们可以总结在基类有虚函数的时候C++的内存布局了:派生类新增的虚函数并不会导致新的虚表指针的诞生,而是会复用第一个虚表指针,使其指向一个新的虚表,这个虚表前几项都是基类的虚函数地址(当然,如果派生类覆写[override
]了该虚函数,新的虚函数地址会在该位置覆盖基类的虚函数地址),后面则是派生类的虚函数地址。
1.1.4 指针的调整 为了支持多态,C++支持将派生类的指针赋值给基类的指针,那C++是如何保证这个指针能正确索引到成员变量和成员函数的呢? 其实很简单,在派生类中,有着完整的基类对象,包括基类的虚函数指针,而且虚函数表中的虚函数地址也已经被正确设置,因此只需要把指针调整到该基类对应的位置即可。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <cstdio> class Base1 {public : Base1 (int i, char c) :m_i (i), m_c (c) {} virtual char getChar () const { return m_c; } int getIntOfBase1 () const { return m_i; } private : int m_i; char m_c; }; class Base2 {public : Base2 (short s, int i) : m_s (s), m_i (i) {} virtual short getShort () const { return m_s; } int getIntOfBase2 () const { return m_i; } private : short m_s; int m_i; }; class Derived : public Base1, public Base2 {public : Derived (int i1, char c, short s, int i2, long l) : Base1 (i1, c), Base2 (s, i2), m_l (l) {} virtual long getLong () const { return m_l; } virtual int getInt () { return getIntOfBase2 (); }private : long m_l; }; int main () { Derived d (1 , static_cast <char >(2 ), static_cast <short >(3 ), 4 , static_cast <long >(5 )) ; Derived* pd = &d; Base1* pb1 = pd; Base2* pb2 = pd; }
反汇编正如注释:
1.2 有虚继承 按照《深度探索C++对象模型》的描述,对于有虚继承的类,会有一个偏移量值表征当前指针与虚基类基址距离的偏移量。那么这个偏移量在哪里?在对象实例的内存布局中?还是在虚表里面?如果在虚表里面会不会添加新的虚表指针?还是继续复用基类的虚表指针?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 class Point2D {public : Point2D (int x, int y) : m_x (x), m_y (y) {} virtual int getX () const { return m_x; }private : int m_x; int m_y; }; class Point3D : virtual public Point2D {public : Point3D (int x, int y, int z) : Point2D (x, y), m_z (z) {} virtual int getZ () { return m_z; }private : int m_z; }; class VertexInterface {public : virtual VertexInterface* getNext () = 0 ;}; class Vertex : virtual public Point2D, public VertexInterface {public : Vertex (int x, int y, Vertex* next) : Point2D (x, y), m_next (next) {} virtual Vertex* getNext () override { return m_next; }private : Vertex* m_next; }; class Vertex3D : public Point3D, public Vertex {public : Vertex3D (int x, int y ,int z, int mumble) : Point3D (x, y, z), Vertex (x, y, nullptr ), Point2D (x, y), m_mumble (mumble) {} virtual long getMumble () { return m_mumble; }private : int m_mumble; }; int main () { Point3D p3d (1 ,2 ,3 ) ; Point3D* pp3d = &p3d; Point2D* pp2d = pp3d; Vertex vex (1 , 2 , nullptr ) ; Vertex* next = vex.getNext (); Vertex3D v3d (1 , 2 , 3 , 4 ) ; Vertex3D* pv3d = &v3d; Point2D* pp2d_1 = pv3d; int mumble = v3d.getMumble (); }
1.2.1 派生类的所有非虚基类里都没有虚表指针 对上面代码生成的程序使用gdb进行调试。 首先查看各个类对象的size。 我们可以看到,Point2D
的size为16,这是可以预见的,虚表指针vptr
占用8个字节,两个int
类型的非static
成员变量占用8个字节,一共16个字节。 而Point3D
的size为32,虚基类Point2D
的size为16,对齐要求为8,自己的一个int
类型的非static
成员变量占用4个字节,如果没有新添加东西,则加上对齐要求则只需要24字节size,说明必然加了新的东西,要么是基类offset,要么是新的虚表指针。 可以看到虚基类Point2D
被放在了offset0x10
的位置,而Point3D::m_z
在offset0x8
,那在offset0x0
的位置那里的是什么?可以确定不是虚基类的偏移,因为虚基类的偏移为0x10
,那就应该是虚表指针了,那offset会在里面吗? 可以看到在vptr[-3]
处有一个0x10
的值,那虚基类的偏移量会是在vptr[-3]
处吗?,我们看一下反汇编是如何对指针进行操作的。 可以看到汇编代码也正是这样做的,相当于:pp2d = (pp3d == nulltr ? nullptr : pp3d + vptr[-3])
。其中vptr
为位于Point3D开头,保存虚基类的虚表指针,而不是虚基类Point2D
的虚表指针. 也就是说虚基类的偏移量不会保存在对象的内存布局中,而是放在虚表中,而派生类不会复用虚基类的虚表指针(也应该这样,虚基类都找不到怎么用它的虚表指针找偏移量)。而且派生类的虚表指针和新成员变量是放在类的开头的,并不是像没有虚继承那样基类对象在最开头(offset0x0
的位置)。 其实在《深度探索C++对象模型》中说的是虚基类的偏移放到了vptr[-1]
的位置,但我们知道在g++的实现里这个位置给了该类对应的typeinfo
对象,g++实际上将虚基类偏移量放在了vptr[-3]
。
1.2.2 派生类的非虚基类里都有虚表指针 那么如果派生类还继承(非虚继承)了其他有虚函数表指针的类,那么派生类会不会复用其虚表指针呢?类Vertex
正是这样,Point2D
是它的虚基类,而VertexInterface
是它的普通基类。类Point2D
的size为16,VertexInterface
的size为8(只包含一个虚表指针)。Vertex
还有自己的成员变量m_next
,size为8,这样就凑够32个字节了,说明Vertex
没有生成自己的虚表指针,会复用非虚基类的虚表指针。 从调试结果来看,g++会在派生类的最前面按照生命顺序放置非虚基类对象,然后放置派生类自己的成员,最后放置虚基类对象。
1.2.3 派生类继承了拥有公共虚基类的两个类 我们来算一下Vertex3D
类里面的各个成员及其size,首先Point3D
除了虚基类Point2D
外还有16字节(虚表指针8,int
成员4,对齐4),Vertex
除了虚基类Point2D
外还有16字节(VertexInterface
的虚表指针8,Vertex*
成员8),Vertex3D
自己的int
成员4字节,最后虚基类Point2D
的size为16,对齐为8,所以前面要填充4字节一共16+16+4+4+16=56字节。正好是Vertex3D
类的大小,所以继承有公共虚基类的两个类不会增加虚表指针,而是直接复用基类的虚表指针,用gdb查看可以看到其内存布局正如上面所言。 所以,对于派生了继承了有公共虚基类的两个类,g++会将非虚基类的所有成员按声明顺序在内存中排列,再将派生类自己的成员放在内存里,最后就是虚基类。 最后,在使用分析类对象的时候,可以使用 p /x {objName}
来查看对象内容和各个成员的值,我在写这篇博客的时候就忘记对Vertex3D::m_mumble
成员初始化,结果其是一个未初始化的值,而且我还我忘记了Vertex3D
内还有这个成员,一直没发现这个是什么值,不知道为什么出现了这未知的8字节,使用p /x v3d
命令后才发现是Vertex3D::m_mumble
,浪费了很多时间。自己还有很多要提升的地方,gdb的这个用法来自《C++ 虚函数之一:对象内存布局》 。最后给大家展示一下p /x v3d
后的结果:
1 2 3 (gdb) p /x v3d $2 = {<Point3D> = {<Point2D> = {_vptr.Point2D = 0x8201b70 <vtable for Vertex3D+96>, m_x = 0x1, m_y = 0x2}, _vptr.Point3D = 0x8201b28 <vtable for Vertex3D+24>, m_z = 0x3}, <Vertex> = {<VertexInterface> = {_vptr.VertexInterface = 0x8201b50 <vtable for Vertex3D+64>}, m_next = 0x0}, m_mumble = 0x4}
《深度探索C++对象模型》里还有对于成员指针的讲解,但这篇博文已经很长了,就放在下一篇总结吧。