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

目录

1.结构体

1.1结构的基本知识

1.2结构体的声明

1.3特殊的声明

1.4结构的自引用

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

 1.6结构体内存对齐

1.7修改默认对齐数

1.8结构体传参

2.位段

2.1什么是位段?

2.2位段的内存分配

2.3位段的跨平台问题

2.4位段的应用

3.枚举

3.1枚举的定义

3.2枚举的优点

4.联合(共用体)

4.1联合体的定义

4.2联合体的使用 

 4.3联合体大小的计算


1.结构体

1.1结构的基本知识

结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同类型的变量。

我们前面说数组是一组相同类型元素的集合,而结构体中的每个成员可以使不同类型的变量。

1.2结构体的声明

例如,描述一个学生:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};//分号不能丢

我们也可以创建结构体变量s1,s2,s3:

法一:

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}s1,s2,s3;//分号不能丢

法二:

#include<stdio.h>
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};//分号不能丢
int main()
{
	struct Stu s1, s2, s3;
	return 0;
}

1.3特殊的声明

在声明结构体时,可以不完全的声明,这种也可以叫匿名结构体类型。

如下所示:

struct
{
	int a;
	char c;
	float f;
}s1,s2;

在声明结构体时,结构体类型名被省略了,注意此时要定义一个结构体变量只能使用法一的方法,不能用法二的方法。

下面我们再来创建一个和上面一样的匿名结构体类型,并将它定义为指针,那编译器会不会认为这两个结构体类型是一样的呢?

#include<stdio.h>
struct
{
	int a;
	char c;
	float f;
}x;
struct
{
	int a;
	char c;
	float f;
}*p;
int main()
{
	p = &x;
	return 0;
}

运行:

可以看到,编译器报错了,两种类型不兼容,所以说,虽然我们声明的结构体类型看似相同,但是编译器不认为它们是一种类型。所以一般情况下,匿名结构体只能使用一次。

1.4结构的自引用

在这之前,我们先来了解一些数据结构的知识。

顺序表是顺序存储的,链表是通过节点1找到节点2,然后根据节点2找到节点3.......,那要怎样实现链表呢?

有人说,只要通过这个节点能找到下一个节点就行了,那我们就把自己包含在自己里面就能一直找下去啊,这种做法行吗?

如下面代码所示:

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

 显然是不行的,大家考虑一下,如果使用上述方法,sizeof(struct Node)该怎么计算?根本无法计算,因为第一个next里面包含data和next,下一个这个next又包含data和next,这样一直下去,结构体类型的大小根本计算不了,所以这种方法行不通。

其实这里使用结构体指针就能解决了,只要把节点2的地址存放在节点1里面,节点3的地址存放在节点2里面........就能根据地址找到后续的节点。

struct Node
{
	int data;//数据域
	struct Node* next;//指针域
};

这就叫做结构体的自引用。

下面我们再来思考一个问题:

下列对匿名结构体类型进行重命名,然后在结构体内使用它重命名后的类型名,这种方式正确吗?

typedef struct 
{
	int data;
	Node* next;
}Node;
int main()
{
	Node n = { 0 };
	return 0;
}

这种方式是不对的,因为我们在对匿名结构体进行重命名为Node之前,这个结构体类型应该是已经存在的,但是我们还没有命名为Node呢,它在结构体内部就提前使用了Node*,这明显就不对,就像是先有蛋还是先有鸡的问题。

正确的写法应该是这种,不要用匿名结构体类型:

typedef struct Node 
{
	int data;
	struct Node* next;
}Node;
int main()
{
	Node n = { 0 };
	return 0;
}

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

结构体的定义:上文中也讲过

#include<stdio.h>
struct SN
{
	char c;
	int i;
}sn1,sn2;//全局变量
int main()
{
	struct SN sn3, sn4;//局部变量
	return 0;
}

结构体变量的初始化:

#include<stdio.h>
struct SN
{
	char c;
	int i;
}sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量
int main()
{
	//struct SN sn3, sn4;//局部变量
	printf("%c %d", sn2.c, sn2.i);
	return 0;
}

上述代码对sn1,sn2进行初始化及打印。

运行结果:

当然,我们的结构体内部也可以出现结构体变量:

#include<stdio.h>
struct SN
{
	char c;
	int i;
}sn1 = { 'q',100 }, sn2 = {.i=200,.c='w'};//全局变量

struct S
{
	double d;
	struct SN sn;
	int arr[10];
};
int main()
{
	struct S s = { 3.14,{'w',10},{1,2,3} };
	printf("%lf %c %d\n", s.d, s.sn.c, s.sn.i);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", s.arr[i]);
	}
	return 0;
}

运行结果:

 1.6结构体内存对齐

我们现在已经掌握了结构体的基本使用,现在我们来深入讨论一个问题:计算结构体的大小。

这也是一个热门考点:结构体内存对齐

