一、结构体
1.定义
结构体是一种特殊数据类型,可以描述复杂对象,当然也可以实现自定义的其中的变量类型,比如就可以定义一个用来储存学生信息的结构体 stu,其中的成员变量就包括有姓名、性别、年龄、学号等信息,且信息类型都可以不一样的,就突破了只能使用单一得数据类型固定的限制。
2.声明
结构体由三个部分组成:类型关键字struct 、结构体变量(标签)tag、主体{}; 既struct {} ;当然
也可以省略结构体标签,此时的话称他为匿名结构体(之后会介绍,先提一嘴)
此时上面的结构体声明就完成了,可以对其进行使用(初始化,调用等)
注意:
- 在结构体定义时,关键字struct和结构体{ };都不能少二者缺一不可
- 结构体标签tag 可以省略,使用起来会不太方便,但一些场景还是能使用到
- 切实末尾的分号; 一定不能丢vs会自己添加,而别的编译器是不会提供的
3.特殊声明
特殊声明相对普通声明,实际的意义上就是少了 一个结构体标签 tag,这个便是上文所提到的匿名结构体类型,他使用的场景也有限,并且只能创建全局性的结构体变量,且使用一次便消失了。
匿名结构体只能创建好的结构体全局变量,当同时出现俩个匿名结构体时,编译器会认为这俩个类型不同匿名结构体,对它们进行操作会出现警告。
注意:
- 匿名结构体只能创建在全局性的结构体变量、
- 全局性的结构体变量创建后,只能紧接这对其初始化,无法调用环境初始化
- 当出现多个匿名结构体时,编译器会认为这个不同的类型,强行使用会引发警告
- 匿名结构体类型,如果没有对结构体进行重命的话,基本只能使用一次。
当然匿名结构体也是可以将他们使用typedef 是重命名,如下图所示:
4.自引用
自引用是指结构体中找到一个和自己类型相同的成员,有点像递归,但俩个者本质又不是一个东西。结构体自引用出现在单链表中有一个data 数据域 和 一个next指针域,其中的成员变量next的类型是结构体指针,此行为就是自引用。
结构体自引用是链表实现的必须项,理解透彻了 链表学起来很容易
注意:
- 自引用时,其中的某个成员变量名必须和结构体类型相同,关键字、标签名、指针一样都不能少
- 使用自引用时,必须变量首尾关系要理清
5.变量的定义和初始化
定义和初始值有俩种方式,在结构体声明后和使用前,前者所创建的结构体变量具有全局属性,后者就只是一个普通的局部变量,结构体支持嵌套定义和指定元素的初始化
声明后初始化:
使用前初始化:
上面这些都是默认顺序初始化,当然也可以指定顺序初始化,如图所示:
嵌套定义:
指定成员初始化:
注意事项:
- 全局变量默认初始值为0,局部变量为随机值
- 当对局部变量进行指定成员初始化时,其他成为会初始值为0
- 结构体嵌套定义,初始化值字符串需要再次访问
6.内存对齐
学到这我们也基本对结构体有了基本认识,接下来我们再继续深究问题也是近期必要热门的考点:结构体内存对齐。
简言之,内存对齐就是结构体中的数据在内存中存储的更合理、更有规律,方便读取数据,减少字节。接下来我们以一个内存对齐的实际案例进行分析。按常理来说,此结构体占空间应为13字节,但事实是否如此嘛,我们继续往下看
计算偏移量是从首元素0地址处开始到c的最终存储位,进行的计算得到的16个字节
//内存对齐
#include<stddef.h> //这是求offsetor的头文件 用来计算结构体的大小
struct test
{
//偏移量就是距离结构体首位置的距离
// 单位是字节
int a; //偏移量 0
char b; //偏移量 4
double c; //偏移量 8
};
int main()
{
//offsetof 是一个宏,可以用来计算偏移量
printf("%zd\n", offsetof(struct test,a)); //计算偏移量的大小
printf("%zd\n", offsetof(struct test, b));
printf("%zd\n",offsetof(struct test,c));
printf("%zd\n",sizeof(struct test)); //结构体大小最终是为16字节
return 0;
}
显然,最终结果不是我们所猜想13字节,而是16字节,可为啥编译器会浪费掉这3个字节呢?其实主要还是为可了更好方便数据读取,所以就诞生了内存对齐这种奇妙规则:用空间换来更快得时间,提高程序运行效率
7.修改默认对齐数
vs中默认对齐数是8字节,linux中没有规定默认对齐数,当然我们可以通过特殊的手段修改默认对齐数,让数据内存故意不对齐,结构体大小计算的更简单(但只是玩玩 不太推荐哈)
内存对齐这个规则也不是完全定死的,我们也可以通过pargma来进行修改默认对齐数,如果把他修改默认对齐数为1,这样相当于没有对齐,虽然空间是节省下来了,但是这个效率又会下降了。 (不对齐的话本可以访问一次内存便能解决的,因被分为俩个模块的地方导致多访问一次)
可以看到,结果是之前我们是预想的13字节一样,由此可见说明内存对齐是真实存在的
注意:
- 一般情况下不要轻易去修改默认的对齐数,避免破坏代码的可植性
- 当结构体对齐方式真的不适应的时候,那么可以自己更改默认对齐数
- 修改完默认对齐数后,也别忘记修改回来,引起不必要的麻烦
8.结构体传参
结构体传参有俩种方式:传值与传址,传值不会对原数据造成影响,但会申请一快同样大小的空间;传址能间接修改原数据,且只占一个指针大小的空间。虽说结构体名是结构体首元素的地址,但在接收时是以一级指针接收的,相当于接收了变量值,因此最好是传递&结构体名(既传递结构体指针变量),指针毕竟只需要4/8字节空间,拥有传值的功效,且不像传值那样临时拷贝,造成不必要空间浪费。
注意:
- 结构体传参,首选传址传递,节省空间,也高效
- 如果执意选择值传递,参数压栈的开销会比较大,导致性能下降
二. 位移
1.定义
位移这个概念比较少见 ,因为位段这个东西本身就有很多不确定的因素:比如可移值性差,最大位数不确定等,因次用的也比较少,但如果是在固定环境下频繁的使用代码,位段就是一个很厉害的工具,他能控制变量所占字节数,最大限度节省空间
2.声明
位段的基本形式 struct tag{}; 与结构体一致,区别在于
- 位段中的成员必须是整形家族(int、char),因为位段按4字节或者1字节进行空间开辟
- 位段成员后面要有冒号:和数字,冒号表示这是一个位段成员,数字表示占用的空间(单位是比特)
//位段
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
//结构体定义
struct B
{
int a;
int b;
int c;
};
int main()
{
printf("有位段->%d\n", sizeof(struct A));
printf("无位段->%d\n", sizeof(struct B));
return 0;
}
3.内存分配
当我们了解完位段的基本结构后是否好奇它在内存中的存储方式呢?
以上是vs 2022环境测试的
4.位段的跨平台问题
- int位段当成有符号数还是无符号数是不确定的
- 位段的最大位的数目也不能确定也就是叫可移值(16位机器最大时16,32位机器最大时是32,当随机写成27的数字话,在32位机器下是能保证正确运行,而在16位机器边会出现报错)
- 位段中的成员在内存中从左向右分配,还是从右到左分配标准也是尚未定义的标准
- 当一个结构体包含二个位段,第二个位段成员比较大,无法容纳第一个位段剩余位时,便可舍弃剩余的位还是利用,这也是不确定,还得全取决于编译器
总结:
跟结构相比,位段可以达到同样的效果(提前是要设计的合理),并且可以更加的节省空间,但是有跨平台的问题存在
5.位段的应用
位段的使用场景比较有限,但利用好这方法就十分便利,也较节省空间,使数据传输更加高效,接下来可以看看网络数据传输中就用到位段
如图所示,前五行每行占了4个字节大小的空间,不同的地方需要存入不同的数据,此时利用位段能够大大的提升空间,只需要使用20字节就能存在下重要关键信息也就是五行,大大提高了数据传输的效率
注意:
- 位段中也是存在内存对齐,但不仅是针对整形的对齐,既段位大小为要为其中最大对齐数的整数倍
- 还是要多多注意这位段的可移性,只有在平台位相同可移性才能更好实现,节省空间。
6.位段使用的注意事项
当然在位段中几个成员都是公用一个字节,而他们得单位是bit也就是说位段得成员一般是没有地址。而字节是有地址的哈。
所以说不能对位段的成员取地址(&),这样就不能使用scanf直接给位段的成员输入值,只能先输入放在一个变量内,然后再进行赋值。
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = {0};
//这是错误的
// scanf("%d", &s.a);
//正确方式
int b = 0; //创建一个变量
scanf("%d", &b);
s.a = b; //再进行赋值操作
return 0;
}
三.联合体
1.定义
联合体有点像结构体的竞争对手,为啥这么说呢?因为结构体会追求成员变量的对齐性,而联合体不会;结构体使用多个成员变量,而联合只能用一个 。由此可见,联合体中的所有成员都是共用一个最大内存空间,比如其中定义一个char和int,最终结构体的大小为4字节(也就是一个整形),联合体的内存对齐,不会像结构体那么严格,联合体在进行内存对齐时,会判断此时占字节是否为其中最大内存对齐的倍数,如果不是,就会自动内存对齐。
2.声明
还是老样子,形式和结构体差不多,为nuion tag{};内部的成员是共用一块空间
注意:
- 由此可见,给联合体其中一个值赋值,其他成员的值也会跟着改变
- 联合体的成员是公用一块内存空间的
- 联合体是有能力保护最大的成员,并申请下来空间最大的内存空间存储
3.联合体大小的计算
可以看到接下来的俩个这俩个式子,猜测一下输出的结果是什么?
注意:
- 联合的大小至少是最大成员的大小
- 当最大成员大小不是最大对齐数的整数倍的时候,就要进行内存对齐到最大成员的共倍数
4.妙用解题
数据在内存中有俩种存储方式:小端字节存储和大端字节存存储,小端看着是反的,大端看着是正的,这就是为什么有时候通过内存调试,发现数据于预想不一样的原因(在vs都是按小端字节存储的),我们可以自己用程序也是能判断的,如今我们可以利用联合体巧妙判断大小端。
注意:
- 要学会变通哈,联合体的所有成员都是共用一个最大公倍数的空间的,无论怎么取地址,地址都是那一个不会发生改变
- 还有边是需要注意不仅结构体内有内存对齐,联合体也是有内存对齐
当然呐,也不止只有这一个案例,比如,我们需要搞个活动,要的上线一个礼物兑换单,礼品兑换单中有三种商品:图书、杯子、衬衫。每一种都有:库存量、价格、商品类型等信息。
图书:书名、作者、⻚数
杯⼦:设计
衬衫:设计、可选颜⾊、可选尺⼨
可以看到以上这些只会出现一次的变量,而不像库存量、价格、商品类型会反复的出现,这时我们就可以利用的联合体结构的特性在联合体的成员公用一个空间,便可以把那些这些只使用一次的变量放在一起,这样等有需要的时候就拿到呐,也十分节省内存空间的占用。
可以看到上述的结构十分简单,用起来也是方便,但是结构体要是包含那些很多杂七杂八的属性,边会使得结构体成员会十分的大,而浪费内存空间。所以我们就可以共同属性的写进结构,剩余别的各类商品属性使用联合体放在一起,便会在一定程度下节省内存。
四.枚举
1.定义
枚举既——列举,枚举一般称为枚举常量,枚举的形式和结构体相似,既enum tag{};值得一提的是,枚举中的成员定量时,不是以分号;结尾的,而是已逗号,区分,并且最后一位枚举成员 不用加任何符号,关于枚举的常量的大小(标准未定义),vs中是4字节。
2.声明
下面是枚举的类型声明,其中的成员变量可以自由定义。当然也可以赋初值
3.实际运用
枚举常量也是可以配合switch ,用来优化部分逻辑,使得更好疏通思路
//枚举运用
enum test
{
//利用枚举写出五个通道数据
exit,
add,
sum,
sub,
div
}s;
int main()
{
int input = 1;
while (input)
{
scanf("%d", &input);
//利用枚举常量配合通道
switch (input)
{
case exit:
printf("退出程序\n");
break;
case add:
printf("加法\n");
break;
case sum:
printf("减法\n");
break;
case sub:
printf("乘法\n");
break;
case div:
printf("除法\n");
break;
default:
printf("输入有误,请重新输入"); break;
}
}
return 0;
}
当然这只是枚举的基本用法,关于更多枚举的高阶用法还需要再多多学习,更多靠自己悟。
4.注意
- 提高程序的可读性和可维护性
- 有类型可调试或者检查,比较严谨(#define 是不可以调试,enum是可以的哈)
- 防止命名污染,因为枚举常量已封装好了
- 使用方便,一次性可以定义多个常量
- 枚举常量是遵守作用域的规则,假设枚举声明在函数内,只能在函数内使用
- 不能在整形常量的值对他进行赋值,会(cpp)报错,而c不严谨导致不会报错
可以看到一个在c是不会出现报错,而在cpp是出现了报错,由此可见在枚举在c++的类型检查严格
总结:
讲到这里也就是把所有自定义类型的全部结构了,除了结构体其他都会比较少见,总之呢自定义是可以描述复杂的对象,有点偏底层逻辑,我们经常用到(通讯录、职工工资管理系统),都是会哟用到自定义类型,感谢大家的观看。
如果觉得不错的话,可以用你发财的小手点点赞!!!