一.引言
在C语言中,除了 int ,char 等类型,还有一些可供我们自定义的类型,如结构体、联合体和枚举等,在不同场景中合理的使用这些自定义类型可以增强我们代码的效率或可读性。
二.结构体和结构体变量
结构体是一些值的集合,这些值被称为成员变量,结构的每个成员可以是不同类型的变量
2.1 结构体的声明
struct tag
{
member - list;
} variable - list;
其中,struct是结构体的关键字,tag是结构体标签,member - list是成员变量,variable - list是结构体变量名,例如我们要用结构体表示一个学生,其中包含年龄,姓名,成绩:
struct Stu
{
int age;
char name[20];
float score;
} ;
这样我们就完成了一个结构体的声明
2.2 结构体变量的创建
结构体变量的创建有两种方式
(1)先声明结构体,再创建结构体变量
struct Stu
{
int age;
char name[20];
float score;
} ;
struct Stu s1;
(2)声明结构体的同时创建结构体变量
struct Stu
{
int age;
char name[20];
float score;
} s1;
2.3 结构体变量的初始化
结构体变量的初始化也有两种方式
(1)按照结构体成员的顺序初始化
struct Stu
{
int age;
char name[20];
float score;
};
struct Stu s = {20, "张三", 76.5};
(2)按照指定的顺序初始化
struct Stu
{
int age;
char name[20];
float score;
};
struct Stu s = {.name = "张三", .score = 76.5, .age = 20};
2.4 结构体的特殊声明
在声明结构体的时候,我们可以不完全的声明,也就是省略掉结构体标签:
struct
{
int a;
char b;
float c;
long d;
} x;
struct
{
int a;
char b;
float c;
long d;
} *p;
上面两个结构在声明的时候省略掉了结构体标签,属于匿名结构体类型
那么问题来了,这两个匿名结构体里的成员变量一模一样,他们属于同一个类型吗?
//在上面代码的基础上,这一段代码是否合法?
p = &x;
答案是:编译器会把上面的两个匿名结构体声明当成两个完全不同的类型,所以是非法的

匿名的结构体类型,如果没有对结构体类型重命名(typedef)的话,基本上只能使用一次
2.5 结构体的自引用
我们能否将结构体本身作为结构体成员包含进该结构体呢?
比如我们定义一个链表的节点:
struct Node
{
int data;
struct Node next;
};
上述代码看似正确,但是仔细想想,如果我们要计算其类型大小,一个结构体又包含了一个同类型的结构体变量,这不是无限套娃了吗?这样结构体的大小就会无限大,所以是不合理的方法
正确的自引用方式应该是:
struct Node
{
int data;
struct Node *next;
};
创建一个结构体指针作为结构体变量就可以了
另外的,当我们在对结构体自引用的过程中,使用typedef对匿名结构体类型重命名的话,也容易出现问题,例如:
typedef struct
{
int data;
Node *next;
} Node;
因为对匿名结构体类型重命名之后才有Node类型,但是匿名结构体内部提前使用了Node类型来创建成员变量,这种操作是非法的,所以我们在重命名结构体的时候最好还是不要用匿名结构体了
三.结构体内存对齐
当我们掌握了结构体的基本使用方法之后,就来到了一个比较重要的知识点:计算结构体的大小
这里面涉及到一个特别热门的考点:结构体内存对齐
3.1 结构体的对齐规则
首先我们先大致了解一下结构体的对齐规则:
1.结构体的第一个成员要对齐到结构体变量的起始位置
2.其他成员变量要对齐到对齐数的整数倍的地址处
这里要引入一个概念:对齐数
对齐数 = 编译器默认的一个对齐数 和 该成员变量大小的较小值
- vs中默认对齐数为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体的总大小必须为最大对齐数的整数倍(结构体中每个成员变量都有自己的对齐数,取所有对齐数中最大的)
4.如果出现了结构体嵌套结构体的情况,嵌套的结构体对齐到自己的成员中的最大对齐数的整数倍处,结构体的总大小就是所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍
光这么说太抽象了,我们引入几个例子来方便理解
#include <stdio.h>
int main()
{
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
return 0;
}
例如这段代码,两个结构体类型中包含的结构体变量一模一样,但是顺序不同,它们的大小是否相同呢

我们通过画图来方便理解

