内存字节对齐一网打尽,再也不纠结
结构体内存地址对齐概念
内存地址对齐,是一种在计算机内存中排列数据、访问数据的一种方式,包含了两种相互独立又相互关联的部分:基本数据对齐和结构体数据对齐。当今的计算机在计算机内存中读写数据时都是按字(word)大小块来进行操作的(在32位系统中,数据总线宽度为32,每次能读取4字节,地址总线宽度为32,因此最大的寻址空间为232=4GB,但是最低2位A[0],A[1]是不用于寻址,A[2-31]才能与存储器相连,因此只能访问4的倍数地址空间,但是总的寻址空间还是230*字长=4GB,因此在内存中所有存放的基本类型数据的首地址的最低两位都是0,除结构体中的成员变量)。
地址总线的最后两位到底用作什么?还不知道。
基本类型数据对齐就是数据在内存中的偏移地址必须等于一个字(word)的倍数,按这种存储数据的方式,可以提升系统在读取数据时的性能。为了对齐数据,可能必须在上一个数据结束和下一个数据开始的地方插入一些没有用处字节,这就是结构体数据对齐。
举个例子,假设计算机的字大小为4个字节,因此变量在内存中的首地址都是满足4地址对齐,CPU只能对4的倍数的地址进行读取,而每次能读取4个字节大小的数据。假设有一个整型的数据a的首地址不是4的倍数(如下图所示),不妨设为0X00FFFFF3,则该整型数据存储在地址范围为0X00FFFFF3~0X00FFFFF6的存储空间中,而CPU每次只能对4的倍数内存地址进行读取,因此想读取a的数据,CPU要分别在0X00FFFFF0和0X00FFFFF4进行两次内存读取,而且还要对两次读取的数据进行处理才能得到a的数据,而一个程序的瓶颈往往不是CPU的速度,而是取决于内存的带宽,因为CPU得处理速度要远大于从内存中读取数据的速度,因此减少对内存空间的访问是提高程序性能的关键。从上例可以看出,采取内存地址对齐策略是提高程序性能的关键。
结构体(struct)是C语言中非常有用的用户自定义数据类型,而结构体类型的变量以及其各成员在内存中的又是怎样布局的呢?怎样对齐的呢?很显然结构体变量首地址必须是4字节对齐的,但是结构体的每个成员有各自默认的对齐方式,结构体中各成员在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个成员的首地址等于整个结构体变量的首地址。下面列出了在Microsoft,Borland,GNU上对于X86架构32位系统的结构体成员各种类型的默认对齐方式。
char(1字节),1字节对齐
short(2字节),2字节对齐
int(4字节),4字节对齐
float(4字节),4字节对齐
double(8字节),Windows系统中8字节对齐,Linux系统中4字节对齐
当结构体某一成员后面紧跟一个要求比较大的地址对齐成员时(例如char成员变量后面跟一个double成员变量),或是在,这时要插入一些没有实际意义的填充(Padding)。而且总的结构体大小必须为最大对齐的倍数。
总结结构体的数据对齐方式满足条件:
(1)结构体内部各基本数据满足各自的基本数据的对齐方式
(2)结构体整体满足自己的对齐方式:总的结构体的大小必须是最大对齐的倍数。
但是同样的一个结构体,调整内部成员的顺序,能改变这个结构体的大小吗?看下面的例子:
struct AlignData1 { char c; short b; int i; char d; }Node;
这个结构体在编译以后,为了字节对齐,会被整理成这个样子:
struct AlignData1 { char c; char padding[1]; short b; int i; char d; char padding[3]; }Node;
所以编译前总的结构体大小为:8个字节。编译以后字节大小变为:12个字节。
但是,如果调整顺序:
struct AlignData2 { char c; char d; short b; int i; }Node;
那么这个结构体在编译前后的大小都是8个字节。
那么编译后不用填充字节就能保持所有的成员都按各自默认的地址对齐。这样可以节约不少内存!一般的结构体成员按照默认对齐字节数递增或是递减的顺序排放,会使总的填充字节数最少。
三个小的知识点:
空结构体 struct S5 { }; sizeof( S5 ); // 结果为1 空结构体(不含数据成员)的大小不为0,而是1。 试想一个“不占空间”的变量如何被取地址、两个不同的“空结构体”变量又如何得以区分呢?
于是,“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。
有static的结构体 struct S4 { char a; long b; static long c; //静态 }; 静态变量存放在全局数据区内,而sizeof计算栈中分配的空间的大小。 故不计算在内,S4的大小为4+4=8。
#pragma pack(n)指令设置1.2.4对齐。linux下最高位4.#pragma pack()指令默认为四。 如果参数大于每一个成员的对齐大小,就按成员的最大的对齐大小对齐。否则就按 参数的对齐大小。
结构体的内存的对齐方式先说到这里,下面看一下联合体的内存对齐的方式。
union联合体的内存的对齐方式
当多个数据需要共享内存或者多个数据每次只取其一的时候,可以利用联合体union。在C Programming Language这本书中对联合体是这么描述的:
(1)联合体是一个结构
(2)它的所有的成员相对于基地址的偏移量都是0
(3)此结构空间要大到足够容纳最“宽”的成员
(4)其对齐方式要适合其中的所有的成员
下面解释这四条描述:
由于联合体中的所有的成员共享一段内存,因此每个成员的存放首地址相对于联合体变量的基地址的偏移量为0,也就是说所有成员的首地址都是一样的。为了使得所有的成员能够共享这一段内存,因此这个联合体的空间必须能够容纳这些成员中最宽的那个成员。对于那句“对齐方式要适合其中所有的成员”是指其必须符合所有成员的自身对齐方式。
看下面的这个联合体的例子:
union U { char s[9]; int n; double d; };
s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:
1)大小足够容纳最宽的成员;
2)大小能被其包含的所有基本数据类型的大小所整除。
(三)总结题
先来看第一个题目:
union A { int a[5]; char b; double c; }; struct B { int n; A a; char c[10]; } //sizeof(B) = ?
题目中很容易的就能知道sizeof(A)=24,并且A的对齐大小是8。所以在B中,a的首地址要是8的倍数,所以int n后面要加入四个padding,然后放A,最后的整个结构体大小要是8的倍数,所以c的后面要补充6个padding。所以sizeof(B)=48。
看第二个题目:
分析下面的这个程序,说明每一步的输出结果
#include <iostream> using namespace std; union U1 { char s[9]; int n; double d; }; int main(int argc, char *argv[]) { union U1 u1; //测试联合体的大小 printf("%d\n",sizeof(u1)); //测试联合体的首地址 printf("0x%x\n",&u1); printf("0x%x\n",&u1.s); printf("0x%x\n",&u1.n); printf("0x%x\n",&u1.d); //测试联合体的数据排列 u1.n=1; printf("%d\n",u1.s[0]); unsigned char *p=(unsigned char *)&u1;
printf("%d\n",*p); printf("%d\n",*(p+1)); printf("%d\n",*(p+2)); printf("%d\n",*(p+3)); printf("%d\n",*(p+4)); printf("%d\n",*(p+5)); printf("%d\n",*(p+6)); printf("%d\n",*(p+7)); printf("%lf\n",u1.d); return 0; }
解析:
从打印出的每个成员的基地址可以看出,联合体中每个成员的基地址都相同,等于联合体变量的首地址。
对u1.n=1,将u1的n赋值为1后,则该段内存的前4个字节存储的数据为:
00000001 00000000 00000000 00000000 |
上面的框就是模拟内存的存储单元的情况
因此从u1.s[0]这个地址,读取一个int的值(也就是从这个地址连续的读出四个字节),读到的就是00000000 00000000 00000000 00000001,结果的值就是1。
下面又用一个指针,依次的看前8个存储单元的数据,看到的结果是:
00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 |
最后一句话又把u1.d的地址作为起始地址读取一个double的值,也就是连续的8个字节的长度。所以读到的内容是:
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001
然后把64位解析成double类型的数据:
63位为符号位,62-52位阶码为,后面的所有是尾数。这样经过换算就是得到的结果值0.000000
再看第三个题目:
#include <iostream.h> #pragma pack(8) struct example1 { short a; long b; }; struct example2 { char c; example1 struct1; short e; }; #pragma pack() int main(int argc, char* argv[]) { example2 struct2; cout << sizeof(example1) << endl; cout << sizeof(example2) << endl; cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl; return 0; }
分析输出的结果:
输出结果:
8
16
4