先来看下面这段代码:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct s1
{
	char c1;
	int i;
	char c2;
};
struct s2
{
	int i;
	char c1;
	char c2;
};
int main()
{
	printf("%zd\n", sizeof(struct s1));
	printf("%zd\n", sizeof(struct s2));
	return 0;
}

大家觉得结果是什么呢?

直接来看吧:

是不是和预想的结果差距很大,那这又是为什么呢?

这就不得不提一下结构体的对齐规则了:

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

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

    对齐数 = 编译器默认的一个对齐数与该成员自身大小之间的较小值。

     VS中对齐数默认为8

     Linux中没有默认对齐数,对齐数就是成员自身的大小

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

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

了解了结构体对齐规则,我们来分析一下上述代码:

先来看s1,根据第一条规则,第一个成员char c1放在相对于结构体变量偏移量为0的地址处,

第二个成员int i根据第二条规则,要对齐到对齐数的整数倍的地址处,VS默认的对齐数是8,i自身的大小是4,所以对齐数应该是4,所以i应该从地址为4的位置往后占用4个字节,

第三个成员char c2,它的对齐数是1,而任何数都是1的整数倍,所以接着上一个成员地址往下存一个字节,

现在三个成员占用到9个字节的空间,而根据规则3,结构体总大小是最大对齐数的整数倍,c1的对齐数是1,i的对齐数是4,c2的对齐数是1,所以最大对齐数是4,9显然不是4的整数倍,所以要往后浪费3个字节的空间到12,所以最终打印结果是12。

我们来看结构体struct s1在内存中的存储:

同理,结构体struct s2也是参考上述对齐规则:


 

下面我们再来看一个例题:

#include<stdio.h>
struct s3
{
	double d;
	char c;
	int i;
};
int main()
{
	printf("%zd\n", sizeof(struct s3));
	return 0;
}

自己试着做一下

下图是答案:

以上是对对齐规则前3个的使用,下面我们来看看,对齐规则第4个规则该怎么使用:

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

 如果我们在结构体struct s4中嵌套一个结构体struct s3,那struct s4的大小是多少?

#include<stdio.h>
struct s3
{
	double d;
	char c;
	int i;
};
struct s4
{
	char c1;
	struct s3 s3;
	double d;
};
int main()
{
	printf("%zd\n", sizeof(struct s4));
	return 0;
}

结果应该是:32

首先,第一个成员c1存储在偏移量为0的地址,而第二个成员s3是个结构体,按照规则4,嵌套的结构体对齐到自己的最大对齐数的整数倍处,上文中struct s3中的最大对齐数是8,那s3就应该从地址8往后占用16个字节,到地址23,24恰好是第三个成员d的对齐数8的整数倍,所以d应该从地址24往后占用8个字节空间,此时结构体一共占用32个字节的空间,32恰好是所有对齐数中最大对齐数8的整数倍,所以结构体最终的大小就是32。

那什么存在内存对齐呢?

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

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

2.性能原因:

数据结构(尤其是栈)应该尽可能的在自然边界上对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存仅需要一次访问。

例如:我们要存一个char型和一个int 型的数据,(假设是32位机器,一次读取4个字节),如果没有对齐,那我们从char开始读,第一次只能读到int型的3个字节,还得读一次才能凑够4个字节:

如果对齐了,char型在存储时会浪费掉3个字节,然后再存储int型数据,这样我们只需读一次就能得到int型的数据:

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

 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到?

这个可以看看我们之前s1和s2,

struct s1
{
	char c1;
	int i;
	char c2;
};
struct s2
{
	int i;
	char c1;
	char c2;
};

成员相同,写法不同,结构体的大小不同,分别是12和8,由此可以总结出,在设计结构体的时候,要想既满足对齐,又要节省空间,把占用空间小的成员尽量写在一起即可

1.7修改默认对齐数

之前我们见过了#pragma这个预处理命令,这里我们可以用它来修改默认对齐数:

#include<stdio.h>
#pragma pack(1)//修改默认对齐数为1
struct s1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消修改的默认对齐数,还原为默认
int main()
{
	printf("%zd\n", sizeof(struct s1));
	return 0;
}

修改默认对齐数为1后,结构体成员的存储应该是挨着存储的,那打印结果就是6。

一般我们在设置默认对齐数时,设置为2的次方数。

1.8结构体传参

这个我们在之前的章节中讲过,这里不做细讲,直接看代码:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S
{
	int arr[100];
	int num;
};
//结构体传参
print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	struct S s = { {1,2,3},100 };
	print1(s);//传结构体
	print2(&s);//传地址
	return 0;
}

上面有两种结构体传参方式,我们选择的那种方式比较好呢?

答案是第二种,结构体地址传参。

原因

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

 形参是是实参的一份临时拷贝,要是选择结构体传参的话,需要额外开辟一处空间,会造成空间的浪费。

2.位段

2.1什么是位段?

位段的声明和结构体是类似的,但是有两个不同:

1.位段的成员必须是int、unsigned int、或signed int。

