【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

相关文章
|
25天前
|
存储 C语言
如何在 C 语言中实现结构体的深拷贝
在C语言中实现结构体的深拷贝,需要手动分配内存并逐个复制成员变量,确保新结构体与原结构体完全独立,避免浅拷贝导致的数据共享问题。具体方法包括使用 `malloc` 分配内存和 `memcpy` 或手动赋值。
31 10
|
24天前
|
安全 编译器 Linux
【c语言】轻松拿捏自定义类型
本文介绍了C语言中的三种自定义类型:结构体、联合体和枚举类型。结构体可以包含多个不同类型的成员,支持自引用和内存对齐。联合体的所有成员共享同一块内存,适用于判断机器的大小端。枚举类型用于列举固定值,增加代码的可读性和安全性。文中详细讲解了每种类型的声明、特点和使用方法,并提供了示例代码。
23 3
|
24天前
|
存储 大数据 编译器
C语言:结构体对齐规则
C语言中,结构体对齐规则是指编译器为了提高数据访问效率,会根据成员变量的类型对结构体中的成员进行内存对齐。通常遵循编译器默认的对齐方式或使用特定的对齐指令来优化结构体布局,以减少内存浪费并提升性能。
|
28天前
|
编译器 C语言
共用体和结构体在 C 语言中的优先级是怎样的
在C语言中,共用体(union)和结构体(struct)的优先级相同,它们都是用户自定义的数据类型,用于组合不同类型的数据。但是,共用体中的所有成员共享同一段内存,而结构体中的成员各自占用独立的内存空间。
|
28天前
|
存储 C语言
C语言:结构体与共用体的区别
C语言中,结构体(struct)和共用体(union)都用于组合不同类型的数据,但使用方式不同。结构体为每个成员分配独立的内存空间,而共用体的所有成员共享同一段内存,节省空间但需谨慎使用。
|
1月前
|
编译器 C语言 C++
C语言结构体
C语言结构体
25 5
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
9天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
25 6
|
29天前
|
存储 缓存 C语言
【c语言】简单的算术操作符、输入输出函数
本文介绍了C语言中的算术操作符、赋值操作符、单目操作符以及输入输出函数 `printf` 和 `scanf` 的基本用法。算术操作符包括加、减、乘、除和求余,其中除法和求余运算有特殊规则。赋值操作符用于给变量赋值,并支持复合赋值。单目操作符包括自增自减、正负号和强制类型转换。输入输出函数 `printf` 和 `scanf` 用于格式化输入和输出,支持多种占位符和格式控制。通过示例代码详细解释了这些操作符和函数的使用方法。
35 10
|
22天前
|
存储 算法 程序员
C语言:库函数
C语言的库函数是预定义的函数,用于执行常见的编程任务,如输入输出、字符串处理、数学运算等。使用库函数可以简化编程工作,提高开发效率。C标准库提供了丰富的函数,满足各种需求。