系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。如某个机器的整型长度为4个字节且它的起始存储位置能够被4整除,那么结构体
struct ALLGN {
char a;
int b;
char c;
};
在内存中的存储的起始位置必须是一个能够被4整除的地址。a的地址跟整个结构体的起始地址一个值,结构体中的整型b必须跳过3个字节跳到合适的边界上才能存储,存储在b之后的是最后一个字符变量c。如果声明了相同类型的第二个结构体变量,它的起始存储位置也必须满足4这个边界,所以在第一个结构体的后面还要跳过3个字节才能够存储第二个结构。因此每个结构体占用12字节内存,但只是用其中的6个字节。《C和指针》“10.3结构的存储分配”章节(Page 227)内容。
1 内存对齐
内存对齐概念暂针对C中的结构体。掌握内存对齐主要是用来避免内存自动对齐,调整结构体所占的内存达到最小。
为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐内存访问仅需要一次访问。字,双字和四字在自然边界上不需要内存对齐(对它们来说,自然边界分别是偶数地址,可以被4或8整除的地址)。一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的。一个字起始地址是奇数,但没有跨越字边界被认为是对齐的。[C语言深度剖析—阮正冲]
自然边界
在系统(编译器)默认下,在存储变量时,当前变量vn的地址减去紧凑存储关系区域的第一个变量v1的地址的差值为变量vn长度的整数倍。此时当前变量vn的地址被称为自然边界[自己推测的一个概念]。
如以下结构体:
struct _test{
char ch;
int i;
}test;
在32位机上,若&test.ch的值为0x0000,则地址为0x0004为test.i的自然边界。在自然边界前的地址段必须能够容纳下正整数个当前变量。int占用一个字,那么i应该出现在自然边界自上(能够整除4的地址之上)。
内存对齐
以上的结构体在32位编译器下,地址为0x0000的test.ch占一个字节内存后,按理说&test.i的起始地址应为0x0001,结束地址应为0x0003。但由于编译器的内存对齐会将test.i自动后移到它的自然边界上存储,即&test.i的值为0x0004(假设编译器内存对齐默认值为4),结束地址为0x0007。编译器默认的内存对齐就是将当前变量存储到自然边界之上。
在内存对齐下,对于以下结构体:
struct _test{
int i;
char ch;
}test;
假若
&test.i = 0x0000
,则
&test.ch = 0x0004
且
test.ch
元素会占用
4
个字节,也就是说
test
结构体还是会占用
8
个字节大小。
X86 32位编译器的默认内存对齐值一般为变量自身的长度值(将其存储在自身的自然边界上)。对于C来说,可以使用预处理命令#pragram pack()来修改编译器的默认内存对齐值。
如使用#pragram pack(2)修改内存对齐值为2后,内存对齐=min(2,sizeof(int)),&test.i的起始地址为0x0002,结束地址为0x0005。
综上,内存对齐(以下3层,层层递进)
- 不使用#pragram pack(n)修改编译器的内存对齐值,内存对齐值 = sizeof(vn)。
- 使用#pragram pack(n)修改编译器的内存对齐值为n后,在#pragram pack(n)与语句#pragram pack(),内存对齐值= min(n,sizeof(vn))。
- 结构体内任一元素之前的地址段能够存储正整数个长度为内存对齐值的元素。
(1)结构体内元素的内存对齐
struct _test2{
char ch1;
char ch2;
int i;
long li;
}test2;
[1]结构体所占内存大小
[x86 32bit]根据内存对齐的特征瞄一眼结构体给出结构体所占内存大小值:1 + 3 + 4 + 4 =12字节。
[x86 32bit] sizeof(test2)= 12字节。
[2]每元素占内存大小
分析:ch1占1 byte,ch2占3 bytes(两字节的空闲地址,由i对齐造成),i占4 bytes,li占4 bytes。
(2)结构体内含结构体的内存对齐
struct _test3{
char ch1;
struct _test2 t2;
int i;
}test3;
[1]结构体所占内存大小
[x86 32bit]瞄内存值为:4 + 12 + 4 = 20 bytes
[x86 32bit] sizeof(test3)= 20 bytes
[2]每元素占内存大小
分析:结构体内的结构体元素内存对齐方式:以结构体元素内占内存最大的元素的长度对齐。
所以:ch1 占4 bytes(有3bytes为空闲,由t2对齐造成),t2占12 bytes(对齐方式不变),i占4 bytes。
(3)含#pragram pack()的内存对齐
#pragma pack(n),n=1, 2, 4,8,…
[1]#pragram pack()设定值小于sizeof(vn)
#pragma pack(2)
struct _test3{
char ch1;
struct _test2 t2;
int i;
}test3;
#pragram pack()
[x86 32bit]瞄内存值为:2 + 12 + 4 = 18bytes。此时int类型的i就跨越了4字节的一个边界,被认为是内存是未对齐的。
[x86 32bit]sizeof(test3) = 18 bytes。
分析:ch1 占2 bytes(1bytes空闲,由#pragram pack(2)造成,它的值比t2的对齐长度4要小),t2占12 bytes[因为struct _test2不在#pragma pack(2)与#pragma pack()之间,struct _test2 t2只是一个内存拷贝,故而t2的占内存大小不变],i占4 bytes。
[2]#pragram pack()设定值大于sizeof(vn)
#pragma pack(8)
struct _test3{
char ch1;
struct _test2 t2;
int i;
}test3;
#pragram pack()
[x86 32bit] 瞄内存值:4 + 12 + 4 = 20 bytes
[x86 32bit]sizeof(test3)= 20 bytes
分析:ch1 占 4 bytes(#pragma pack(8) > t2的内存对齐值4),t2占12 bytes,i占4 bytes。
2 大端小端
(1)大小端存数据方式
大端:数据高字节内容存在低地址中。
[x86 32bit] int i = 1;
0x00 | 0x00 | 0x00 | 0x01 |
0xijkm, 0xijk(m+1) , xijk(m+2), 0xijk(m+3)
小端:数据高字节内容存在高地址中。
[x86 32bit] int i = 1;
0x00 | 0x0 | 0x00 | 0x01 |
0xijk(m+3) 0xijk(m+2) 0xijk(m+1) 0xijkm
大端小端成为了存储数据的一个特点。在必要的时刻就需要明确大端小端的存储方式来明确变量的值(人为判断时)。如union类型就是一个例子。
(2)union和大小端
[1]union数据元素存储方式
union _utest{
char ch1;
char ch2;
int i;
}utest;
union类型变量内的所有元素的内存地址共享起始地址。为union变量开辟其内占内存最大的那个内存值。
ch1(ch2)int[0] | int[1] | int[2] | int[3] |
如以上sizeof(utest) = 4 bytes。且&utest.ch1、&utest.ch2、&utest.i的值都一样。
[2]union元素值
union变量的某段内存地址上定是多元素的地址。故而给union变量的某元素赋值时,可能其它的元素的值也被“潜”定了。
如utest.i = 1;
大端下
0x00 | 0x00 | 0x00 | 0x01 |
ch1(ch2) i
utest.i,utest.ch1,utest.ch2起始地址相同。大端时,utest.i的值在高地址字节中。
utest.ch1 =utest.ch2 = 0x00;
小端下
0x01 | 0x00 | 0x00 | 0x00 |
ch1(ch2) i
utest.i,utest.ch1,utest.ch2起始地址相同。大端时,utest.i的值在低地址字节中。
utest.ch1 = utest.ch2 = 0x01;
测试了一下Debian Linux 5.0是小端模式。
如果是测试当给utest.ch1或utest.ch2赋值时,i的值为多少。这种情况先要对i的其它位(如utest.i=0)初始化,不然输出来是乱码。
C Note Over。