C++对象模型:g++的实现(一)
刚看完了《深度探索C++对象模型》第三章,这里做一下总结,也写一下我自己在g++ 7.5.0上的验证。
本文中所有的源文件都可以在这里拿到(百度网盘链接)。
注意,这里所说的“对象”是指在C++中使用class
或struct
关键字创建的类的实例。
创新互联建站-专业网站定制、快速模板网站建设、高性价比河南网站开发、企业建站全套包干低至880元,成熟完善的模板库,直接使用。一站式河南网站制作公司更省心,省钱,快速模板网站建设找我们,业务覆盖河南地区。费用合理售后完善,10余年实体公司更值得信赖。
1. 无继承情况下的C++对象内存布局
首先当然是从最基础的情况来讲,在没有继承的情况下的C++对象内存布局是什么样的?这又分为两种:无虚函数和有虚函数。
1.1 无虚函数
C++类内成员变量分为两类:static
成员变量和非static
成员变量。static
成员变量不在类的实例的内部,在整个内存中只有一份,只需要使用类名即可访问;而非static
成员变量在类的实例内部,需要为其分配空间。
在这种情况下C++的对象和C的结构体是一样的,毕竟要实现和C的兼容,主要就是结构体/类内成员变量的对齐。
其一般规则总结如下:
- 所有成员按照在类内的声明顺序在内存中排列;
// test00.cpp
#include
int main();
class Test00 {
friend int main();
public:
int i1;
private:
int i2;
public:
int i3;
};
#define showOffset(ClassName, memberName) (reinterpret_cast( &(static_cast(nullptr)->memberName)))
int main() {
std::cout << showOffset(Test00, i1) << std::endl;
std::cout << showOffset(Test00, i2) << std::endl;
std::cout << showOffset(Test00, i3) << std::endl;
}
// Output:
// 0
// 4
// 8
- 任一非
static
成员变量的偏移(offset
)要是其大小的倍数;
// test01.cpp
#include
struct Test01 {
char c;
int i; // 如果紧凑排列,则i的偏移为1,但i的size为4,偏移要是4的倍数,因此i的偏移为4
};
#define showOffset(ClassName, memberName) (reinterpret_cast( &(static_cast(nullptr)->memberName)))
int main() {
std::cout << showOffset(struct Test01, i) << std::endl;
}
// Output:
// 4
- 结构体整体的size需要为最大非
static
成员变量size的倍数;
// test02.cpp
#include
// 如果紧凑排,Test02_1的size应为9,
// 但要与int(size为4)对其,所以其size为12
struct Test02_1 {
char c1; // Offset: 0
int i; // Offset: 4
char c2; // Offset: 8
};
// Test02_2成员和Test02_1相同,但顺序不同,
// 受规则2和3影响,其size为8
struct Test02_2 {
char c1; // Offset: 0
char c2; // Offset: 1
int i; // Offset: 4
};
int main() {
std::cout << "sizeof Test02_1: " << sizeof(Test02_1) << std::endl;
std::cout << "sizeof Test02_2: " << sizeof(Test02_2) << std::endl;
}
// Output:
// sizeof Test02_1: 12
// sizeof Test02_2: 8
- 空对象的size为1,为了保证每个对象都有唯一的内存位置(memory location)
// test03.cpp
#include
struct Test03 {}; // Empty class
int main() {
Test03 a, b;
std::cout << "sizeof Test03: " << sizeof(Test03) << std::endl;
if (&a == &b)
std::cerr << " Error! &a == &b, at " << static_cast(&a) << std::endl;
else
std::cout << "a and b has different address, &a = " << static_cast(&a) << " and &b = " << static_cast(&b) << std::endl;
}
// Output:
// sizeof Test03: 1
// a and b has different address, &a = 0x7fffe62e8486 and &b = 0x7fffe62e8487
- 当类(
class
)/结构体(struct
) A 作为一个类B的内部成员变量时,其对齐要求为类A内部最大的对齐要求;
// test04.cpp
#include
// 规则2中的类,size为8,对齐要求为4
struct Test01{
char c;
int i;
};
struct Test04 {
char c; // Offset: 1, size 1
Test01 t; // Offset: 4, size 8
};
#define showOffset(ClassName, memberName) (reinterpret_cast( &(static_cast(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test04, t) << std::endl;
std::cout << "sizeof Test04: " << sizeof(Test04) << std::endl;
}
// Output:
// Offset of t in struct Test04: 4
// sizeof Test04: 12
- 空的类(empty class)A作为作为一个类B的成员变量时,类A占用一个字节的空间,对其要求也为1;
// test05.cpp
#include
// 规则4中的类,空类,size = 1
struct Test03 {};
struct Test05 {
char c; // Offset: 0, size: 1
Test03 t; // Offset: 1, size: 1
};
#define showOffset(ClassName, memberName) (reinterpret_cast( &(static_cast(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test05, t) << std::endl;
std::cout << "sizeof Test05: " << sizeof(Test05) << std::endl;
}
// Output:
// Offset of t in struct Test04: 1
// sizeof Test05: 2
1.2 有虚函数
C++使用虚函数来实现多态,非虚函数不展现多态性,当调用非虚函数时,只要调用一个写死的地址即可,无论是使用对象调用还是使用指针/引用调用;而当使用指针/引用调用虚函数需要视其绑定到的实际对象来调用对应的虚函数,以展现多态性(用对象调用虚函数不展现多态性)。
而C++实现虚函数用到的便是虚表。所谓虚表,就是保存该类所有虚函数地址的一张表,一个类的某个确定的虚函数在虚表的确定位置,而类实例中有一个虚表指针指向该虚表,当出现类继承并覆写(override)了该虚函数时,只需要将虚表指针指向另一张虚表,该虚表中对应位置的函数指针换为新的函数即可。另外,一个类的所有对象共享同一张虚表,因此不会带来大的内存消耗。该虚表由编译器生成。
这里只是对于虚函数和虚表进行了简单的描述,详细可查询网络资源,这里不再赘述。
就像上面所说,相比于没有虚函数的类,由虚函数的类的实例只是多了一个指向虚表的指针,其放在类的开头或者结尾(g++将其放在类的开头),大小和对其要求视平台而定,在x86-64平台上,虚表指针大小和对其要求为8字节。
// test06.cpp
class Point {
public:
Point(int x)
:m_x(x)
{}
virtual
int getX()
{ return m_x; }
private:
int m_x;
};
int main() {
Point p(1);
int x = p.getX();
}
使用gdb观察,可以看到Point
类实例p
的size为16,包括size为8的虚表指针和size为4的int类型的成员变量m_x
,同时,由于虚表指针的对其要求为8,所以Point
的size必须是8的倍数,所以其size为16。
同时查看p
的内存布局,可以看到虚表指针被放置于类实例的头部,占用8个字节,后面紧跟4个字节的int
类型的成员变量m_i
,最后填充了4个字节以使类Point
的size为8的倍数。
我们在查看一下虚表指针指向的内存,我这里使用的是64位系统和程序,所以函数指针是8位大小,虚表指针指向的虚表的第一个表项是地址0x0b2
,同时查看反汇编,因为我们使用对象来调用虚函数,不展现多态性,这里直接call了Point::getX()
的地址,可以看到其地址为0x0b2
,正好是前面虚表的第一个表项。
还有就是Point
类型对应的typeinfo
对象的地址,在《深度探索C++对象模型》中提到其位于虚表的第一个表项,但前面我们看到虚表第一个表项存放的是虚函数,那typeinfo
的地址放在哪里呢?我们来找一下。
// file test07.cpp
#include
2 class Point {
3 public:
4 Point(int x)
5 :m_x(x)
6 {}
7
8 virtual
9 int getX()
10 { return m_x; }
11
12 private:
13 int m_x;
14 };
15
16 int main() {
17 Point p(1);
18 auto& ti = typeid(p);
19 int x = p.getX();
20 }
可以看到反汇编中保存了0x8200da8
这一地址到栈上,再结合我们的源码,很可能gdb所提示的<_ZTI5Point>
这一对象就是Point
类的typeinfo
对象,我们使用工具c++filt来看_ZTI5Point
这个被修饰过的符号是什么含义,不出所料,正是Point
类对应的typeinfo
对象。
liuyun@DESKTOP-Q5AT31V:/tmp/test/cppObjectModel/chap03/blog$ c++filt _ZTI5Point
typeinfo for Point
既然Point
对应的typeinfo
对象的地址为0x8200da8
,我们查看虚表附近的地址,发现虚表指针指向的地址的前面的一个QWORD的内容正好是typeinfo
的地址,那是不是虚表指针指向的并不是虚表的开头,而是第一个虚函数所在的地址,而在虚表中,第一个虚函数这一表项前面便是该类对应的typeinfo
的地址?
在查阅资料的时候,《C++虚函数之二:虚函数表与虚函数调用》这篇博客提到g++支持-fdump-class-hierarchy
这一编译选项,可以生成一个名为{source_file_name}.002t.class的文件,文件中详细记录了各个类的信息,包括其虚表信息。
正如我们所想,如果我们使用vptr
指代虚表指针,那么vptr[0]
就是第一个虚函数的地址,vptr[-1]
则是该类对应的typeinfo的地址,而在最前面,g++还填充了一个空的表项。
最后还有一个问题,再没有虚函数的时候,编译器为了让每一个对象都有自己独一无二的地址,会在对象中插入一个字节占位,而在有虚函数的时候类中会有一个原生的虚表指针vptr
,从而至少占8字节大小(x86-64上),那么是否就不需要再插入一个字节了呢?事实正如我们所想,Test08
类的size为8而不是16。
// test08.cpp
#include
class Test08 {
public:
virtual
int getNumber() { return s_i++; }
private:
static int s_i;
};
int Test08::s_i = 0;
int main() {
std::cout << "sizeof Test08: " << sizeof(Test08) << std::endl;
Test08 t;
int i = t.getNumber();
}
// Output:
// sizeof Test08: 8
这一篇博客就先写到这里,下一篇再谈谈在继承体系下g++是如何实现C++对象的内存布局的。
本文名称:C++对象模型:g++的实现(一)
链接URL:http://myzitong.com/article/dsoieip.html