最新C语言自定义类型详解

 

前言

随着学习的进一步深入,我们不再满足于做简单的计算题,想着做一些像样的东西,如学生管理系统、酒店入住系统等等......而C语言提供的基础数据类型已经不足以支撑我们的想法,于是便要学习自定义类型,根据自己的需求来创建类型。

 

结构体

生活当中有很多物品是不能简单的用整型、浮点型、字符型来区分,它们常常是复杂的集合,比如人,一个人拥有年龄,身高、体重、学历......等信息。我们可以用结构体来实现准确描述人这种复杂集合。

 

结构体的基础知识

结构和数组的区别

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

结构体的声明

对于结构体的声明,需要有结构体名、成员列表、变量列表(可省略)。

struct tag
{
	member_list;//成员列表
}variable_list;//变量列表

我们以描述一个学生为例。

struct student
{
	int id;//学号
	char name[20];//姓名
	char sex;//性别
	int age;//年龄
}s1,s2,s3;

结构体的特殊声明

在声明结构体的时候,我们可以以不完全声明(匿名结构体类型),也就是省略掉结构体的标签。

就像这样:

struct
{
	int a;
	char b;
	float c;
}d;

但这种声明并不安全。

例如:

struct
{
	int a;
	char b;
	float c;
}d;

struct
{
	int a;
	char b;
	float c;
}*p;

我们令*p=&d 是非法的。因为编译器会将这两个声明当成完全不同的两个类型。

结构体的自引用

我们能不能结构体套结构体呢?

是可以的,比如我们描述一辆车的构造,它的成员变量分别是车门、轮子、发动机、玻璃......等,而这些成员也是复杂的集合,比如车门的内部也是复杂的,这就需要结构体套结构体。我们称为结构体的自引用。

那么怎么实现结构体自引用呢?

先看一段错误示范:

我们结构体变量的创建,是要在结构体变量声明过后,这种写法属于是在这个结构体类型还没有创建完成的时候就使用了该类型的结构体变量。

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

正确示例:

将该类型的结构体变量改成该类型的结构体指针就能解决这一问题了~。

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

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

我们尝试一下初始化上面的学生结构体变量。

#include<stdio.h>
struct student
{
	int id;//学号
	char name[20];//姓名
	char sex;//性别
	int age;//年龄
}s1, s2, s3;
int main()
{
	s1 = { 1,"aa",'M',18 };
	printf("%d %s %c %d", s1.id, s1.name, s1.sex, s1.age);
}

是不是发现和数组的初始化差不多?定义一个结构体变量和定义一个整型变量的方式差不多,这里就不列举了。

结构体内存对齐

我们已经掌握了结构体的基本使用,现在我们进入到下一个问题——结构体的大小怎么计算?

猜想:结构体的大小就是结构体内的成员大小加起来

还是以学生结构体类型为例:

struct student
{
	int id;//学号
	char name[20];//姓名
	char sex;//性别
	int age;//年龄
}s1,s2,s3;

我们按照猜想一计算一下,计算出该结构体类型大小为29个字节,事实果真如此?

用代码验证一下:

#include<stdio.h>
struct student
{
	int id;//学号
	char name[20];//姓名
	char sex;//性别
	int age;//年龄
}s1, s2, s3;
int main()
{
	printf("%d", sizeof(struct student));
}

我们发现,结果是32个字节,说明猜想一错误。

结构体的对齐规则

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

二、其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。(ps:对齐数=编译器默认的一个对齐数与改成员大小的较小值,vs编译器的默认对齐数为8)

三、结构体总大小为最大对齐数的整数倍。

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

对结构体对齐规则的解析

id变量便是学生结构体变量的第一个成员,它对齐的位置应该在于该结构体变量的地址偏移量为0处,又因为id是int类型,占4个字节。

看图,红色所占空间就是id所占空间:

第二个成员name变量每个元素所占大小为1个字节,与默认值对齐数8个字节相比,1<8,所以name要对齐到1的整数倍数处,也就是4的位置,又因为name占20个字节,其所占空间如图中绿色区域所示。

第三个成员sex,类型所占字节数为1,1<8同样是对齐到1的整数倍处,且sex占一个字节,sex所占区域便是黄色位置。

第四个成员为age,为int类型,所占大小为4个字节,4<8,所以age要对齐到4的整数倍出,也就是28处,又因为它本身占4个字节,所以age所占空间便是蓝色的区域。

31位置到0位置,中间有32二个字节,这就是为什么student结构体变量大小不是29个字节而是32个字节。

