写点什么

C 语言自定义类型的介绍(结构体,枚举,联合体,位段)

作者:未见花闻
  • 2022 年 6 月 23 日
  • 本文字数:5323 字

    阅读完需:约 17 分钟

🍇1.结构体

🍉 1.1 结构体概述

🍓1.1.1 结构体概念

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

🍓1.1.2 结构体的声明与使用

结构体的声明与定义


//结构体struct Stu{  char name[20];//姓名  char sex[5];//性别  char id[20];//学号  int age;//年龄  double grade;//成绩}student;
复制代码


其中struct Stu是自定义结构体类型,花括号内的一系列数据类型称为结构体成员student为定义的结构体类型变量。结构体的不完全声明


//匿名结构体类型struct{ int a; char b; float c; }x;
struct{ int a; char b; float c;}a[20], *p;
复制代码


上面的两个结构体声明时省略了结构体标签,但是只能跟在结构体声明后创建结构体变量,其他情况下是无法创建该结构体体变量的,这也就说明无法创建该结构体指针,就算再声明一个一模一样的结构体指针p,将x的地址赋值给p编译器会发生报错,编译器会当做两个完全不同的结构体处理。


结构体的自引用


struct Node{ int data; struct Node next;};
复制代码


直接将结构体嵌套在相同结构体是不可取的,你可以想象一下,如果可以这么做,那这个结构体的大小是多少,他会无限制地嵌套下去,导致栈溢出,如果硬是需要嵌套自身,应该嵌套相同结构体类型的指针。


struct Node{ int data; struct Node* next;};
复制代码


在使用typedef定义结构体自引用时,不能直接在结构体中使用typedef替换的结构体类型名直接定义结构体指针,而是应该使用原类型名定义在结构体中定义结构体指针.


//错误示例:typedef struct NODE{ int data; Node* next; }Node;
//正确示例:typedef struct NODE{ int data; struct NODE* next; }Node;
复制代码


结构体的使用与传参


#include <stdio.h>int main(){  struct Stu s = { "张三","男","20210618",20,99 };//结构体声明与初始化  printf("姓名:%s\n", s.name);//结构体成员访问方式1:结构体变量名.结构体成员名  printf("性别:%s\n", s.sex);  struct Stu* ps = &s;  printf("学号:%s\n", ps->id);//结构体访问成员方式2:结构体指针->结构体成员名  printf("年龄:%d\n", ps->age);  printf("C语言考试成绩:%.2lf\n", ps->grade);  return 0;}
复制代码


运行结果:


姓名:张三性别:男学号:20210618年龄:20C语言考试成绩:99.00
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 26272)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码


对于结构体传参,尽量使用结构体指针传参,因为直接传结构体,当结构体很大时,会占用大量栈区的空间,甚至导致栈溢出。

🍉 1.2 结构体对齐及其大小计算

🍓1.2.1 偏移量

计算机汇编语言中的偏移量定义为:把存储单元的实际地址与其所在段的段地址之间的距离称为段内偏移,也称为"有效地址或偏移量"。


🍓1.2.2 结构体大小计算

结构体的大小不是将所有成员变量的大小加起来这么简单,实际上在内存中各成员是需要对齐的,在 vs 编译器中默认对齐值为8,在其他的编译环境中可能没有默认值,也可以默认值是其他的值。当然,这个默认对齐值是可以修改的,可以使用"pragma pack()"修改。如果对齐数为1,相当于结构体没有内存对齐。


#pragma pack(4)//设置默认对齐数为4#pragma pack()//无参数表示默认值为8(vs编译器)
复制代码


那为什么需要对内存对齐呢?


  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


结构体内存对齐规则:


  • [ ] 结构体的首成员与结构体的偏移量为0

  • [ ] 结构体成员需要对齐到对齐数的整数倍地址(偏移量)处。

  • [ ] 每个结构体成员都有一个对齐数,对齐数为编译器默认值与结构体成员大小中的较小值。

  • [ ] 结构体的总大小为最大对齐数(结构体各成员对齐数中最大的一个对齐数)的整数倍。

  • [ ] 如果结构体中存在嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。


