自定义数据类型:结构体+枚举+联合

前言

已知C语言为我们提供了一些内置类型,如:charintflaot……我们可以使用这些内置的类型对一些简单的数据进行描绘。但是当我们面对诸如学生、书籍、以及其他更复杂的对象时,C语言中内置的单一的数据类型已不足以对其进行描述,于是C语言赋予了我们自定义数据类型的权利,随即便出现了:结构、枚举、联合这样的自定义数据类型。

一、结构体

1.结构体类型的声明

(1)一般声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。对于结构体我们通常这样声明:

例如创建一个学生类型,我们可以如下声明:

//学生=名字+年龄+性别+身高
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
};

注释:在声明结构体的时候,结构体变量列表可以省略,但后面的分号不可省略(语法规定)。上面我们只是创建了一个如int、flaot的类型,并未定义变量。 类型就相当于模具,有了模具才能创建变量。

(2)特殊声明

在声明结构的时候,也可以不完全的声明,我们称这种结构为匿名结构体类型。
比如:

//匿名结构体类型-1
struct
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
}s1,s2;

//匿名结构体类型-2
struct
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
}a[20],*p;

注意:匿名结构体类型,可以省略标签名,但是结构体变量需要在分号前定义
思考:在上面代码的基础上,这行代码合法吗?p = &s1;
答案是:不合法
结论:对于匿名结构体类型,虽然结构体成员是完全相同的,但是编译器会把上面的两个声明当成完全不同的两个类型,所以p = &s1;是不合法的。

拓展:对于匿名结构体类型,可以省略标签名,但是结构体变量只能在分号前定义,由于没有标签这种结构体类型只能使用这一次,之后就不能再次使用了。如果想要重复使用,有没有好的办法呢?我们可以使用关键字-typedef对匿名结构体进行类型重命名,细心的你可能会发现这种方法似乎有些鸡肋,这种方法其实就是低配版的stuct Node{……};,这是笔者的一个小拓展,大家仅供参考。

typedef struct
{
	int a;
	char b;
	float c;
}Node;

2.结构的自引用

(1)错误的自引用

引入:在结构中包含一个类型为该结构本身的成员是否可以呢?

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

首先我们先来计算一下此结构体类型的大小:sizeof(struct Node),在计算时发现,我们会陷入一个无限套娃的死循环中,即无法计算出此结构体的大小。这也间接说明以这种方式自引用的错误性。

(2)正确的自引用

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

拓展:使用typedef时的结构体自引用

typedef struct Node
{
 int data;
 //注意这里的struct不可以省略,因为Node
 //是在typedef重命名之后才产生的,不能在这之前使用。
 struct Node* next;
}Node;

补充:匿名结构体不能自引用!
应用场景:数据结构—— 链表

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

(1)结构体变量的定义

两种方式
(1)声明结构体类型的同时定义结构体变量
(2)声明结构体类型之后定义变量
在声明结构体的同时定义的成员变量为全局变量(在main函数外定义的变量),在main函数内定义的变量为局部变量。

//学生=名字+年龄+性别+身高
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
}s1,s2,s3;//全局变量
struct Stu s4;//全局变量
int main()
{
	struct Stu s5;//局部变量
	//通常情况下定义局部变量
	return 0;
}

(2)结构体变量的初始化

普通初始化:定义变量的同时赋初值。

struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化

struct Node
{
 int data;
 struct Stu p;
 struct Node* next; 
}n1 = {10, {"zhangsan", 20}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {"lisi", 21}, NULL};//结构体嵌套初始化

按结构体成员名初始化
在上述结构体类型的基础上,可以这样初始化:

#include<stdio.h>
int main()
{
	struct Node n2 = {.p.y=2, .data = 20,.p.x = 1,NULL };
	return 0;
}

4.结构体成员的访问

访问结构体成员有两种方式:
1.结构体变量.结构体成员名;
2.结构体指针->结构体成员名
例如:

//坐标类型
struct Point
{
	int x;
	int y;
};
//学生类型
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	int hight;//身高
};
int main()
{
	//结构体初始化
	struct Point p = { 10,20 };
	struct Stu s = { "张三",20,"男",180 };
	//结构体成员访问 点操作符
	printf("x = % d y = % d\n", p.x, p.y);
	printf("%s %d %s %d\n", s.name, s.age, s.sex, s.hight);
	
	//结构体成员访问 ->操作符
	struct Point* ps = &p;
	struct Stu* pt = &s;
	printf("x = % d y = % d\n", ps->x, ps->y);
	printf("%s %d %s %d\n", pt->name, pt->age, pt->sex, pt->hight);
	return 0;
}

