在 C 语言的学习和开发过程中,基础数据类型(如int、char、float)虽然能满足简单的编程需求,但面对复杂的实际场景,它们就显得有些 “力不从心” 了。这时,自定义类型的出现就为我们提供了更灵活、更贴合实际需求的数据组织方式。本文将详细介绍 C 语言中三种核心的自定义类型 —— 结构体(struct)、枚举(enum)和共用体(union),并通过实战案例展示它们的具体运用,帮助你真正掌握自定义类型的精髓。
一、结构体(struct):复杂数据的 “组合容器”
1. 什么是结构体?
结构体是 C 语言中最常用的自定义类型,它允许我们将不同类型的数据(如整数、字符、数组甚至其他结构体)组合在一起,形成一个新的、具有特定含义的数据类型。例如,要描述一个 “学生”,需要包含学号(int)、姓名(char数组)、年龄(int)、成绩(float)等信息,这些分散的数据就可以通过结构体整合为一个统一的 “学生类型”。
2. 结构体的定义与使用
结构体的定义格式如下:
// 定义结构体类型(struct 结构体名)
struct 结构体名 {
数据类型1 成员名1;
数据类型2 成员名2;
// ... 更多成员
};
示例 1:定义并使用 “学生” 结构体
#include .h>
#include .h>
// 1. 定义结构体类型:struct Student
struct Student {
int id; // 学号
char name[20]; // 姓名(字符数组)
int age; // 年龄
float score; // 成绩
};
int main() {
// 2. 声明结构体变量(两种方式)
// 方式1:直接声明
struct Student stu1;
// 方式2:定义类型时同时声明变量(不推荐,不利于代码复用)
// struct Student { ... } stu2;
// 3. 给结构体成员赋值(通过“.”访问成员)
stu1.id = 2025001;
strcpy(stu1.name, "张三"); // 字符数组不能直接用“=”赋值,需用strcpy
stu1.age = 20;
stu1.score = 92.5;
// 4. 访问结构体成员(输出信息)
printf("学号:%d\n", stu1.id);
printf("姓名:%s\n", stu1.name);
printf("年龄:%d\n", stu1.age);
printf("成绩:%.1f\n", stu1.score);
return 0;
}
运行结果:
学号:2025001
姓名:张三
年龄:20
成绩:92.5
3. 结构体的进阶用法:指针与数组
- 结构体指针:通过指针访问结构体成员时,需使用 “->” 运算符(而非 “.”),这在函数传参中非常常用(避免拷贝整个结构体,提高效率)。
- 结构体数组:当需要存储多个同类型结构体(如多个学生)时,可使用结构体数组。
示例 2:结构体指针与数组的实战(学生成绩管理)
#include >
#include >
struct Student {
int id;
char name[20];
float score;
};
// 函数:打印单个学生信息(接收结构体指针)
void printStudent(struct Student *pStu) {
printf("学号:%d | 姓名:%s | 成绩:%.1f\n",
pStu->id, pStu->name, pStu->score); // 指针用“->”访问成员
}
// 函数:计算学生平均成绩(接收结构体数组和数组长度)
float calcAvgScore(struct Student stuArr[], int len) {
float sum = 0;
for (int i = 0; i
sum += stuArr[i].score; // 数组元素用“.”访问成员
}
return sum / len;
}
int main() {
// 定义结构体数组(存储3个学生)
struct Student stuArr[3] = {
{2025001, "张三", 92.5}, // 初始化方式1:按成员顺序赋值
{.id=2025002, .name="李四", .score=88.0}, // 初始化方式2:指定成员赋值(更灵活)
{2025003, "王五", 79.5}
};
int len = sizeof(stuArr) / sizeof(stuArr[0]); // 计算数组长度
// 1. 遍历打印所有学生信息(用指针)
printf("所有学生信息:\n");
for (int i = 0; i ++) {
printStudent(&stuArr[i]); // 传入结构体地址(指针)
}
// 2. 计算并打印平均成绩
float avg = calcAvgScore(stuArr, len);
printf("\n全班平均成绩:%.1f\n", avg);
return 0;
}
运行结果:
所有学生信息:
学号:2025001 | 姓名:张三 | 成绩:92.5
学号:2025002 | 姓名:李四 | 成绩:88.0
学号:2025003 | 姓名:王五 | 成绩:79.5
全班平均成绩:86.7
二、枚举(enum):有限状态的 “清晰标识”
1. 什么是枚举?
枚举(enum)用于定义一组具有离散值的常量,它的核心作用是 “用有意义的名字替代无意义的数字”,让代码更易读、更易维护。例如,一周的七天(周一到周日)、颜色(红 / 绿 / 蓝)、状态(成功 / 失败 / 等待)等,都适合用枚举表示。
2. 枚举的定义与使用
枚举的定义格式如下:
// 定义枚举类型(enum 枚举名)
enum 枚举名 {
常量1, // 默认值为0
常量2, // 默认值为1(依次递增)
常量3 // 默认值为2
// ... 更多常量
};
示例 3:枚举的基础用法(表示 “星期”)
#include >
// 定义枚举类型:表示星期
enum Weekday {
Monday, // 0
Tuesday, // 1
Wednesday, // 2
Thursday, // 3
Friday, // 4
Saturday, // 5
Sunday // 6
};
int main() {
// 声明枚举变量
enum Weekday today = Wednesday;
// 1. 打印枚举常量的值(本质是整数)
printf("Wednesday 的值:%d\n", Wednesday); // 输出:2
// 2. 根据枚举变量判断逻辑
if (today == Saturday || today == Sunday) {
printf("今天是周末,休息!\n");
} else {
printf("今天是工作日,努力工作!\n"); // 输出:今天是工作日,努力工作!
}
return 0;
}
运行结果:
Wednesday 的值:2
今天是工作日,努力工作!
3. 枚举的进阶用法:自定义初始值
枚举常量的默认值是从 0 开始递增的,但我们也可以手动指定某个常量的值,后续常量会从该值继续递增。
示例 4:枚举自定义初始值(表示 “错误码”)
#include 定义枚举类型:表示错误码(自定义初始值)
enum ErrorCode {
SUCCESS = 0, // 成功:0
FILE_NOT_FOUND = 1, // 文件未找到:1
PERMISSION_DENIED = 2, // 权限不足:2
NETWORK_ERROR = 5 // 网络错误:5(手动指定,后续常量从5递增)
// 若后续有其他常量,如 TIMEOUT,其值会是6
};
// 函数:模拟文件读取,返回错误码
enum ErrorCode readFile(const char *filename) {
// 模拟逻辑:若文件名是"test.txt",返回成功;否则返回“文件未找到”
if (strcmp(filename, "test.txt") == 0) {
return SUCCESS;
} else {
return FILE_NOT_FOUND;
}
}
int main() {
// 调用函数,获取错误码
enum ErrorCode err = readFile("data.txt");
// 根据错误码打印提示信息
switch (err) {
case SUCCESS:
printf("文件读取成功!\n");
break;
case FILE_NOT_FOUND:
printf("错误:文件未找到(错误码:%d)\n", err); // 输出:错误:文件未找到(错误码:1)
break;
case PERMISSION_DENIED:
printf("错误:权限不足(错误码:%d)\n", err);
break;
case NETWORK_ERROR:
printf("错误:网络异常(错误码:%d)\n", err);
break;
default:
printf("未知错误(错误码:%d)\n", err);
}
return 0;
}
运行结果:
错误:文件未找到(错误码:1)
三、共用体(union):内存共享的 “高效工具”
1. 什么是共用体?
共用体(union)与结构体类似,也是由多个成员组成,但它的核心特性是:所有成员共享同一块内存空间,整个共用体的大小等于最大成员的大小(而结构体的大小是所有成员大小之和,需考虑内存对齐)。
这意味着:修改共用体的一个成员,会影响其他所有成员(因为它们在同一块内存中)。共用体适合用于 “同一内存区域存储不同类型数据” 的场景,例如硬件寄存器(同一寄存器可能存储不同含义的数值)。
2. 共用体的定义与使用
共用体的定义格式如下:
// 定义共用体类型(union 共用体名)
union 共用体名 {
数据类型1 成员名1;
数据类型2 成员名2;
// ... 更多成员
};
示例 5:共用体的基础用法(内存共享特性)
#include 定义共用体类型:包含int和char成员
union Data {
int i; // 4字节(假设系统中int为4字节)
char c; // 1字节
};
int main() {
// 声明共用体变量
union Data data;
// 1. 打印共用体的大小(等于最大成员的大小)
printf("共用体 Data 的大小:%lu 字节\n", sizeof(union Data)); // 输出:4
// 2. 给int成员赋值,观察char成员的变化
data.i = 0x12345678; // 十六进制数(4字节)
printf("data.i = 0x%x\n", data.i); // 输出:data.i = 0x12345678
printf("data.c = 0x%x\n", data.c); // 输出:data.c = 0x78(小端模式下,低字节存低地址)
// 3. 给char成员赋值,观察int成员的变化
data.c = 0x99;
printf("\n修改 data.c 后:\n");
printf("data.c = 0x%x\n", data.c); // 输出:data.c = 0x99
printf("data.i = 0x%x\n", data.i); // 输出:data.i = 0x12345699(低字节被修改)
return 0;
}
运行结果(小端模式系统,如 x86):
共用体 Data 的大小:4 字节
data.i = 0x12345678
data.c = 0x78
修改 data.c 后:
data.c = 0x99
data.i = 0x12345699
3. 共用体的实战场景:判断 CPU 大小端
CPU 的 “大小端” 是指多字节数据在内存中的存储顺序:
- 小端模式:低字节存低地址(如0x12345678在内存中存储为78 56 34 12)。
- 大端模式:高字节存低地址(如0x12345678在内存中存储为12 34 56 78)。
利用共用体的内存共享特性,我们可以轻松判断 CPU 的大小端。
示例 6:用共用体判断 CPU 大小端
#include // 定义共用体:int(4字节)和char(1字节)共享内存
union EndianTest {
int i;
char c;
};
// 函数:判断CPU大小端
void checkEndian() {
union EndianTest test;
test.i = 1; // 十六进制:0x00000001(4字节)
// 若c为1,说明低字节(0x01)存在低地址 → 小端;否则为大端
if (test.c == 1) {
printf("CPU 是小端模式\n");
} else {
printf("CPU 是大端模式\n");
}
}
int main() {
checkEndian(); // 大多数PC(x86架构)会输出:CPU 是小端模式
return 0;
}
运行结果(x86 架构 PC):
CPU 是小端模式
四、自定义类型的选型建议
在实际开发中,如何选择合适的自定义类型?可以参考以下原则:
- 结构体(struct):当需要组合不同类型的数据(如学生、商品、坐标)时,优先使用结构体,它是 “聚合数据” 的首选。
- 枚举(enum):当需要表示有限个离散状态(如错误码、星期、状态值)时,使用枚举,它能让代码更易读、更易维护(避免魔法数字)。
- 共用体(union):当需要共享内存空间(如硬件寄存器、节省内存的场景)时,使用共用体,它的核心优势是 “内存高效”,但需注意成员间的相互影响。
五、总结
自定义类型是 C 语言灵活性的重要体现,也是从 “基础编程” 走向 “实战开发” 的关键一步。通过本文的介绍,我们可以总结出:
- 结构体是 “组合器”,用于整合不同类型的数据;
- 枚举是 “标识器”,用于清晰表示离散状态;
- 共用体是 “共享器”,用于高效利用内存空间。
掌握这三种自定义类型的概念和用法,并在实际项目中灵活运用,能让你的 C 语言代码更简洁、更高效、更具可读性。如果你在实践中遇到具体问题(如结构体内存对齐、共用体的复杂场景),欢迎在评论区交流讨论!