在内存中,第一个成员对齐到起始位置,所以c1对齐到0处
变量 i 的大小是4,vs的默认对齐数是8,取较小值,也就是对齐数是4,所以 i 要对齐到4的倍数处。但是c1的下一个位置是1,并不是4的倍数,所以往后找,一直到4的位置保存下来,占据四个字节
变量c2的大小是1,vs的默认对齐数是8,取较小值,也就是对齐数是1,所以c2要对齐到1的倍数处,因为所有数都是1的倍数,所以在变量 i 后面8的位置保存就行了
但是,规则中明确,结构体的总大小必须为最大对齐数的整数倍。结构体中变量 i 的对齐数最大,为4,所以总大小必须是4的倍数,所以取12
所以sizeof(struct S1)就是12
按照上面的方法,我们就很容易理解S2的大小为什么是8了
关于结构体嵌套结构体的情况,我们再看看下面这段代码
#include <stdio.h>
int main()
{
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
return 0;
}
结构体S4中嵌套了结构体S3,此时的S4大小是多少呢?

答案是32,你答对了吗?
我们还是画一张图方便理解一点

3.2 为什么存在内存对齐
(1)平台原因(移植性)
不是所有的硬件平台都能随意访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则就会造成硬件异常。内存对齐可以提高程序的可移植性。
(2)性能原因
数据结构(特别是栈)应该尽可能的在一个设定的边界上保持对齐,不然处理器为了访问未对齐的内存(跨边界的内存),就要作两次内存访问;而对齐了的内存只需要访问一次。假设一个处理器总是一次从内存中读取8个字节,则地址必须是8的倍数,如果我们能把所有double类型的数据都对齐到8的倍数处,那么就可以只进行一次操作来读写数据了。如果我们不进行内存对齐,把数据放在两个8字节内存块中,就要执行两次内存访问,影响效率
我们会发现内存对齐的过程中会浪费一些空间,事实上,结构体的内存读取就是拿空间换时间
那么当我们设计结构体的时候,怎么既满足内存对齐,又能尽可能的节省空间呢?
将占用空间小的成员尽量集中在一起即可
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
还是这段代码,S2中占用较小空间的char都集中在一起,所以整个结构体占的空间就更小了
3.3 修改默认对齐数
事实上,虽然有些编译器定义了默认对齐数,我们还是可以对其进行修改
#pragma这个预处理指令,可以改变编译器的默认对齐数

将默认对齐数修改为1后,c1还是对齐到0处,而此时变量 i 就不是对齐到4了,因为默认对齐数比int类型更小,所以对齐数为1,也就是变量 i 要对齐到1处

四.结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1, 2, 3, 4}, 1000};
void print1(struct S s)
{
printf("%d\n", s.num);
}
void print2(struct S *ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s);
print2(&s);
return 0;
}
上面的两个函数中,哪个更好呢?
实际上print2更好。函数print1中,我们将结构体传入函数;函数print2中,我们将结构体的地址传入函数,二者的区别在哪呢?
函数传参的时候,参数需要进行压栈,会有时间和空间上的系统开销
如果传递一个结构体对象,结构体过大的话,参数压栈的时候造成的系统开销就比较大,会导致性能的下降,所以我们在结构体传参的时候,最好传结构体的地址
五.联合体
5.1 联合体类型的声明
类似结构体的,联合体也是由一个或多个不同类型的成员构成
但是联合体和结构体的区别在于:编译器只为联合体中占空间最大的成员分配空间,联合体所有的成员共同使用这一块空间
所以联合体也叫:共用体
联合体的这一特点导致当我们给联合体的其中一个成员赋值时,其他成员的值也跟着改变
#include <stdio.h>
struct S
{
char c;
int i;
};
union Un
{
char c;
int i;
};
int main()
{
struct S s1;
union Un un;
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(un));
return 0;
}
运行上面这段代码,结果是什么呢?

这里就展示了联合体和结构体的区别
5.2 联合体的特点
联合体的所有成员共用一块空间,所以一个联合体变量的大小,至少是最大成员的大小,因为联合体至少得有能力存放最大成员)
上面的代码中,联合体变量Un的最大成员是int i,占空间为4,所以Un的大小也为4
说到联合体的所有成员共用一块空间,有的同学可能对此还有疑惑,我们再写一段代码验证一下
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = { 0 };
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
printf("%p\n", &un);
return 0;
}
我们分别将联合体变量和联合体变量的两个成员的地址取出来并打印,结果为:

