前言
大家好!本期博客给大家讲讲C语言中3中比较重要的自定义类型 结构体、枚举、联合体,
并教大家如何使用!
一、结构体是什么
结构是一些值得集合,这些值统称为成员变量,结构的每个成员可以是不同类型的变量。
1.结构体的定义:
//struct - 结构体关键字
//tag - 结构体标签
struct tag
{
memlist//成员列表
}variable-list//变量列表;
2.结构体的实现:
Eg: 描述一个人【人有很多属性】
- 名字
- 年龄
- 性别
- 电话
struct People
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char tele[12];//电话
};
特别注意:
- 1️⃣结构体声明的同时,在结构体变量列表内定义的结构体类型变量为
全局变量
- 2️⃣在主函数定义的结构体类型变量为
局部变量
3. 结构体的特殊声明
结构体特殊声明:
- 匿名结构体类型
简单来说: 匿名结构体类型就是没有了tag【标签】
特殊情况:
struct
{
char c;
int i;
char ch;
double d;
}s;
struct
{
char c;
int i;
char ch;
double d;
}* ps;
❓同学们结合上述代码,觉得下列代码可以正常执行吗
int main()
{
ps = &s; // ?
return 0;
}
答案是不行
哦
- 因为编译器会把上面的两个声明当成完全不同的两个类型。
- 即使在编译器看来这两个结构体的成员变量是相同的,但编译器仍认为它们是不同的类型
所以是非法
的
❗特别注意: 匿名结构体类型没有标签
- 所以声明结构体的时候,匿名结构体的变量也要跟在后面声明
- 1️⃣以防在后续的程序中,找不到此
结构体类型
- 2️⃣即匿名结构体类型一旦没有一次性定义完所需的结构体类型变量,在后续再想定义就找不到此结构体类型了
- 1️⃣以防在后续的程序中,找不到此
综上: 匿名结构体类型具有局限性
,不建议使用呀~
4. 结构体的自引用
结构体的自引用
有没有想过,在结构中包含一个类型为该结构本身的成员是否可以呢?
struct Node
{
int data;
struct Node next;
};
//可行否?
//如果可以,那sizeof(struct Node)是多少?
答案是:它在里面其实是无限嵌套的
- 始终在创建结构体变量,没停下来,所以无法计算大小
- 那么怎么解决呢?
- 对于下列代码,才是结构体的自引用
struct Node
{
int data;
struct Node* next;//存下一个节点的地址
};
- 通过创建包含同类型的结构体指针,从而找到下一个同类型的节点【即自己可以找到同自己类型的元素】
- 这是数据结构中对于链表类型的定义,以后我们会深入学习。
❗特别注意:
匿名结构体类型
不可以自引用
Eg:
typedef struct
//这里typedef的意思是以后只要写 Node 就表示 这个匿名结构体类型】
{
int data;
Node* next;
}Node;
上述写法是错误的
这个结构体类型在创建得时候,成员变量里就已经拥有了 Node了,而此时Node的创建还在后面,即改名为Node的结构体还没创建好,就被调用了,所以是错误的
✨综上: 结构体自引用不是包含创建同类型的结构体变量,而是包含同类型的结构体的指针【一般多用于实现链表结构】
5. 结构体的内存对齐
经过上述结构体类型的了解
现在我们深入讨论一个问题:计算结构体的大小
其中,里面涉及了一个非常重要的知识点:结构体内存对齐
示例:
同学们觉得下面的结构体空间大小是多少呢?
//练习1
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
➡️有的同学可能认为:结构体的空间大小就是成员变量大小的总和
上述的想法只能对一半❗
但经过我的讲解你就能完全掌握啦~
让我们先来了解内存对齐
的对齐规则:
①第一个成员变量存放在在与结构体变量偏移量为0的地址处
②其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
③结构体总大小:成员变量中最大对齐数(每个成员变量都有一个对齐数)的整数倍
④如果嵌套了结构体的情况:
嵌套的结构体以自己结构体中成员变量中的最大对齐数为嵌套结构体的对齐数,然后再对齐到外面结构体对应的嵌套结构体最大对齐数的整数倍处
外面结构体的整体大小就是所有最大对齐数(含嵌套结构体自己的最大对齐数)的整数倍。
特别注意:
VS
中默认对齐数的值为8
Linux
没有默认对齐数的概念
对齐数
: 编译器默认的一个对齐数 与 该成员大小的较小值
有了以上了解,我们再看回题目:
所以上面一共浪费了2字节
的空间,结构体的空间大小为:8字节
- 同学们再来看看此题的答案是多少呢?
本题目就运用到结构体嵌套
的其情况了~
解题方法:
- 先求嵌套结构体
S3
的结构体总的空间大小&最大对齐数
再回到外面的结构体S4
,进行上一题的做题步骤即可
有了以上了解,内存对齐
这一类的问题往后就不是问题啦~
补充: 为什么存在内存对齐?
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
Eg:假设计算机每次读4个字节,从地址偏移量为0的处开始读
- 这样 即使读了
4
个字节,浪费了3
个字节的空间,但还是依然能读取到c
,再读4
个字节,也顺便把i
也拿出来了
但是,如果是下面的情况去存放,每一次读
4
个字节,就不方便了
- 为了读取到
i
【仅仅对读取i
来说,而不是整体字节需要读取几次】,要对它进行读取两次才拿到i
的全部字节- 第一次读了
i
的3个字节,第二次才读取到i
的最后一个字节,相当于要完全读取到i的内容,需要读取2次
- 第一次读了
而按照上面的内存对齐
的放法,就可以直接读取一次就读到i
了
❗在设计结构体的时候,我们既要满足对齐
,又要节省空间
,我们可以:
- 让占用空间小的成员尽量集中在一起
结构在对齐方式不合适的时候,我们也可以自己更改
默认对齐数:
- 利用
#pragma pack()
:- 1️⃣
#pragma pack(4)
= 设置默认对齐数为4 - 2️⃣
#pragma pack()
= 取消设置的默认对齐数,还原为默认
- 1️⃣
✨总的来说: 结构体的内存对齐
是拿空间
来换取时间
的做法
二.位段
1.什么是位段
位段
的声明和结构体是类似的,有两个不同:
- 1️⃣位段的成员必须是
int
、unsigned int
或signed int
- 2️⃣位段的成员名后边有一个
冒号
和一个数字
示例:
struct S
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
❗特别注意:
位段
的意思就是:将这个成员变量存储于自己设定的空间大小内(单位:比特
)
➡️就如上述的代码:
- 变量
a
存储于2
个比特位里 - 变量
b
存储于5
个比特位里
2.位段的内存分配
分配规则:
- 1️⃣位段的成员可以是 int、 unsigned int、 signed int 或者是 char (属于整形家族)类型
- 2️⃣位段的空间上是按照需要,以4个字节( int )或者1个字节( char )的方式来按需开辟的
例子:
struct S
{
int a:3;
int b:4;
int c:5;
int d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
➡️位段的空间是如何存储的呢?
注意:
- 1️⃣
比特位的放置
与高低地址
无关:比特位在一个字节内由低位
向高位
放置 - 2️⃣
大小端
讨论的是字节
被机器读取、存放的顺序
,与字节内比特位
的放置无关
❗特别注意:
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段,因为在跨平台中可能出现如下问题:
1️⃣int 位段被当成有符号数还是无符号数是不确定的
2️⃣ 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
3️⃣位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
4️⃣当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
3.位段的应用
位段可用于通信传输
中,以节省一个信息传输的空间大小
总结
➡️简单来说:跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在
三.枚举类型
1.枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这里就可以使用枚举了。
2.枚举类型的定义
e.g.
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值
例如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
3.枚举的优点
为什么使用枚举呢?
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量枚举的优点:
4. 枚举的使用
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;//ok?? 答案是不行的
return 0;
}
四.联合体(共用体)
1.联合类型的定义
- 1️⃣联合也是一种特殊的自定义类型
- 2️⃣这种类型定义的变量也包含一系列的成员,
特征
是这些成员公用
同一块空间
➡️这也就为什么: 联合体也叫共用体啦~
2.联合的大小&特点
1️⃣联合的成员是共用同一块内存空间的
2️⃣这样一个联合变量的总大小:至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)
联合的 大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
大家可能对联合体不太具象化,接下来紧跟我步伐~
Eg:
union Un1
{
char c[5];
int i;
}
大家觉得下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
❗特别注意:
- 计算结构类型的大小时,若遇到像上面一样的
数组类型
的时候:- 计算
数组
类型的对齐数
只看数组类型
所对应的对齐数
- 又因为
数组
是连续存放
的,所以数组只要首元素对齐后,剩下的其它元素就不需要再对齐,只需要排下去
即可
- 计算
再看回上题:
所以答案为:8
字节
✨综上:
- 对于
联合体(共用体)
可以理解为:成员变量
都在同一块空间
的同一起始位置
开始存储的【即在同一块空间上堆叠存储
】
❗正因为这样:
- 在
同一时间
里,只能调用共用体的一个
成员变量
➡️因为当改变一个共用体的成员变量,对于其它成员变量来说共用的这部分空间的值
也会被改变
3. 联合体的使用
知道其用法,那我们便可以用的内容去实现某平台机器数据存储模式大小端的判断
啦~
思路:
在内存中存储整型1【即对于二进制序列来说:000000……0001】
使用char类型的指针对其进行读取【读取1字节(8个比特位)的内容】,看读取的内容为1还是0
判断:若为1则是小端 ,若为0则是大端
实现:
有了以上思路,我们便可以用联合体将以前的前两个步骤整合在一起
int check_sys()
{
union U
{
char c;
int i;
}u;
u.i = 1;
return u.c;
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
我们得到结果,当前的机器采取的是小端存储的模式。
好了!今天的内容就分享到这里,希望对各位同学有些帮助,咱们下期再见!