深入C语言4——自定义类型:结构体、枚举、联合体

23 篇文章 1 订阅
19 篇文章 0 订阅
本文深入讲解C语言中的自定义类型,包括结构体的声明与使用,内存对齐的原理和规则,枚举类型的定义及其优势,以及联合体(共用体)的特性。通过实例解析结构体在传参时的注意事项,并介绍了结构体内存对齐对大小的影响。此外,还探讨了枚举和联合体在实际编程中的应用场景和优缺点。
摘要由CSDN通过智能技术生成

今天我们来学习C语言提供的自定义类型,我们知道C语言里有几个基本数据类型,但是这是C语言为你提供的,但是C语言也规定自己也能定义类型,用自己定义的类型定义的变量就是按照自己的意愿来存放的,为了研究这到底是个什么东西,我们来展开今天的内容。

1.结构体的声明

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

第一个自定义类型是结构体,我们之前或多或少可能听说过这个神圣,因为它在C语言中的用处还是非常大的,尤其是到了后期学习数据结构时就会经常和结构体照面,那什么是结构体呢?依照定义,我们知道想把不同类型的数据放在一起,这个类型就是结构体,里面有你想要的数据,比如我定义了一个int 一个char,现在想把它们放在一个变量里,有必要时会去访问其中一个,那么我给这个结构体起个名字Stru,这个Stru就是一个结构体类型,注意这只是类型,说明我们可以用其创建变量,当我们用Stru创建了一个s变量时,它体内就蕴含着int和char类型的数据,我们也就可以通过访问它们来实现我们的目的。

那如何创建一个结构体呢?(也叫做结构体声明)

struct Stru
{
    int a;
    char ch;
};

这就是结构体的创建方式,记住要在main函数外部创建,因为结构体所创建的自定义类型或者变量都是全局变量,每个函数或者接口可能都要用到,所以要在main函数外部创建,不过我们讲过了三子棋和扫雷小游戏就知道我们需要用到的头文件,函数的声明,和结构体的创建都可以在自己定义的头文件里创建,然后在其他的.cpp/.c文件里进行包含即可。

例如我现在想创建一个结构体来表示一个学生:

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

当我想用结构体来表示一本书时又可以这样:

struct Book
{
	char name[20];
	char author[20];
	int price;
}b1, b2;//全局变量

struct Book b3;//全局变量

int main()
{
	struct Book b4;//局部变量
	return 0;
}

而且在结构体的声明中也有一种特殊的方式,就是不声明结构体的标签,就像这样:

//匿名结构体类型
struct
{
 int a;
 char b;
 float c;
}x;//代表用该结构体创建一个x变量
struct
{
 int a;
 char b;
 float c;
}a[20], *p;//代表用该结构体创建两个变量,一个是a数组,一个是p指针

//当我们
p = &x;
//编译器会自动把两个变量当作两个不同的类型,所以是非法的

那结构体里可不可以创建一个结构体自身类型的变量呢?

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

答案是可以,像上方这样的创建方式正好就是我们学习数据结构的结点最基本的创建方法,但是切记用struct Node创建变量时不要直接在自身结构体内部创建变量,后期讲到数据结构时会讲到详细用法,这里只了解就好。

结构体的一些用法:

struct Point
{
	int x;
	int y;
}p3 = { 5,6 }, p4 = { 7,8 };
struct Point p2 = { 1,2 };
struct S
{
	double d;
	struct Point p;
	char name[20];
	int data[20];
};
int main()
{
	struct Point p1 = { 3,4 };
	struct S s = { 3.14, {1,5},"zhangsan",{1,2,3} };
	printf("%lf\n", s.d);
	printf("%d %d\n", s.p.x, s.p.y);
	printf("%s\n", s.name);
	for (int i = 0; i < 2 - ; i++)
	{
		printf("%d ", s.data[i]);
	}
	return 0;
}

这段代码大家自行研究,如果无压力看懂说明对结构体的创建和使用你已经掌握了。

2.结构体内存对齐

想必大家都知道基本数据类型的大小,我们已经演示过很多次了,1244848,想必大家都背烂了,那大家有没有想过一个结构体的大小应该是多少呢?

struct Stru
{
    int i;
    char ch;
};
//大家计算一下该结构体的大小是多少?

肯定不少人都是4+1=5脱口而出,没错,我刚开始学习的时候也是这么算的,而且认为自己的答案不可能有问题,那么实际上并不然,这个结构体的大小是8,为什么会这样呢?一个4字节的变量加上一个1字节的变量为什么总共是8字节呢?难道结构体在创建的时候额外开辟空间了?带着这些疑问我们开启我们的内存对齐。

为什么存在内存对齐? 大部分的参考资料都是如是说的: 1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址 处取某些特定类型的数据,否则抛出硬件异常。 2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。我用大白话来解释一下内存对齐的概念:

 为了避免这种因为访问问题造成的不方便和计算机因为访问造成的效率降低,我们的计算机就干脆慷慨解囊,现在不论是char或者short int 我都为你独自开辟4字节的空间,能用完最好,用不完就闲置,这样以来当我们的计算机进行访问时每次都可以完整的将数据访问到,也能达到提升效率的目的,这种方法就是拿空间换时间。

我们来看C语言中结构体内存对齐的规则:
1.结构体的第一个成员永远放在结构体起始位置偏移量为0的位置
2.结构体成员从第二个成员开始,总是放在偏移量为一个对齐数的整数倍处  对齐数=编译器默认的对齐数和变量自身大小的较小值  vs-8 linux-0
3.结构体总大小必须是各个成员的对齐数中最大那个对齐数的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

