自定义类型:结构体、位段、枚举、联合 ------- C语言

C语言中,7可以存放再整型变量中,'c' 可以存放在字符型变量中,3.14可以存放在浮点型变量中,一个学生有:姓名、年龄、性别、学号等。C语言中是否有这样一个可以存放学生的类型呢?答案是肯定的,这一种类型教自定义类型,本篇博客将和各位小兄弟一起学习C语言中的自定义类型,其中包括结构体枚举联合,跟着本篇博客学习完后,你对C语言的理解肯定会再上一个level!

665aaeaa33c689ae6bc40a5131f677ba.jpeg


1. 结构体

我们开头提到的一个学生有:姓名、年龄、性别、学号等,可以用一个结构体变量来存储,首先我们要学习如何声明以及初始化一个结构体类型。

1.1. 结构体的声明及其初始化

1.1.1. 结构体声明

08846e75a7ae48bcbbbd9f3552d6a273.png

特殊的声明:

93f8e0e5b99647df8676dc107fbedc51.png

 注:匿名结构体类型只能使用一次,走后续的代码中无法再次使用该结构体类型

1.1.2. 结构体变量的定义和初始化

struct Point
{
	int x;
	int y;
}p1;                       //声明结构体类型的同时定义变量p1,此时变量p1为全局变量

int main()
{
	//...

	struct Point p2;       //在main函数中定义变量p2,此时变量p2为局部变量

	//...

	return 0;
}



struct Stu
{
	char name[15];
	int age;
}s1 = { "zhangsan",19 };   //定义变量的同时进行初始化,变量s1为全局变量

int main()
{
	//...

	struct Stu s2 = { "lisi",23 };  //变量s2为局部变量

	//...

	return 0;
}



struct Node
{
	int data;
	struct Point p;
}n1 = { 13,{2,4} };      //结构体嵌套初始化,n1为全局变量

int main()
{
	//...

	struct Node n2 = { 88,{42,1} };  //n2为局部变量

	//...

	return 0;
}

1.2. 结构体的自引用

结构体的自引用光看这几个字好像很专业,但其实用大白话概括就是:结构体里包含同类型的结构体指针变量。接下来用一个例子带大家深刻理解理解。

struct Node
{
	int data;
	struct Node* next;   //同类型的结构体指针变量
};

1.3. 结构体的内存对齐

struct S1
{
	char c1;
	int i;
	char c2;
};

一个字符型数据在内存中占1个字节,一个整型数据在内存中占4个字节,那么上面这个结构体数据在内存中占几个字节呢?有的小兄弟可能会说,“简单,不就是将所有的结构体成员变量相加嘛”。这种想法就是一个大错特错,计算一个结构体数据的大小可不是简单的将所有结构体成员变量相加就得出来的,想要计算一个结构体数据的大小,首先要明白结构体的内存对齐是怎么一回事,继续跟随博客的脚步一起学习结构体的内存对齐吧!

| 对齐规则 |

①  第一个成员在与结构体变量偏移量为零的地址处。

②  其他成员变量要对齐某个数字(对齐数)的整数倍的地址处。其中:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值 (Visual Studio 2022中默认的对齐数为8)

③  结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

④  如果结构体内嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数中最大的对齐数(含嵌套结构体的对齐数)的整数倍。

读完上面四条规则,各位小兄弟对如何计算结构体的大小应该有了一定的了解,但光是文字化的规则还是略显生硬了点,有图有真相,下面通过几个例子再来解释解释结构体内存对齐到底是怎么一回事。

57ef0b1ccaae49bbb8ad3f71295fa98d.png

看了上面的图解如果你感觉你又行了不妨趁热打铁,依葫芦画瓢,计算以下两个结构体变量的大小。

struct S2
{
    char c1;
    char c2;
    int i;
};

>> sizeof(struct S2) = 8

struct S3
{
    double d;
    char c;
    int i;
};

>> sizeof(struct S3) = 16

如果我们仔细观察会发现结构体类型S1和结构体类型S2的结构体成员相同,但成员顺序有所不同。

8438b551148f43a08f7e1cdbfdbd3d70.png

从以上两个图解可以看出,拥有相同的结构体成员,但结构体成员的顺序不同,会导致最终结构体变量的大小不同。从上图不难发现结构体变量S2的大小要小于结构体变量S1的大小。

在设计结构体类型的时候,既要满足内存对齐,又要节省空间,我们可以让占用空间小的成员尽量集中在一起

使用 #pragma pack( ) 修改默认对齐数

当我们在设计结构体类型时,结构体在对齐方式不合适的时候,我们可以使用#pragma pack( )来修改默认对齐数。

下面我们通过一段实例代码来进行讲解。

#pragma pack(1)    //将默认对齐数设置为1

struct S2
{
 char c1;
 int i;
 char c2;
};

#pragma pack()     //取消设置的默认对齐数,将默认对齐数还原为初始值

