结构体内存对齐
结构体内存对齐问题是结构体中非常重要的知识点
请看代码:
struct s1
{
char c1;
char c2;
int i;
};
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
struct s1 S1;
struct s2 S2;
printf("%d %d\n",sizeof(S1),sizeof(S2));
return 0;
}
为什么成员顺序不一样,结构体的大小就不一样呢?
结构体的对齐规则说明:
1 第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数:系统默认最大对齐数或该成员的大小,二者之中的较小值
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
图片中,是S2的结构体内存对齐,按照上述方式,来分析一下S1:
(1)首先C1在0偏移量地址处;
(2)C2大小为1字节与VS默认的8对齐数相比选小值,所以C2的对齐数是1的整数倍,1即可;
(3)i的大小为4字节,相比VS默认的8对齐数,选小值,所以 i 的对齐数是4的整数倍,而当前C2在1偏移量处,需要浪费2、3偏移量地址,才是4的整数倍,所以是4、5、6、7处的偏移量地址
(4)结构体的总大小,是所有对齐数里(1、1、4中)最大的对齐数的整数倍,而此时的字节总大小刚好是8个,所以结构体的大小就是0-7偏移量处的地址
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
struct S3
{
double d;
char c;
int i;
}s3;
//练习-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
struct S4 S4;
printf("%u %u\n",sizeof(S4),sizeof(s3));
return 0;
}
S3的自己的最大对齐数是8,总大小是16
在S4中,char c1在0处;
S3的对齐数倍数从8开始到23,共16个字节大小;
double d的对齐数是8,从24开始到31共8个字节大小;
S4的总大小就是成员里最大对齐数的整数倍,就是8的整数倍,当前在31处,刚好是32个字节大小,是8的整数倍
以上4条及其示例就是结构体内存对其的基本规则解释
结构体内存对其的基本规则
1. 第一个成员在与结构体变量偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数:系统默认最大对齐数或该成员的大小,二者之中的较小值
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数 倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
5. 没有默认对齐数时,自身大小就是对齐数
计算偏移量
利用 offsetof - 宏,头文件<stddef.h>,可以计算结构体成员,相对起始位置的偏移量,代码如下:
#include <stddef.h>
struct S3
{
double d;
char c;
int i;
}s3;
int main()
{
printf("%d %d\n",offsetof(struct S3,i),offsetof(struct S3,d));
return 0;
}
可以对比上述内存对齐的输出结果进行比较
为什么会有内存对齐?
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
例如32位的平台,每次操作4个字节,若没有内存对齐,char型后面接int,当想要读取到int的内容,需要读取两次(char和int共5个,第一次读前4个,第二次往后读4个),才能完全读完;而使用内存对齐,int在char后面找其整数倍对齐,刚好一次就读到了int的内容
所以结构体的内存对齐是拿空间来换取时间的
所以在设计结构体的时候,让占用空间小的成员尽量集中在一起。
例如最上面的 struct S1,就是前面两个char型挨着,减少了内存浪费
还可以修改默认对齐数,来达到减少内存浪费
修改对齐数
#pragma 这个预处理指令,可以修改对齐数
#pragma pack(8) 设置默认对齐数为8
#pragma pack() 取消设置,还原默认
不同的编译平台,默认对齐数是不同的
结构体的自引用
若是结构体自引用时,要使用指针,否则会循环
struct Node
{
int data;
struct Node next;
};
上述结构体打印它的大小时,是个未知数
struct Node
{
int data;
struct Node* next;
}
自引用时要使用指针
结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
输出结果一样:
但是当结构体传参时,参数会有压栈的过程,是会产生时间和空间开销(形参是实参的一份临时拷贝,在栈区临时创建一份相同的,结束时销毁),假如结构体过大,那么传参大效率就会很低;
若是传的地址,则只是创健一个“临时的一级指针”(32位上为4字节大小)来保存传过来的结构体地址,然后通过地址就可以间接操作原结构体成员,在空间和时间上节省很多,若是只想读取,可在开头加上const即可
所以:结构体传参尽量传结构体的地址
位段
位段的声明和结构是类似的,有两个不同:
-
位段的成员必须是整型家族的数据类型
-
位段的成员名后边有一个冒号和一个数字。
例如:struct A { int _a:2; a占2个bit (位) int _b:5; b占5个bit (位) int _c:10; c占10个bit(位) int _d:30; d占30个bit(位) };
struct S {
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
int main()
{
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
请看内存中的现象,解释如下:
第一个字节
a在 0000 0000 中占3位,a是10,就是1010,取三位就是 0000 0010
b在 0000 0 中占4位,b是12,就是1100,取四位就是 0110 0010
c在 0 中占5位,而第一个字节剩一位,则再新申请一个字节
第二个字节
则c在 0000 0000 占5位 ,c是3,就是0011 取5位就是 0000 0011
d在 000 中占4位,剩余3位,则再次申请一个字节
第三个字节
d在 0000 0000 占4位,d是4,就是 0100 取四位就是 0000 0100
第一个字节 0110 0010 = 0x62,第二个字节 0000 0011 = 0x03,第三个字节 0000 0100 = 0x04
所以共占3个字节,小端存储 62 03 04 与输出结果一致
位段的设计就是为了节省空间,而内存对齐是牺牲空间,所以位段就是为了不内存对齐
位段不可跨平台:
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。(平台和平台之间的大小端不一样)
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
所以位段在内存分配上:
5. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
6. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
7. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
枚举
枚举enum是一组被命名的整型常量的集合,是C语言中的一种基本类型,是把可能的取值一 一列举出来
枚举的定义
enum Day//星期
{
Mon,
Tues, 注意这里是 , 逗号
Wed,
Thur,
Fri,
Sat,
Sun
}; 结尾是;分号
int main()
{
printf("%d %d %d\n",Mon,Tues,Wed);
return 0;
}
若没给枚举初始化,则第一个常量被默认为0,后面的常量会递增 1,2,3…
若只给其中指定成员初始化会什么结果?请看代码:
enum Day//星期
{
Mon = 1,
Tues,
Wed,
Thur = 9,
Fri,
Sat,
Sun
};
int main()
{
printf("%d %d %d\n",Tues,Thur,Fri);
return 0;
}
输出结果可知:其他未初始化的成员,会在初始化成员的后面接着递增
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
#define Mon #define则没有
如 enum Day c = Mon; 会提示有多个相同
-
防止了命名污染(封装)
-
便于调试
程序编译的过程是:预编译 -> 编译 -> 汇编 -> 链接 -> .exe..... 假如是#define Mon,在预编译时,就把代码里所有Mon替换为一个常量并删除Mon了,当需要调试时,虽然写着是Mon但实际已经是一个常量了,与真实的代码相比看不出变化,不便于调试; 用枚举,就不会完全替换了,读到Mon,就是Mon,便于调试
-
使用方便,一次可以定义多个常量
联合(共用体)
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 比如:
对联合体赋值会怎样?请看代码:
//联合类型的声明
union Un
{
char c ;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
int main()
{
un.c = 1;
printf("%d\n", un.i);
return 0;
}
说明,int i 的4个字节与 char c 的有一个字节是共用的,具体分析如下:
c在 00 00 00 00 00 00 00 00只占最后两个(1字节),当赋值为1时,内存是:00 00 00 00 00 00 00 01,i打印是,打印4个字节,所以也是1,同时证明了是小端存储
联合体的计算
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un1
{
char c[5]; 0~4
int i; 最大对齐数4,5不是4的整数倍
}; 浪费3个内存,结果为8
union Un2
{
short c[7]; 0~13
int i; 最大对齐数4,14不是4的整数倍则浪费2个内存 结果为16
};
printf("%d\n", sizeof(union Un1)); 8
printf("%d\n", sizeof(union Un2)); 16
总结
- 要注意结构体的对齐规则
- 在设计结构体的时候,让占用空间小的成员尽量集中在一起。
- 使用位段时,不要对齐,但不可跨平台
- 结构体传参尽量传结构体的地址
- 枚举是常量,要注意枚举初始化的(常量之间递增1)
- 联合体也需要内存对齐