当前位置:   article > 正文

[C++] 内存布局完整解读

[C++] 内存布局完整解读

说明:在C++中,内存布局(Memory Layout)是指程序中的数据结构(如类、结构体、联合体等)在内存中的排列和存储方式。它包括数据成员的顺序、大小以及它们之间的对齐和填充情况。内存布局对程序的性能有直接影响,因为它决定了数据访问的效率和内存的使用情况。

接下来我们看看C++11之前的布局和C++11之后的布局有什么差异。

C++11之前的内存布局:

  • 数据成员布局:在C++11之前,类的非静态数据成员按照它们在类中声明的顺序紧密排列。
  • 内存对齐:编译器会根据数据类型的大小和默认的对齐规则来插入填充字节(padding),以保证内存访问的效率。
  • 继承和多重继承:在多重继承中,基类的数据成员会先于派生类的数据成员排列,且每个基类的数据成员会按照它们在基类中的顺序排列。
  • 虚函数:如果类包含虚函数,编译器会为该类添加一个指向虚函数表(vtable)的指针,这个指针通常位于对象的起始位置。

C++11内存布局的变化:

  • 强类型别名:使用alignas和alignof关键字,C++11允许开发者更明确地控制类型或变量的对齐要求,这在C++11之前是通过编译器特定的扩展来实现的。
  • 统一的内存对齐模型:C++11提供了跨编译器和平台的一致内存对齐模型,这有助于提高代码的可移植性。
  • 标准库支持:C++11标准库中增加了如std::aligned_storage等工具,用于创建对齐的存储空间。
  • 多线程内存模型:C++11引入了新的多线程内存模型,定义了原子操作和内存顺序,这在并发编程中非常重要,但在内存布局方面并没有直接影响。
  • 右值引用和移动语义:虽然这些特性主要影响临时对象的处理和函数参数的传递,但它们也可能间接影响内存布局,因为它们允许更高效的资源转移和避免不必要的复制。

总之,C++11在内存布局方面提供了更多的控制和标准化,使得开发者能够更精确地管理数据结构在内存中的排列,同时也提高了代码的可移植性和性能。

接下来我们详细解读为什么引入内存布局。

1 为什么引入内存布局?

C++中的内存布局对于程序的执行至关重要,原因包括但不限于以下几点(实际上和内存对齐的原因基本上是一致的)

  • 性能优化:合理的内存布局可以减少内存访问的时间,提高数据访问速度。内存对齐和缓存的利用可以显著提升程序的运行效率。
  • 硬件兼容性:不同的硬件平台对数据对齐有不同的要求。正确的内存布局可以确保程序能够在多种硬件架构上正确运行。
  • 内存使用效率:内存布局影响数据结构的大小和内存的使用效率。通过减少填充字节,可以更有效地利用内存空间。
  • 接口兼容性:C++类和结构体的内存布局需要与C语言结构体保持一致,以确保可以轻松地与C语言库进行交互。
  • 多线程支持:在多线程环境中,内存布局决定了如何安全地访问和修改共享数据,以及如何避免竞态条件和内存一致性问题。
  • 跨语言互操作:内存布局的一致性允许C++程序与其他使用不同编程语言编写的程序交换数据。
  • ABI(应用二进制接口)兼容性:内存布局需要遵循特定的ABI,以确保不同编译单元或不同程序之间的二进制兼容性。
  • 安全性:不正确的内存布局可能导致未定义行为,包括程序崩溃或数据损坏。内存布局的规则有助于避免这些安全问题。
  • 代码可维护性:清晰的内存布局有助于开发者理解和维护代码,特别是在进行代码审查和重构时。
  • 标准遵循:C++标准定义了内存布局的规则,遵循这些规则可以确保代码的标准化和可预测性。

内存布局是C++对象模型的核心组成部分,它直接关系到程序的运行时行为和性能。合理的内存布局可以提高程序的可移植性、性能和安全性。

2 内存布局详细解读

内存布局在C++中的相关代码主要体现在以下几个方面:

  1. 数据成员的排列顺序:成员变量按照在类中声明的顺序排列。
  2. 内存对齐和填充:编译器会在成员之间插入填充字节以达到特定类型的对齐要求。
  3. 继承:基类成员和派生类成员的布局顺序。
  4. 虚函数:包含虚函数的类会有一个指向虚函数表(vtable)的指针。
  5. 虚继承:虚基类表(vbtable)的使用。
  6. 静态成员:不在对象内存布局中,它们有单独的存储位置。

以下是一些演示这些方面的代码示例。

2.1 基础内存布局和对齐