算一下修改默认对齐数后结构体类型S2的大小是多少。

>> sizeof(struct S2) = 6

结构体内存对齐存在的意义

1. 平台原因(移植原因):

        不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因:

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

总的来说,结构体的内存对齐是拿空间来换取时间的做法。


2. 位段

位段又是什么,位段这种自定义类型与结构体有哪些区别,位段有什么作用。

接下来我们就继续一起探讨什么是位段。

2.1. 什么是位段

位段是在结构体中实现。

位段的“位”指的是比特位。

位段的声明和结构体是类似的,但也存在以下两个不同点:

1. 位段的成员必须是整型家族成员(char、int、unsigned int、signed int)

2. 位段的成员名后边有一个冒号和一个数字,冒号 + 数字表示只给该成员分配多少个比特位的空间

| 例 |

struct A
{
	int a : 2;     //只给整型变量a分配2个比特位的空间
	int b : 5;     //只给整型变量b分配5个比特位的空间
	int c : 10;    //只给整型变量c分配10个比特位的空间
	int d : 30;    //只给整型变量d分配30个比特位的空间
};

位段可以灵活的按需给成员分配空间。

例如,当我需要一个整型变量flag来帮我判断真假时(1表示真,0表示假),实际上我只需要一个比特位的空间就可以达到判断真假的目的。若不使用位段,系统会为flag开辟一个整型的空间,也就是4个字节,32个比特位;若使用位段来对变量flag进行处理,可以做到只开辟一个比特位的空间,从而达到节省空间的目的。

2.2. 位段的内存分配

上文我们学习了结构体的内存对齐,那么位段也有内存对齐吗?答案是肯定的,但在位段这里不是内存对齐,而是内存分配。想要搞懂位段的内存分配,就要熟悉位段内存分配的规则。

| 内存分配规则 |

1. 位段的空间上是根据需求以4个字节(int类型)或者1个字节(char类型)的方式开辟的。

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

看完以上几行文字,总觉得懂了又没完全懂。别急,下面带入几个实例加上图解来深刻理解位段的内存分配到底是怎么一回事。

#include<stdio.h>

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;

	printf("sizeof(struct S) = %d", sizeof(struct S));

	return 0;
}

5f27521095e942f3943ce2bea7a2dbd4.png

抛出了两个猜想,接下来就是验证哪个是正确的。

4b7bcba994764bd7ad255ddc0f7072ee.png

 在Visual Studio 2022的环境下,程序的运行结果证实了猜想一是正确的。

也就是说,当剩余的比特位不足以为当前成员分配空间时,将剩余的比特位舍弃,再次开辟4个字节或1个字节的空间(开辟4个字节还是1个字节由成员的类型决定,位段的成员类型一般是同一类型)。

了解了位段对于剩余比特位的处理后,就已经可以计算出使用位段的自定义类型的大小了。

那么使用位段的自定义类型的数据在内存中又是如何存储的呢,接下来我们还是以struct S为例,就这个问题展开探讨。

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

首先提出一个猜想,假设位段在内存中存储数据时,先使用高地址处的空间,后使用低地址处的空间

接下来我们用图例简单的分析我们的猜想,然后运行一段简单的代码来判断猜想正确与否。

d7af87c5a3f54a81bc409bbcf9fd84db.png

3e14f76e87b442d4b071e5f123bf495c.png

 在VS2022的内存监视窗口中证实了我们的猜想,至少在Visual Studio 2022的环境下是成立的。

由此我们可以得出两个结论:

① 在位段的内存分配中,当剩余的比特位不足以为当前的成员分配空间时,舍弃剩余的比特位,再次开辟4个字节或1个字节的空间(开辟4个字节还是1个字节由成员的类型决定)。

② 位段在内存中存储数据时,先使用高地址处的空间,后使用低地址处的空间。

<以上结论在Visual Studio 2022的环境下成立,其他环境有待考证>

2.3. 位段的跨平台问题

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

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

3. 位段中的成员在内存中存储数据时,是先使用高地址后使用低地址,还是先使用低地址后使用高地址,标准尚未定义。

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


3. 枚举

顾名思义枚举就是把可能的取值一一列举。

例如,在我们的生活中:

一周的周一到周日,七天可以一一列举

月份有12个月,可以一一列举

性别有,男、女、保密,也可以一一列举

像以上这样的情况,可能的取值情况比较少的,可以使用枚举类型来解决相关的问题,若可能的取值较多,则不适合用枚举类型来解决问题。

3.1. 枚举类型的定义及使用

41c6ce2643194ae88f2dfbdb6378d2a8.png

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

int main()
{
	enum Day d = Fri;    //创建一个枚举类型的变量d,将枚举常量Fir赋值给d

	return 0;
}

 以上定义的 enum Day 是枚举类型,{ }中的内容是枚举类型的可能取值,也叫枚举常量

这些可能取值都是有数值的,默认第一个枚举常量的数值为0,往后依次递增1,当然如有特殊需要,可以在定义枚举类型的时候同时赋初值。

