内存对齐和位域

一、pragma pack()用法详解

1.什么是对齐?为什么要对齐?

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。

2.pragma pack语法

用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题,有时候为了内存对齐需要补齐空字节。通常写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。

语法:#pragma pack( [show] | [push | pop] [, identifier], n )
作用:指定结构,联合和类的包对齐方式(pack alignment)

show(optical): 显示当前packing aligment的字节数,以warning message的形式显示。
push(optical): 将当前打包对齐值推入编译器内部堆栈,并将当前打包对齐值设置为n。如果n未指定,则将当前打包对齐值推入。
pop(optical): 从内部编译器堆栈顶部移除记录。如果pop没有指定n,那么与堆栈顶部的结果记录关联的打包值就是新的打包对齐值。如果指定了n,例如,#pragma pack(pop, 16), n将成为新的打包对齐值。如果你弹出带有标识符的记录,例如,#pragma pack(pop, r1),那么栈上的所有记录都会弹出,直到找到有标识符的记录。该记录将弹出,与顶部生成的记录关联的打包值就是堆栈,即新的打包对齐值。如果您弹出的标识符在堆栈上的任何记录中都没有找到,那么这个弹出窗口将被忽略。
identifier(optional): 当与push一起使用时,给内部编译器堆栈上的记录赋一个名称。当与pop一起使用时,pops记录从内部堆栈中删除,直到标识符被移除;如果在内部堆栈上没有找到标识符,则不会弹出任何内容。
n (optional): 指定用于打包的值(以字节为单位)。如果没有为模块设置编译器选项,则n的默认值为8。取值范围为1、2、4、8、16。成员的对齐将位于一个边界上,该边界要么是n的倍数,要么是成员大小的倍数,以更小的为准。

3.字节对齐:

说明:为了提高 CPU 的存储速度,编译器会对 struct 和 union的存储进行优化,即进行字节对齐。

1. 指定对齐参数值:通过#pragma pack(push, n)设置。

2. 自身对齐参数值:每个内部类型自身也都有一个对齐参数,一般来说这个对齐参数就是 sizeof(type) 的值,也就是char的自身对齐参数是1,short是2,int是4,float也是4,double是8等。

3. 有效对齐参数值:内部类型的有效对齐是指它的自身对齐参数和指定对齐参数中较小的那个值;

4. 结构体整体的有效对齐参数值:是指它的成员中,有效对齐参数最大的那个值。

例如:

///< 假设按4字节对齐
#pragma pack(push, 4)
struct data {
  char a;     //a的有效对齐参数值是min(1,4)为1字节,从第1字节开始,占1字节
  char b;      //b的有效对齐参数值是min(1,4)为1字节,从第2字节开始,占1字节
  long long c; //d的有效对齐参数值是min(8,4)为4字节,从第5个字节开始,占8字节
  short d;     //e的有效对齐参数值是min(2,4)为2字节,从第13个字节开始,占2字节
};

二、位域:

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。
说明:C/C++中以一定区域内的位(bit)为单位来表示的数据成为位域,位域必须指明具体的数目,位域的作用主要是节省内存资源,使数据结构更紧凑。

1. 取地址操作符&不能应用在位域字段上;

2. 位域字段不能是类的静态成员;

3. 位域字段在内存中的位置是按照从低位向高位的顺序放置的;

4. 位域的对齐

  1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
  2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
  3. 如果相邻的两个位域字段的类型不同,则各个编译器的具体实现有差异,VC6采取不压缩方式,采用第一节说的字节对齐方式进行。
  4. 整个结构体的总大小采用字节对齐方式的第4点。
  5. 如果位域字段之间穿插着非位域字段,则不进行压缩,采用第一节说的字节对齐方式进行。
  6. 无名的位域不能使用,只能用于填充,位宽为0表示强制下一位域对齐到当前类型的边界。

例如:

///< 假设按4字节对齐
#pragma pack(push, 4)
struct data {
    short a : 7;       //从第1个字节开始,占1字节,使用7位
    short b : 5;       //从第2个字节开始,占1字节,使用5位
    long long e : 4;   //与上个类型不一致,e的有效对齐参数值是min(8,4)为4字节,所以从第5个字节开始,占8字节,使用4位
    char f : 5;        //与上个类型不一致,f的有效对齐参数值是min(1,4)为1字节,所以从第13个字节开始,占1字节,使用1位
};

它们的和为1字节(a)+1字节(b)+2字节(填充)+8字节(e)+1字节(f)=13字节。

因为整体结构体还需要进行对齐,结构成员最大有效对齐参数值为4,所以整个结构为4的整数倍,最后的总大小为4 * 4 = 16字节。

///< 假设按4字节对齐
#pragma pack(push, 4)
struct bits8 {
    short a : 3; //从第1个字节开始,占1字节,使用3位
    short : 0;  //占位,占13位,强制下一类型对齐到边界
    char : 3;   //占位,从第3个字节开始,占3位
    char b : 5;  //从第3字节第4位开始,使用5位 
    int i : 4;    //从第5个字节开始,占4字节,使用4位
}prnt;

它们的和为2字节(a)+1字节(b)+1字节(填充)+4字节(i)=8字节。

因为整体结构体还需要进行对齐,结构成员最大有效对齐参数值为4,所以整个结构为4的整数倍,满足条件,最后的总大小为8字节。

现在,可以通过普通的结构成员运算符(.)单独给这些字段赋值:

prnt.a= 0:
prnt.b= 1;

位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。
位域的取值范围非常有限,数据稍微大些就会发生溢出,请看下面的例子:

#include <stdio.h>

struct pack
{
 unsigned a:2;  // 取值范围为:0~3
 unsigned b:4;   // 取值范围为:0~15
 unsigned c:6;   // 取值范围为:0~63
};

int main(void)
{
 struct pack pk1;
 struct pack pk2;
 
 // 给pk1各成员赋值并打印输出
 pk1.a = 1;
 pk1.b = 10;
 pk1.c = 50;
 printf("%d, %d, %d\n", pk1.a, pk1.b, pk1.c);
 
 // 给pk2各成员赋值并打印输出
 pk2.a = 5;
 pk2.b = 20;
 pk2.c = 66;
 printf("%d, %d, %d\n", pk2.a, pk2.b, pk2.c);

 return 0;
}

程序输出结果为:

pk1.a = 1, pk1.a = 10, pk1.c = 5
pk2.a = 1, pk2.b = 4, pk2.c = 2

显然,结构体变量pk1的各成员都没有超出限定的位数,能够正常输出。而结构体变量pk2的各成员超出了限定的位数,并发生了上溢。
C++字节对齐与位域

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值