为什么要讨论内存对齐问题呢?
因为最近在写BMP头文件的时候出现一些有趣的问题,发现是内存对齐的问题所以就:
1,将内存中的数据写入文件时,文件中的数据排列与内存中的是一样的。即如果是小段模式,那么文件中也是小段模式。
2,内存对齐
先看一下程序
1 #include
2 struct t1_stru
3 {
4 char ch;
5 int in;
6 short sh;
7 }t1;
8
9
10 int main()
11 {
12 t1.ch = 0x12; // 这三句赋值语句可以不用理会
13 t1.in = 0x
14 t1.sh = 0xBCDE;
15 printf("sizeof(t1.ch)=%d\n", sizeof(t1.ch));
16 printf("sizeof(t1.in)=%d\n", sizeof(t1.in));
17 printf("sizeof(t1.sh)=%d\n", sizeof(t1.sh));
18 printf("sizeof(t1)=%d\n", sizeof(t1));
19 return 0;
20 }
输出结果:
sizeof(t1.ch)=1
sizeof(t1.in)=4
sizeof(t1.sh)=2
sizeof(t1)=12
为什么会出现这样的结果呢?其实是编译器其对结构体成员进行内存对齐的所导致的。
在默认情况下,C/C++的编译器会将结构体,栈中的成员进行内存对齐。
什么是内存对齐呢?即把成员安排在某一些符合某种规律的内存空间地址,从而加快CPU对数据的读写速度。
如果想深入了解内存对齐如何加快CPU对数据的读写速度请参考:
http://www.ibm.com/developerworks/library/pa-dalign/
Data alignment:Straighten up and fly right
而下面则重点讲述内存对齐的规则:
首先指出 #pragma pack (n) 这个语句用于设置结构体的内存对齐方式,具体作用下面再说。在linux gcc 下n可取的值为:1,2,4,当n大于4时按4处理。如果程序中没用显试写出这个语句,那么在linux gcc下,它会对所有结构体都采用#pragma pack (4)的内存对齐方式。
需要注意的是,在不同的编译平台上默认的内存对齐方式是不同的。如在VC中,默认是以
#pragma pack (8) 的方式进行对齐。
#pragama pack (n)使用方法
#pragama pack (2)
struct structname
{
…
}
#pragama pack ()
上面表示在#pragama pack (2) 到 #pragama pack()之间采用n为2的内存对齐方式。
#pragma pack () 表示取消自定义字节对齐方式,则在#pragama pack ()以下的程序不在使用#pragma pack (2) 的对齐方式,恢复#pragma pack (4) 这种编译器默认的对齐方式。当然没有#pragma pack ()也可以,那么则表示#pragma pack (2)以下到程序尾都采用此对齐方式。
总规则:
结构体成员的地址必须安排在成员大小的整数倍上或者是#pragma pack(n) 所指定的n的倍数上;取两者的最小值,即MIN(sizeof(mem), n),称MIN(sizeof(mem), n)为该结构体的成员对齐模数。同时该结构体的总大小必须为MIN(n, MAX(sizeof(mem1), siezof(mem2)…))的整数倍;而称MIN(n, MAX(sizeof(mem1), siezof(mem2)…))为该结构体的对齐模数。
下面的细则符合上面所说的总规则:(下面所说的偏移指某一个数据成员的首地址到该结构体头的地址偏移)
(1) 对结构体的成员,第一个数据位于偏移为0的位置,以后每个数据成员的偏移量必须是成员对齐模数的倍数。
(2) 为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的偏移是否为成员对齐模数的整数倍,若是,则存放本成员,反之,则在本成员与上一成员之前填充一定的字节,从而达到整数倍的要求。
(3) 在数据成员完成自身的对齐后,结构体本身也要进行对齐。意思是该结构体的大小必须是结构体的对齐模数的整数倍。如果其大小不是,那么则在最后一个成员的后面填充字节。
首先计算出成员对齐模数与结构体的对齐模数:
ch_mod = MIN(4, sizeof(t1.ch)) = 1;
in_mod = MIN(4, sizeof(t1.in)) = 4;
sh_mod = MIN(4, sizeof(t1.sh)) = 2;
t1_mod = MIN(4, MAX(ch_mod, in_mod, sh_mod)) = 4;
然后用gdb调试上面的程序分析内存对齐的规则:
(gdb) p &t1
$1 = (struct t1_stru *) 0x80496d8 // t1结构体的首地址
(gdb) p &t1.ch // ch的首地址
$2 = 0x80496d8 ""
(gdb) p &t1.in
$3 = (int *) 0x80496dc // in的首地址
(gdb) p &t1.sh
$4 = (short int *) 0x80496e0 // sh的首地址
根据细则1:
可以知道t1的结构体的首地址就是t1结构体第一个成员ch的首地址。
根据细则2:
当为in开辟空间时,编译器检查预开辟空间的偏移,即ch后面一个一节的地址空间d9偏移,为1,不是in_mod的整数倍,所以向后找,一直到地址dc,偏移为4,刚好为in对齐模数的1倍,故在此开辟向后开辟4字节的地址空间。在d8与dc之间的地址中填充数据。
根据细则3:
当为sh分配空间后,此时结构体的大小为4+4+2 = 10, 10并不是t1_mod的整数倍,故在sh后填充两字节的数据,从而达到结构体自身的对齐。所以结构体总大小为12。
从下图可以看出该结构体成员赋值的过程:
再看一下程序:
1 #include
2 #include
3
4 struct t1_stru
5 {
6 uint8_t ch;
7 uint32_t in;
8 uint16_t sh;
9 };
10
11 struct t2_stru
12 {
13 uint8_t ch;
14 struct t1_stru t1;
15 uint16_t sh;
16 }t2;
17
18 int main()
19 {
20 t2.ch = 0x12;
21 t2.t1.ch = 0x23;
22 t2.t1.in = 0x
23 t2.t1.sh = 0xABCD;
24 t2.sh = 0xACEF;
25 printf("sizeof(t2) = %d\n", sizeof(t2));
26 return 0;
27 }
输出结果为:
sizeof(t2) = 20
分析的方法跟上面的例子一样,当结构体中含有结构体时,计算其大小时,其实就是根据细则不断的嵌套的计算。
首先计算出t2成员对齐模数与t2结构体的对齐模数:
t2.ch_mod = MIN(4, sizeof(t2.ch) = 1;
t2.t1_mod = MIN(4, sizeof(t2.t1)) = MIN(4, 12) = 4;
(计算siezeof(t2.t1)则是按照上面的例子那样计算,得12)
t2.sh_mod = MIN(4, sizeof(t2.sh)) = 2;
t2_mod = MIN(4, MAX(t2.ch_mod, t2.t1_mod, t2.sh_mod)) = 4;
故sizeof(t2) = 20;
下图为t2的内存示意图:
3,关于C语言的位段
位段以位为单位定义结构体(或共用体)中成员所占存储空间的长度。还有位段的结构体类型成为位段结构。对于定义位段的变量类型只能为:字符型与整形。在Linux gcc 下,对不同类型的位段结构采用压缩存放的方法。位段结构的引用同结构体成员中的数据引用一样,但应注意位段的最大取值范围不要超出二进制位数定的范围,否则超出部分会丢弃。
位段结构的定义格式为:类型 <成员名>:<占用位数>
(1)一个位段必须存储在同一存储单元(即字)之中,不能跨两个单元。如果其单元空间不够,则剩余空间不用,从下一个单元起存放该位段。
(2)可以通过定义长度为0的位段的方式使下一位段从下一存储单元开始。
(3)可以定义无名位段。
(4)位段的长度不能大于存储单元的长度。
(5)位段无地址,不能对位段进行取地址运算。
(6)位段可以以%d,%o,%x格式输出。
(7)位段若出现在表达式中,将被系统自动转换成整数。
最新评论