第23章:结构体基础——结构体的定义、声明、初始化,访问成员,结构体数组

说实话,C语言里我最喜欢的数据类型就是结构体。为什么?因为它让代码有了“灵魂”。

你想想看,之前我们学的int、char、float,都是单一的数据。但现实世界里的东西,哪有这么简单?一本书有书名、作者、价格、ISBN号;一个学生有学号、姓名、成绩、年龄。用单独的变量去描述,代码会乱成一锅粥。

结构体就是用来解决这个问题的。它把多个不同类型的数据打包成一个整体。嗯,就像把散落一地的零件装进一个工具箱。

23.1 结构体的定义

定义结构体,说白了就是“画图纸”。你告诉编译器:我要造一种新类型,它长这样。

// 定义一个"学生"结构体类型
struct Student {
    int id;          // 学号
    char name[20];   // 姓名
    float score;     // 成绩
    int age;         // 年龄
};  // 注意这个分号!我见过太多人漏掉它

这里有几个要点:

  • struct 是关键字,告诉编译器我要定义结构体
  • Student 是结构体标签,相当于这个“图纸”的名字
  • 花括号里是成员变量,每个成员都有自己的类型和名字
  • 最后的分号不能丢——我曾经因为这个分号被编译器骂了半小时
我的习惯:结构体定义通常放在头文件里,或者源文件的最前面。这样整个文件都能用。

23.2 结构体的声明

图纸画好了,接下来就是“造东西”。声明一个结构体变量,就像按照图纸造一个实物。

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

// 方式二:定义类型的同时声明变量
struct Student {
    int id;
    char name[20];
    float score;
    int age;
} stu3, stu4;  // 这里直接声明了两个变量

// 方式三:匿名结构体(不常用,但面试可能会问)
struct {
    int x;
    int y;
} point;  // 这种结构体类型没有名字,只能在这里用一次

我个人最常用的是方式一。代码清晰,别人一看就懂。方式二虽然省事,但可读性差一些。

23.3 结构体的初始化

变量声明了,总得给个初值吧?结构体的初始化有好几种玩法。

// 方式一:按成员顺序初始化
struct Student stu1 = {1001, "张三", 89.5, 20};

// 方式二:指定成员初始化(C99标准支持)
struct Student stu2 = {
    .id = 1002,
    .name = "李四",
    .score = 92.0,
    .age = 21
};

// 方式三:部分初始化(未指定的成员自动为0)
struct Student stu3 = {1003, "王五"};  // score=0.0, age=0

// 方式四:先声明后赋值(注意:不能整体赋值!)
struct Student stu4;
stu4.id = 1004;
strcpy(stu4.name, "赵六");  // 字符串要用strcpy
stu4.score = 78.5;
stu4.age = 19;
注意:结构体变量不能直接用等号整体赋值(除了初始化时)。比如 stu4 = {1005, "xxx", 90, 22}; 是错的!必须逐个成员赋值。

我在项目中遇到过一个问题:有个同事用方式一初始化,后来结构体成员顺序调整了,所有初始化代码全崩了。从那以后,我强烈推荐方式二——指定成员初始化。顺序变了也不怕,代码还更清晰。

23.4 访问结构体成员

结构体造好了,怎么用里面的数据?用点号(.)操作符。

struct Student stu = {1001, "张三", 89.5, 20};

// 访问成员
printf("学号:%d\n", stu.id);
printf("姓名:%s\n", stu.name);
printf("成绩:%.1f\n", stu.score);
printf("年龄:%d\n", stu.age);

// 修改成员
stu.score = 95.0;  // 成绩更新了
strcpy(stu.name, "张三丰");  // 改名了

如果是结构体指针,就要用箭头(->)操作符。这个后面讲指针时会细说,先留个印象:

struct Student *p = &stu;
printf("成绩:%.1f\n", p->score);  // 等价于 (*p).score

23.5 结构体数组

一个学生用结构体,那一个班50个学生呢?总不能写50个变量吧?这时候结构体数组就派上用场了。

// 声明一个能存50个学生的数组
struct Student class[50];

// 初始化结构体数组
struct Student class[3] = {
    {1001, "张三", 89.5, 20},
    {1002, "李四", 92.0, 21},
    {1003, "王五", 78.5, 19}
};

// 访问数组中的结构体成员
for (int i = 0; i < 3; i++) {
    printf("第%d个学生:%s,成绩:%.1f\n", 
           i+1, class[i].name, class[i].score);
}

// 修改某个学生的信息
class[1].score = 95.5;  // 李四成绩改高了

结构体数组的操作和普通数组一模一样,只是每个元素是一个结构体。你想想看,用数组存50个int,和用结构体数组存50个学生,哪个更贴近现实?

实用技巧:处理结构体数组时,我经常用typedef给结构体起个短名字,省得每次写struct Student那么长。
typedef struct Student {
    int id;
    char name[20];
    float score;
    int age;
} Stu;  // 以后直接用 Stu 代替 struct Student

Stu class[50];  // 简洁多了

23.6 知识体系总览

下面这张图把结构体的核心知识点串起来了。我画图时习惯把“定义”放在最上面,因为它是基础中的基础。

结构体知识体系 结构体 (struct) 1. 定义 struct Student { ... }; 2. 声明 struct Student stu; 3. 初始化 = {1001, "张三", ...} 4. 访问成员 stu.name / p->name 5. 结构体数组 Stu class[50];

23.7 避坑指南

做嵌入式这些年,结构体相关的坑我踩过不少。挑几个典型的说说:

  • 结构体对齐问题:编译器会在结构体成员之间插入填充字节,保证对齐。比如char后面跟int,char占1字节,但int要从4字节对齐地址开始,中间会空3个字节。用sizeof()算出来的大小往往比你想的大。我曾在通信协议里用结构体直接打包数据,结果两端对齐方式不同,数据全解析错了。
  • 不要用memcmp比较结构体:因为填充字节的值是不确定的,直接内存比较可能得到错误结果。应该逐个成员比较。
  • 结构体赋值是浅拷贝:如果结构体里有指针成员,直接赋值只是复制了指针值,两个结构体指向同一块内存。修改一个,另一个也跟着变。这个坑我帮别人排查过好几次。
我的建议:刚开始学结构体,先掌握最基本的定义、声明、初始化和成员访问。结构体数组和指针可以多练练。等用熟了,再去研究对齐、位域、柔性数组成员这些进阶内容。

结构体是C语言里组织数据的核心工具。说白了,它就是让你用代码描述现实世界的东西。学生、图书、坐标、传感器数据……任何有多个属性的东西,都可以用结构体来表达。嗯,掌握了结构体,你的C语言水平会上一个大台阶。


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