结构体

结构体

从4个阶段理解

第一阶段:为什么要用结构体?

想象一下,你要编写一个程序来管理学生信息。每个学生有:姓名、年龄、分数。

如果不使用结构体,你可能需要这样定义变量:

1
2
3
4
5
6
7
char name1[20] = "Zhang San";
int age1 = 18;
float score1 = 85.5;

char name2[20] = "Li Si";
int age2 = 19;
float score2 = 92.0;

痛点:

  1. 数据零散name1age1 在逻辑上属于同一个人,但在代码中它们是毫无关系的独立变量。
  2. 管理困难:如果有100个学生,你得定义300个变量,甚至建立3个独立的数组。一旦要通过函数传递一个学生的信息,你需要传3个参数。

解决方案: 我们需要一种机制,能够把不同类型的数据(char, int, float)打包到一个“盒子”里。这个“自定义的盒子类型”,就是结构体(Structure)


第二阶段:结构体的定义与创建

结构体的学习分为两步:先画图纸(定义类型),再盖房子(定义变量)。

1. 定义结构体类型(画图纸)

这只是告诉编译器这个“盒子”长什么样,不占用内存

1
2
3
4
5
6
// 定义一个名为 Student 的结构体类型
struct Student {
char name[20]; // 成员1:姓名
int age; // 成员2:年龄
float score; // 成员3:分数
}; // <--- 注意:这里必须有分号!

2. 定义结构体变量(盖房子)

有了图纸,我们就可以在内存中分配空间了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <string.h>

struct Student {
char name[20];
int age;
float score;
};

int main() {
// 方式一:定义并初始化
struct Student s1 = {"Tom", 18, 95.5};

// 方式二:C99标准引入的“指定初始化”(推荐,更清晰)
struct Student s2 = {
.age = 19,
.name = "Jerry",
.score = 88.0
};

return 0;
}

第三阶段:如何操作结构体(访问与赋值)

1. 访问成员:点操作符 .

我们可以通过 . 来读取或修改结构体内部的数据。

1
2
3
4
5
// 修改 s1 的年龄
s1.age = 20;

// 打印信息
printf("Name: %s, Age: %d\n", s1.name, s1.age);

2. 这里的“深坑”:字符串赋值

初学者最容易犯的错误:

1
2
3
4
5
// 错误写法!!!
// s1.name = "Jack"; // 数组名是地址常量,不能直接赋值

// 正确写法:使用 strcpy
strcpy(s1.name, "Jack");

第四阶段:结构体的高级用法(数组与指针)

1. 结构体数组(学生花名册)

如果要管理全班同学,我们不需要定义 s1, s2, s3...,而是使用数组。

1
2
3
4
5
6
7
8
struct Student class_A[3] = {
{"Tom", 18, 90},
{"Jerry", 19, 80},
{"Bob", 18, 85}
};

// 访问第二个学生的年龄
printf("%d", class_A[1].age);

2. 结构体指针与箭头操作符 ->(核心重点)

在C语言中,结构体往往占用较大内存。如果直接在函数间传递结构体变量,会进行值拷贝(复制整个结构体),效率很低。

通常我们传递结构体的地址(指针)

1
2
3
4
5
6
7
8
9
10
11
struct Student s1 = {"Tom", 18, 95.5};
struct Student *p = &s1; // p 指向 s1 的地址

// 访问方式对比:

// 方式1:解引用(太麻烦,不常用)
// (*p).age = 20;

// 方式2:箭头操作符(标准写法,常用!)
p->age = 20;
// 读作:p 指向的那个结构体里的 age

总结口诀:

  • 结构体变量用点 .s1.age
  • 结构体指针用箭头 ->p->age

第五阶段:实战演练(综合代码)

让我们把上面的知识串起来,写一个函数来修改学生的分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

// 1. 定义类型
struct Student {
char name[20];
int age;
float score;
};

// 2. 使用 typedef 起别名 (让代码更简洁)
// 以后用 Stu 代替 struct Student
typedef struct Student Stu;

// 3. 函数:修改分数
// 注意:必须传指针,否则修改的是副本!
void add_score(Stu *s, float points) {
s->score += points; // 使用 -> 访问
}

int main() {
Stu s1 = {"Alice", 20, 85.0};

printf("修改前分数: %.1f\n", s1.score);

// 传入 s1 的地址
add_score(&s1, 5.0);

printf("修改后分数: %.1f\n", s1.score);

return 0;
}

进阶:你需要知道的“内存对齐” (Memory Alignment)

这通常是面试题或底层开发关注的点。

问题: 下面的结构体占用多少字节?

1
2
3
4
struct Data {
char c; // 1 字节
int i; // 4 字节
};

你可能以为是 1 + 4 = 5 字节。 但实际上用 sizeof(struct Data) 输出,结果通常是 8 字节

原因: CPU 为了读取速度更快,不喜欢访问“不对齐”的地址。编译器会自动在 char c 后面填充 3 个空闲字节(Padding),让 int i 从 4 的倍数地址开始存放。

扩展

image-20260112234828665

这张图片里的代码它同时用到了C语言结构体的两个“骚操作”:定义时声明变量嵌套匿名结构体

1. 定义时声明变量

——这是“画完图纸立刻盖房子”。

结构体定义(struct student { ... })通常只是“画图纸”,不占内存。但如果你在定义的结尾、分号的前面写上名字(这里的 s),就意味着:

在定义 struct student 类型的同时,直接创建一个名为 s 的变量。

它等价于下面这两行代码的合并:

1
2
3
4
5
6
7
8
9
// 1. 先定义类型
struct student {
int no;
char name[20];
// ... 其他成员
};

// 2. 再定义变量
struct student s;

这种写法的特点:

  • 省事:少写一行代码。
  • 局限:通常这个 s 会变成一个全局变量(如果在 main 函数外面定义),或者你只想用这一次。

2. 为什么内层 struct 后面没有名字,直接是大括号?

——这是“匿名结构体”作为成员。

注意看内层的这一段:

1
2
3
4
5
struct {   // <--- 这里没名字!
int year;
int month;
int day;
} birth; // <--- 这里是成员变量名
  • 没有类型名(匿名)struct 后面空的,说明程序员觉得:“这个日期的结构体太简单了,我不需要给它起个专门的名字(比如 struct Date),反正我就在这个 student 里面用一次,外面也不用。”
  • 只有变量名:大括号后面的 birth 是这个内部结构体的变量名(或者叫成员名)。

这意味着:student 里有一个成员叫 birth,它本身又是一个包含了年月日的小结构体。


3. 如何像剥洋葱一样访问数据?

弄懂了上面两点,这道题的答案就呼之欲出了。我们要给“生日的年份”赋值,必须一级一级地找下去,就像剥洋葱或者俄罗斯套娃:

  1. 第一层(最外层变量):找到学生变量 s
  2. 第二层(中间成员):在 s 里找到负责生日的成员 birth
    • 写法s.birth
  3. 第三层(最内层数据):在 birth 里找到年份 year
    • 写法s.birth.year

所以,正确的赋值路径是: s (学生) . birth (生日) . year (年份)


结构体
http://example.com/2026/01/12/结构体/
作者
王柏森
发布于
2026年1月12日
许可协议