C++字节对齐

C++确实是一门很深奥很底层的语言…没想到就连字节对齐也有这么多门道。希望通过本文全面记录并帮助读者理解字节对齐。我觉得计算机知识的学习不仅要知其然,更要知其所以然,知道为什么要这样设计,而不是死记硬背,才能领悟前任这样设计的思想,未来真正运用的时候才能融会贯通

什么是字节对齐

计算机在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

为什么需要字节对齐

  1. 提高访问效率:比如intel 32位cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,而且还要对两次读取的结果进行拼凑才能得到完整的数据,因此需要在内存中存放数据时进行对齐。
  2. 平台原因(移植原因):不是所有硬件平台都能访问任意地址上的任意数据,有些平台可能智能从某些特定地址开始读取。

对齐模数概念

在讲解如何进行字节对齐前,先介绍对齐模数的概念

  • 许多实际的计算机系统对基本类型数据在内存中的存放位置有限制(也就是需要进行字节对齐),会要求这些数据的首地址值是某个值K(K是四的倍数)
  • 这个值K就是该数据类型的对齐模数
  • 当一种类型T1的对齐模数 / 另一种类型T2的对齐模数 > 1(且为整数),则称类型T1的对齐要求比T2严格,T2比T1宽松
  • 可以通过预编译命令#pragma pack(n)来指定对齐模数,n为2的整数幂
    • 实际的有效对齐模数是 指定对齐模数与类型自身对齐模数中的较小值,即 实际对齐模数 = min(指定对齐模数,这个数据成员的字节数)—这里要非常注意,下面的规则会频繁用到“实际对齐模数”这一概念

ps:在我的64位OS上对齐模数是8,我猜测对齐模数可能与电脑的位数有关

如何进行字节对齐

结构体中字节对齐的规则

先从结构体开始讲解,类的字节对齐规则实际上也是基于结构体的字节对齐规则。

提示:以下例子的运行结果是在64位GCC编译器下得到的,在这一情况下每种基本类型占用的字节数位:char 1 short 2 int 4 float 4 double 8 指针 8

不管是结构体还是类,内存对齐都遵循以下三个原则:

  1. 结构体每个数据成员的起始地址能够被该数据成员的实际对齐模数
  2. 结构体每个成员相对于起始地址的偏移能够被实际对齐模数整除,如果不能则在前一个成员后面补充字节(为了寻址)
  3. 结构体总体大小能够被最宽成员的实际对齐模数整除,如不能则在后面补充字节

字节对齐的过程:

  1. 各成员变量在存放时会根据在结构中出现的顺序依次申请空间,同时按照上面的三个原则调整位置,空缺的字节会自动填充
  2. 为了确保结构的大小时结构的对齐模数的倍数,在为最后一个成员变量申请了空间之后,还会根据需要自动填充空缺的字节数

下面给出一个实例:

//我的电脑是64位机器,默认的对齐模数是8字节
struct student_info {
    char name;//偏移量为0,满足对齐方式,name占用一个字节
    int age;//偏移量为1,不是 sizeof(int)的倍数,需要补足3个字节,age存放在偏移量为4的地址上,它自己占用4个字节
    int number;//偏移量为4,是sizeof(int)的倍数,不需要补足字节,number存放在偏移量为8的地址上,它自己占用4个字节
    char add;//偏移量为12,是sizeif(char)的倍数,不需要补足字节,add存放在偏移量为12的地址上,它自己占用1个字节
    double test;//偏移量为13,不是sizeof(double)的倍数,需要补足7个字节,补足后test存放在偏移量为16的地址上,它自己占用8个字节
    char tttt;//此时偏移量为24,是sizeof(char)的倍数,不需要补足字节,tttt存放在偏移量为24的地址上,它自己占用1个字节
};//,所有成员变量都分配了空间,空间的总大小为25,不是实际对齐模数的倍数,需要填充字节,填充至32

int main() {
    cout << sizeof(student_info) << endl;
    return 0;
}
//输出32

假如上面的例子指定对齐模数为2

#pragma pack(2)

答案会是22

嵌套结构体的字节对齐规则

(这部分参考了别人的博客)
内嵌结构体的第一个成员编程在外结构体中的偏移量,是“MIN(指定对齐模数,内嵌结构体中最大数据类型)”的倍数。
例子:

struct TEST 
{
    int a;  
    char b; 
    char c; 
    int d;
    char e;
    struct child {
        int f;
        long long g;  //8 bytes,long long 是chile结构体的最大数据类型
    }ch;
    char ci;
};
// sizeof(struct TEST)  = 40

在这个例子中,前面a,b,c,d,e的计算规则与普通结构体的计算规则相同,字符e的起始位置为12,占一个字节,然而由于child这个内嵌结构体的偏移量需要是min(最长数据成员所占字节 long long 8,对齐模数8) = 8的整数倍,因此需要在char e后进行填充,填充到16,然后再存,接下来的过程如下

  1. 存放int f:起始地址为 16,占4个字节
  2. 存放long long g:按照规则,起始地址需要是8的倍数,因此需要填充4个字节,该数据的实际其实地址为24,该数据占8个字节
  3. 存放char ci:起始地址为32,该数据占1个字节
  4. 最后,所有成员变量都分配了空间,空间的总大小为33,不是实际对齐模数的倍数,需要填充字节,填充至40,因此sizeof(struct TEST) = 40

类中的字节对齐规则

需要首先类中的字节对齐规则比较复杂,涉及虚函数、静态成员、虚继承、多继承、空类等情况。类本身是没有大小可言的,这里计算的sizeof(xxx)实际上是该类所对应对象的大小

具体规则总结如下:

  1. 假如不是虚类,计算实际上与结构体的一致,不需要对函数、静态数据进行计算。类的静态数据成员被该类所有的对象所共享,并不属于具体哪个对象,静态数据成员定义在内存的全局区
  2. 假如是虚类,需要明确C++在实现虚类时其实隐藏了虚表指针(本质上就是指针类型)。注意基类有一个虚表指针。此外对于派生类,考虑到多继承的情况,有多少个继承就会有多少个虚表指针,在计算类大小的时候需要将虚表指针也纳入考虑
  3. 关于空类:因为C++中凡是一个独立的(非附属)对象都必须具有非零大小,不同的对象不能具有相同的地址,所以这里做了一个特殊处理:空类大小为1。假如是继承空类:这个空类对于派生类不占用空间。假如是拥有一个空类对象:空类对象占一个字节

总结

字节对齐看似简单,但是深究起来其实是对很多知识点的总结,包括

  • OS是怎么寻址的
  • C++虚函数的实现原理
    在平时学习的时候也要注意对知识点的融会贯通,学什么都是这样的。

参考资料

  1. C++中的字节对齐_云飞扬_Dylan的博客-CSDN博客
  2. c++类的大小计算_rotation ㅤ   的博客-CSDN博客
  3. C++中的字节对齐
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值