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

举报
未见花闻 发表于 2022/04/29 23:16:36 2022/04/29
【摘要】 C语言自定义类型的介绍(结构体,枚举,联合体,位段)

⭐️前面的话⭐️

大家好!在C语言中,有个叫“自定义类型”玩意,它究竟是什么呢?其实,就是字面意思,可以自己定义的类型就是自定义类型。具体说就是我们熟知的结构体,枚举,位段,联合体(共用体)。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年4月29日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《C语言程序设计》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!


🍇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
年龄:20
C语言考试成绩:99.00

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

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

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

🍓1.2.1偏移量

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

1

🍓1.2.2结构体大小计算

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

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

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

  1. 平台原因(移植原因)
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

结构体内存对齐规则:

  • [ ] 结构体的首成员与结构体的偏移量为0
  • [ ] 结构体成员需要对齐到对齐数的整数倍地址(偏移量)处。
  • [ ] 每个结构体成员都有一个对齐数,对齐数为编译器默认值与结构体成员大小中的较小值。
  • [ ] 结构体的总大小为最大对齐数(结构体各成员对齐数中最大的一个对齐数)的整数倍。
  • [ ] 如果结构体中存在嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
//练习1
struct S1
{
	char c1;
	int i;
	char c2;
};
//练习2
struct S2
{
	char c1;
	char c2;
	int i;
};
//练习3
struct S3
{
	double d;
	char c;
	int i;
};
//练习4-结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

练习1:char c1为首个结构体成员,大小为1,偏移量为0,对齐数为1,存入内存位置如下:
2
第二个结构体成员int i,大小为4,对齐数为4,所以需存入偏移量为4处的地址处。
2
第三个结构体成员char c2,大小为1,对齐数为1,偏移量81的整数倍,所以c2存入偏移量为8的位置。
3
存入c2后结构体一共使用了9个字节,但是结构体大小需为最大对齐数的整数倍,该结构体中最大对齐数为4,所以结构体大小应该为4的整数倍,大小为12

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

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

运行结果:

12
8
16
32

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语言标准中并没有给出位段具体开辟内存空间的细节,我们来尝试探究一下位段内存分配细节。
55

66
位段跨平台问题:

  1. int位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

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

位段的应用:
21

🍇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

运行结果:

8
16

D:\gtee\C-learning-code-and-project\test_922\Debug\test_922.exe (进程 12864)已退出,代码为 0。
按任意键关闭此窗口. . .
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。