【C语言进阶】 详解自定义类型:结构体,枚举,联合

目录

结构体

结构体基础知识

结构的自引用

 结构体内存对齐

结构体大小计算 

存在内存对齐的原因

设计结构体时的技巧 

修改默认对齐数

结构体实现位段(位段的填充&可移植性)

什么是位段

位段的内存分配

位段的跨平台问题

位段的应用

枚举

枚举类型的定义

枚举的优点

枚举的使用

示例一

 示例二

联合(共用体)

联合类型的定义

联合的特点

联合体的应用 

总结 


 

结构体

结构体基础知识

关于结构体的一些基础知识博主在前面的博文:《初始结构体》中有讲到,有兴趣的宝子可以点下面的链接进行学习初识结构体_遇事问春风乄的博客-CSDN博客https://blog.csdn.net/m0_71731682/article/details/130754959?spm=1001.2014.3001.5502这次博主就《初始结构体》进行一个补充

结构的自引用

结构体的自引用就是指在结构体内部,包含指向自身类型结构体的指针。

正确使用方式如下

struct Node
{
 int data;
 struct Node* next;
};

这是正确的使用。可是有些同学写成下面的代码

struct Node
{
 int data;
 struct Node next;
};

 这就错了,切记,结构体自引用,成员定义只能是指针,如果结构体内成员定义为struct Node; 则会报错,因为next定义中又有next,无限循环,系统无法确定该结构体的长度,会判定定义非法

初次之外还有的同学写成下面的代码

typedef struct
{
 int data;
 Node* next;
}Node;

这也是错的,因为我们这里是先打开的stuct,使用了Node,而这时Node还未定义。进行编译后就会出现以下错误

2365f125f44f4d558cfb77b520df5e8c.png

 对于这个错误我们可以进行改进一下就可以用了,实现如下

typedef struct Node
{
 int data;
 struct Node* next;
}Node;

 结构体内存对齐

我们已经掌握了结构体的基本使用了。 现在我们深入讨论一个问题:计算结构体的大小。
那么我们该如何计算结构体的大小呢?

结构体也有自己的大小,但是结构体的大小并不是简单地将每个结构体成员的大小相加就能得到。

结构体的大小计算遵循结构体的对齐规则

1. 第一个成员在与结构体变量偏移量为0的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
       VS中默认的值为8
       Linux中没有默认对齐数,对齐数就是成员自身的大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

结构体大小计算 

知道了结构体内存对齐规则,我们就可以计算结构体的大小了。计算结构体的大小可分为 三个步骤。我们拿下面这个结构体举例(博主使用编译器为vs):
struct S1
{
 char c1;
 int i;
 char c2;
};

第一步:找出每个成员变量的大小将其与编译器的默认对齐数相比较,取其较小值为该成员变量的对齐数

585e152c56f34aa7a0b06dc30044be7d.png

 

第二步:根据每个成员对应的对齐数画出它们在内存中的相对位置。 

6be87e3b88ff4e7a9333b41fc5f7e787.png

第三步:通过最大对齐数决定最终该结构体的大小。

1a602ed9e58548de828318ebaf39dc5e.png

注意:大多数情况下,成员变量已经占用的总字节个数并不一定正好为其成员变量中的最大对齐数的整数倍,这时我们需要将其扩大为最大对齐数的整数倍。如图中绿色后的黄色部分

结构体内有结构体的计算图解 

b3c6ec87d78c4ea59a6fb0cbb762b498.png

存在内存对齐的原因

平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
比如,当一个平台要取一个整型数据时只能在地址为4的倍数的位置取得,那么这时就需要内存对齐,否则无法访问到该整型数据。

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

在画图时可能有博友会想,内存这么重要,在进行内存对齐的时候怎么还有内存被白白浪费掉呢?
现在看来,其实结构体的内存对齐是拿空间来换取时间的做法。

47af4d2b9884424d994fba93f393829f.png

设计结构体时的技巧 

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。 例如以下代码
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

修改默认对齐数

要修改编译器的默认对齐数,我们需要借助于以下预处理命令:

#pragma pack()

如果在该预处理命令的括号内填上数字,那么默认对齐数将会被改为对应数字;如果只使用该预处理命令,不在括号内填写数字,那么会恢复为编译器默认的对齐数。

