二、位段
1、什么是位段
在我们的生活中总有一些数据的取值情况是小于一个字节的,比如月份的取值是1~12,那么只需要4个比特位就能表示所有的月份;一周的星期是1 ~ 7,那么只需要3个比特位就能涵盖所有取值;又比如人的性别是男和女,那么只需要一个比特位就能表示所有情况。基于上面这种情况,C语言中出现了位段的概念。
位段:C语言允许在一个结构体中以位(比特位)为单位来指定其成员所占内存长度,这种以位为单位的成员称为 " 位段"或称 “位域” ( bit field) ;利用位段能够用较少的位数存储数据。
2、位段的声明
位段的声明和结构是类似的,只有两个不同:
- 位段的成员必须是 int、unsigned int 、signed int 或者是 char 。 (一般来说,一个结构体的所有位段成员的数据类型是相同的 ,即要么全为 int,要么全为 char)
- 位段的成员名后边有一个冒号和一个数字。
例如:
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;; }
3、位段的内存分配
位段的成员可以是 int unsigned int signed int 或者是 char(属于整形家族) 类型
位段的空间是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
总结:跟结构体相比,位段可以达到同样的效果,且可以很好的节省空间,但是有跨平台的问题存在。
4、位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器下 int 最大为16比特,32位机器最大为32比特,如果在32位机器下写成27,在16位机器会上运行时就出问题。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,这些剩余的位是舍弃还是利用,这是不确定的。
以上面的 struct S 为例:
首先,位段成员的数据类型是 char,那么编译器就会在内存中为 struct S 开辟一个字节的空间,如果不够,再继续开辟;
接着, a 占3个比特,b 占4个比特,加起来一共7个比特,所有第一个字节中现在还剩下一个比特的空间;
然后,c 需要5个比特的空间,这里问题来了,c 是直接从后面一个字节中4中拿5个比特,还是说先从后面的字节中拿4个比特,再从前面的字节中拿剩下的一个比特,即当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,这些剩余的位是舍弃还是利用呢?这是C语言标准未定义的;
最后,我们再来看 main 函数,在 main 函数中我们把10赋给结构体中的,我们知道10的二进制序列为 1010,但是 a 变量只有3个比特的大小,所以10会发生截断后将 010 放入 a 中,但是这里问题又来了,010是放进靠左的三个比特,还是放进靠右的三个比特呢?即位段中的成员在内存中从左向右分配,还是从右向左分配呢?这也是C语言标准未定义的;
所以我们说,位段涉及很多不确定因素,是不跨平台的,注重可移植的程序应该避免使用位段。
5、VS下位段的使用习惯
这里我直接说结论,在VS编译器下,位段的使用习惯是这样的:
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,直接舍弃剩余的位;
- 位段中的成员在内存中是从右向左分配的;
接下来我们来验证这个结论:还是用上面那个结构体
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;; }
以VS下位段的使用习惯条件下:
首先,此结构体 a 和 b 变量占去一个字节中的7的比特位,并把最后一个比特位丢弃;c 变量占一个比特位,并把剩余的3个比特位丢弃;d 变量占4个比特位,并把剩下的四个比特位丢弃;所以 struct S 一共占3个字节打下;然后在 main 函数中把结构体成员全部初始化为0;此时内存中的数据是:00 00 00 00 | 00 00 00 00 | 00 00 00 00;
然后,对于 a 变量来说,由于10的二进制序列为 1010,大于3个比特,所以会10会截断变成 010 后放入第一个字节中靠右的3个比特位中,此时内存中的数据是:00 00 010 10 | 00 00 00 00 | 00 00 00 00;
对于 b 变量来说,12的二进制序列是 1100,b 变量能放下,所以 1100 会放入第一个字节中 a 数据的前面,此时内存中的数据是:01 10 00 10 |00 00 00 00 | 00 00 00 00;
对于 c 变量来说,,3的二进制序列为 11,小于5个比特,所以补0变成 00011 后放入第二个字节中靠右的比特位中,此时内存中的数据是:01 10 00 10 |00 00 00 11 | 00 00 00 00;
对于 d 变量来说,4的二进制序列为 100,小于4个比特,补0变成 0100 后放入第三个字节中靠右的比特位中,此时内存中的数据是:01 10 00 10 |00 00 00 11 | 00 00 01 00;
所以最终内存中的数据变为:01 10 00 10 |00 00 00 11 | 00 00 01 00,转变为16进制就是 0x 62 03 04;
在VS下测试发现结果正如我们预料的那样,所以结论成立。
6、位段的用途
我们了解了位段的优缺点之后,可能有的同学会有疑惑,位段存在这么大的问题,在实际开发中真的会用到它吗?其实是会的,位段的一个常见的用途就是用于 ip数据报,如图:
如图:在 ip数据报中,版本只占4个比特,头部长度只占4个比特,服务类型只占8个比特,等等,如果这些数据我们都用一个整形大小,即32个比特位来存储的话,那么就会在一定程度上增加数据报的大小,从而增加网络负载,减缓传输效率,所以在这里,位段的作用就得到了很好的体现。
三、枚举
1、什么是枚举
顾名思义,枚举就是一一列举,把一个数据可能的取值全部列举出来,比如一周有七天,一年有12个月,性别有男、女,这些都是枚举的使用场景。
2、枚举类型的声明
enum Day//星期 { Mon, Tues, Wed, Thur, Fri, Sat, Sun }; enum Sex//性别 { MALE, FEMALE, SECRET }; enum Color//颜色 { RED = 3, GREEN, BLUE };
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 大括号中的内容是枚举类型的可能取值,也叫枚举常量 。这些枚举常量都是有值的,默认从0开始,每次递增1,当然我们也可以在定义的时候为其赋初值,给某一枚举常量赋初值之后,其后面的常量仍然是每次递增1。
3、枚举的优点
我们知道,在C语言中我们可以利用 #define 来定义常量,那为什么还要单独设计出一个枚举类型来定义枚举常量呢?其实是因为枚举有如下优点:
增加代码的可读性和可维护性 :我们使用枚举常量来给枚举变量赋值,可以使得这个变量变得有意义,增加其可读性和可维护性;
和 #define 定义的标识符相比,枚举有类型检查,更加严谨:在使用像C++这种语法检查较为严格的编程语言时,枚举变量必须用枚举常量来赋值,而不能使用普查常量来赋值;
防止了命名污染(封装);
便于调试 :用 #define 定义的常量在程序的预处理阶段就会被替换掉,不便于调试观察;
使用方便,一次可以定义多个常量;
4、枚举的使用
enum Color//颜色 { RED = 1, GREEN = 2, BLUE = 4 }; enum Color clr = GREEN; //使用枚举类型定义枚举变量并初始化
四、联合
1、什么是联合
联合是一种特殊的自定义类型,这种类型定义的变量包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
2、联合的声明
联合的声明与结构体的声明十分类似,只是把关键字 struct 变为了 union。
union tag //struct:结构体关键字 tag:结构体标签 { member - list; //成员列表 }variable - list; //变量列表(可以省略)
例如:
//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un;
3、联合的特点
联合的成员是共用同一块内存空间的,所以一个联合变量的大小,至少是最大成员的大小(因为联合至少得有保存最大的那个成员的能力)。
union Un { int i; char c; }; union Un un; int main() { printf("%d\n", &un); printf("%d\n", &(un.i)); printf("%d\n", &(un.c)); }
因为联合体成员公用同一块内存空间,所以联合变量的地址与每个联合成员变量的地址都是相同的。
4、联合大小的计算
联合大小的计算规则如下:
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,要对齐到最大对齐数的整数倍。
例如:
union Un1 { char c[5]; int i; }; union Un2 { short c[7]; int i; }; int main() { printf("%d\n", sizeof(union Un1)); printf("%d\n", sizeof(union Un2)); }
union Un1:c 占的对齐数为1,占5个字节;i 的对齐数为4,占4个字节;二者共用一块内存本来只需要5个字节的大小,但是由于需要对齐到最大对齐数的整数倍处,所以 union Un1 最终占8个字节;
union Un2:c 的对齐数为2,占14个字节;i 的对齐数为4,占4个字节;二者共用一块内存本来只需要14个字节的大小,但是由于需要对齐到最大对齐数的整数倍处,所以 union Un2 最终占16个字节;
5、利用联合判断大小端
在前面的文章中我们介绍大大小端,并且提供了判断大小端的代码,今天我们用联合的方法来实现对判断大小端的判断:
#include <stdio.h> int Check_Sys() //规定返回1为小端,返回0位大端 { union //因为联合体只在本函数内调用一次,所以可以声明为匿名联合体 { char c; int i; }un; un.i = 1; //将联合中的整型变量赋值为1 return un.c; //返回 c 变量,即返回内存中第一个字节的数据,小端,内存中第一个字节数据为1 ,大端则为0 } int main() { int ret = Check_Sys(); if (ret == 1) printf("小端\n"); else printf("大端\n"); return 0; }