❤️欢迎来到我的博客❤️ |
枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
像这些容易并且可以一一列举的数据就可以定义成枚举类型
枚举类型的定义
例:
enum Day//星期
{
//以下是未来可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
//可能取值
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
//可能取值
RED,
GREEN,
BLUE
};
//通过枚举类型定义变量
enum Day a;
enum Sex b;
enum Color c;
enum Sex s = 5;//注:这种写法不严谨,在C语言中不会报错但在C++中会报错
enum Sex s = FEMALE;//正确写法
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫枚举常量 。(不可被改变的量,可以给定初始值)
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
例如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color//颜色
{
//未指定初始值
RED,//默认为0
GREEN,//1
BLUE//2
};
enum Color//颜色
{
RED=-5,//赋值-5
GREEN,//默认递增1,为-4
BLUE//-3
};
enum Color//颜色
{
RED,//默认为0
GREEN=5,//赋值5
BLUE//默认递增1,为6
};
使用枚举提升代码可读性
我们可以使用 #define 定义常量,为什么非要使用枚举,枚举有什么特点?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
增加可读性:
我们来看我们之前做的通讯录中的switch语句
void menu()
{
printf("**************************************************\n");
printf("******* 通讯录 *******\n");
printf("******* 1.添加联系人 2.删除联系人 *******\n");
printf("******* 3.查找联系人 4.修改联系人 *******\n");
printf("******* 5.显示联系人 0.退出通讯录 *******\n");
printf("**************************************************\n");
}
int main()
{
int input = 0;
do
{
menu();//菜单
printf("请选择->");
scanf("%d", &input);
switch (input)
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 0:
break;
default:
break;
}
} while (input);
return 0;
}
可以看到在这个switch语句中,如果不看菜单,光从数字是看不出选择后需要执行什么功能。
我们就可以用枚举类型进行一些小小的修改,定义出一个枚举类型
enum Option//选项
{
//枚举常量,默认从0开始
EXIT,//0.退出通讯录
ADD,//1.添加联系人
DEL,//2.删除联系人
SEARCH,//3.查找联系人
MODIFY,//4.修改联系人
SHOW//5.显示联系人
};
然后把case进行修改:
#include <stdio.h>
enum Option//选项
{
//枚举常量,默认从0开始
EXIT,//0.退出通讯录
ADD,//1.添加联系人
DEL,//2.删除联系人
SEARCH,//3.查找联系人
MODIFY,//4.修改联系人
SHOW//5.显示联系人
};
void menu()
{
printf("**************************************************\n");
printf("******* 通讯录 *******\n");
printf("******* 1.添加联系人 2.删除联系人 *******\n");
printf("******* 3.查找联系人 4.修改联系人 *******\n");
printf("******* 5.显示联系人 0.退出通讯录 *******\n");
printf("**************************************************\n");
}
int main()
{
int input = 0;
do
{
menu();//菜单
printf("请选择->");
scanf("%d", &input);
switch (input)
{
case ADD:
break;
case DEL:
break;
case SEARCH:
break;
case MODIFY:
break;
case SHOW:
break;
case EXIT:
break;
default:
break;
}
} while (input);
return 0;
}
这样是不是就增加了代码的可读性,在没有菜单的情况下也能知道这个选项执行的是什么功能。
联合(共用体)
联合类型的定义
联合的关键字是:union
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
#include <stdio.h>
//联合体类型
union UN
{
char c;
int i;
};
int main()
{
union UN un;//创建联合体变量un
printf("%d\n", sizeof(un));//运行结果为4
return 0;
}
这里的联合体大小为什么为4呢?
我们来看他们的地址:
可以看到,他们的地址一模一样,那我们就可以进行分析:c和i共用了一块空间,所以这两个变量不能同时使用。
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
联合大小的计算
#include <stdio.h>
union Un
{
char c[5];//大小为5
int i;//大小为4
};
int main()
{
printf("%d\n", sizeof(union Un));//运行结果为8
return 0;
}
这里得到的结果为什么是8呢,其实联合体也是有对齐的。
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
c成员自身大小为5,c是个数组我们就看他的每个元素的类型来确定他的对齐数,他的每个元素是char类型,那么他的对齐数就是:1 默认对齐数为:8 取较小值,对齐数就为1。
i成员自身大小为:4 默认对齐数为:8 取较小值,对齐数就为4。
那么最大成员的大小为5,5不是4(最大对齐数)的倍数,浪费了3个字节的空间变成了8个字节,所以他的大小就为:8
——————————————————————————————————————————————————
再来看一个例子:
union Un2
{
short c[7];//自身大小14,对齐数为2/8取较小值为:2
int i;//自身大小4,对齐数为4/8取较小值为:4
};
最大对齐数为:4,最大成员大小为:14,不为4的倍数,浪费2个空间,最后的大小为:16。
使用联合体判断当前计算机的大小端存储
一个数存到内存之后,按照字节的顺序不同分为大端和小端。
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
基础判断代码:
#include <stdio.h>
int main()
{
int a = 1;//0x 00 00 00 01
//低------------->高
//01 00 00 00 - 小端存储模式
//00 00 00 01 - 大端存储模式
//我们只需要拿出第一个字节,就可以区分是大端存储还是小端
if (*(char*) & a == 1)//访问第一个字节
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
使用联合体判断大小端:
int main()
{
int a = 1;//0x 00 00 00 01
//低------------->高
//01 00 00 00 - 小端存储模式
//00 00 00 01 - 大端存储模式
//我们只需要拿出第一个字节,就可以区分是大端存储还是小端
union Un
{
char c;
int i;
}un;
un.i = 1;
if (un.c == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
为什么这样也能判断大小端呢?
这里用到了联合体的一个特点:联合的成员是共用同一块内存空间的
如果是小端机器,那么我们在赋值i的时候放入的就是:01 00 00 00
如果是大端机器,放入的就是:00 00 00 01
取出的时候用c来取出,取出的就是第一个字节,就可以进行大小端的判断。
什么是位段
今天我们要来了解C语言中的位段。
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。(有些平台的成员也可以为char类型)
2.位段的成员名后边有一个冒号和一个数字。
比如:
//位段
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
printf("A=%d\n", sizeof(struct A));
return 0;
}
在一个成员的后面加一个冒号和一个数字,就是位段。
那么位段式的结构体创建一个变量的时候这个变量的大小是多少呢,我们来看运行结果:
4个整形应该为16个字节,但是为什么结果是8个字节呢?
其实位段中的位指的是二进制位,成员后面的数字表示的是几个二进制位的意思。
struct A
{
int a : 2;//表示a只占用2个二进制位
int b : 5;//表示b只占用5个二进制位
int c : 10;//表示c只占用10个二进制位
int d : 30;//表示d只占用30个二进制位
//注:数字的大小不能超过类型的大小
};
当我们在定义结构体类型的时候,结构体类型中的某个成员的取值不需要太大的变量的时候就可以用位段的方式来节省空间。
位段的作用和功能和结构体一样,但是位段更能节省空间。
那既然位段能节省空间,我们以后能不能用位段来代替结构体呢?
凡是有好就有坏,有利就有弊。位段虽然能节省空间,但也有不足之处,我们接着往下看。
位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
最后的15个bit有没有被使用就是一个不确定因素,C语言也没有明确规定要不要使用剩余bit。
位段的内存分配
我们在VS2022上验证:位段的内存开辟和使用
//一个例子
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
我们来看一下上述代码中s的数据到底是怎样放的,a使用的是8个bit中的哪3个bit,数据是从右向左使用的还是从右向左使用的?
我们假设数据从低位往高位使用,并且不使用剩余bit位。
那么在VS2022下得到的是不是我们算出的结果呢,我们来验证一下。
可以看到结果和我们算出的一致,那么这里真的只有3个字节吗。
我们来计算一下:
printf("=%d\n",sizeof(s));
结果证明,s确实只开辟了3个字节。
总结
位段:
如果成员是char那么在开辟空间的时候一次开辟一个字节,开辟的字节先使用,不够使用再继续开辟。
使用方式是从低位向高位使用,高位如果剩余的字节不够下一个成员使用时,把剩余的字节丢弃,在开辟一个字节的空间,再让他从低位向高位使用,以此类推。
位段是不存在对齐的。
位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题)
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
位段的应用
那么位段什么情况能用呢,有没有真实的使用案例,其实是有的。
比如在网络的底层就有一种应用。
以下是IP数据包的封装格式:
有了这些封装才能准确的保证数据能从A传到B,包括传丢失了该怎么处理。
这些成员就使用了位段,如果不使用位段就会导致空间浪费非常严重。
网络上的数据节省的空间会给我们带来很多收益。网络上的数据就不会太大,网络的负载就会小一点。
以上就是本篇的全部内容了,希望大家看完能有所收获
❤️ 创作不易,点个赞吧~❤️ |