我们发现三个地址一模一样,由此可以证明:联合体的所有成员共用一块空间
至于不同成员在内存中的布局,我们再写一段代码来验证
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un = { 0 };
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
return 0;
}
运行代码

我们发现,i 的第四个字节的内容被修改为55了,根据上一篇博客中提到的大小端字节序,我们就可以画出un的内存布局图

5.3 联合体大小的计算
联合体大小的计算有两个规则:
- 联合体的大小至少是最大成员的大小
- 当联合体中存在数组的时候,联合体的大小至少要能容纳的下整个数组,然后对齐到最大对齐数的整数倍
例如下面这两个联合体类型,你能计算一下它们的大小吗?
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
答案如下:
在Un1中,数组c的大小为5,所以Un1的大小至少要为5,但是int是占空间最大的类型,所以最大对齐数是4,所以Un1的大小得是4的整数倍,也就是8
在Un2中,数组c的大小为14(short类型大小为2),所以Un2的大小至少要为14,但是int还是占空间最大的类型,所以Un2的大小得是4的整数倍,也就是16
5.4 联合体的应用
联合体和结构体很相似,有的人就会觉得联合体可有可无。实际上在合适的情况下恰当的使用结构体,可以帮助我们节省大量的空间。
比如我们制作一个网购网站,网站中有三种商品:图书、杯子和衬衫
这三种商品都有库存量、价格、商品类型等信息
图书独有的信息有:书名、作者、页数
杯子独有的信息有:设计
衬衫独有的信息有:设计、颜色、尺寸
如果我们用结构体类型来存储这些信息,可能会这么写:
struct goods_list
{
// 公共属性
int stock_number; // 库存量
double price; // 定价
int item_type; // 商品类型
// 独有属性
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // ⻚数
char design[30]; // 设计
int colors; // 颜⾊
int sizes; // 尺⼨
};
上述代码设计很简单,就是把所有的属性,不管是公共的还是独有的,全部都一股脑放进结构体中。虽然合理,但是独有的属性并不是三种商品都要用到,浪费许多内存。
所以我们可以把公共属性单独放一块,剩余的独有属性包装进联合体中,共用同一块空间,这样就可以有效节省空间。
struct goods_list
{
int stock_number; // 库存量
double price; // 定价
int item_type; // 商品类型
union
{
struct
{
char title[20]; // 书名
char author[20]; // 作者
int num_pages; // ⻚数
} book;
struct
{
char design[30]; // 设计
} mug;
struct
{
char design[30]; // 设计
int colors; // 颜⾊
int sizes; // 尺⼨
} shirt;
} item;
};
六.枚举
6.1 枚举类型的声明
枚举,顾名思义:一 一列举
把所有的可能都列举出来,就是枚举
例如把周一到周日都列举出来,或者12个月份都列举出来
enum Day //星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
其中定义的 enum Day,就是枚举类型,enum是枚举的关键字
大括号中的内容就是列举的所有可能取值,也叫做枚举常量
这些枚举常量都有自己的数值,默认第一个枚举常量为0,第二个为1,依次递增
当然,在声明枚举类型的时候我们也可以给枚举常量赋一个初始值
enum Color // 三原色
{
RED,
GREEN = 4,
BLUE
};
需要注意的是,例如这里,我们给GREEN赋初始值为4,所以后面的枚举常量就要以4为基础递增
6.2 枚举类型的优点
枚举可以定义常量,#define宏定义也可以定义常量,那我们为什么不直接用宏定义呢?
枚举的优点:
- 可以增加代码的可读性和可维护性
- 和#define定义的标识符相比,枚举有类型检查,更严谨
- 便于调试,而#define定义的符号在预处理阶段会被删除
- 使用方便,一次可以定义多个常量
- 枚举常量遵循作用域规则,例如在函数中声明枚举类型,就只能在函数内使用该类型
完.
本文详细介绍了C语言中的自定义类型,包括结构体的声明、成员变量、内存对齐规则,以及联合体和枚举的使用,讨论了内存对齐的原因、结构体传参的优化和联合体的特性。
710

被折叠的 条评论
为什么被折叠?



