这是在读winnt.h头文件的源代码时发现了一段自己弄不明白的代码,继而牵扯到了结构体存储的内存边界对齐的问题。该问题在MSDN中说的比较少。以下是阅读MSDN中的一些重要信息:
1.可以使用 #pragma pack 指令来指定对齐字节,该指令指定内存对齐字节的功能和编译器选项 /Zp 是等效的。
2.#pragma指令对该指令之后的 struct 或 union 的声明有效。如果把这条指令放在介于数据结构的声明后面和这些数据结构的实例的定义前面,则不起任何效果。
3.在使用了 #pragma pack(n) 指定了内存对齐字节数后,struct 中除第一个成员外的每一个成员都会进行内存对齐,对齐的字节数是该成员本身类型所占的字节数和 #pragma pack(n) 中指定的对齐字节数中较小的那一个。比如一个int占4个字节,程序中指定8字节对齐,那么在存储这个int时会使用4字节对齐,意思就是这个int存储的起始地址必须是4的整数倍。struct 中的第一个成员存储的起始地址就是该 struct 的地址,不需要对齐(但可能会在它后面填充字节,这是属于存储第二个成员的问题)。一个 struct 本身所占的字节数是它的成员中需要对齐的字节数最大的字节数的整数倍。这些东西在以下的测试代码的输出结果中会有详细注释。
4.就是 #pragma pack 指令的一般语法形式为:#pragma pack( [ [ { push | pop}, ] [ identifier, ] ] [ n ] ).push表示把当前程序中的对齐字节数压入到编译器栈中,pop表示弹出编译器栈中的对齐字节数并把它做为当前程序的对齐字节数。至于 identifier 的用法就是为了更好地以更容易理解的形式保存和恢复,MSDN中有详细的说明和示例。
下面是测试代码:
#pragma pack(8)
typedef unsigned char uchar;
#define PRINT_FIELDS(fields)
printf(#fields " " );
printBinary((uchar * ) & fields, sizeof (fields))
#define PRINT_STRUCTURE(structure)
printf(#structure " " );
printBinary((uchar * ) & structure, sizeof (structure))
struct Test1 {
char x1;
short x2;
double x3;
char x4;
} ;
struct Test2 {
char x1;
short x2;
float x3;
char x4;
char x5;
} ;
void printBinary(unsigned char * mem, int size) {
int i;
while(size--) {
printf("%p: ", mem);
for(i = 7; i >= 0; i--) {
if(*mem & (1 << i)) {
printf("1");
} else {
printf("0");
}
}
mem++;
printf(" ");
}
}
int main() {
puts("Test Case 1:");
Test1 test1 = {0x10, 0x1020, 20.3f, 0x20};
puts("print fields:");
printf("sizeof(Test1) is: %d ", sizeof(Test1));
PRINT_FIELDS(test1.x1);
PRINT_FIELDS(test1.x2);
PRINT_FIELDS(test1.x3);
PRINT_FIELDS(test1.x4);
puts("print structure");
PRINT_STRUCTURE(test1);
puts(" Test Case 2:");
Test2 test2 = {0x10, 0x1020, 20.3f, 0x20, 0x20};
puts("print fields:");
printf("sizeof(Test2) is: %d ", sizeof(Test2));
PRINT_FIELDS(test2.x1);
PRINT_FIELDS(test2.x2);
PRINT_FIELDS(test2.x3);
PRINT_FIELDS(test2.x4);
PRINT_FIELDS(test2.x5);
puts("print structure");
PRINT_STRUCTURE(test2);
return 0;
}
下面是该测试代码产生的输出以及对这个输出结果的解释:
输出结果:
Test Case 1:
print fields:
sizeof(Test1) is: 24
test1.x1
0012FF58: 00010000 // test1.x1是char类型的。从起始边界存储。占一个字节
test1.x2
0012FF5A: 00100000 // test1.x2是short类型的,起始边界必须是min(sizeof(short), pack(n))
0012FF5B: 00010000 // 的整数倍。但刚才存了一个字节,必须填充一个字节才能凑成2的整数倍。
// 所以为了对齐边界,0012FF59被用来作为填充字节。
test1.x3
0012FF60: 00000000 // test1.x3是double类型的,起始边界必须是min(sizeof(double),pack(n))
0012FF61: 00000000 // 的整数倍。但刚才存了一个char,一个short,char后面有一个字节填充。
0012FF62: 00000000 // 到现在为止只存了4个字节,而test1.x3需要起始边界为8的
0012FF63: 11000000 // 整数倍。所以为了对齐边界,0012FF5C,0012FF5D,0012FF5E,
0012FF64: 11001100 // 0012FF5F这4个字节被用来作填充字节。
0012FF65: 01001100
0012FF66: 00110100
0012FF67: 01000000
test1.x4 // test1.x4是一个char,存储起始边界必须是1的倍数。紧挨着
0012FF68: 00100000 // 上面存就可以了。但是结构体到这里还没有结束。因为指定对齐
print structure // 字节数为8.而test1结构中最大对齐单元为test1.x3即8,
test1 // 到现在为止该结构占了 1(test1.x1) + 1(填充)
0012FF58: 00010000 // + 2(test.x2) + 4(填充) + 8(test.x3) + 1(test.x4) = 17.
0012FF59: 11001100 // 必须得凑成8个倍数,所以需要在最后填充7个字节使得该结构
0012FF5A: 00100000 // 占24个字节,恰好是8的倍数。所以该结构大小是24.
0012FF5B: 00010000
0012FF5C: 11001100
0012FF5D: 11001100
0012FF5E: 11001100
0012FF5F: 11001100
0012FF60: 00000000
0012FF61: 00000000
0012FF62: 00000000
0012FF63: 11000000
0012FF64: 11001100
0012FF65: 01001100
0012FF66: 00110100
0012FF67: 01000000
0012FF68: 00100000
0012FF69: 11001100
0012FF6A: 11001100
0012FF6B: 11001100
0012FF6C: 11001100
0012FF6D: 11001100
0012FF6E: 11001100
0012FF6F: 11001100
Test Case 2:
print fields:
sizeof(Test2) is: 12
test2.x1
0012FF4C: 00010000
test2.x2
0012FF4E: 00100000
0012FF4F: 00010000
test2.x3
0012FF50: 01100110
0012FF51: 01100110
0012FF52: 10100010
0012FF53: 01000001
test2.x4
0012FF54: 00100000
test2.x5
0012FF55: 00100000
print structure
test2
0012FF4C: 00010000
0012FF4D: 11001100
0012FF4E: 00100000
0012FF4F: 00010000
0012FF50: 01100110
0012FF51: 01100110
0012FF52: 10100010
0012FF53: 01000001
0012FF54: 00100000
0012FF55: 00100000
0012FF56: 11001100
0012FF57: 11001100
至于test2的结构大小,因为test2结构中最大对齐边界成员为test2.x3,一个float,对齐
边界是min(sizeof(float), pack(8)) = 4,所以test2结构的自然对齐边界为4字节。
所以在存储完test2.x5之后,总共
1(test2.x1) + 1(填充) + 2(test2.x2) + 4(test2.x3) + 1(test2.x4) + 1(test2.x5)
是10个字节,不是4的倍数,所以在后面填充2个字节,使得该结构的大小是4的倍数。
所以sizeof(test2) = 12。
以下是那个面试题的分析:
那个面试题的题目:
#pragma pack(8)
struct example1 {
short a;
long b;
};
struct example2 {
char c;
example1 e;
short s;
};
#pragma pack()
这两个结构,问它们各自的大小。
现在问题就很简单了,在example1中,short是2个字节,long是4个字节,但long要进行边界对齐,即它的存储起始地址必须是min(4,8)=4的整数倍。所以short后面会填充两个字节。这样,example1的大小就是:
2(short) + 2(填充) + 4(long) = 8个字节。对于example2,则是
1(char) + 7(填充) + 8(example1) + 2(short) + 6(填充) = 24个字节。对于第二个结构最后面的填充,在上面的注释里面已经解释。