前言:
我们知道,在C中,允许我们自己创建一些自定义类型,例如结构体,枚举,联合等,下面,我们就来看这些自定义数据类型在C中的相关知识。
目录详情
1.结构体struct
结构体是一些值得集合,这些值称为成员变量,结构体的每个成员可以是不同类型的变量这一点是和数组的区别。
1.1结构体的声明
struct tag{
成员列表;
}变量列表;
需要注意的是,在变量列表里声明的变量是全局变量,在main函数声明的是局部变量。tag这个名字也可以省略掉(匿名结构体),但是在声明结构体的时候只能在变量列表中声明,存在一些限制,不推荐使用。
1.2结构体的自引用
struct node {
Elemtype data;
struct node* next;
};
这就涉及到了链表的定义,你有没有想过,链表的定义为什么是一个指针而不是一个原结构体的变量数据,这是因为计算机在判断结构体大小时,会根据其变量类型进行判断,当判断一个结构体大小时,如果内部还有一个结构体变量,就会导致在判断大小时陷入一个循环中,类似于递归会无限进行下去,造成死循环,所以我们采用指针的形式,可以的进行结构体的大小。
1.3结构体的内存对齐
到了这里,就是进阶的知识了,其实前面的那些都是结构体基础知识,所以就一笔带过了,到了这里才是真正的重点,话不多说,我们直接开始。
背景引入
如果你能清楚的解释这三个输出,说明你是大佬,关公面前舞大刀,见笑了,嘿嘿~~
咳咳,还是我来介绍一番吧,
1.3.1结构体的对齐规则
1.第一个成员在与结构体偏移量为0的地址处;
2.其他结构体成员变量要对齐到某个数字(对齐数)的整数倍的地址处去
对齐数=min(编译器默认的一个数字,该变量的大小);
而对于不同的编译器来说,vs上的默认的数字是8,后序将会以vs环境下的编译进行讲 解;而Linux没有默认的对齐数,对齐数就是成员变量的自身大小;
3.结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
4.如果是嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体整体的大小就是所有的最大对齐数(含嵌套结构体)的整数倍;
规则上比较抽象,我们回到我们刚才的背景题目中,用题目来解释这些规则:
对于s1结构体,我们输出的答案是8,我们用内存结构来解释一下:
首先,我们的s1有int和char两个数据,对于第一个数据int自然没什么好说的,占用内存的四个字节,且偏移量为0,接着,从第二个开始,我们就要执行对齐规则,我们知道vs环境下默认的对齐数是8,gcc环境下没有默认对齐数,对于char类型的变量来说,对齐数=min(8,1)=1,所以我们要选择的地址的位置一定要是1的倍数,实际上就在int型数据的后面紧挨着就行,这也才5个字节啊,为啥输出的是8个字节呢?这就要看我们的第三条规则了
3.结构体的总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
在这里int的对齐数是4,char的对齐数是1,所以我们取4,4的倍数还要比5大,最小的只能是8了,此时我们就会有三个字节的地址空间被浪费掉,形成下面的地址结构:
同样的,对于s2,我们也可以画出如下的内存结构图:
这里,我们引入偏移量计算函数offsetof:
offsetof(结构体类型,结构体成员变量名);
对于s2,我们使用offsetof来查看每个成员变量的字节偏移量
对于s3不在展开说明,这里只给出内存结构图示
我们再来看嵌套结构体的情况,也就是第四条规则:
4.如果是嵌套结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体整体的大小就是所有的最大对齐数(含嵌套结构体)的整数倍;
我们还是来一个例子
如上代码中,s2的结构体中嵌套了结构体s1,那么s2的结构体大小就是所有成员变量中对齐数的最大对齐数(包括被嵌套的结构体),也就是将被嵌套的结构体看成一个普通的成员变量来计算其结构体的大小:
下面是两个结构体的内存结构示意图:
为什么会存在内存对齐规则?
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的数据的,某些地址平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常;
2.性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐,原因在于,处理器在访问对齐的内存时只需要做一次内存访问,而访问未对齐的内存时需要做两次访问,本质上是空间换时间的做法。
我们举个简单的例子:
struct s{
char a;
int b;
};
那么,我们应该如何设计结构体来尽量的节省空间和时间呢?
1.尽可能的让类型较小的成员集中在一起;
通过前面的例子我们可以知道,从第二个成员开始,对齐数要满足默认对齐数和自身类型的较小值的倍数,所以我们前面的类型成员越小,那么前面造成浪费的空间就会越小,从而使结构体整体大小变小。
2.修改默认对齐数
#pragma pack(修改的默认对齐数);
#pragma pack();//恢复默认对齐数
1.4拓展:offsetof宏的实现
前面我们简单介绍了一下offsetof宏的功能,下面我们来简单实现一下:
思路上:我们可以将结构体的起始地址设置默认的0,然后结构体的地址找到成员变量的地址,注意前值类型转换为size_t就行了,最好加上括号
s1在上面的例子中的内存结构图已经给出,需要可自行上去比对。
1.5结构体传参问题
我们都知道,传参有两种方式,传址和传值,这两种的区别,就是在函数传参的时候系统带来的函数栈的开销,在函数传参时,我们需要开辟新的内存空间用于存储参数压入栈中,如果传值,那么开辟空间的大小和结构体的大小有关,如果结构体内存过大,就会导致压入栈的内存过大,造成空间浪费,所以我们一般采用传址调用,只开辟一个指针的大小的空间,再加上适当的const修饰符来避免传址调用导致原数据被外部函数修改。
*1.6位段
什么位段,我只知道“段位”,嘿嘿~~
1.6.1什么是位段
位段--“位”-二进制位
位段和结构体的声明类似,但是也有不同点:
1.位段的成员变量类型必须是int,unsigned int或者是signed int和char(属于整形家族);
2.位段的成员名后面有一个冒号和数字(如果没有,则按其数据类型分配比特位即可);
比如:
struct A{
int _a:10;
unsigned int _b:20;
int c:30;
};
我们说位段中成员变量的冒号后面的数字就是给该变量开辟的比特位数,这种情况使我们在已知该变量所表示的数据范围的情况下才能使用,比如a表示的范围是0-7,那么我们就可以只给他开辟3个比特位即可表示该范围内的任意数,其他的同理;
1.6.2位段的内存分配
1.位段的空间是按照需要以4个字节(int)或1个字节(char)的方式开辟的;
2.位段涉及很多不确定因素,应当尽量避免跨平台使用,可移植程序也应该尽量避免使用位段
背景引入
注意,我们还是在vsX86环境下分析这个问题:
首先,我们先开辟4个字节(32个比特位)的空间,-a,_b,_c三个变量都用完之后还剩(32-2-5-10=15个),我们需要再开辟4个字节来继续存储_d变量,出现了第一个不确定因素,我们前面剩下的15个比特位还用不用?(这一点c标椎库并未给出明确的规定,这并不影响我们的结果),最终我们的内存还是开辟了8个字节;
接下来我们再来讨论一下在vs编译器环境下,内存是如何具体分配的:
我们下来假设两个点:
1.假设分配到的内存是从右向左使用的(即从低地址想高地址使用的);
2.分配好的内存不够使用的时候,剩余的部分将不再使用。
我们在vsX86环境下进行调试,并查看内存的存入情况;
我们再来看一下我们的调试结果:
事实证明,我们的假设是正确的(咳咳,我是不会告诉你们我先找到结论然后假设的给你们看的,哎嘿~~)
1.6.3位段的跨平台问题
1.int位段被当成有符号数还是无符号数是不确定的;
2.位段中的最大数目不能确定(16位机器最大为16, 32位机器上最大为32)
3.位段中的成员内存分配从右向左还是从左向右是不确定的(vs上从右向左);
4.在上面我们的例子中的第一个分配单位的剩余位是否能再次被下一个成员变量使用也是不确定的;
总之,位段相比如结构体来说确实能节省空间,但是也会存在跨平台问题;
1.6.4位段的应用
在网络数据报文的传输中,我们可以根据不同的报文段的数据大小因地制宜的设计数据报长度,可以最大程度的节省网络带宽开销,提高传输速度。
2.枚举
2.1枚举类型的定义
把可能的取值一一列举出来
如:
enum Sex{ //性别
//枚举的可能取值从0开始,每次+1,可以在定义时给一个初始值改变其默认值
Male,
Female,
Secret
};
枚举的优点:
1.增加代码的可读性和可维护性;
2.和#define定义的标识符比较枚举有类型检查,更加严谨;
3.便于调试,一次可以定义多个常量
3.联合
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。 比如:
//联合类型的声明
union Un {
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
3.1联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
可以看到,成员i和c的起始地址完全一样,所以有我们的i和c共同占用4个字节的空间,并且i和c不能同时使用,因为他们共用一块空间,这在一定程度上也给我们节省了空间,几个变量共享一块空间。
3.2联合体大小的计算
1.联合体的大小至少是最大成员的大小;
2.当最大成员大小不是最大对齐数的整数倍时,要对齐到最大对齐数的整数倍上去;
拓展:判断当前机器的大小端存储方式
union Un
{
char i; //每个元素的最大对齐数为8,类型大小为1,取对齐数为1
int n; //min(4,8)
}un;
int main()
{
un.n = 1;//十六进制为 0x 00 00 00 01
/*
而我们的小端存储对应的由低地址到高地址为 01 00 00 00
... 大端 低 ...高. 00 00 00 01
我们可以利用i和c共用一块空间,并且起始地址相同,所以i的值如果被改为为1,我们就可以判断其为小端存储,反之为大端
*/
if (un.i == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
好了,三大自定义类型就讲解完毕了,希望可以让你们能够更加深度的理解和认识三大自定义类型的相关知识。
4.金句省身
累了看父母,倦了看前程,你考的不是试,是前途和父母暮年的欢喜,你提升的学历和能力,是将来做选择和拒绝时的底气。