了解这些,我们来做一些例题吧:

//练习1
struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));
//练习2
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));
//练习3
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S3));

第一个就是12字节,因为char虽然只占一个,但是根据内存规则,内存需要为它开辟4字节的空间,剩下的3字节闲置,而int类型要再char闲置的3字节后新开辟4字节,正好全部用完,一个char也是跟第一个一样的道理,所以它也占4字节,所以是12字节。第二个是8,因为前两个char变量并没有用完4个字节,它们加起来只占2字节,所以为它们开辟的4字节并没有用完,而后面的int占4字节,所以总共8字节。第三个是16,因为double要用掉8字节,char用掉4字节,int用掉4字节,所以答案是16字节。而第四个要注意了,是32,因为刚刚的结构体内存对齐规定了如果结构体嵌套结构体的话最大的偏移数就为嵌套的结构体对齐到自己最大对齐数的整数倍处,意思就是S3的最大偏移数为8为S3最大,所以8也就是S4的最大偏移数,因为S4里double也是8字节,所以我们在计算时char这时就不是占一个字节了,它要占到8个字节,因为最大偏移数发生了改变,所以8+16+8=32。

在这里介绍一个函数:offsetof

这个函数能返回我们结构体变量的对齐数,可以告诉我们结构体关于首地址的偏移量,这也是一道百度考过的真题:

struct S
{
	char c1;
	int a;
	char c2;
};
//offsetof 是一个宏
int main()
{
	printf("%u", offsetof(struct S, c1));//0
	printf("%u", offsetof(struct S, a));//4
	printf("%u", offsetof(struct S, c2));//8
	return 0;
}

看完这些题目后我们发现就算结构体内变量定义的顺序发生改变都会导致内存占用的大小发生改变

//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

例如这样两个结构体占用的内存大小一个是12一个是8,所以我们再创建结构体时也要注意定义的顺序。

3.结构体传参

我们知道当我们想写一个接口写一个函数时就必须要有参数,一般变量,数组,指针等都可以进行传参,那么结构体肯定不例外,那么结构体究竟该如何传参呢?传参时有没有什么需要我们注意的呢?

struct S
{
	int data[1000];
	int num;
};
void print1(struct S tmp)
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", tmp.data[i]);
	}
	printf("\n%d", tmp.num);
}
void print2(const struct S* ps)
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ps->data[i]);
	}
	printf("\n%d", ps->num);
}
int main()
{
	struct S s = { {1,2,3,4,5,6,7,8,9,10},100 };
	print1(s);
	print2(&s);
	return 0;
}

大家可以发现结构体在传参时和一般的变量传参大同小异,也是类型+名称,那么既然print1和print2都可以完成打印功能,那么究竟哪个更好呢?答案是print2,因为函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降,所以结构体传参的时候,要传结构体的地址。

4.枚举类型

我们再来介绍一种新类型,枚举,所谓枚举类型就是一一列举,也就是当我们需要某些元素是一一列举出来的的时候可以用枚举来定义,比如一星期里的天数,月份,年份,还有颜色的代数,都可以用枚举类型来实现,比如:

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

这就是我们定义的几个枚举类型的变量,当我们想对其进行使用时,枚举类型的变量都是以0开始的,依次递增加一,如果我们在其中定义了一个不符合其递增加一规律的变量,则其后面的变量都会根据该变量的值依次递增加一。

enum Color
{
	red,
	green,
	blue
};
int main()
{
	//printf("%d\n", red);//0
	//printf("%d\n", green);//1
	//printf("%d\n", blue);//2
	enum Color c = green;
	if (c == green)
	{
		printf("绿色\n");
	}
	return 0;
}

那么枚举类型有什么优点呢?我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点: 1. 增加代码的可读性和可维护性 2. 和#define定义的标识符比较枚举有类型检查,更加严谨。 3. 防止了命名污染(封装) 4. 便于调试 5. 使用方便,一次可以定义多个常量。

5.联合体

然后就进入到自定义类型的最后一个板块,也就是联合体类型,也叫共用体,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间。

//联合类型的声明
union Un 
{ 
 char c; 
 int i; 
}; 
//联合变量的定义
union Un un; 
//计算连个变量的大小
printf("%d\n", sizeof(un)); 

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

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

如果大家运行这段代码就会发现他们的地址都是一样的,说明它们都是共用一块空间的,这也就说明了它们为什么叫共用体。

记得之前我们写过一个小程序来测试电脑的是大端存储还是小端存储吗?我们也可以通过共用体来是实现这个功能,既然共用体内的元素数据使用的都是同一块内存,那么我们就打印一个字节,如果是第一个的值说明就是大端,如果是后面的值,说明就是小端。

int check_system()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	//int a = 1;
	//低----------------------高
	//01      00      00      00 小端
	//00      00      00      01 大端
	//char* pc = (char*)&a;
	//if (*pc == 1)
	//{
	//	printf("小端\n");
	//}
	//else
	//{
	//	printf("大端\n");
	//}
	
	if (check_system() == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

6.联合大小的计算

1.联合的大小至少是最大成员的大小。                                                                                          2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

比如:

union Un1
{
	char c[5];//5
	int i;//4
};
union Un2
{
	short s[7];//14
	int i;//4
};
int main()
{
	printf("%d\n", sizeof(union Un1));//8
	printf("%d\n", sizeof(union Un2));//16
	return 0;
}

好了,这就是本次自定义类型博客的全部内容,大家好好消化一下,我们下次再见。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值