#pragma pack(8)//设置默认对齐数为8
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

于是,当结构体的对齐方式不合适的时候,我们可以自己更改默认对齐数。

结构体实现位段(位段的填充&可移植性)

什么是位段

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

位段和结构体其实是非常相似的,但是有两个不同点:

1. 位段的成员必须是 char、int、unsigned int 或signed int 。
2. 位段的成员名后边有一个冒号和一个数字。

举个例子

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

说明:

首先我们要明白位段中的这个“位”字其实指的是二进制位。
我们知道一个二进制位就是1个比特位。
所以,A中int _a : 2;其实表示的就是
_a的大小是2bit;
同理:
_b的大小是5bit
_c的大小是10bit
_d的大小是30bit 

位段的内存分配

我们还是用上述代码,我们求一下上述代码的大小
0f69b2760b7444cf9c496f6f7cdb47e3.png

我们惊奇的发现为8,占了8个字节,那么为什么是8呢?

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
 
就是说,如果位段的成员全部是整型的(位段成员一般都是同类型的),那上去就先给这个位段开辟4个字节的空间,如果不够用,放不下所有的成员,那就再开辟4个字节的空间,还不够用,继续开辟,以此类推。如果成员全部是char类型的,那就一次开辟1个字节的空间,直至放得下所有成员。
具体分配为:
由于A的成员都是整型(int ),所以一次给A分配4个字节。4个字节是32给比特位,A的前3个成员_a、_b、_c占了17个bit,32-17还剩15bit,但是A的第四个成员_d大小是30bit,而15<30不够。怎么办?再分配4个字节,这下就能放下_d,因此,struct A的大小是4+4=8个字节。
 
注意:位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
舍弃剩余的位还是利用,这是不确定的
总结: 跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

位段的应用

用于IP数据报,格式如下

901790d0e9b14494b023c43eeaa67d13.png

枚举

枚举顾名思义就是一一列举。 把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这里就可以使用枚举了。

接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:

#define MON 1

#define TUE 2

#define WED 3

#define THU 4

#define FRI 5

#define SAT 6

#define SUN 7

这个看起来代码量就比较多,接下来我们看看使用枚举的方式:

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};

这样看起来是不是更简洁了。

注意:第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

枚举类型的定义

前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。

我们可以通过以下三种方式来定义枚举变量

1、先定义枚举类型,再定义枚举变量

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;

2、定义枚举类型的同时定义枚举变量

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

 3、省略枚举名称,直接定义枚举变量

enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

枚举的优点

1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 便于调试
4. 使用方便,一次可以定义多个常量

枚举的使用

示例一

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};
 
int main()
{
    enum DAY day;
    day = WED;
    printf("%d",day);
    return 0;
}

输出结果为

939e078afeb0495aa6fba3aec2c15ba0.png

 示例二

在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。

以下示例使用 for 来遍历枚举的元素:

#include <stdio.h>
 
enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
    // 遍历枚举元素
    for (day = MON; day <= SUN; day++) {
        printf("枚举元素:%d \n", day);
    }
}

输出结果为

b373ca3dc5f743a686936019d5e7a386.png

联合(共用体)

联合类型的定义

联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
比如:
//联合类型的声明
union Un
{
 char c;
 int i;
};

联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。
例如以下代码
union Un
{
	char c;
	int i;
};

int main()
{
	printf("%d\n", sizeof(union Un));
	union Un un;
	printf("%p\n", &un);
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));
	return 0;
}

我们再看一下运行结果

b84f730814e545c7aa9b9490869b468a.png

我们发现联合体大小为 4,而且&un,&(un.i),&(un.c)的地址相同,那么这样即可证明联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小·

8eb20122a93d49f9940238833d09ea2a.png

 

联合体的应用 

进行系统大小端的判断

代码如下

int check_sys()
{
	union
	{
		int i;
		char c;
	}un = {.i = 1};
	return un.c;
}

int main()
{
	int ret = check_sys();
	

	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}

原理图如下:

048d8f655b76415286f6cea6dc78698f.png

博主所用的为小段存储模式,运行结果如下 

5db7441930464ee4bb497394cc1c03fc.png

总结 

关于自定义类型就讲解到这儿,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下。

 

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

遇事问春风乄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值