1. 引文
C语言中已经有很多内置类型,如:char,int,float,double等等,但是只有这几个内置类型是远远不够的,假如我们需要描述一个学生的基本信息,这只靠一个内置类型是不行的。
C语言为了解决这种问题,增加了结构体这种自定义的数据类型,让程序员可以创建适合的类型。
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
2. 结构体
2.1 结构体的声明
C语言中我们使用struct来定义结构体,不能直接用结构体名当类型,需要加 struct。
括号里面的就是成员变量;我们用实例来分析:
struct是结构体的关键字,Stu是结构体的名字,大括号里面定义的变量是成员变量,至于括号外面的s3和s4就是创建的结构体变量,这种创建结构体变量的方法比较简洁,如果这种创建方式是放到main函数里面的话就是创建的局部变量,我们也可以使用其他的创建方法:
注意无论是那种创建方法都大括号外面都必须用分号结尾。
2.2 结构体变量的创建和初始化
创建的话还是很简单的,就是注意分号,初始化有两种初始化的方式,一种是按照结构体的成员变量的顺序来进行初始化,这种方式十分简便;还有一种就是不按成员变量的顺序初始化,这种方式需要我们将被初始化的成员变量的名称也写上,这里注意一下:名称前面有一点,这点千万不要忘记了;初始化时我们可以不写结构体变量名,但是我们在打印的时候必须写上结构体变量名。
结构体是支持循环嵌套的。
2.3 结构体成员的直接访问和间接访问
我们前面的结构体变量.成员名,这就是直接访问。
有时候我们得到的不是⼀个结构体变量,而是得到了⼀个指向结构体的指针。这时候我们就要通过间接访问。
这种结构体指针->成员名,这就是间接访问的形式,注意这里是指针。
2.4 结构体的特殊声明
这种没有命名的结构体类型只能使用一次。
但是如果要想长期使用,我们可以对匿名结构体进行重命名:
使用typedef进行类型重命名,注意并不是只有匿名结构体时才能使用typedef来进行重命名,使用已有名称的结构体使用typedef来重命名可以省去每次创建结构体变量都要敲struct:
这里跟我前面的重命名方式有点不一样,这是另一种方式:
但是我那种也没错。
2.5 结构体的自引用
自引用就是结构体里面包含(指向自身类型的指针)。
注意:只能放指针不能直接放结构体变量(如果放结构体变量那就会无限套娃,内存大小根本无法计算)。放入指针,指针的大小是固定的。
结构体一般是结合typedef来一起使用的,这是我们后续学习链表的基础。
typedef在别名没有生效之前不能省略struct,这也就导致匿名结构体不能进行自引用。
2.6 结构体内存对齐
我们已经基本掌握结构体的使用方法了,接下来我们要讨论一个问题:结构体的内存大小怎么计算?
结构体有专门的对齐规则,这些规则你光看是没有一点用的(但是要记住,配合案例理解性记忆),我们直接上实践:
根据这个例子,我们一步一步套公式,分析为什么会是12和8,明明结构体里面我们认为只有6个字节,为什么将顺序换了一下之后,结构体的字节大小也会变?
S1的内存分布图
首先,第一个成员对齐结构体变量起始位置偏移量为0的地方,这个是固定的必须对齐,c1是char类型占1个字节,第二个成员,首先要求对齐数,我们使用的vs编译器默认为8,i是int类型,所以对齐数是8和4中的较小值,也就是4,然后找到对齐数的最小的整数倍的地方也就是4的那块空间,int类型是占4个字节的所以从4开始填充到7的位置,同样的道理,c2的对齐数是1,即找到1的最小倍数位置,每一个都是1的倍数所以就在7的旁边,这时候你数数也就只有9个字节的空间,为什么输出结果是12呢?这时候我们还有第三条,结构体总大小等于最大对齐数的整数倍,前面的c1,c2和i中对齐数最大的是i,最大对齐数是4,我们已经9个字节空间了,最近的4的倍数就是12了,这也就是输出结果了。
同理,我将S2的图画出来,你们试试推出输出结果来。
2.7 为什么存在内存对齐的情况
不知道你们有没有发现在我们在求结构体总大小时,没有填充的空间都是属于浪费的,我们为什么要浪费呢?
我们不是平白无故的浪费,是用这点空间来换取时间上的加速。
那我们怎么满足既要对齐,又要节省空间呢?
我们前面的S1和S2中的成员变量只是因为顺序不一样就导致空间大小也不一样,所以我们还是有方法使得既要对齐又能节省空间的。
我们可以将占用空间小的放到一起。
2.7.1 修改默认对齐数
导致内存空间浪费的原因还有一个:就是默认对齐数,通过缩小对齐数,可以减少填充,压缩结构体占用内存。
#pragma这是个预处理指令(预处理后续会讲),可以修改编译器的默认对齐数。
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
2.8 结构体传参
我们在结构体传参的时候,尽量传结构体的地址,可以节约时间和空间上的传统开销。
为什么优先使用传址的方式比较节省空间和时间呢?使用传址的方式传入指针的大小不是4就是8,使用传值的方式,既要重新创建一个结构体变量空间又很可能大于8。
如果说你能思考到使用传址可能不小心将结构体的数据修改,我们可以使用const来约束指针。
3. 结构体实现位端
位段是结构体中小众但是比较使用的一个技巧。
3.1 什么是位段
我们来看一下位段的实际形态:
位段中的_只是一种命名习惯,没有说位段一定要用_,位段中的“位”指的是bit位,位段后面的数字指的就是成员变量占的bit位,可以看到使用位段的方式大大减少了使用空间,这时候聪明的你,肯定会想到位段中的总共也就47个bit位啊,为什么占了8个字节?
位段依托的是基础容器,一个位段容器不能成员不能跨越两个容器,空间不够就再开一个容器,刚开始时,一个int容器32个bit位,_a+_b+_c=17个bit位,不够再将_d也装上,所以说这时候我们就要再开一个int容器来存放_d,2个int容器也就是8个字节空间。
这时候聪明的你就可能有疑问,那位段会不会里面的类型都一样?并不是的,位段允许混合不同类型的位段成员,但是有一条关键规则:相邻、同类型的位段,才会放进同一个容器;一旦类型切换,立刻开启新容器。
3.2 位段的内存分配
位段有很多的不确定因素,如果是跨平台最好不要使用位段
3.3 位段的注意事项
位段中的成员可能没有地址,我们前面三个成员变量存放到一个容器中,_d放不下就重新开辟一个容器,但是前一个容器是还有空间的,如果整个字节的起始地址就是那个没有使用的空间,那存放到里面的成员是不是就没有地址,所以我们不能使用&来获取地址。
4. 总结
结构体是自定义类型中的一个重要模块,也是干货最多的,后续还有其他的自定义类型,例如联合和枚举,本文文章篇幅较长,希望看到这里的各位能有所收获。