一、结构体:把零散数据打包成“一个整体”

写程序久了你会发现,光靠 int、char、float 这些基本类型,根本不够用。

比如你要描述一个学生:有学号、姓名、成绩、年龄。用四个独立变量?

int stu_id;
char stu_name[20];
float stu_score;
int stu_age;

这样写,数据是散的。你传参得传四个,排序得来回倒腾。说白了,数据之间没有“归属感”

结构体就是干这个的——把相关的数据打包成一个自定义类型。

1.1 结构体的定义

语法很简单,用 struct 关键字:

struct Student {
    int id;           // 学号
    char name[20];    // 姓名
    float score;      // 成绩
    int age;          // 年龄
};  // 别忘了分号!

嗯,这里要注意:结构体定义末尾的分号不能丢。我见过不少新手在这里栽跟头,编译器报错半天找不到原因。

1.2 结构体变量的声明与初始化

定义完类型,就可以声明变量了:

// 方式一:先定义类型,再声明变量
struct Student stu1;

// 方式二:定义类型的同时声明变量
struct Student {
    int id;
    char name[20];
    float score;
} stu2, stu3;

// 方式三:初始化
struct Student stu4 = {1001, "张三", 89.5f, 20};

我个人习惯用方式一,代码更清晰。初始化时,成员顺序必须和定义一致

1.3 访问结构体成员

用点号 . 操作符:

stu1.id = 1002;
strcpy(stu1.name, "李四");
stu1.score = 92.0f;
stu1.age = 21;

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

我在项目中遇到过一个问题:结构体里有字符数组成员,直接用 = 赋值是不行的,必须用 strcpy。这点和基本类型不一样,容易踩坑。

⚠️ 避坑指南: 我曾经在嵌入式项目里,直接用 stu1.name = "王五";,结果编译没报错,运行直接崩溃。因为 name 是数组名,是常量地址,不能赋值。记住:数组成员用 strcpy,基本类型直接用 =

二、结构体数组:批量管理同类数据

一个学生用结构体,一百个学生呢?用结构体数组。

struct Student class[100];  // 一个班100个学生

// 初始化
struct Student class[3] = {
    {1001, "张三", 89.5f, 20},
    {1002, "李四", 92.0f, 21},
    {1003, "王五", 78.0f, 19}
};

// 访问
for (int i = 0; i < 3; i++) {
    printf("学号:%d,姓名:%s\n", class[i].id, class[i].name);
}

结构体数组的用法和普通数组一模一样,只是每个元素是一个结构体。你想想看,配合循环,批量处理数据是不是很方便?

三、结构体指针:高效传递,避免拷贝

结构体可能很大,如果直接传值给函数,整个结构体都会被复制一遍,浪费内存和时间。

用指针就高效多了——只传一个地址。

struct Student stu = {1001, "张三", 89.5f, 20};
struct Student *p = &stu;

// 两种访问方式
printf("成绩:%f\n", (*p).score);   // 先解引用,再访问
printf("成绩:%f\n", p->score);     // 直接用 -> 操作符

我个人强烈推荐用 -> 操作符,代码更简洁,可读性更好。

3.1 结构体指针作为函数参数

void printStudent(const struct Student *p) {
    printf("学号:%d,姓名:%s,成绩:%.1f\n",
           p->id, p->name, p->score);
}

// 调用
printStudent(&stu);

这里加 const 是个好习惯——告诉编译器这个函数不会修改结构体内容。我在代码审查时经常强调这一点。

💡 小技巧: 如果函数需要修改结构体内容,就不要加 const。比如一个“更新成绩”的函数,肯定要改 score 字段。

四、结构体嵌套:描述复杂对象

现实中的对象往往不是一层结构。比如学生还有“出生日期”,日期本身又包含年、月、日。

struct Date {
    int year;
    int month;
    int day;
};

struct Student {
    int id;
    char name[20];
    struct Date birthday;  // 嵌套结构体
    float score;
};

