【C语言】自定义类型详解:结构体、枚举、联合(2)

简介: 【C语言】自定义类型详解:结构体、枚举、联合(2)

二、位段

1、什么是位段

在我们的生活中总有一些数据的取值情况是小于一个字节的,比如月份的取值是1~12,那么只需要4个比特位就能表示所有的月份;一周的星期是1 ~ 7,那么只需要3个比特位就能涵盖所有取值;又比如人的性别是男和女,那么只需要一个比特位就能表示所有情况。基于上面这种情况,C语言中出现了位段的概念。

位段:C语言允许在一个结构体中以位(比特位)为单位来指定其成员所占内存长度,这种以位为单位的成员称为 " 位段"或称 “位域” ( bit field) ;利用位段能够用较少的位数存储数据。

2、位段的声明

位段的声明和结构是类似的,只有两个不同:

  1. 位段的成员必须是 int、unsigned int 、signed int 或者是 char 。 (一般来说,一个结构体的所有位段成员的数据类型是相同的 ,即要么全为 int,要么全为 char)
  2. 位段的成员名后边有一个冒号和一个数字。

例如:

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编译器下,位段的使用习惯是这样的:

  1. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,直接舍弃剩余的位;
  2. 位段中的成员在内存中是从右向左分配的;

接下来我们来验证这个结论:还是用上面那个结构体

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;

20200623104134875.png

2020062310470442.png

在VS下测试发现结果正如我们预料的那样,所以结论成立。

6、位段的用途

我们了解了位段的优缺点之后,可能有的同学会有疑惑,位段存在这么大的问题,在实际开发中真的会用到它吗?其实是会的,位段的一个常见的用途就是用于 ip数据报,如图:

2020062310470442.png

如图:在 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。

2020062310470442.png

3、枚举的优点

我们知道,在C语言中我们可以利用 #define 来定义常量,那为什么还要单独设计出一个枚举类型来定义枚举常量呢?其实是因为枚举有如下优点:

增加代码的可读性和可维护性 :我们使用枚举常量来给枚举变量赋值,可以使得这个变量变得有意义,增加其可读性和可维护性;

和 #define 定义的标识符相比,枚举有类型检查,更加严谨:在使用像C++这种语法检查较为严格的编程语言时,枚举变量必须用枚举常量来赋值,而不能使用普查常量来赋值;

2020062310470442.png

防止了命名污染(封装);

便于调试 :用 #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));  
}

因为联合体成员公用同一块内存空间,所以联合变量的地址与每个联合成员变量的地址都是相同的。

2020062310470442.png

4、联合大小的计算

联合大小的计算规则如下:

  1. 联合的大小至少是最大成员的大小。
  2. 当最大成员大小不是最大对齐数的整数倍的时候,要对齐到最大对齐数的整数倍。

例如:

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));
}

2020062310470442.png

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;
}

2020062310470442.png

相关文章
|
8天前
|
C语言
|
8天前
|
网络协议 编译器 Linux
结构体(C语言)
结构体(C语言)
|
2天前
|
存储 编译器 数据库
结构体数组在C语言中的应用与优化技巧
结构体数组在C语言中的应用与优化技巧
|
7天前
|
编译器 C语言
C语言枚举:深入探索下标默认值、自定义值及部分自定义情况
C语言枚举:深入探索下标默认值、自定义值及部分自定义情况
6 0
|
7天前
|
C语言
C语言中的结构体
C语言中的结构体
5 0
|
9天前
|
编译器 C语言 C++
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
【海贼王编程冒险 - C语言海上篇】自定义类型:结构体,枚举,联合怎样定义?如何使用?
8 0
|
11月前
|
存储 C语言
【C语言】 条件操作符 -- 逗号表达式 -- []下标访问操作符,()函数调用操作符 -- 常见关键字 -- 指针 -- 结构体
【C语言】 条件操作符 -- 逗号表达式 -- []下标访问操作符,()函数调用操作符 -- 常见关键字 -- 指针 -- 结构体
|
10月前
|
C语言
【C语言】——define和指针与结构体初识
【C语言】——define和指针与结构体初识
|
存储 C语言
C语言初识-关键字-操作符-指针-结构体
C语言初识-关键字-操作符-指针-结构体
48 0
【C语言】指针,结构体,链表
【C语言】指针,结构体,链表