C语言基础教程:枚举、结构体、共用体

简单数据类型和复合数据类型都讲过了,简单类型是单一类型的数据,复合数据类型在C语言里也要求必须都一样,那如果要把身高体重姓名学号都放在一起存,得怎么办呢?这次咱们就讨论一下这些。

如果说数组是整整齐齐的储物柜,里面放的东西都得一样(全是整数或全是字符),那么今天我们要学的结构体就像是一个个人档案袋。在这个袋子里,你可以放照片(图像数据)、身份证(字符串)、体检表(浮点数)等等各种不同类型的东西。

为了解决更复杂的现实问题,C 语言提供了三种强大的构造类型:枚举(给数字起名字)、结构体(打包不同类型的数据)和共用体(节省内存)。

一、枚举(Enum):给数字起个好听的名字

在写代码的时候,我们经常遇到一些选项类的数据。比如交通灯有红、黄、绿三种状态,或者一周有周一到周日七天。

如果直接用数字 0, 1, 2 来表示红黄绿,代码写多了容易晕:if (light == 0) 到底是什么意思?是红灯还是绿灯?

这时候,枚举就派上用场了。它允许我们给这些枯燥的数字起一个有意义的名字,让代码像人话一样易读。

示例 1:交通灯与工作日

看看 ex1_enum.c

// [引用: code_examples/struct_enum_union/ex1_enum.c]
#include <stdio.h>

// 1. 定义枚举类型
// 就像给 "0, 1, 2" 这些数字起了有意义的名字
// 默认从 0 开始编号:RED=0, YELLOW=1, GREEN=2
enum TrafficLight {
    RED,
    YELLOW,
    GREEN
};

// 也可以手动指定数值
enum Weekday {
    MONDAY = 1, // 从1开始
    TUESDAY,    // 自动变成2
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
};

int main() {
    printf("=== 枚举演示:交通灯 ===\n");

    // 使用枚举变量
    enum TrafficLight current_light = RED;

    // 枚举本质上就是整数,所以可以用 %d 打印
    printf("红灯的数值是: %d\n", current_light);

    // 配合 switch 使用非常清晰
    switch (current_light) {
        case RED:
            printf("信号灯指示:停车!\n");
            break;
        case YELLOW:
            printf("信号灯指示:等一等!\n");
            break;
        case GREEN:
            printf("信号灯指示:通行!\n");
            break;
    }

    printf("\n=== 枚举演示:工作日 ===\n");
    enum Weekday today = FRIDAY;
    printf("今天是星期 %d\n", today);

    if (today == SATURDAY || today == SUNDAY) {
        printf("状态:休息日,睡懒觉。\n");
    } else {
        printf("状态:工作日,去搬砖。\n");
    }

    return 0;
}

运行结果

=== 枚举演示:交通灯 ===
红灯的数值是: 0
信号灯指示:停车!

=== 枚举演示:工作日 ===
今天是星期 5
状态:工作日,去搬砖。

代码解析

  1. 可读性提升case RED:case 0: 清楚太多了。以后维护代码的人(包括未来的你自己)会感谢现在的你。
  2. 本质是整数:枚举在 C 语言底层就是 int。你可以直接把它当数字用,但为了代码规范,最好只当枚举用。

二、结构体(Struct):打包不同类型的数据

这是 C 语言里最常用的工具之一。

想象你要在程序里描述一个学生。他有姓名(字符串)、年龄(整数)和成绩(浮点数)。 如果不用结构体,你得定义三个独立的变量:char name[], int age, float score。如果有 50 个学生,你就得维护 3 个大数组,很容易搞乱。

结构体允许你把这些相关联的数据,打包成一个整体,就像填写一张学生登记表

示例 2:定义和使用结构体

看看 ex2_struct_basic.c

// [引用: code_examples/struct_enum_union/ex2_struct_basic.c]
#include <stdio.h>
#include <string.h>

// 1. 定义结构体
// 就像设计一张 "学生信息登记表"
struct Student {
    char name[50];  // 姓名 (字符串)
    int age;        // 年龄 (整数)
    float score;    // 分数 (浮点数)
};

int main() {
    printf("=== 结构体基础演示 ===\n");

    // 2. 创建并初始化结构体变量
    // 就像填写一张具体的表格
    struct Student stu1 = {"张三", 18, 95.5};

    // 3. 访问结构体成员
    // 使用 . 运算符 (点号)
    printf("姓名: %s\n", stu1.name);
    printf("年龄: %d\n", stu1.age);
    printf("成绩: %.1f\n", stu1.score);

    printf("\n--- 修改数据后 ---\n");

    // 修改成员的值
    stu1.score = 98.0; 
    // 注意:字符串数组不能直接用 = 赋值,要用 strcpy
    strcpy(stu1.name, "张三丰"); 

    printf("姓名: %s\n", stu1.name);
    printf("成绩: %.1f\n", stu1.score);

    // 结构体的大小
    // 注意:可能会有 "内存对齐" 现象,所以大小可能比成员加起来略大
    printf("\n结构体总大小: %lu 字节\n", sizeof(stu1));

    return 0;
}

运行结果

=== 结构体基础演示 ===
姓名: 张三
年龄: 18
成绩: 95.5

--- 修改数据后 ---
姓名: 张三丰
成绩: 98.0

结构体总大小: 60 字节

2.1 结构体指针与箭头运算符 ->

我们在讲函数时说过,C 语言函数参数默认是值传递(复印件)。 结构体通常比较大(比如上面的 Student 有 60 字节)。如果每次传参都复印一份,不仅浪费内存,还很慢。

