为了提高CPU访问内存的效率,可能CPU在读取数据时会一次性读取4字节、或者是2字节、8字节等大小的数据,所以编译器在把数据存放于内存的时候,会自动对齐。
1. 字节对齐规则
字节对齐有下面几种规律:
1.1 以最大的成员占据的空间大小对齐
typedef struct _t{
char a; //1Byte
char b; //1Byte
int i; //4Byte
}t; //占据8Byte
4字节对齐,占据8字节,内存分布为:
1.2 剩余的空间足够,后面的变量会往前填补
typedef struct _t{
int i; //4Byte
char a; //1Byte
char b; //1Byte
short d; //2Byte
}t; //占据8Byte
4Byte对齐,其内存的分布为:
1.3 根据变量占据的空间从大到小,依次对齐
typedef struct _t {
double d; //8Byte
short s1; //2Byte
int i; //4Byte
short s2; //2Byte
}t;
先是8Byte对齐,后是4Byte对齐,共占据20Byte,内存分布为:
1.4 包含其他结构体时,不会拿该结构体的总大小来对齐
typedef struct _m {
int a; //4Byte
char b; //1Byte
char c; //1Byte
short n; //2Byte
}m; //共占据8Byte
typedef struct _t {
int aa; //4Byte
short bb; //2Byte
m mm; //8Byt
}t; //共16Byte
结构体类型m总共占据8Byte,但在计算结构体t类型的大小时,并不会把m为整体看成一个整体的大小来计算对齐字节,而是拆开m结构体,依次拿m里的成员来存放于内存中:
总共占据16Byte。
由于字节对齐特性的存在,可能会使得我们设计的结构体占据的空间会变大。这在某些场合是应该要避免的,如网络通讯,如无避免则会产生流量损失。所以合理设计结构体,即合理排放其成员变量十分重要。
2. 去除编译器的字节对齐规则
解决字节对齐带来的空间损耗问题,一种方法就是不要使用字节对齐规则,即使其1字节对齐:
#pragma pack(1)
struct t {
char a;
short b;
int i;
};
#pragma pack(0)
这对关键字包含起来的结构体就是1字节对齐的,同时,还可以使其用其他字节对齐,只要修改开头的”#paragma pack(n)”中的n即可。也就说,这样子操作后结构体的大小就单单是所有结构体成员占据的空间之和了。
上述的关键字是在C99标准下的,在C89中实现去除字节对齐的关键字是attribute((packed)):
struct t {
char a;
short c;
int k;
}__attribute__((packed));
3. 结构体中的位域
位域只能是在结构体中使用,其作用是为了节省空间。
比如我们要在结构体中定义一个变量来表示小学生的年龄(一般是6到14岁),显然,用char类型来定义变量都大了,所以可以借助位域的功能来限制char的位数。14(岁)转为二进制是0b1110,共占据5位,再加上符号位共5位,所以在描述一个学生信息的结构体中,其年龄可以声明为:
typedef struct _stu {
char age : 4;
char* name;
}stu;
这样,当age等于二进制位超过4位的数值时,编译会产生溢出警告。
限定了结构体成员的位域,那么其占据的空间也会随之改变:
typedef struct _stu{
char age : 4;
char a : 2;
char b : 2;
}stu; //4bit + 2bit + 2bit = 8bit = 1BYTE
如上占据了1字节。当然,如果在最后再定义一个char v : 2;,那么其大小自然就是2字节了。虽然说限制位域确实节省了空间,但是它节省的是位级别的空间,十分小,如果连位都要考虑浪费问题,那就有点变态了。