参考代码如下:

  1. #include <iostream>
  2. struct SimpleStruct {
  3. char a; // 占用1字节
  4. int b; // 占用4字节,可能会有3字节填充以满足4字节对齐
  5. double c; // 占用8字节,可能会有4字节填充以满足8字节对齐
  6. };
  7. int main() {
  8. std::cout << "Size of SimpleStruct: " << sizeof(SimpleStruct) << std::endl;
  9. return 0;
  10. }

sizeof(SimpleStruct)将输出SimpleStruct的大小,这将包括成员变量的大小加上编译器可能插入的填充字节。对齐后输出为16。

2.2 虚函数和vtable指针

参考代码如下:

  1. struct WithVTable {
  2. virtual void func() {}
  3. };
  4. int main() {
  5. std::cout << "Size of WithVTable: " << sizeof(WithVTable) << std::endl;
  6. return 0;
  7. }

sizeof(WithVTable)将输出包含虚函数的WithVTable的大小,这通常比不包含虚函数的结构体大,因为需要额外的空间来存储指向vtable的指针。因包含一个指向虚函数表的指针,通常是4或8字节(取决于系统架构)。

2.3 单继承

参考代码如下:

  1. struct Base {
  2. char baseMember;
  3. };
  4. struct Derived : Base {
  5. int derivedMember;
  6. };
  7. int main() {
  8. std::cout << "Size of Derived: " << sizeof(Derived) << std::endl;
  9. return 0;
  10. }

sizeof(Derived)输出的是派生类Derived的大小,它将包括基类Base的成员和派生类自己的成员,以及可能的填充字节。在32位系统上,一个int通常是4字节,所以Derived结构体的总大小可能是1(Base::baseMember)+ 3(填充)+ 4(Derived::derivedMember)= 8字节。同理,在64位系统上,考虑到更大的对齐边界,填充可能会不同,但一般情况下,由于intchar都是较小的类型,总大小可能仍然是8字节,除非编译器为了优化内存对齐而采用了更大的填充。

2.4 虚继承

参考代码如下:

  1. struct VirtualBase {
  2. char baseMember;
  3. };
  4. struct DerivedVirtual : virtual Base {
  5. int derivedMember;
  6. };
  7. int main() {
  8. std::cout << "Size of DerivedVirtual: " << sizeof(DerivedVirtual) << std::endl;
  9. return 0;
  10. }

sizeof(DerivedVirtual)将输出虚继承自BaseDerivedVirtual的大小,它将包括虚基类指针(vbptr)和虚基类表(vbtable)的额外空间。输出的分析如下:

  • 在 32 位系统上,可能的总大小是 1(Base::baseMember)+ 3(可能的填充)+ 4(vbptr)+ 4(DerivedVirtual::derivedMember)+ 3(可能的填充)= 15 字节,但实际大小可能会因为对齐而减少。
  • 在 64 位系统上,vbptr 占用 8 字节,所以总大小可能是 1(Base::baseMember)+ 7(可能的填充)+ 8(vbptr)+ 4(DerivedVirtual::derivedMember)+ 4(可能的填充)= 24 字节。

2.5 使用alignas指定对齐

参考代码如下:

  1. struct AlignedStruct {
  2. alignas(16) int alignedInt;
  3. };
  4. int main() {
  5. std::cout << "Size of AlignedStruct: " << sizeof(AlignedStruct) << std::endl;
  6. return 0;
  7. }

alignas(16)指定alignedInt成员需要16字节对齐,sizeof(AlignedStruct)将反映出这一点,可能会有填充字节被插入以保持对齐。int alignedInt 本身占用4字节,但由于alignas(16),整个结构体将被填充至16字节对齐。

2.6 静态成员

参考代码如下:

  1. struct WithStaticMember {
  2. static int staticMember;
  3. };
  4. int WithStaticMember::staticMember = 10;
  5. int main() {
  6. std::cout << "Size of WithStaticMember: " << sizeof(WithStaticMember) << std::endl;
  7. return 0;
  8. }

尽管WithStaticMember包含一个静态成员,sizeof(WithStaticMember)将输出非常小的值,因为静态成员不属于对象的内存布局,它们有单独的存储空间。因为静态成员staticMember不包含在对象的内存布局中,所以sizeof通常只反映出编译器可能插入的一个字节以确保对象有一个唯一的地址。

这些示例展示了C++中内存布局的不同方面,以及编译器如何通过插入填充字节和使用指针来管理内存布局和对齐。实际的内存布局和大小可能会因编译器和平台的不同而有所差异。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/458625
推荐阅读
相关标签
  

闽ICP备14008679号