在讨论之前我们先看一个栗子:
#include
using namespae std;
int main()
{
struct A
{
int a;
char b;
char c;
};
struct B
{
char b;
int a;
char c;
}
cout<<sizeof(A)<"pause");
return 0;
}
从上面代码中,我们首先计算结构体A的大小,int类型占4字节,char类型占1字节,那么结构体的大小是不是4+1+1=6呢?
我们再来看结构体B与结构体A的成员是相同的,但是所占内存大小是不是相同呢?我想你们心中已经有答案了,不管你的答案是什么,我们现在来验证验证?
嗯?结构体A的大小不是6么?而且结构体A和结构体B大小不应该相同吗?
那么带着上面这些疑问,我们来探究为什么结构体A的大小为8而不是6,为什么结构体B与结构体A的大小不同。
其实这一切都是内存对齐搞的鬼!!
那么,问题来了:1.什么是内存对齐?
2.为什么要内存对齐?
3.怎样内存对齐?
百度词条上面的解释:编译器为程序中的每个“数据单元“安排在适当的位置上。
平台原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。————- 比如,有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。
性能原因:内存对齐可以提高存取效率。————- 比如,有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。
我们知道一字节占用8位,那么32位的硬件平台一次最多读取4字节,如果读取上图中的int类型数据,假若没有内存对齐,cpu会首先读取前4字节,将多余的数据(char类型变量)剔除,然后再次读取4字节,将多余的数据剔除,然后再次将两次保留的数据进行拼合。为了产生对比,我们用内存对齐的方式作比较,如图:
显而易见,内存对齐的int数据可以一次性读取,提高了读取数据的效率!
如果不是很明白为什么内存对齐,也可以参考为什么要内存对齐 Data alignment: Straighten up and fly right
说完了内存对齐的原因,现在我们就看看内存对齐的规则:
1. 结构体变量的首地址是有效对齐值的的整数倍。
2. 结构体第一个成员的偏移量是0,以后每个成员相对于结构体首地址的偏移量都是该成员大小有效对齐值中较小那个的整数倍。
3. 结构体的大小为有效对齐值的整数倍,如有需要,编译器会在最后一个成员之后填充字节。
4. 结构体内类型相同的连续元素将在连续空间内,和数组相同。
有效对齐值:是 ==#pragma pack指定值==和结构体中最长数据类型长度 中较小的那个。有效对齐值也叫对齐单位。
有以上4条规则来回头看我们开始的例子:
从例子可以得知,我们只是调换了结构体中成员变量的顺序。结构体中最长的数据类型为int,4字节,vs2015默认#pragma pack(8),所以有效对齐值为4字节,我们可以画出如下示例图:
因此,我们可以知道为什么结构体A的大小为8,而且和结构体B的大小不同。
注意:不同编译器支持的有效对齐值不同,比如GCC只支持1,2,4对齐。
结构体嵌套内存对齐规则:
在一个结构体中遇到另一个结构体类型变量,则进入到另一结构体中对于其成员变量以相同规律进行对齐,最后结构体总大小等于两个结构体中所有成员变量中最大对齐数的整数倍.
#pragma pack(4)
struct A{
int a;
double b;
};
struct B{
A s; //有效对齐值为4
char c;
};
结构体B的大小为16。
还是三个问题:
1. 什么是位域?
2. 为什么用位域?
3. 位域怎么用?
1.什么是位域?
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。
2.为什么使用位域?
为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
3.位域怎样用?
1. 位域的定义和位于变量的说明(位域的定义与说明与结构体的定义相仿)
struct 位域结构名
{
位域列表
};
位域列表的形式为:类型说明符 位域名:位域长度
struct bs{
int a:8;
int b:2;
int c:6;
}data;
data占两个字节,其中a占有8位,b占有2位,c占有6位。
位域的对齐:
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
系统会先为结构体成员按照对齐方式分配空间和填塞(padding),然后对变量进行位域操作。
在使用位域还需注意:
1) 一个位域必须存储在同一个字节中,不能跨两个字节。
2) 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度。
3) 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
4) 一般声明位域时用==signed int==或==unsigned int==,而不用int。因为若使用的是int,则到底是解释为有符号数还是无符号数由编译器决定。
struct bs{
unsigned a:4;
unsigned b:5; //从下一字节存放
unsigned c:1
unsigned :3; //无位域名,无法使用
};
我们可以看到位域大大减少了内存的浪费。当然,在位域的运用中,我们还需注意大小端对位域的影响问题。因为位域中的成员在内存中是从左向右分配还是从右向左分配由处理器的大小端决定。