//练习1struct S1{  char c1;  int i;  char c2;};//练习2struct S2{  char c1;  char c2;  int i;};//练习3struct S3{  double d;  char c;  int i;};//练习4-结构体嵌套问题struct S4{  char c1;  struct S3 s3;  double d;};
复制代码


练习 1:char c1为首个结构体成员,大小为1,偏移量为0,对齐数为1,存入内存位置如下:



第二个结构体成员int i,大小为4,对齐数为4,所以需存入偏移量为4处的地址处。



第三个结构体成员char c2,大小为1,对齐数为1,偏移量81的整数倍,所以c2存入偏移量为8的位置。



存入c2后结构体一共使用了9个字节,但是结构体大小需为最大对齐数的整数倍,该结构体中最大对齐数为4,所以结构体大小应该为4的整数倍,大小为12


练习 2:这个较练习 1 中的结构体,将两个char类型变量提前了,也就是将较小的成员变量放到了一起。我们来看看这个结构体相比于练习 1 的结构体大小会不会有变化吧!第一个结构体成员c1与练习 1 一样还是老位置,存入第二个结构体成员c2时,大小为1,偏移量为1,对齐数为1满足整数倍关系,因此c2存入c1的后。第三个成员大小为4,偏移量为2,对齐数为4,不满足对齐数为偏移量整数倍关系,所以要从偏移量为4的位置开始存,存完之后结构体一共用了8个字节,满足为最大对齐数整数关系,所以这个结构体大小为8。我们发现当把空间小的结构体成员尽量放在一起是,结构体的大小变小了,所以我们在定义结构体时尽量将空间小的结构体成员集中在一起。



练习 3:double8字节,存入后偏移量变为了8,然后存入char 大小为1,存入后偏移量为9,最后存入int大小为4,对齐数为4,从偏移量为12开始存入,结构体一共使用了16字节,满足为最大对齐的整数倍,所以结构体大小为16



练习 4:这个结构体里面嵌套了一个结构体S3,这个结构体的最大对齐数为8,大小为16。首先存一个char,大小为1,存完后偏移量为1,然后存S3,大小为16,最大对齐数为8,所以S3的对齐数为8,所以从偏移量为8的地址开始存,存完后,偏移量为24,最后存double,大小为8,对齐数为8,从偏移量为24的地址开始存,存完后,结构体一共使用了32字节,满足最大结构体成员的整数倍,所以该结构体大小为32



int main(){  printf("%d\n", sizeof(struct S1));  printf("%d\n", sizeof(struct S2));  printf("%d\n", sizeof(struct S3));  printf("%d\n", sizeof(struct S4));  return 0;}
复制代码


运行结果:


1281632
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 34308)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码

🍉 1.3 结构体与位段

🍓1.3.1 位段

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

🍓1.3.2 位段实现结构体

位段的声明和结构是类似的,有两个不同:1.位段的成员必须是 int、unsigned int 或 signed int 。2.位段的成员名后边有一个冒号和一个数字。


struct A { int _a:2; int _b:5; int _c:10; int _d:30;};
复制代码


这个使用位段实现的结构体大小多大?如果按前面所说的方法计算结构体大小,得出的结果是16,但是真是如此吗?我们使用程序来计算一下这个结构体大小,运行结果:


8
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 22660)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码


大小为8,并不是16,其实结构体成员名冒号后面那个数,其实是占用的空间大小,单位为 bit,在使用位段实现的结构体中,内存是一个一个对应数据类型开辟的,比如上面这个结构体 A,先开辟4字节大小空间也就是32bit 大小,然后存_a,占2bit,还剩30bit,再存_b,占5bit,还剩25bit,再存_c,占10bit,还剩15bit,最后存_d30bit,但是第一次开的那一块内存不够用了,所以需要再开辟一块大小为4字节(32bit)的空间,来存入_d,在整个结构体中一共开辟了8字节的空间,所以结构体大小为8。位段在内存中的分配


  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

  2. 位段的空间上是按照需要以 4 个字节( int )或者 1 个字节( char )的方式来开辟的。

  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。