5.结构体传参

结构体传参主要分为两种:分别为传值调用传址调用

(1) 传值调用(函数参数为结构体)

struct S
{
	int data[1000];
	char buf[100];
};
void print1(struct S ss)//参数是结构体类型
{
	int i = 0;
	for (i = 0; i < 10; i++)//打印前十个元素
	{
		printf("%d ", ss.data[i]);
	}
	printf("%s\n", ss.buf);
}

int main()
{
	struct S s = { {1,2,3} , "hehe"};
	print1(s);//传值调用
	return 0;
}

(2) 传址调用(函数参数为结构体指针)

struct S
{
	int data[1000];
	char buf[100];
};

void print2(struct S* ps)//参数是结构体指针类型
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("%s\n", ps->buf);
}

int main()
{
	struct S s = { {1,2,3} , "hehe"};
	print2(&s);//传址调用
	return 0;
}

总结:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。如果传递结构体地址,地址占用内存很小,时间空间都会节省,程序效率会更高。`因此结构体传参的时候,要传结构体的地址。

6.结构体内存对齐

理解了以上结构体基本的知识点后,下面我们来学习结构体中一个非常重要的知识点同时也是一个特别热门的考点——结构体内存对齐!!!

(1)结构体内存对齐规则

引入:计算两个结构体的大小

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

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


如果对结构体计算没有相关了解,对于s1、s2两个具有相同成员变量的结构体,很多人会直白的认为结果应该是1+1+4=6,怎么也不会想到不仅输出的结果不一样,而且成员变量的顺序不同也导致结构体变量的大小不同。究其原因,我们继续往下看👇

C语言中提供了一个宏——offsetof 用来计算结构体成员相对于起始位置的偏移量。

#include<stddef.h>
offsetof (type,member)

下面以s1、s2为例,使用offsetof展示结构体在内存中的存储方式。
图1:S1在内存中的存储:

图2:S2在内存中的存储:


观察发现,结构体在内存中并非简单按顺序存储,而是有一定的规则存在。这个规则就是结构体内存对齐。

结构体内存对齐规则:

  1. 结构体的第一个成员直接对齐到相对于结构体变量起始位置为0的偏移处。
  2. 从第二个成员开始,要对齐到某个【对齐数】的整数倍的偏移处
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数值为8
    linux环境默认不设对齐数(对齐数是结构体成员的自身大小)。
  3. 结构体总大小为最大对齐数的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面以此规则解释S1

练习:计算嵌套结构体的大小

struct S1
{
	char c1;
	int i;
	char c2;
};
struct S4
{
	char c1;
	struct S1 s1;
	double d;
};

(2)为什么存在内存对齐?

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

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

总体来说:

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,我们可以这样做:

让占用空间小的成员尽量集中在一起。

如引入中的例子:

//大小为——12
struct S1
{
 char c1;
 int i;
 char c2;
};
//将占用较小的成员集中在一起——8
struct S2
{
 char c1;
 char c2;
 int i;
};

(3)修改默认对齐数

我们可以使用 #pragma 这个预处理指令,改变我们的默认对齐数。

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

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

//恢复默认对齐数
#pragma pack()
int main()
{
	//由于设置了默认对齐数为1,则大小为1+4+1=6
    printf("struct S1的大小为:%d\n", sizeof(struct S1));
    return 0;
}

结论:

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

二、位段

1.什么是位段

位段的声明和结构是类似的,都以 struct 为关键字。但有两个不同:

1.位段的成员必须是 intunsigned intsigned int
2.位段的成员名后边有一个冒号:和一个数字

比如:

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

注释:

int _a:2     表示整形_a占用2bit
int _b:5     表示整形_b占用5bit
int _c:10   表示整形_c占用10bit
int _d:30   表示整形_d占用30bit

2.位段的内存分配

上述A就是一个位段类型。那位段A的大小是多少?类比结构的计算,可得出:

printf("%d\n", sizeof(struct A));


不同于结构体,位段的使用和内存分配要注意以下3个要点:

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

下面以一个例子具体展示位段的空间是如何开辟的(VS编译器下运行)

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

3.位段的跨平台问题

当然,以上的内存分配方式仅为VS下的数据,位段的不确定性主要为跨平台问题。

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

总结:

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

4.位段的应用

虽然位段具有跨平台问题,使用时具有一定的歧义,但是在特定的场景下,位段相比于其他类型可以节省空间。
例如:数据在网络上传播的时候需要通过IP数据包

IP数据包又分为很多固定的部分,每一个部分都有固定的大小,如果每一个部分都定义一个char或int类型,将会导致大量空间的浪费,这时使用位段就可以按位分配内存,可以很好的节省空间。由于位段具有跨平台的特点,为了达到节省空间的需求我们可以在每个平台定义不同的位段从而达到相同的效果。

三、枚举

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

一周的星期一到星期日是有限的7天,可以一一列举。
三原色有红、黄、蓝三种,也可以一一列举。

1.枚举类型的定义

enum Day//星期
{
//枚举的可能取值
//每一个可能的取值都是常量
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

enum Color//颜色
{
//枚举的可能取值
//每一个可能的取值都是常量
	RED,
	GREEN,
	BLUE
};

类似于结构, enum 为枚举关键字,enum Day , enum Color 都是枚举类型。{}中的内容为枚举的可能取值,也叫枚举常量

{}中的枚举常量是有数值的,默认情况下是从0开始,依次递增1。我们也可以对默认值进行设置。如:

enum ENUM_A
{
	X1,
	Y1,
	Z1 = 255,
	A1,
	B1,
};
int main()
{
	enum ENUM_A enumA = Y1;
	enum ENUM_A enumB = B1;
	printf("%d %d\n", enumA, enumB);
	return 0;
}

注释:上面代码中的Y1从0递增1为1;由于Z1修改了默认值,则B1从255递增2为257。

补充:枚举类型只存储其中的一种可能取值,即大小为一种取值的大小

2.枚举的优点

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

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨
  3. 防止了命名污染(封装)
  4. 便于调试(#define定义的符号在预处理时就替换掉了)
  5. 使用方便,一次可以定义多个常量

四、联合

1.联合类型的定义

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

//联合类型的声明
union Un
{
	char c;
	int i;
	double d;
};
//联合变量的定义
union Un un;

2.联合的特点

引入:输出联合变量及成员的地址,观察特点。

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


结论:

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

3.联合的使用

应用场景:当在特定情况下,我们只会使用成员变量中的一个成员时,我们可以考虑使用联合而不是结构。

📝例如:判断机器的大小端
思路:已知整形1在内存中的存储,如果为小端存储模式,取出第一个字节内容为1,如果是大端存储,取出第一个字节内容为0。

方法一:
思路:创建1个临时整形变量i初始化为1,取出变量i的地址,强制类型转换为char*类型,并解引用访问1个字节空间,返回访问到的数值,小端存储返回1,否则返回0。

int check_sys()
{
	int i = 1;
	return *(char*)&i;
}

方法二(使用联合):
思路:利用联合的成员是共用同一块内存空间的特点,可以先令int成员初始化为1,再返回char类型成员。如果为小端存储则返回1,否则返回0。这种方法是非常巧妙的,可以说是神来之笔。

int check_sys()
{
	union//匿名联合体
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

4.联合大小的计算

类似于结构,联合大小的计算也有独特规则:

  1. 联合的大小至少是最大成员的大小。
  2. 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  3. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

📝例如

union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};

注:当自定义类型中出现数组时,大小为数组的类型,而不是数组的总大小。

  1. 对于Un1
    VS默认对齐数为8,char c[5]大小为char即为1。对齐数为较小值1;i的成员大小为4,对齐数为较小值4。1<4所以Un1最大对齐数为4,且最大成员大小为5×1,对齐到最大成员的整数倍为4×2=8.

  2. 对于Un2
    VS默认对齐数为8,short大小为2。对齐数为较小值2;i的成员大小为4,对齐数为较小值4。2<4所以Un2的最大对齐数为4,且最大成员大小为7×2,对齐到最大成员的整数倍为4×4=16.

总结

本章对C语言中的自定义数据类型进行了一个全面的介绍,希望通过阅读本文,能对自定义类型有一个更加深刻的理解。
写在最后:Keep coding!

  • 45
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

不摸鱼的程序员

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

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

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

打赏作者

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

抵扣说明:

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

余额充值