struct Student stu = {1001, "张三", {2000, 5, 15}, 89.5f};

// 访问嵌套成员
printf("出生年份:%d\n", stu.birthday.year);

嵌套的层次不要太深,我一般控制在两层以内。超过三层,代码可读性会急剧下降。

五、联合体(union):同一内存,不同解读

联合体和结构体长得很像,但有一个本质区别:所有成员共享同一块内存

union Data {
    int i;
    float f;
    char str[4];
};

union Data data;
data.i = 42;
printf("int: %d\n", data.i);

data.f = 3.14f;
printf("float: %f\n", data.f);  // 此时 i 的值已经被覆盖了

为什么会这样?因为联合体的大小等于最大成员的大小。所有成员从同一个地址开始存储。你写了一个成员,其他成员的值就变得不可预测了。

我在嵌入式项目中常用联合体来解析通信协议。比如接收一个数据包,可以用联合体把同一段内存解释为不同的字段:

union Packet {
    unsigned char raw[4];   // 原始字节
    struct {
        unsigned char id;
        unsigned char cmd;
        unsigned short value;
    } fields;
};

这样,你可以用 raw 接收数据,然后用 fields 解析具体内容。说白了,就是同一块内存,不同视角

⚠️ 注意: 联合体的大小端问题。不同平台的字节序可能不同,跨平台通信时要格外小心。我曾经在 ARM 和 x86 之间传数据,就因为大小端问题排查了两天。

六、枚举(enum):让代码自己说话

枚举的本质是给整数起个名字。比如表示一周的七天:

enum Weekday {
    MON,     // 默认 0
    TUE,     // 1
    WED,     // 2
    THU,     // 3
    FRI,     // 4
    SAT,     // 5
    SUN      // 6
};

enum Weekday today = WED;
if (today == SAT || today == SUN) {
    printf("周末啦!\n");
}

你也可以手动指定值:

enum Color {
    RED = 1,
    GREEN = 2,
    BLUE = 4
};

枚举的好处是让代码自文档化。你看到 RED 就知道是红色,看到 1 还得猜一下。我在状态机里特别喜欢用枚举,每个状态一目了然。

💡 个人习惯: 枚举的命名用全大写,和普通变量区分开。这样一眼就能看出是枚举常量。

七、知识体系总览

下面这张图总结了本章的核心内容,帮你理清结构体、联合体、枚举之间的关系:

结构体、联合体、枚举知识体系 结构体 struct • 定义与声明 • 成员访问(. 操作符) • 结构体数组 • 结构体指针(-> 操作符) • 结构体嵌套 • 作为函数参数 特点:成员独立存储 大小 = 各成员大小之和 (考虑内存对齐) 联合体 union • 定义与声明 • 所有成员共享内存 • 同一内存不同解读 • 大小 = 最大成员大小 特点:节省内存 一次只能使用一个成员 常用于协议解析 注意大小端问题 枚举 enum • 定义枚举类型 • 枚举常量命名 • 默认值从0开始 • 可手动指定值 特点:代码自文档化 本质是整数 常用于状态机 提高代码可读性 三者都是构造类型,用于组织和管理数据

八、总结与避坑

这一章的内容,说白了就是三件事:

  • 结构体:把不同类型的数据打包,成员独立存储
  • 联合体:所有成员共享内存,同一块内存不同解读
  • 枚举:给整数起名字,让代码自己说话

最后分享几个我踩过的坑:

  1. 结构体赋值别用 = 给数组——用 strcpy
  2. 联合体写一个成员,别指望另一个成员还保留值——它们共享内存
  3. 枚举本质是 int——别指望它做类型安全检查
  4. 结构体传参尽量用指针——避免拷贝大结构体

📌 核心要点: 结构体是“打包”,联合体是“共享”,枚举是“命名”。理解这三者的本质区别,你就能在合适的场景用对工具。


公众号:蓝海资料掘金营,微信deep3321