其实在 C 语言标准中并没有给出位段具体开辟内存空间的细节,我们来尝试探究一下位段内存分配细节。




位段跨平台问题:


  1. int 位段被当成有符号数还是无符号数是不确定的。

  2. 位段中最大位的数目不能确定。(16 位机器最大 16,32 位机器最大 32,写成 27,在 16 位机器会出问题。

  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。


跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。


位段的应用:


🍇2.枚举

🍉 2.1 枚举概述

🍓2.1.1 枚举概念

enum,枚举在 C/C++/c#,还有 Objective-C 中,是一个被命名的整型常数的集合,枚举在日常生活中很常见。例如表示星期的 SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,SATURDAY, 就是一个枚举。枚举的说明与结构和联合相似。

🍓2.1.2 枚举的声明与使用

关键字:enum


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};
复制代码


枚举的使用


enum Color//颜色{ RED=1, GREEN=2, BLUE=4};enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。clr = 5;//错误,枚举常量不可被赋值
复制代码

🍉 2.2 枚举大小计算

枚举变量的大小,即枚举类型所占内存的大小,枚举类型变量占4字节。


enum A  {    QSW,    BSW,    CWS  }a;int main(){  printf("%d\n", sizeof(a));  return 0;}
复制代码


4
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 21156)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码

🍉 2.3 枚举与宏的区别

使用枚举定义的枚举常量是有类型的,为枚举类型,而使用#define宏是替换,并没有枚举类型这种性质。我们可以使用 #define 定义常量,为什么非要使用枚举?枚举的优点:


  1. 增加代码的可读性和可维护性

  2. #define定义的标识符比较枚举有类型检查,更加严谨。

  3. 防止了命名污染(封装)。

  4. 便于调试。

  5. 使用方便,一次可以定义多个常量。

🍇3.联合体

🍉 3.1 联合体概述

🍓3.1.1 联合体概念

在进行某些算法的 C 语言编程的时候,需要使几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术,几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构,在 C 语言中,被称作"共用体"类型结构,简称共用体,也叫联合体。

🍓3.1.2 联合体的声明与使用

关键字:union联合也是一种特殊的自定义类型.这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。


//联合类型的声明union Un{ char c; int i;};//联合变量的定义union Un un;
复制代码


联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。使用联合体判断大小端:


int is_bl(){  union BL  {    char a;    int b;  }un;  un.b = 1;  //返回1为小端,返回0位大端  if (un.a == 1)  {    return 1;  }  else  {    return 0;  }}int main(){  int ret = is_bl();  if (ret == 1)  {    printf("小端\n");  }  else  {    printf("大端\n");  }  return 0;}
复制代码


VS 编译器使用的是小端:


小端
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 23328)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码

🍉 3.2 联合体大小计算

  • [ ] 联合的大小至少是最大成员的大小。

  • [ ] 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。


union Un1{ char c[5]; int i;};union Un2{ short c[7]; int i;};//下面输出的结果是什么?printf("%d\n", sizeof(union Un1));printf("%d\n", sizeof(union Un2));
复制代码


联合体Un1最大成员大小为5char类型对齐数为1int类型对齐数为4,所以最大对齐数为4,联合体大小应为最大对齐数的整数倍,大小为8。联合体Un2最大成员大小为14short类型对齐数为2int类型对齐数为4,所以最大对齐数为4,联合体大小应为最大对齐数的整数倍,大小为16


运行结果:


816
D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 12864)已退出,代码为 0。按任意键关闭此窗口. . .
复制代码


发布于: 刚刚阅读数: 3
用户头像

未见花闻

关注

坚持+努力=诗+远方 2021.11.15 加入

一位热爱技术热爱分享的大学生!

评论

发布
暂无评论
C语言自定义类型的介绍(结构体,枚举,联合体,位段)_6月月更_未见花闻_InfoQ写作社区