| 例 |

enum Color
{
	RED = 1,
	GREEN = 2,
	BULE = 5
};

 若只对部分枚举常量赋初值,则其余枚举常量还是往后依次递增1。

| 例 |

enum Day
{
	Mon = 23,
	Tues,       //24
	Wed,        //25
	Thur = 77,
	Fri,        //78
	Sat,        //79
	Sun         //80
};

3.2. 枚举的优点

通过上面的学习我们了解到,枚举列举的是其可能的取值,所有可能的取值都是一个常量,我们也能使用 #define 定义常量,那为什么我们还要使用枚举类型呢?

枚举的优点:

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

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

        3. 防止命名污染 (将同类型的取值封装在一个枚举类型里) 。

        4. 便于调试。

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


4. 联合 (共用体)

什么是联合,为什么又叫共用体?接下来就和大家一起扒扒联合的底子。

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

4.1. 联合类型的定义

1432ac014a9443edad4595999aee23e9.png

union Un
{
	int i;
	char c;
};

int main()
{
	union Un u;
	u.i = 7;

	return 0;
}

4.2. 联合的特点

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

union Un
{
	int i;
	char c;
};

int main()
{
	union Un u;

	printf("sizeof(u) = %d\n", sizeof(u));

	return 0;
}

以上这个联合类型的大小是多少呢?以我们常规思路来看,整型变量i占4个字节,字符型变量c占一个字节,怎么说这个联合的大小也得5个字节吧。

564d82075ccc4763bfd1ef48cdd94467.png

 当我们把程序运行起来后,发现这个联合类型的大小为4个字节,这一点确实印证了我们刚刚描述的联合的特点,但是为什么是4个字节呢?别着急,我们继续往下探讨。

 如何证明联合类型中的成员都共用同一块内存空间呢?这里采用的方法是将联合类型u的地址,联合类型u中成员i的地址,联合类型u中成员c的地址分别打印出来,观察这三个地址的情况。

union Un
{
	int i;
	char c;
};

int main()
{
	union Un u;

	printf("%p\n", &u);
	printf("%p\n", &(u.i));
	printf("%p\n", &(u.c));

	return 0;
}

e61037dd6ca94db4a212d815df78de08.png

当程序运行起来后,可以发现这三个地址是一模一样的。这又该如何解释呢?

下面通过图解来解释对上述情况。

0c2f74d4e22a456d81dc70f64a93bc69.png

 由上图分析可以看出,成员i和成员c至少在第一个字节上是重叠的,共用了第一个字节的空间,所以这种结构类型也叫共用体。

由于联合的特性,各成员都共用一块内存空间,导致在使用联合类型时,只能使用众多成员中的一个。例如,当我们使用联合类型u的时候,我们使用成员i时,不能使用成员c;使用成员c时,不能使用成员i ( 当我们使用成员i时,给i赋值,同时使用成员c,给c赋值的同时会将成员i第一个字节的数据给覆盖掉,从而导致了成员i的值被修改这一情况 ) 。

仅用文字来描述上述的情况还是不够直观,我们还是用一个简单的程序来验证一下联合的特性。

76e07a19a9f946cd940771372775a0e7.png

在调试中的内存窗口可以发现,执行完第15行代码后,联合类型u的4个字节内存空间中存储了成员i的值,当执行完第16行代码后,联合类型u的内存空间中第一个字节的数据被成员c的值覆盖了,有图有真相,联合类型中的各成员共用同一块内存空间,这一结论是属实的。

4.3. 联合大小的计算

| 计算规则 |

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

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

| 例1 |

union Un1
{
	char arr[5];
	int i;
}u1;

sizeof(u1) = 8

成员arr的对齐数为1,成员i的对齐数为4,所以最大对齐数为4。

数组arr占5个字节的内存空间,i占4个字节的内存空间,5不是最大对齐数4的整数倍,还要浪费3个字节的空间来对齐到最大对齐数的整数倍8,所以联合类型u1的大小为8个字节。

606110fa409b4335a5c4bc16aa9cca4e.png

| 例2 |

union Un2
{
 short arr[7];
 int i;
}u2;

sizeof(u2) = 16

成员arr的对齐数为2,成员i的对齐数为4,所以最大对齐数为4。

数组arr占14个字节的内存空间,i占4个字节的内存空间,14不是最大对齐数4的整数倍,还要浪费2个字节的空间来对齐到最大对齐数的整数倍16,所以联合类型u2的大小为16个字节。

f0092c9c669c4189bb7b204873ee898a.png


本次与大家一起探讨C语言中的自定义类型 ( 结构体、位段、枚举、联合 ) 到这就已经接近尾声了,期待下次与你相遇。

b8684054acbc4c178cabc301c68046bc.gif

< 你的关注点赞评论收藏都是对我创作最大的鼓励 > 

( 若本篇博客存在错误,望指出,感谢! ) 

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值