上一篇博客《C++对象模型:g++实现(一)》用我的理解总结了在无继承体系下g++实现的C++对象的内存布局,这篇就来总结一下在有继承情况下的C++对象的内存布局。
本文中所有代码均可从这里拿到(百度网盘)。
有继承情况下的C++对象的内存布局
C++是支持多继承的,而多继承可能就会出现继承的两个(或多个)类有公共的父类,为了防止在一个对象中出现多个公共父类的实体,C++就提出了虚继承。因此总的来说C++的继承有两种:无虚继承和有虚继承。
无虚继承
在无虚继承情况下,无论是public
继承还是protected
、private
继承,其内存布局都是一样的,因此这里只提供public
继承的例子。
所有的基类和派生类都没有虚函数
在这种情况下,其实就相当于构造了一个对象,其内部按基类的声明顺序添加了各个基类对象,最后再添加派生类自己的对象,其对齐要求和类作为(非静态)成员变量是一样的。
// test09.cpp
#include <cstdio>
class Base1 {
public:
Base1(int i, char c)
:m_i(i), m_c(c)
{}
private: // 在Deriverd中:
int m_i; // Offset: 0
char m_c; // Offset: 4
}; // size: 8
class Base2{
public:
Base2(short s, int i)
: m_s(s), m_i(i) {}
private: // 在Deriverd中:
short m_s; // Offset: 8
int m_i; // Offset: 10
}; // size: 8
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; // Offset: 16
}; // size: 8 + 8 + 8 = 24
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();
}
// Output:
// sizeof Derived: 24
使用gdb调试,查看内存布局,如下:
如果不是按基类整体作为一个(非静态)成员变量的话,Base2::m_s
的offset应为6,但实际上其offset为8,因为Base2
的对齐要求为4。
基类无虚函数,派生类有虚函数
这个也很简单,只需要在整个类的头部安插一个虚表指针vptr
即可。
// test10.cpp
// 只是令Derived::getLong()为虚函数
#include <cstdio>
class Base1 {
public:
Base1(int i, char c)
:m_i(i), m_c(c)
{}
private: // 在Deriverd中:
int m_i; // Offset: 0
char m_c; // Offset: 4
}; // size: 8
class Base2{
public:
Base2(short s, int i)
: m_s(s), m_i(i) {}
private: // 在Deriverd中:
short m_s; // Offset: 8
int m_i; // Offset: 10
}; // size: 8
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; // Offset: 16
}; // size: 8 + 8 + 8 = 24
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();
}
// Output:
// sizeof Derived: 32
基类也有虚函数
在这种情况下,派生类会继承基类的虚函数,也会复用基类放置虚表指针的内存位置,无论基类有没有定义虚函数,有没有新增虚函数,都不会增加新的虚表指针,而是复用基类的虚表指针的内存位置。
// test11.cpp
// 添加了:
// virtual char Base1::getChar();
// int Base1::getIntOfBase1();
// virtual char Base2::getShort();
// int Base2::getIntOfBase2();
// virtual int Derived::getInt();
#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();
}
// Output:
// sizeof Derived: 40
使用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
]了该虚函数,新的虚函数地址会在该位置覆盖基类的虚函数地址),后面则是派生类的虚函数地址。
指针的调整
为了支持多态,C++支持将派生类的指针赋值给基类的指针,那C++是如何保证这个指针能正确索引到成员变量和成员函数的呢?
其实很简单,在派生类中,有着完整的基类对象,包括基类的虚函数指针,而且虚函数表中的虚函数地址也已经被正确设置,因此只需要把指针调整到该基类对应的位置即可。如:
// file test12.cpp
#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 在Derived类开头,不需要调整指针
Base1* pb1 = pd;
// Base2 在Derived类0x10偏移处,
// pb2 = (pd == nullptr ? nullptr : pd + 0x10)
Base2* pb2 = pd;
}
反汇编正如注释:
有虚继承
按照《深度探索C++对象模型》的描述,对于有虚继承的类,会有一个偏移量值表征当前指针与虚基类基址距离的偏移量。那么这个偏移量在哪里?在对象实例的内存布局中?还是在虚表里面?如果在虚表里面会不会添加新的虚表指针?还是继续复用基类的虚表指针?
// test13.cpp
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();
}
派生类的所有非虚基类里都没有虚表指针
对上面代码生成的程序使用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[-2]
。
派生类的非虚基类里都有虚表指针
那么如果派生类还继承(非虚继承)了其他有虚函数表指针的类,那么派生类会不会复用其虚表指针呢?类Vertex
正是这样,Point2D
是它的虚基类,而VertexInterface
是它的普通基类。类Point2D
的size为16,VertexInterface
的size为8(只包含一个虚表指针)。Vertex
还有自己的成员变量m_next
,size为8,这样就凑够32个字节了,说明Vertex
没有生成自己的虚表指针,会复用非虚基类的虚表指针。
从调试结果来看,g++会在派生类的最前面按照生命顺序放置非虚基类对象,然后放置派生类自己的成员,最后放置虚基类对象。
派生类继承了拥有公共虚基类的两个类
我们来算一下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
后的结果:
(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++对象模型》里还有对于成员指针的讲解,但这篇博文已经很长了,就放在下一篇总结吧。