2.位段的成员名后面有一个冒号和一个数字。

比如:

#include<stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	printf("%zd\n", sizeof(struct A));
	return 0;
}

其中A就是一个位段类型,那struct A的大小是多少呢?

答案是8。

为什么是8呢?有人说,2+5+10+30=47,一个int型不够用,得用两个int 型,就是8个字节了,这种说法对吗?

下面我们来讲一下,位段的内存分配:

2.2位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

下面我们来看一个例子:

#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("%zd\n", sizeof(s));
	return 0;
}

我们假设位段中的成员a,b,c,d在内存中是从右向左分配的,并且空间不够用就舍弃,重新开辟新的空间,那在内存中应该如下图所示:

只需要3个字节就能将a,b,c,d都存放下,那我们来验证一下运行结果是不是3:

可以看到,确实是3,那在VS中可能就是像上面所说的那样存储的,为什么说可能呢,这个问题放在位段的跨平台问题中讲。

现在我们接着来将10、12、3、4存放在a,b,c,d中,看看是怎样存进去的?

存进去后是01011010 00000011 00000100,化为16进制数应该是:620304,

打开监视窗口可以看到:

那我们就可以确定,当前VS编译器中,位段中的成员的存储就是从左往右分配的,并且当开辟的空间不够用时,剩余的空间会被舍弃,重新开辟一个新的空间。

这只能说明在VS编译器中位段的内存分配方式,不一定适用于其他编译器。

这就牵扯到位段的跨平台问题了。

2.3位段的跨平台问题

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

总结一下:跟结构体相比,位段可以达到同样的效果,可以节省空间,但是位段存在跨平台问题。

2.4位段的应用

以上是一个IP数据包的格式,如果我们要给另外的人发送信息,在计算机中不可能是简单的信息裸奔过去,而是把它打包成数据包的格式,这时我们用位段可以最大程度的节省空间。

3.枚举

枚举就是把可能得取值一一列举。

比如生活中的,一周从星期一到星期日可以一一列举,性别可以一一列举,三原色可以一一列举。

3.1枚举的定义

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
};
enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

注意枚举列举的是可能取值,称为枚举常量,用逗号一一隔开,和结构体不同,结构体里面时成员变量,用分号隔开。

下面我们也可以创建一个枚举类型的变量:

#include<stdio.h>
enum Color
{
	RED,
	GREEN,
	BLUE
};
int main()
{
	enum Color c = GREEN;
	return 0;
}

枚举常量也是有值的,默认从0开始,依次递增。

我们可以打印一下:

当然,我们也可以自己指定值:

3.2枚举的优点

我们可以使用#define定义常量,为什么要使用枚举?

枚举的优点:

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

 如果我们用#define定义常量的话,RED GREEN BLUE就是一堆数字,根据数字很难联想到颜色。

2.和#define定义的标识符相比,枚举有类型检查,并且更加严格

我们把文件名改为.cpp运行下面代码会发现报错了:

枚举有类型检查,而#define定义的标识符常量是没有类型检查的。

3.便于调试

如果我们用#define RED 0;我们使用时int a=RED;此时我们看到的是RED,但是编译器在预处理阶段早就令a=0了,这样我们看到的和编译器内部的不一致,调试时就会发生一些无法预估的问题。 

4.枚举使用方便,一次可以定义多个常量

4.联合(共用体)

4.1联合体的定义

联合是一种特殊的自定义类型。

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

下面看一段代码:

#include<stdio.h>
union UN
{
	char c;
	int i;
};
int main()
{
	union UN un = { 0 };
	printf("%zd\n", sizeof(un));
	printf("%p\n", &un);
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));
	return 0;
}

 运行结果:

为什么联合类型变量un的大小是4呢,不应该是5吗?

这个根据我们打印的地址就可以看出来,它们的地址空间是相同的,之前也讲过联合体成员共用同一块空间,所以c和i在内存中应该如下图所示:

所以注意在同一时间内,共用体成员只能用一个,不能同时使用。

上述代码,如果我们要改c的值,那i的值也会改变:

4.2联合体的使用 

前面的章节中我们讲过,大小端的定义,也讲过如何判断大小端,这里不再详细说明,想要了解的,可以看之前的文章:

​​​​​https://blog.csdn.net/syh163/article/details/133034092

今天了解了联合体,我们可以根据联合体来写一个判断当前编译器是大端字节序存储还是小端字节序存储的函数:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int check_sys()
{
	union UN
	{
		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;
}

 4.3联合体大小的计算

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

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

下面直接看例子:

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
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));
	return 0;
}

 运行结果:

联合体union un1中,数组c的大小是5个字节,i的大小是4个字节,因为联合体至少是最大成员的大小,所以union un1的大小至少是5,而c的对齐数是1,i的对齐数是4,所以最大对齐数是4,5不是4的倍数,所以要对齐到8。

同理可得union un2的大小是16。 

以上就是今天所有的内容了,未完待续。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

成屿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值