测试代码:
引用#include <stdio.h>
struct demo {
char c;
int i;
} test;
int main()
{
test . c = 'X';
test . i = 8;
printf( "struct demo sizeof : %d /n " , sizeof( struct demo));
printf( "struct test sizeof : %d /n " , sizeof( test));
return 0;
}
运行及输出:
引用beyes@linux-beyes:~/C/base> ./sizeof.exe
struct demo sizeof : 8
struct test sizeof : 8
说明:
按照预想,输出应该为 5 ( 1 个字节的 char 和 4 个字节的 int 类型 ),但输出为 8。究其原因,是涉及到了字节对齐。因为字节对齐可以加快计算机的读取速度,不然就得增加指令和多花指令周期。为此,编译器默认会对结构体进行处理,实际上其他地方的数据变量也是如此。让宽度为 2 的基本数据类型 ( 如 short 类型 )都位于被 2 整除的地址上,让宽度为 4 的基本类型 ( 如 int 类型 )都位于被 4 整除的地址上,以此类推。这样,两个数的中间就有可能需要加入填充字节,所以整个结构体的 sizeof 的值就增长了。对上面的例子,用 GDB 调试跟踪看看:
引用$1 = (struct demo *) 0x804a01c
(gdb) x/16b 0x804a01c
0x804a01c <test>: 0x58 0x00 0x00 0x00 0x08 0x00 0x00 0x00
在 0x58 后面填充了 3 个字节0x00 .
字 节对齐和编译器实现有关,一般情况下满足 3 个准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小整除。
- 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是成员大小的整数倍,如果需要编译器会在成员之间加上填充字节( internal adding )。
- 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节 (trailing padding)。也就是说结构体或者类的自身对齐值是成员中自身对齐值最大的那个值
如果指定对齐值:
4. 指定对齐值:#pragma pack (value)时的指定对齐值value。
5. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
对于上面第 3 条,如果把上面程序中的结构体中的成员 c 和 i 的定义调换一下顺序,sizeof() 的结果同样是 8 ,这是因为在 char c; 后面同样的填充了 3 个字节。
对于上面第 2 条,结构体某个成员相对于结构体首地址的偏移量可以通过宏 offsetof() 来获得,如下代码:
引用#include <stdio.h>
#include <stddef.h>
struct demo {
char c;
int i;
char name [ 20 ];
} test;
int main()
{
test . c = 'X';
test . i = 8;
printf( "struct demo sizeof : %d /n " , sizeof( struct demo));
printf( "struct test sizeof : %d /n " , sizeof( test));
printf( "test.name offset is : %d /n " , offsetof( struct demo , name));
return 0;
}
运行及输出:
引用beyes@linux-beyes:~/C/base> ./sizeof.exe
struct demo sizeof : 28
struct test sizeof : 28
test.name offset is : 8
字节填充例2:
引用#include <stdio.h>
#include <stddef.h>
struct demo {
char c;
int i;
char name [ 20 ];
} test;
struct student {
char name [ 10 ];
int date;
//struct demo test;
double k;
};
int main()
{
test . c = 'X';
test . i = 8;
printf( "struct demo sizeof : %d /n " , sizeof( struct demo));
printf( "struct test sizeof : %d /n " , sizeof( test));
printf( "test.i offset is : %d /n " , offsetof( struct demo , name));
printf( "--------------------------------------------------- /n ");
printf( "struct student sizeof is : %d /n " , sizeof( struct student));
printf( "student.k offsetof is : %d /n " , offsetof( struct student , k));
return 0;
}
运行及输出:
引用beyes@linux-beyes:~/C/base> ./sizeof.exe
struct demo sizeof : 28
struct test sizeof : 28
test.i offset is : 8
---------------------------------------------------
struct student sizeof is : 24
student.k offsetof is : 16
上面,对于结构体 student 的大小是 24,其中 k 元素的偏移为 16。这里的填充基于的原则还是数据的存储对齐:k 是 double 类型,8 个字节; date 是 int 型 ,4 个字节;char name[10] 数组,原本是 10 个字节。但 char name[10] 数组可分为两部分来看,(8 + 2) 个字节,其中前面 8 个字节是 4 个字节的 2 倍,而剩下的 2 个字节需要再填充 2 个字节才能是 4 个字节的 1 倍。所以,整个结构体被填充了 2 个字节,从而是 24 个字节,也就是 k 的偏移为 16 。
基本类型有有 char , short , int , float ,double 等内置数据类型,这些数据类型的宽度用 sizeof() 来计算。对于字节填充,涉及到一个“最宽简单类型” 的概念。最宽简单类型可以描述为,是其所有小于它的类型的整数倍,而又能被大于它的数据类型整除。在满足此条件后,在出现的所有符合这个规律的数据类型中,最宽的那个。比如在只有 char 与 short 两个类型的结构体中,最宽简单类型是 short ,如下测试代码:
引用#include <stdio.h>
#include <stddef.h>
struct max {
char c;
short x;
char u;
};
int main()
{
printf( "struct max sizeof is : %d /n " , sizeof( struct max));
return 0;
}
输出结果:
struct max sizeof is : 6 |
再如,在 char, short, int, double 里,最宽简单类型应该是 int (由于是“简单“的,那么表名这个数据类型在所有出现的,既有比它大的也有比它小的类型里,它肯定不会是最小的那个也不会是最大的那个)。如下测试代码:
引用#include <stdio.h>
#include <stddef.h>
struct demo {
char c;
int i;
char name [ 20 ];
double m;
} test;
struct student {
char name [ 10 ];
int date;
struct demo test;
double k;
};
int main()
{
test . c = 'X';
test . i = 8;
printf( "struct demo sizeof : %d /n " , sizeof( struct demo));
printf( "struct test sizeof : %d /n " , sizeof( test));
printf( "test.i offset is : %d /n " , offsetof( struct demo , name));
printf( "--------------------------------------------------- /n ");
printf( "struct student sizeof is : %d /n " , sizeof( struct student));
printf( "student.k offsetof is : %d /n " , offsetof( struct student , k));
return 0;
}
运行及输出:
引用beyes@linux-beyes:~/C/base> ./sizeof.exe
struct demo sizeof : 36
struct test sizeof : 36
test.i offset is : 8
---------------------------------------------------
struct student sizeof is : 60
student.k offsetof is : 52
上面程序,student 结构体中还包含了demo 型结构体 test。这里比较最宽简单类型不会把 test 这个结构体作为一个整体来看待,而是将其中的各个元素打散来看。 如此以来,student 型结构体中,最宽的数据类型是 double ,共占 8 个字节,而最小的数据类型是 char ,占据 1 个字节,最宽简单类型自然是 int 型,它是 char 的 4 倍,又能被 double 整除。基于此,容易分析出 student 的宽度为 60,student.k 的偏移量为 52:
char name[10] 为 8 + 2 个字节,最后 2 个字节被填充为 4 字节 (最宽简单类型 int 的字节数),共占 12 个字节;
int date 占 4 个字节;
test 中,char c 为 4 个字节,int i 为 4 个字节,char name[20] 为 20 个字节 (20/4 = 5) ,double m 为 8 个字节;
double k 为 8 个字节。
总共:12 + 4 + 4 + 4 + 20 + 8 + 8 = 60 个字节,k 前面偏移了 52 个字节。
字节对齐可能带来的隐患:
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;
p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.
如何查找与字节对齐方面的问题:
如果出现对齐或者赋值问题首先查看
1. 编译器的big little端设置
2. 看这种体系本身是否支持非对齐访问
3. 如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。