在 C 语言中,基础数据类型(int、char、float等)仅能描述单一属性的数据(如数字、字符),但实际开发中,我们常需要描述包含多个不同属性的复杂对象(如学生、商品、设备信息),或一组有明确含义的常量(如星期、颜色)。
结构体(struct)、联合体(union)、枚举(enum)作为 C 语言的三大复合数据类型,正是为解决这类问题而生。本文将从定义、使用、核心特性到实战场景,全方位拆解这三种类型,帮你掌握复合数据类型的核心用法。
一、结构体
结构体是 C 语言中最常用的复合类型,它允许你将不同类型的变量打包成一个整体,用于描述具有多个属性的对象(如学生、图书、员工)。
1、结构体的定义和使用
核心语法
结构体的使用分为两步:定义结构体类型(相当于创建一个新的 “数据模板”)和实例化结构体变量(根据模板创建具体对象)。
#include <stdio.h>
#include <string.h> // 用于strcpy
// 第一步:定义结构体类型(模板)
// struct是关键字,Student是结构体名(可自定义),{}内是成员列表
struct Student {
// 成员可以是不同类型的变量
char name[20]; // 姓名
int age; // 年龄
float score; // 成绩
}; // 注意:结构体定义末尾必须加分号
int main() {
// 第二步:实例化结构体变量(创建具体对象)
// 方式1:定义时直接初始化
struct Student stu1 = {"张三", 18, 92.5};
// 方式2:先定义变量,再逐个赋值(用.访问成员)
struct Student stu2;
// 字符串赋值不能直接用=,需用strcpy
strcpy(stu2.name, "李四");
stu2.age = 19;
stu2.score = 88.0;
// 访问结构体成员并输出
printf("学生1:姓名=%s,年龄=%d,成绩=%.1f\n", stu1.name, stu1.age, stu1.score);
printf("学生2:姓名=%s,年龄=%d,成绩=%.1f\n", stu2.name, stu2.age, stu2.score);
return 0;
}
深度解读
-
类型与变量的区别:结构体定义(
struct Student {...})只是声明了一个新的类型,不占用内存;只有实例化变量(如stu1、stu2)时,才会在内存中分配空间。 -
成员访问符
·:必须通过结构体变量名.成员名的方式访问成员,这是结构体变量访问成员的唯一方式。 -
简化写法:typedef 重命名:反复写
struct Student会繁琐,可通过typedef简化类型名:
// 重命名后,Student等价于struct Student
typedef struct {
char name[20];
int age;
float score;
} Student;
// 实例化时无需写struct
Student stu = {"王五", 20, 90.0};
4.内存对齐:结构体的总大小并非成员大小的简单相加(如char[20]+int+float理论上是 28 字节),编译器会为了访问效率进行内存对齐(比如int成员会对齐到 4 字节边界),实际大小可能略大。
2、结构体数组:存储多个结构体对象
当需要存储一组同类型的结构体对象(如一个班级的学生)时,结构体数组是最佳选择 —— 数组的每个元素都是一个完整的结构体变量。
#include <stdio.h>
#include <string.h>
// 用typedef简化结构体类型
typedef struct {
char name[20];
int age;
float score;
} Student;
int main() {
// 定义结构体数组并初始化(3个学生)
Student stu_arr[3] = {
{"张三", 18, 92.5},
{"李四", 19, 88.0},
{"王五", 20, 90.0}
};
// 遍历结构体数组
printf("班级学生列表:\n");
for (int i = 0; i < 3; i++) {
printf("第%d个学生:姓名=%s,年龄=%d,成绩=%.1f\n",
i+1, stu_arr[i].name, stu_arr[i].age, stu_arr[i].score);
}
// 修改数组中某个元素的成员
strcpy(stu_arr[1].name, "李华");
stu_arr[1].score = 95.5;
printf("\n修改后第2个学生:姓名=%s,成绩=%.1f\n", stu_arr[1].name, stu_arr[1].score);
return 0;
}
深度解读
- 结构体数组的内存是连续的:每个数组元素(结构体)的内存紧密排列,可通过下标快速访问。
- 访问规则:
数组名[下标].成员名,本质是先通过下标找到结构体元素,再通过.访问成员。 - 适用场景:批量存储同类型的复杂对象(如学生列表、商品列表、员工信息)。
3、结构体指针:指向结构体的 “快捷方式”
结构体指针存储的是结构体变量的首地址,通过指针访问结构体成员,既能节省内存(避免拷贝整个结构体),又能提高程序效率。
#include <stdio.h>
#include <string.h>
typedef struct {
char name[20];
int age;
float score;
} Student;
int main() {
Student stu = {"张三", 18, 92.5};
// 定义结构体指针,指向结构体变量的地址
Student *p_stu = &stu;
// 方式1:通过指针访问成员(推荐):指针名->成员名
printf("通过指针访问:姓名=%s,年龄=%d,成绩=%.1f\n",
p_stu->name, p_stu->age, p_stu->score);
// 方式2:等价写法(解引用指针后用.访问):(*指针名).成员名
// 注意:*p_stu必须加括号,因为.的优先级高于*
printf("等价写法:姓名=%s,年龄=%d,成绩=%.1f\n",
(*p_stu).name, (*p_stu).age, (*p_stu).score);
// 通过指针修改结构体成员
p_stu->age = 19;
p_stu->score = 95.0;
printf("\n修改后:年龄=%d,成绩=%.1f\n", stu.age, stu.score);
return 0;
}
深度解读
-
核心操作符
->:结构体指针访问成员的专用操作符,等价于(*指针).成员,是实际开发中最常用的写法。 -
结构体指针的优势:
-
占用内存少:指针仅占 4/8 字节(32/64 位系统),而结构体可能占几十 / 上百字节;
-
支持修改原数据:指针直接指向原结构体的内存,修改会同步到原变量。
-
-
指向结构体数组的指针:指针 ++ 会自动偏移一个结构体的大小,可用于遍历数组:
Student stu_arr[2] = {{"张三",18,92.5}, {"李四",19,88.0}};
Student *p = stu_arr; // 数组名是首地址,无需&
// 遍历第一个元素
printf("%s\n", p->name); // 张三
p++; // 指针偏移到第二个元素
printf("%s\n", p->name); // 李四
4、嵌套结构体:描述更复杂的对象
当结构体的某个成员本身也是一个复杂对象时,可将该成员定义为另一个结构体类型,这就是嵌套结构体(比如学生包含 “地址” 结构体,订单包含 “商品” 结构体)。
#include <stdio.h>
#include <string.h>
// 第一步:定义被嵌套的结构体(地址)
typedef struct {
char province[20]; // 省份
char city[20]; // 城市
char detail[50]; // 详细地址
} Address;
// 第二步:定义主结构体(学生),包含Address类型的成员
typedef struct {
char name[20];
int age;
Address addr; // 嵌套结构体成员
} Student;
int main() {
// 初始化嵌套结构体
Student stu;
strcpy(stu.name, "张三");
stu.age = 18;
// 访问嵌套成员:外层成员.内层成员
strcpy(stu.addr.province, "江苏省");
strcpy(stu.addr.city, "南京市");
strcpy(stu.addr.detail, "玄武区XX路XX号");
// 输出嵌套结构体内容
printf("学生信息:\n");
printf("姓名:%s,年龄:%d\n", stu.name, stu.age);
printf("地址:%s-%s-%s\n", stu.addr.province, stu.addr.city, stu.addr.detail);
return 0;
}
深度解读
-
访问规则:层层递进,
外层结构体.内层结构体.内层成员,每一层都需要用.访问。 -
内存布局:嵌套结构体的内存是 “扁平化” 的 —— 主结构体的内存包含了嵌套结构体的所有成员,没有额外的指针开销。
-
初始化技巧:嵌套结构体也可在定义时直接初始化:
Student stu = {
"张三", 18,
{"江苏省", "南京市", "玄武区XX路XX号"} // 对应Address成员
};
5、结构体传参:值传递 vs 地址传递
结构体作为函数参数传递时,有两种方式,核心区别在于是否拷贝整个结构体,以及是否能修改原数据。
#include <stdio.h>
#include <string.h>
typedef struct {
char name[20];
int age;
float score;
} Student;
// 方式1:值传递(拷贝整个结构体)
void modify_stu1(Student stu) {
stu.age = 20;
stu.score = 99.0;
printf("函数内(值传递):年龄=%d,成绩=%.1f\n", stu.age, stu.score);
}
// 方式2:地址传递(传递指针,仅拷贝地址)
void modify_stu2(Student *p_stu) {
p_stu->age = 20;
p_stu->score = 99.0;
printf("函数内(地址传递):年龄=%d,成绩=%.1f\n", p_stu->age, p_stu->score);
}
// 方式3:地址传递+const(保护数据不被修改)
void print_stu(const Student *p_stu) {
// p_stu->age = 21; // 报错:const修饰的指针无法修改原数据
printf("学生信息:姓名=%s,年龄=%d,成绩=%.1f\n", p_stu->name, p_stu->age, p_stu->score);
}
int main() {
Student stu = {"张三", 18, 92.5};
// 测试值传递:修改的是拷贝的副本,原数据不变
modify_stu1(stu);
printf("主函数(值传递后):年龄=%d,成绩=%.1f\n\n", stu.age, stu.score);
// 测试地址传递:修改的是原数据
modify_stu2(&stu);
printf("主函数(地址传递后):年龄=%d,成绩=%.1f\n\n", stu.age, stu.score);
// 测试const保护的地址传递
print_stu(&stu);
return 0;
}
深度解读
-
值传递的缺点:
-
内存开销大:结构体越大,拷贝的成本越高(比如一个包含 10 个成员的结构体,值传递会拷贝所有成员);
-
无法修改原数据:函数内修改的是 “副本”,主函数的原变量不受影响。
-
-
地址传递的优势:
-
高效:仅传递 4/8 字节的指针,无拷贝开销;
-
可修改原数据:指针直接指向原结构体的内存。
-
-
const 修饰的最佳实践:如果函数仅需读取结构体数据(无需修改),务必用
const Student *修饰指针 —— 既享受地址传递的高效,又能防止误修改原数据,提升代码安全性。
二、联合体:共享内存的复合类型
联合体(也叫共用体,union)与结构体语法相似,但核心特性完全不同:联合体的所有成员共享同一块内存空间,同一时间只能有效使用一个成员。
1、联合体的定义和使用
核心语法
联合体的定义和使用与结构体几乎一致,但内存分配规则完全不同。
#include <stdio.h>
// 定义联合体类型:所有成员共享内存
union Data {
int i; // 4字节
float f; // 4字节
char c; // 1字节
}; // 总大小为最大成员的大小(4字节)
int main() {
// 实例化联合体变量
union Data data;
// 赋值第一个成员
data.i = 100;
printf("赋值int后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
// 输出:i=100,f=0.0(乱码),c=d(ASCII 100对应'd')
// 赋值第二个成员(覆盖内存)
data.f = 3.14f;
printf("赋值float后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
// 输出:i=1078523331(乱码),f=3.1,c=?(乱码)
// 赋值第三个成员(覆盖内存)
data.c = 'A';
printf("赋值char后:i=%d,f=%.1f,c=%c\n", data.i, data.f, data.c);
// 输出:i=65(ASCII 'A'),f=0.0(乱码),c=A
// 查看联合体大小
printf("\n联合体Data的大小:%zu字节\n", sizeof(union Data)); // 输出4
return 0;
}
深度解读
-
内存共享规则:联合体的所有成员从同一块内存地址开始存储,赋值一个成员会覆盖其他成员的内容 —— 因此同一时间只有最后赋值的成员是有效的。
-
大小规则:联合体的总大小等于最大成员的大小(如上例中
int和float都是 4 字节,因此联合体大小为 4),目的是容纳最大的成员。
与结构体的核心区别:
| 类型 | 内存分配方式 | 总大小 | 成员有效性 |
|---|---|---|---|
| 结构体 | 成员各自占独立内存 | 成员大小之和(含对齐) | 所有成员同时有效 |
| 联合体 | 所有成员共享同一块内存 | 最大成员的大小 | 同一时间仅一个有效 |
2、联合体的核心特点与应用场景
核心特点
-
内存复用:极大节省内存,适合 “同一时间仅需使用一个成员” 的场景;
-
类型共用:同一块内存可存储不同类型的数据,灵活适配不同场景;
-
初始化规则:只能初始化第一个成员(C 语言标准),后续成员需手动赋值。
经典应用场景:判断系统大小端
大小端是指多字节数据在内存中的存储顺序:
-
大端:高字节存低地址,低字节存高地址(如
0x1234→ 地址 0 存0x12,地址 1 存0x34); -
小端:低字节存低地址,高字节存高地址(如
0x1234→ 地址 0 存0x34,地址 1 存0x12)。
利用联合体共享内存的特性,可快速判断系统的大小端:
#include <stdio.h>
union EndianTest {
int i; // 4字节
char c; // 1字节(共享i的低地址字节)
};
int main() {
union EndianTest test;
test.i = 1; // 二进制:00000000 00000000 00000000 00000001
// 如果c=1,说明低地址存的是0x01 → 小端;否则大端
if (test.c == 1) {
printf("当前系统是小端模式\n");
} else {
printf("当前系统是大端模式\n");
}
return 0;
}
深度解读
-
小端模式是目前主流(x86 架构、ARM 默认),大端模式多用于网络传输、嵌入式设备;
-
联合体的这一用法是 C 语言面试高频考点,核心原理就是 “共享内存,访问低地址字节”。
三、枚举:给常量命名,提升代码可读性
枚举(enum)是用于定义一组有名字的整数常量的类型,目的是替代魔法数字(如0、1、2),让代码更易读、易维护。
1、枚举的定义和使用
核心语法
枚举的核心是 “给整数常量起名字”,默认从 0 开始递增,也可手动指定值。
#include <stdio.h>
// 定义枚举类型:Weekday是枚举名,{}内是枚举常量
// 默认:Mon=0, Tue=1, Wed=2, ..., Sun=6
enum Weekday {
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
Sun
};
// 手动指定枚举常量的值:Red=10, Green=20, Blue=30
enum Color {
Red = 10,
Green = 20,
Blue = 30
};
int main() {
// 实例化枚举变量(本质是int类型)
enum Weekday today = Wed;
enum Color favorite = Green;
// 输出枚举常量的值
printf("Wed的值:%d\n", Wed); // 输出2
printf("today(Wed):%d\n", today); // 输出2
printf("Green的值:%d\n", Green); // 输出20
printf("favorite(Green):%d\n", favorite); // 输出20
// 枚举的典型用法:switch语句(提升可读性)
switch (today) {
case Mon:
case Tue:
case Wed:
case Thu:
case Fri:
printf("今天是工作日\n");
break;
case Sat:
case Sun:
printf("今天是周末\n");
break;
default:
printf("无效的日期\n");
}
return 0;
}
深度解读
-
枚举的本质:枚举常量是不可修改的整数常量,枚举变量本质是
int类型(可直接赋值整数,如today = 2)。 -
赋值规则:
-
未手动指定值的枚举常量,值为前一个常量 + 1;
-
部分指定值:
enum Num {A=5, B, C=10, D}→ A=5, B=6, C=10, D=11。
-
-
对比 #define 的优势:
-
#define只是简单的文本替换,无类型检查; -
枚举是类型,编译器会进行基础的类型检查,且能批量定义相关常量。
-
-
适用场景:
-
表示状态(如
Success=0, Error=1); -
表示选项(如
Male=0, Female=1); -
表示固定范围的常量(如星期、月份、颜色)。
-
总结
-
结构体:核心是 “打包不同类型数据”,成员各自占内存,用于描述复杂对象;传参优先用地址传递(高效 + 可修改),只读场景加
const保护。 -
联合体:核心是 “共享内存”,同一时间仅一个成员有效,大小为最大成员的大小;典型用法是节省内存、判断系统大小端。
-
枚举:核心是 “给整数常量命名”,替代魔法数字,提升代码可读性;本质是 int 类型,默认从 0 递增,可手动指定值。
以上便是我对于C语言中的复合数据类型的见解,欢迎各位一起探讨。

201

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



