1. 写在开始
外部客观世界纷繁多变,仅仅依靠编程语言提供的固有类型,难以详尽地描述刻画之;针对这一问题,C语言自身提供了自定义类型,供开发人员在实际的业务场景中,根据具体的需求,能够自定义数据类型,从而对现实问题进行抽象与代码实现。笔者今天的博客,主要是向大家介绍一些常用的自定义类型及使用(C语言)。内容如下:
2. 结构体类型
2.1 声明、定义与初始化
结构体的声明
- struct : 结构体声明关键字(由C语言提供)
- Tag : 所定义的结构体命名
- member_lists : 结构体内定义的各成员变量
- variable_lists : 定义的结构体变量列表
如当我们需要描述一本书的时候,就可以定义一个‘书’的结构体类型:
struct Book
{
char name[20]; // 书名
float price; // 价格
char id[10]; // 编号
};
结构体的定义与初始化
如上,我们已经声明了一个名为Book的结构体类型,当我们需要使用它时,就可以进行定义(实例化)并初始化:
typedef struct Book Book; --- 1
int main()
{
struct Book n1 = { "《高质量C/C++编程》", 54.5f,"B1002" }; --- 2
printf("%-10s %.2f %-8s\n", n1.name, n1.price, n1.id); --- 3
Book n2 = { "《C指针与陷阱》",66.7f, "B1006" }; --- 4
Book* pn = &n2; --- 5
printf("%-10s %.2f %-8s\n", pn->name, pn->price, pn->id); --- 6
return 0;
}
解析:
1. 使用typedef关键字对struct Book类型关键字重命名为Book,方便之后的使用;
2. 我们定义了一个struct book类型的变量n1,并进行了初始化;
3. 第一个printf函数对结构体n1中各成员变量的内容进行了打印,访问各成员变量时,使用了.(结构体成员变量访问操作符)来进行访问内容;
4. 使用重命名的Book结构体关键字,定义了一个结构体变量n2,并进行了初始化;
5. 定义了一个Book* 类型的结构体指针pn,并将n2的地址赋值给了pn;
6. 第二个printf函数对结构体n2中各成员变量的内容进行了打印,访问各成员变量时,使用了->(结构体成员变量访问操作符)来进行访问内容。
- 结构体变量的初始化: 使用 {}
- 结构体变量的访问: 结构体变量.成员变量 / 结构体指针->成员变量
2.2 自引用
当我们在使用结构体变量的过程中,想从一个结构体变量直接访问到另一个结构体变量的时候,我们就可以把该结构体的结构体指针类型定义添加到结构体成员中,从而能够通过结构体指针指向另一个结构体变量;我们将这种结构体声明的方式,称为结构体的自引用。
Tips: 在数据将结构中,我们会经常使用结构体的自引用来定义结点。
2.3 内存对齐
我们在定义一个变量的时候,编译器会自动为该变量在内存中开辟一块空间;结构体变量也不例外,我们将结构体变量在内存开辟空间的方式、规则称为内存对齐。
内存对齐的规则
- 第一个成员变量从与结构体变量偏移量为0的地址处,往后开辟空间。
- 其他成员变量要从对齐到某个数字(对齐数)的整数倍的地址处,往后开辟空间。 (对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。)
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
Tips: VS编译环境下,默认对齐数为 8。
举个例子
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
内存开辟情况
运行结果
再举个例子
// 嵌套结构体
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
内存开辟情况
运行结果
为什么要进行内存对齐?
平台原因
并不是所有的硬件平台都能够访问任意地址上的任意数据的;某些硬件平台只能在某些地址处(如4的整数倍)取某些特定类型的数据,否则抛出硬件异常。
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
究其本质:结构体的内存对齐实际就是一种拿空间来换取时间的做法。
2.4 传参
两种结构体传参方式
然而,我们在结构体传参的时候: 应首选结构体地址传参;
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
2.5 扩展: 位段
位段是结构体实现中的一种特殊类型,特殊在:
- 位段成员类型必须是: unsigned int 、 signed int 或 int
- 位段的成员名后边有一个冒号和一个数字
举个例子
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
值得关注的是:位段是以比特位为单位,根据成员变量后的数字,来为成员变量进行内存空间开辟
值得注意的是:跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
位段存在跨平台的原因:
- int 位段被当成有符号数还是无符号数是不确定的;
- 位段中最大位的数目不能确定(int 在16位机器中占2个字节,在32位机器中占4个字节);
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义;
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
3. 枚举类型
3.1 定义与特点
在现实世界中,有些事物的取值有限,且可以一一列举,如星期、月份、性别、三原色等;这个时候我们就可以定义枚举类型,对之进行刻画与描述。
举个例子
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
-
enum:枚举定义关键字
-
{}中的内容: 枚举类型的可能取值,也称作枚举常量
值得注意的是: 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
我们知道定义常量的时候,我们可以使用#define的方式,那么又为什么还要用到枚举呢?
枚举的优点
-
增加代码的可读性和可维护性;
-
便于调试;
-
防止了命名污染(封装);
-
和 #define 定义的标识符比较枚举有类型检查,更加严谨。
3.2 使用
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
enum Color c = RED --- 1 // √
c = 5 // ×
解析:
1. 我们在定义好一个枚举类型的变量之后,只能通过枚举常量进行赋值。
4. 联合类型
4.1 定义与特点
联合是一种特殊的自定义类型,与结构体类型相似,也包含一系列的成员变量;但区别于结构体的是,这些成员变量共用同一块内存空间(所以联合也叫共用体)。
举个例子
union Un
{
char c;
int i;
};
- union : 联合定义关键字
于是,因为联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
究其本质:当我们使用联合时,只能使用其中的一个成员变量;因为各成员变量共用一块内存空间,当重复使用时,将造成数据的覆盖;因此,在实际的开发过程中,当某个事物可以有多个选项,但在特定的条件下,其只能选择其中的一个时,在该场景中,我们就可以使用联合。
4.2 计算大小
联合大小的计算
-
联合的大小至少是最大成员的大小;
-
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
5. 小结
这篇文章,主要是对C语言中常用自定义类型的介绍与总结,重点要掌握结构体的内存对齐规则,以及了解枚举与联合的特性与使用细节。自定义类型,是我们在实际开发过程中,必须使用的且强大的工具之一,因此大家一定要好好掌握,熟练运用;最后,希望今天的博客内容,能够对你有所帮助!