C++对象模型:g++实现(二)

文章详细探讨了C++中对象内存布局在有继承情况下的不同模式,包括无虚继承和有虚继承。在无虚继承时,无论继承方式如何,内存布局基本一致,而虚继承涉及到虚基类偏移量,这一信息存储在虚表中。文章通过示例代码和调试结果解释了虚表指针的使用以及派生类如何复用或添加虚表指针。同时,讨论了多继承下公共虚基类的影响,指出在这种情况下不会增加新的虚表指针。
摘要由CSDN通过智能技术生成

1. 有继承情况下的C++对象的内存布局

C++是支持多继承的,而多继承可能就会出现继承的两个(或多个)类有公共的父类,为了防止在一个对象中出现多个公共父类的实体,C++就提出了虚继承。因此总的来说C++的继承有两种:无虚继承和有虚继承。

1.1 无虚继承

在无虚继承情况下,无论是public继承还是protectedprivate继承,其内存布局都是一样的,因此这里只提供public继承的例子。

1.1.1 所有的基类和派生类都没有虚函数

在这种情况下,其实就相当于构造了一个对象,其内部按基类的声明顺序添加了各个基类对象,最后再添加派生类自己的对象,其对齐要求和类作为(非静态)成员变量是一样的。

 
// 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。

1.1.2 基类无虚函数,派生类有虚函数

这个也很简单,只需要在整个类的头部安插一个虚表指针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
1.1.3 基类也有虚函数

在这种情况下,派生类会继承基类的虚函数,也会复用基类放置虚表指针的内存位置,无论基类有没有定义虚函数,有没有新增虚函数,都不会增加新的虚表指针,而是复用基类的虚表指针的内存位置。

 
// 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调试编译出来的程序,如下:


我们可以看到只有两个虚函数表,是继承自Base1Base2的。
我们再看一下这两个虚函数表里面的内容:

可以看到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++是如何保证这个指针能正确索引到成员变量和成员函数的呢?
其实很简单,在派生类中,有着完整的基类对象,包括基类的虚函数指针,而且虚函数表中的虚函数地址也已经被正确设置,因此只需要把指针调整到该基类对应的位置即可。如:

 
// 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;
}

反汇编正如注释:

1.2 有虚继承

按照《深度探索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();
}
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后的结果:

 
(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}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野生的狒狒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值