刚看完了《深度探索C++对象模型》第三章,这里做一下总结,也写一下我自己在g++ 7.5.0上的验证。
本文中所有的源文件都可以在这里拿到(百度网盘链接)。
注意,这里所说的“对象”是指在C++中使用class
或struct
关键字创建的类的实例。
1. 无继承情况下的C++对象内存布局
首先当然是从最基础的情况来讲,在没有继承的情况下的C++对象内存布局是什么样的?这又分为两种:无虚函数和有虚函数。
1.1 无虚函数
C++类内成员变量分为两类:static
成员变量和非static
成员变量。static
成员变量不在类的实例的内部,在整个内存中只有一份,只需要使用类名即可访问;而非static
成员变量在类的实例内部,需要为其分配空间。
在这种情况下C++的对象和C的结构体是一样的,毕竟要实现和C的兼容,主要就是结构体/类内成员变量的对齐。
其一般规则总结如下:
- 所有成员按照在类内的声明顺序在内存中排列;
| |
| #include <iostream> |
| |
| int main(); |
| |
| class Test00 { |
| friend int main(); |
| public: |
| int i1; |
| private: |
| int i2; |
| public: |
| int i3; |
| }; |
| |
| #define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(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; |
| } |
| |
| |
| |
| |
| |
- 任一非
static
成员变量的偏移(offset
)要是其大小的倍数;
| |
| #include <iostream> |
| struct Test01 { |
| char c; |
| int i; |
| }; |
| |
| #define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName))) |
| |
| int main() { |
| std::cout << showOffset(struct Test01, i) << std::endl; |
| } |
| |
| |
| |
- 结构体整体的size需要为最大非
static
成员变量size的倍数;
| |
| #include <iostream> |
| |
| |
| |
| struct Test02_1 { |
| char c1; |
| int i; |
| char c2; |
| }; |
| |
| |
| |
| struct Test02_2 { |
| char c1; |
| char c2; |
| int i; |
| }; |
| |
| int main() { |
| std::cout << "sizeof Test02_1: " << sizeof(Test02_1) << std::endl; |
| std::cout << "sizeof Test02_2: " << sizeof(Test02_2) << std::endl; |
| } |
| |
| |
| |
| |
- 空对象的size为1,为了保证每个对象都有唯一的内存位置(memory location)
| |
| #include <iostream> |
| |
| struct Test03 {}; |
| |
| int main() { |
| Test03 a, b; |
| std::cout << "sizeof Test03: " << sizeof(Test03) << std::endl; |
| if (&a == &b) |
| std::cerr << " Error! &a == &b, at " << static_cast<void*>(&a) << std::endl; |
| else |
| std::cout << "a and b has different address, &a = " << static_cast<void*>(&a) << " and &b = " << static_cast<void*>(&b) << std::endl; |
| } |
| |
| |
| |
| |
- 当类(
class
)/结构体(struct
) A 作为一个类B的内部成员变量时,其对齐要求为类A内部最大的对齐要求;
| |
| #include <iostream> |
| |
| |
| struct Test01{ |
| char c; |
| int i; |
| }; |
| |
| struct Test04 { |
| char c; |
| Test01 t; |
| }; |
| |
| #define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(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; |
| } |
| |
| |
| |
| |
- 空的类(empty class)A作为作为一个类B的成员变量时,类A占用一个字节的空间,对其要求也为1;
| |
| #include <iostream> |
| |
| |
| struct Test03 {}; |
| |
| struct Test05 { |
| char c; |
| Test03 t; |
| }; |
| |
| #define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(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; |
| } |
| |
| |
| |
| |
1.2 有虚函数
C++使用虚函数来实现多态,非虚函数不展现多态性,当调用非虚函数时,只要调用一个写死的地址即可,无论是使用对象调用还是使用指针/引用调用;而当使用指针/引用调用虚函数需要视其绑定到的实际对象来调用对应的虚函数,以展现多态性(用对象调用虚函数不展现多态性)。
而C++实现虚函数用到的便是虚表。所谓虚表,就是保存该类所有虚函数地址的一张表,一个类的某个确定的虚函数在虚表的确定位置,而类实例中有一个虚表指针指向该虚表,当出现类继承并覆写(override)了该虚函数时,只需要将虚表指针指向另一张虚表,该虚表中对应位置的函数指针换为新的函数即可。另外,一个类的所有对象共享同一张虚表,因此不会带来大的内存消耗。该虚表由编译器生成。
这里只是对于虚函数和虚表进行了简单的描述,详细可查询网络资源,这里不再赘述。
就像上面所说,相比于没有虚函数的类,由虚函数的类的实例只是多了一个指向虚表的指针,其放在类的开头或者结尾(g++将其放在类的开头),大小和对其要求视平台而定,在x86-64平台上,虚表指针大小和对其要求为8字节。
| |
| 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位大小,虚表指针指向的虚表的第一个表项是地址0x080007b2
,同时查看反汇编,因为我们使用对象来调用虚函数,不展现多态性,这里直接call了Point::getX()
的地址,可以看到其地址为0x080007b2
,正好是前面虚表的第一个表项。
还有就是Point
类型对应的typeinfo
对象的地址,在《深度探索C++对象模型》中提到其位于虚表的第一个表项,但前面我们看到虚表第一个表项存放的是虚函数,那typeinfo
的地址放在哪里呢?我们来找一下。
| |
| #include <typeinfo> |
| 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。
| |
| #include <iostream> |
| |
| 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(); |
| } |
| |
| |
这一篇博客就先写到这里,下一篇再谈谈在继承体系下g++是如何实现C++对象的内存布局的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」