为什么存在内存对齐

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

二是性能原因数据结构应该尽可能的在自然边界上对齐,原因在于,对于访问未对齐的内存,需要处理器访问两次内存,而对齐的话访问一次就行了。以空间换取了时间。

修改默认对齐数

内存对齐是空间换时间的办法,那么我们能不能更进一步,在特定情况下又省空间又省时间呢?修改默认对齐数便是一种办法。

修改默认对齐数需要使用#pragma

#pragma pack(2)就是将默认对齐数修改成了2,使结构体的大小进一步精简。我们来看看效果图(依然是一学生结构体变量为例)

第一个成员id不变,依然是占红色区域。

第二个成员name同样不变,因为1个字节仍然小于2个字节,占绿色区域。

第三个成员sex还是没变,原因与第二个相同,占黄色区域。

第四个成员age改变,2<4,age一个对齐至2的整数倍处,占蓝色区域

由图我们可以发现,这次的学生结构体类型大小只占了30个字节,缩短了两个字节,我们用程序来验证一下。

验证

和我们所设想的一样。

结构体传参

直接上代码,print1使用的是传值,print2使用的是传址。那种方法更好呢?

#include<stdio.h>
struct A
{
	int data[1000];
	int num;
};
struct A a = { {1,2,3,4},666 };
void print1(struct A a)
{
	printf("%d\n", a.num);
}
void print2(struct A* a)
{
	printf("%d\n", a->num);
}
int main()
{
	print1(a);
	print2(&a);
	return 0;
}

print2的方法要优于print1,对于print1,我们需要创建一块内存空间来接收传过来的参数,是需要压栈的,则在空间和时间上会有更多的系统开销,如果传递的结构体对象过大,会导致性能下降。但是对于print2的方法来说,不需要而外的开销,使用的是之前的内存,不需要新创建。

所以结构体传参的时候,要传结构体的地址。

位段

上面我们了解了基础的结构体知识,这里我们介绍一下结构体实现位段的能力。

什么是位段?

位段和结构类似,但多了两条限制。

一是位段的成员必须是int、unsigned int 或者是signed int,也可以出现char。

二是成员名字后面要出现冒号和数字。

例如:

struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

这就是一个位段类型,冒号后面的数字是指该成员占多少个比特位,并且该数字不能超过限制范围,位段空间的开辟是根据类型来开辟的。

位段A的大小是多少?

我们可以看见,位段A占8个字节。

#include<stdio.h>
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};
int main()
{
	printf("%d", sizeof(struct A));
	return 0;
}

如何算位段的大小呢?

猜想:是比特位数加起来/8,算出来字节数。

以位段A为例,位数和为47,按照猜想推算,一个占6个字节,但是结果是8,说明猜想错误

位段的内存分配

对于位段A,先开辟4个字节(因为成分成员类型是int),将a,b,c的位数相加,得到17位,再将d的位数加上去,发现位数超过了开辟的空间,所以需要新开一个4个字节的空间(按照类型来开辟空间),这时能够将位段A所有成员放进开辟的内存之中,所以是8个字节。

位段的缺点

位段不适用于需要跨平台的程序。

一是int位段被当成有符号数还是无符号数并没有明文规定。

二是位段中最大为的数目不确定(16位的机器是16,32位的机器是32)。

三是位段中成员在内存中从左向右分配,还是从右向左分配没有明文规定。

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

位段的意义

就是节约内存,省空间,在缓解网络拥挤的时候有作用。

 

枚举

枚举类型的定义

直接看代码吧。类型+标签+可能的情况。

enum sex
{
	MALE,
	FEMALE
};

枚举的优点

一是增加代码的可读性和可维护性。

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

三是防止了命名污染(封装)。

四是便于调试。

五是方便使用,一次可以定义多个变量,提高编程效率。

六是可以将数字换成符号(例如switch语句中,将case后面的数字换成便于理解的字符)

 

联合(共用体)

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

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

联合类型的定义

#include<stdio.h>
//联合体的声明
union un
{
	char c;
	int i;
};
//联合体的定义
union un u;
int main()
{
	//计算联合体的大小
	printf("%d", sizeof(u));
	return 0;
}

联合体的特点

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

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

关于C语言自定义类型的文章就介绍至此,更多相关C语言自定义类型内容请搜索编程宝库以前的文章,希望以后支持编程宝库

 forward_list 概述forward_list 是 C++ 11 新增的容器,它的实现为单链表。forward_list 是支持从容器中的任何位置快速插入和移除元素的容器 ...