所以,我们通常传递结构体的指针(地址)。 但是,拿到指针后,怎么访问里面的成员呢? * 普通方式:(*p).name (先解引用,再访问,写起来麻烦) * 快捷方式p->name (箭头运算符,专门给指针用的,帅气又快捷)

看看 ex3_struct_pointer.c

// [引用: code_examples/struct_enum_union/ex3_struct_pointer.c]
#include <stdio.h>

struct Point {
    int x;
    int y;
};

// 函数参数传递结构体指针
// 这样做的好处:
// 1. 避免复制整个结构体(如果结构体很大,复制很慢)
// 2. 可以在函数内部修改外面的结构体
void move_point(struct Point *p, int dx, int dy) {
    // 方式一:解引用后用点号 (*p).x
    // 方式二:使用箭头运算符 p->x (推荐,更简洁)

    printf("  [函数内] 移动前: (%d, %d)\n", p->x, p->y);

    p->x += dx;
    p->y += dy;

    printf("  [函数内] 移动后: (%d, %d)\n", p->x, p->y);
}

int main() {
    printf("=== 结构体指针演示 ===\n");

    struct Point my_point = {10, 20};

    printf("原始坐标: (%d, %d)\n", my_point.x, my_point.y);

    printf("\n正在调用 move_point 函数...\n");
    // 传递地址 (&my_point)
    move_point(&my_point, 5, -3);

    printf("\n回到 main 函数,坐标变为: (%d, %d)\n", my_point.x, my_point.y);
    printf("结论:通过指针,函数成功修改了外部的结构体!\n");

    return 0;
}

运行结果

=== 结构体指针演示 ===
原始坐标: (10, 20)

正在调用 move_point 函数...
  [函数内] 移动前: (10, 20)
  [函数内] 移动后: (15, 17)

回到 main 函数,坐标变为: (15, 17)
结论:通过指针,函数成功修改了外部的结构体!

三、共用体(Union):省吃俭用的变色龙

共用体(也叫联合体)是一种非常特殊的类型。它的所有成员共用同一块内存空间。 这就像是一个多功能插座,虽然有三孔也有两孔,但同一时间你只能插一个插头,不能同时用。

  • 结构体:每个人都有自己的房间(内存叠加)。
  • 共用体:大家轮流使用同一个房间(内存重叠,大小取决于最大的那个成员)。

它通常用于节省内存,或者在底层开发中处理二进制数据转换。

示例 3:共用体演示

看看 ex4_union.c

// [引用: code_examples/struct_enum_union/ex4_union.c]
#include <stdio.h>

// 定义共用体
// 就像一个 "多功能插座",同一时间只能插一种插头
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    printf("=== 共用体 (Union) 演示 ===\n");

    union Data data;

    printf("共用体占用内存大小: %lu 字节\n", sizeof(data));
    printf("(它的大小等于最大成员的大小,这里是 str[20] 的 20 字节)\n\n");

    // 1. 存入整数
    data.i = 10;
    printf("存入整数 10 后:\n");
    printf("data.i = %d\n", data.i);

    // 2. 存入浮点数
    // 注意:这会覆盖掉刚才存的整数!
    data.f = 220.5;
    printf("\n存入浮点数 220.5 后:\n");
    printf("data.f = %.1f\n", data.f);
    printf("data.i = %d (乱码了!因为内存被改写了)\n", data.i);

    // 3. 存入字符串
    // 这会覆盖掉刚才的浮点数
    sprintf(data.str, "Hello");
    printf("\n存入字符串 'Hello' 后:\n");
    printf("data.str = %s\n", data.str);
    printf("data.f = %.1f (乱码了!)\n", data.f);

    return 0;
}

运行结果

=== 共用体 (Union) 演示 ===
共用体占用内存大小: 20 字节
(它的大小等于最大成员的大小,这里是 str[20]  20 字节)

存入整数 10 后:
data.i = 10

存入浮点数 220.5 后:
data.f = 220.5
data.i = 1130135552 (乱码了!因为内存被改写了)

存入字符串 'Hello' 后:
data.str = Hello
data.f = 1143139122437582505939828736.0 (乱码了!)

四、结构体 vs 共用体

这两个挺像(都是用 {} 包起来),所以得注意区分异同。

特性 结构体 (Structure) 共用体 (Union)
内存分配 每个成员都有自己独立的房间 所有成员挤在同一个房间里
大小计算 至少是所有成员大小之(可能有填充) 等于最大那个成员的大小
同时存值 可以同时存储所有成员的值 同一时间只能存一个成员的值
互相影响 改了 A 成员,B 成员不受影响 改了 A 成员,B 成员的数据会被覆盖/破坏
生活比喻 公寓:你住你的,我住我的 试衣间:一次只能进一个人,你进去了我就得出来

什么时候用谁? * 绝大多数情况(99%)你需要的都是结构体。 * 只有在你极度缺内存(比如嵌入式开发),或者需要对同一段二进制数据进行不同方式的解读(比如网络协议解析)时,才会用到共用体

总结

今天我们学习了 C 语言中用来处理复杂数据的三个法宝:

  1. 枚举 (Enum):让代码里的数字变成有意义的单词,提高可读性(红灯、绿灯)。
  2. 结构体 (Struct):把不同类型的数据打包在一起,描述一个完整的对象(学生档案)。记住用 -> 来操作结构体指针。
  3. 共用体 (Union):一块内存轮流用,省空间,但要注意同一时间只能存一个有效值。

掌握了结构体,你现在可以把之前学的零散变量组织起来,去描述更真实、更复杂的世界了。比如写一个贪吃蛇游戏,蛇身就是一个结构体数组;或者写一个通讯录,每个联系人也是一个结构体。

分类: C