结构体内存对齐
- 计算结构体的大小:我们都知道数据类型都有大小,不同的数据类型占有的字节不一样。比如,char占有1个字节,int占有4个字节……。结构体也是一种数据类型,也有大小。我们先来举个计算结构体大小的例子,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd\n",sizeof(struct S1));
return 0;
}
大家可以先猜一下这个结构体的大小。我们先来猜测一下:成员变量有 char ,int
和char
。char占有1个字节,int占有4个字节,那么这个结构体的大小为 1+4+1=6个字节。答案是不是这个呢?我们来运行一下这个结果吧。
运行结果与我们的猜测大相径庭,这是为什么呢?这就要掌握结构体的内存对齐规则了。
- 内存对齐规则:如下所示:
- 1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 2 .其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。- VS中默认的值为8.
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小。
- 3 .结构体总体的大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 4 .如果有嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
- 进行讲解: 我们来讲解上面代码的结果为什么是12个字节。首先来看结构体内存对齐规则【1】,遇到结构体时,内存会先创建个预处理空间(不够时再增加,多余时就去掉多余的部分)。然后,我们把第一个成员变量(char c1)放在离起始地址偏移量(偏移量:就是与起始地址相差的字节数)为
0
的地方。如图所示:
我们第一个成员的位置已经放好了,接着放第二个成员(第三个成员类推)。看内存对齐规则的第2个规则,我们根据这个规则进行分析:
类型 占有字节数 默认对齐数 对齐数
int i 4 8 4
char c2 1 8 1
我们要找到偏移量为4(对齐数)的最小整数倍,然后再把 int i给放进去,char c2就紧跟其后,
因为无论偏移量为多少都是1(对齐数)的倍数。
如图所示:
我们按照前面的两个规则,已经把结构体里面的成员变量给安排好了。我们数一下后,发现占有的空间为9个,但是我们输出结果是12,还是不一样。这就要用到内存对齐规则3。我们可以数一下,共占有9个字节(从char c1开始到char c2结束,包括之间的空白字节),9个字节不是最大对齐数(4)的整数倍,我们要再增加3个字节到12个字节,刚好是4的整数倍。所以,最后结构体的大小为12个字节。如图所示:
- 计算含结构体的结构体大小:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S4
{
char c1;
struct S1 s3;
double d;
};
int main()
{
printf("%zd\n",sizeof(struct S4));
return 0;
}
大家可以猜测一下这个结构体的大小。我们进行结果图展示:
我们在struct S4
包含了 struct S1
结构体类型,我们在上面已经计算过了struct S1的大小——12个字节。我们知道,char,double分别占1个字节和8个字节,struct S1占有12个字节,如果我们抛开内存对齐规则,估计也就是21个字节左右。但是,答案是24个字节。这就需要用到对齐规则4了。我们知道 struct S1的最大对齐数是4。所以,struct S1的偏移量要为自己最大对齐数(4)的整数倍。关于double和char对齐就不讲了,类比前面就OK了。进行如图所示:
- 结构体的弊端:不知道大家有没有发现,我们根据内存对齐规则所画的内存结构图,之间有很多的空白字节,而这些空白字节我们什么数据也没存,就那样空着了,这就是结构体的弊端——浪费内存空间。而这些浪费的空间,我们是无法使用的,就那样空着了。我们接下来讲的结构体的位段就很节省空间。
- 修改内存对齐数:在VS中默认对齐数为8,如果我们感觉到不合理的话,我们还可以修改这个默认的对齐数。【注意:我们修改后要再修改回来,以免影响我们后面在使用(万一我们还想使用默认呢)】。我们就以开头的代码进行展示(做个对比):
#pragma pack(数字) //修改默认对齐数的代码
·····
·····
#pragma pack() //恢复默认对齐数值得代码【括号里面。啥也不填】
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#pragma pack(1) //默认对齐数修改为 1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack() //恢复默认对齐数
int main()
{
printf("%zd\n", sizeof(struct S1));
return 0;
}
结果运行图:
- 为什么会有内存对齐数?:我们在一个内存里面直接放上数据不就OK了吗?那样做还可能比内存对齐要省空间些。之所以这样做有两个原因:
- 1.平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些 硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。
- 2.性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
- 3总结:结构体就是拿空间来换取时间得效率。
结构体传参
咱们见过数组传参,函数传参和普通数值传参等。今天讲的结构体传参也没什么新奇的,就是创建个结构体类型数据,然后传过去就OK了。我们来讲两个传参方式:
- 传值传参:进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char c1;
int i;
char arr[20];
};
void my_printf(struct S1 s)
{
printf("%c %d %s\n",s.c1,s.i,s.arr);
}
int main()
{
struct S1 c1 = {'w',100,"zhangsan"};
my_printf(c1);
return 0;
}
结果运行图:
- 传址传参:进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct S1
{
char c1;
int i;
char arr[20];
};
void my_printf(struct S1* s)
{
printf("%c %d %s\n",s->c1,s->i,s->arr);
}
int main()
{
struct S1 c1 = {'w',100,"zhangsan"};
struct S1* P = &c1;
my_printf(P);
return 0;
}
结果运行图:
结构体传参也没什么特别的地方,正常用就OK。
结构体实现位段
前面,咱们提到过,位段可以大大节省内存空间。我们从它的名字——位段。就能猜个大概,猜测它与位有关。答案就是如此.我们先看看这位段长啥样,进行代码展示:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
是不是和结构体长得很像,只是成员变量差别挺大的。
- 位段的解读:
- 1 .位段的声明,定义变量和结构体类似,就是成员变量不同。
- 2.位段的成员变量只能是 int ,unsigned int和char类型(在C99之前)。C99中,位段成员可是其它类型。
- 3.位段的成员格式:数据类型+变量+冒号+数字+分号。
- 4.变量后面的数字就是表示,你想给数据申请多少位。多了更好,不够就发生数据截断。
- 位段的大小计算:进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct s
{
char i : 3;
char k : 5;
char j : 8;
};
int main()
{
printf("%zd\n",sizeof(struct s));
return 0;
}
结果运行图:
- 位段申请空间:编辑器会先给第一个成员变量分配好空间。我们第一个成员申请了3位,现在有个问题 ,这3位是从右边排呢?还是从左边排呢?如图所示:
对于从左向右排,还是从右向左排。C语言中未定义(未规定),具体的排法由编译器厂商决定了。VS中是从右向左排的。当编译器为第一个成员分配耗空间后,我们也申请了3个位,第一个字节还剩5个位。这个时候,char k申请5个位,它俩刚好凑齐一个字节。这个时候 ,到char j 申请了8个位,也刚好一个字节。所以,它们总共占有2个字节。 - 注意事项:
- 1.位段的里面的成员一般都是相同类型的数据,不同类型很少见。因为位段本身不稳定,两个不同的数据可能会占用同一个字节,这就导致不稳定性,读取数据就可能出错。
- 2.位段不可以用指针访问。因为我们只按字节给的单位(硬件就是那么设计的),没有给每个位进行编号,所有,无法通过地址去访问位里面的数据。
- 位段的使用:
- 1.我们可以直接赋值,进行代码展示:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct s
{
char i : 3;
char k : 5;
char j : 8;
}s1;
int main()
{
s1.i = 10;
s1.k = 12;
s1.j = 13;
printf("%d %d %d\n",s1.i,s1.k,s1.j);
return 0;
}
结果运行图:
我们发现,输出的结果与我们输入的数值有些不符合,这就与我们申请多少位有关了!!!。如图所示:
- 为我们还可以输入赋值:进行代码展示:
这样的代码是错的:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct s
{
char i : 3;
char k : 5;
char j : 8;
};
int main()
{
struct s s1 = {0};
scanf("%d",&(s1.i)); //错误的输入方式,不可以取地址
return 0;
}
因为咱们讲过了,位(它没有地址)是不可以通过指针(地址)访问的。
正确的访问方式:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct s
{
char i : 3;
char k : 5;
char j : 8;
};
int main()
{
struct s s1 = {0};
int b = 0;
scanf("%d",&b);
s1.k = b;
printf("%d\n",s1.k);
return 0;
}
结果运行图:
我们需要创建个中间值变量,通过这个中间变量进行赋值。
-
位段的优点: 节省内存空间。
- 位段的应用:基于它比较节省内存空间,所以基于它这个优点,我们应用于网络协议中。如图所示的网络协议图:
我们知道现在是信息很发达的时代,我们每天都在从网络上获取我们想要的数据,我们想要的数据或者发送的数据,都要经过这个网络协议。这个网络协议就像快递一样,把我们要发送或接收的数据进行打包,形成了数据包,放在网络上。既然放在网络上,那么数据包就要小,要不然网络就贼卡。因为,位段很省空间,所以就会按照位段形式进行数据打包。
-
位段的缺点:
- 1.位段不能跨平台使用,因为它申请位的顺序不确定,而且对于多余的空间是舍弃,还是保留,也没定义。不同的编译器处理的方式不同,所以不能跨平台。
- 2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
彩蛋时刻!!!
https://www.bilibili.com/video/BV13P411n7DL/?spm_id_from=333.337.search-card.all.click&vd_source=7d0d6d43e38f977d947fffdf92c1dfad
每章一句:生活不是为了赶路,而是为了感受路
感谢你能看到这里,点赞+关注+收藏+转发是对我最大的鼓励,咱们下期见!!!