1️⃣ 什么是结构体
结构体(struct)是 C 语言中用来组合不同类型数据的一种数据类型。
例如我们定义一个学生结构体:
typedef struct student {
char *name; // 姓名
int age; // 年龄
char gender; // 性别
float score; // 成绩
} STU;
1.1 分析字段
-
name是指针类型 (char *),指向字符串。 -
age、gender、score是普通变量。 -
结构体可以把多个相关信息组合在一起,便于管理。
2️⃣ 结构体变量与结构体指针
2.1 普通结构体变量
STU s1;
s1.age = 18;
s1.score = 90.5;
-
s1是一个完整的结构体变量,内存中直接存放age、gender、score的值。 -
访问字段用
.符号。
2.2 结构体指针
STU *p = &s1;
p->age = 19;
p->score = 95.0;
-
p是指向结构体的指针。 -
使用
->访问结构体字段。 -
优点:可以通过指针动态操作结构体,节省内存并方便函数传递。
3️⃣ 为什么需要结构体指针数组
假设我们有多名学生,需要存储他们的信息。
3.1 静态数组(不灵活)
STU students[10]; // 固定存储10个学生
好处:简单直接。
缺点:数组长度固定,无法动态增加或删除学生。
3.2 结构体指针数组(灵活)
STU *students[10]; // 存储10个学生指针
-
数组元素是结构体指针,而不是结构体本身。
-
可以为每个学生单独分配内存:
for (int i = 0; i < 10; i++) {
students[i] = malloc(sizeof(STU));
students[i]->name = malloc(50); // 为姓名单独分配空间
}
-
好处:每个学生的内存可以独立管理。
-
便于动态增删学生,内存灵活。
4️⃣ 二级指针的概念(核心)
当我们需要动态分配结构体指针数组时,需要二级指针:
STU **students;
int capacity = 5;
students = malloc(capacity * sizeof(STU*));
4.1 分解理解
-
students:指向结构体指针数组的指针,类型为STU **。 -
students[i]:指向第 i 个结构体,类型为STU *。 -
students[i]->name:访问第 i 个学生的姓名。
students (STU **) ├── students[0] -> STU* -> student1 ├── students[1] -> STU* -> student2 └── students[2] -> STU* -> student3
4.2 为什么必须二级指针
-
允许动态分配学生数组 (
STU*的数组) -
允许动态增删学生,重新分配数组大小
-
每个结构体又可以单独动态分配姓名等内存
5️⃣ 二级指针的使用方法
5.1 初始化学生数组(StudentInit)
STU **StudentInit(int *capacity)
{
printf("请输入学生人数: \n");
scanf("%d", capacity);
STU **s = (STU **)malloc(*capacity * sizeof(STU *));
for (int i = 0; i < *capacity; i++)
{
s[i] = (STU *)malloc(sizeof(STU));
s[i]->name = (char *)malloc(50);
}
return s;
}
解析
-
int *capacity-
用指针传入,函数内部可以修改原来的学生数量。
-
-
STU **s = malloc(*capacity * sizeof(STU *))-
分配一个结构体指针数组,数组长度为学生人数。
-
每个元素
s[i]是STU*,暂时还没有分配结构体内容。
-
-
s[i] = malloc(sizeof(STU))-
为每个学生分配实际的结构体空间。
-
-
s[i]->name = malloc(50)-
为姓名单独分配内存(因为
name是char*)。
-
-
返回值
s-
返回的是指向结构体指针数组的二级指针,供
main使用。
-
✅ 总结:二级指针在这里的作用是“数组的数组”——外层是指针数组,内层是每个学生的结构体指针。
5.2 输入学生信息(InputScore)
void InputScore(STU **s, int capacity)
{
for (int i = 0; i < capacity; i++)
{
scanf("%s %d %c %f", s[i]->name, &s[i]->age, &s[i]->gender, &s[i]->score);
}
}
解析
-
s[i]->field的含义:-
s[i]:第 i 个学生的指针(STU*) -
s[i]->name:第 i 个学生的姓名字段 -
s[i]->age、s[i]->gender、s[i]->score同理
-
-
为什么不用
(*s[i]).name?-
->是*+.的简写,写法更方便。
-
✅ 重点:通过二级指针,我们可以在函数里直接修改数组内所有学生的数据。
5.3 增加学生(AddStudent)
STU **AddStudent(STU **s, int *capacity)
{
(*capacity)++;
STU **news = (STU **)realloc(s, (*capacity) * sizeof(STU *));
s[*capacity - 1] = malloc(sizeof(STU));
s[*capacity - 1]->name = malloc(50);
}
解析
-
realloc-
二级指针允许我们扩展指针数组的长度。
-
realloc返回新的数组地址,需要用news接收。
-
-
s[*capacity - 1] = malloc(sizeof(STU))-
为新增学生分配结构体内存。
-
-
s[*capacity - 1]->name = malloc(50)-
为姓名分配空间。
-
✅ 关键:二级指针的灵活性让我们可以“动态扩展数组”,同时每个元素还是结构体指针,可以单独管理内存。
5.4 删除学生(DelStudent)
free(s[index]->name);
free(s[index]);
for (int i = index; i < *capacity - 1; i++)
{
s[i] = s[i + 1];
}
(*capacity)--;
s = realloc(s, (*capacity) * sizeof(STU *));
解析
-
free(s[index]->name); free(s[index]);-
先释放结构体内部分配的内存,再释放结构体本身。
-
-
s[i] = s[i + 1];-
将后面的指针前移,保证数组连续。
-
-
realloc-
缩小数组大小,避免浪费内存。
-
✅ 理解难点:二级指针数组的每个元素是独立的结构体指针,删除操作涉及两步:释放内存 + 调整指针数组。
5.5 总结二级指针使用方法
| 场景 | 为什么用二级指针 | 如何操作 |
|---|---|---|
| 初始化学生数组 | 动态分配指针数组 | STU **s = malloc(capacity * sizeof(STU*)) |
| 存储每个学生 | 每个学生结构体需要独立内存 | s[i] = malloc(sizeof(STU)) |
| 存储姓名 | 字符串长度不固定 | s[i]->name = malloc(50) |
| 增加/删除学生 | 数组长度动态变化 | realloc(s, new_capacity * sizeof(STU*)) |
| 函数传递 | 函数内可以修改原数组 | 函数参数为 STU **s |
6️⃣ 二级指针访问细节
6.1 访问单个学生
假设我们有:
STU **students; // 学生指针数组
int capacity; // 学生人数
-
students[i]→ 第 i 个学生的指针(STU*) -
students[i]->name→ 第 i 个学生的姓名 -
students[i]->age→ 第 i 个学生的年龄 -
*students[i]→ 第 i 个学生整个结构体
students : 指针数组
students[i] : 第i个结构体指针
students[i]->field : 第i个结构体字段
*students[i] : 第i个结构体整体
6.2 遍历数组
for (int i = 0; i < capacity; i++) {
printf("%s %d %c %.2f\n",
students[i]->name,
students[i]->age,
students[i]->gender,
students[i]->score);
}
-
students[i]指向结构体,使用->访问成员。 -
不要忘记:指针本身可以为空或未初始化,访问前必须保证分配了内存。
6.3 函数参数
函数中用 STU **s 接收:
void AverageStudent(STU **s, int capacity)
{
float sum = 0;
for (int i = 0; i < capacity; i++) {
sum += s[i]->score;
}
}
-
传入数组指针,函数内部可以修改每个学生的数据。
-
如果函数想修改数组本身(如
AddStudent返回新数组),必须返回STU**或使用STU ***。
6.4 增删操作
增加学生
STU **news = realloc(s, (*capacity) * sizeof(STU*));
-
realloc返回新地址,原来的指针可能无效。 -
技巧:总是用新指针接收返回值,避免悬空指针。
删除学生
free(s[index]->name);
free(s[index]);
-
先释放结构体内部指针,再释放结构体本身。
-
避免直接释放
s[index]后再访问s[index]->name,会导致段错误。
6.5 调试技巧
-
打印地址:
printf("students[%d] = %p, name = %p\n", i, (void*)students[i], (void*)students[i]->name);
-
可以直观查看指针是否分配成功。
-
初始化为 NULL:
STU **students = NULL;
-
防止野指针。
-
释放顺序:
for (int i = 0; i < capacity; i++) {
free(students[i]->name);
free(students[i]);
}
free(students);
-
顺序不能错:先释放内部,再释放结构体,再释放数组。
6.6 常见错误
| 错误类型 | 原因 | 修正方法 |
|---|---|---|
| 段错误 | 指针未分配就访问 | malloc 分配内存,或检查指针是否 NULL |
| 内存泄漏 | 分配了内存但没有 free | 每个 malloc 都要对应 free |
| 悬空指针 | realloc 后原指针被释放 | 用新指针接收 realloc 返回值 |
| 错误访问 | 使用 s[i] 时越界 | 确保索引 < capacity |
7️⃣ 二级指针在排序中的使用
7.1 升序排序 SortScoreAsc
核心代码:
for (int i = 0; i < capacity; i++) {
for (int j = 0; j < capacity - 1; j++) {
if (s[j]->score > s[j + 1]->score) {
STU *tmp = s[j];
s[j] = s[j + 1];
s[j + 1] = tmp;
}
}
}
指针解析
-
s[j]是第 j 个学生的指针(STU*)。 -
s[j]->score访问第 j 个学生的分数。 -
交换两个学生:
STU *tmp = s[j]; // 保存 j 的指针
s[j] = s[j + 1]; // j 指向 j+1 的学生
s[j + 1] = tmp; // j+1 指向原来的 j 学生
✅ 关键点:
-
我们只是交换指针,而不是交换结构体本身。
-
节省内存开销,效率高。
-
二级指针的好处:只要交换指针,数组顺序就变了,不需要移动整个结构体。
7.2 降序排序 SortScoreDesc
类似升序,只是比较方向不同:
if (s[j]->score < s[j + 1]->score) { ... }
-
同样交换指针而不是结构体。
-
指针数组在排序过程中非常灵活:直接改变指向顺序即可。
8️⃣ 二级指针在查找中的使用
8.1 查找最高分 ShowMaxName
float max = s[0]->score;
int index = 0;
for (int i = 1; i < capacity; i++) {
if (s[i]->score > max) {
max = s[i]->score;
index = i;
}
}
printf("最高分是 %0.2f, %s\n", max, s[index]->name);
指针解析
-
s[i]:第 i 个学生指针。 -
s[i]->score:获取分数。 -
s[index]->name:找到最高分学生的名字。
✅ 注意:
-
不需要修改数组,只是访问指针指向的内容。
-
二级指针让我们可以在函数内部直接遍历和访问数组元素。
8.2 查找最低分 ShowMinName
同理,修改比较条件即可:
if (s[i]->score < min) { ... }
9️⃣ 二级指针在扩容中的使用
9.1 增加学生 AddStudent
(*capacity)++;
STU **news = (STU **)realloc(s, (*capacity) * sizeof(STU*));
news[*capacity - 1] = malloc(sizeof(STU));
news[*capacity - 1]->name = malloc(50);
指针解析
-
realloc返回可能的新数组指针,所以用news接收。 -
news[i]依然是指向结构体的指针。 -
给新学生分配内存:
-
结构体本身:
malloc(sizeof(STU)) -
内部
name字符串:malloc(50)
-
-
更新数组容量:
(*capacity)++。
✅ 关键点:
-
二级指针允许函数内部修改数组本身(
realloc可能移动数组)。 -
如果用一级指针,无法在函数中安全修改原数组的地址。
9.2 删除学生 DelStudent
free(s[index]->name);
free(s[index]);
for (int i = index; i < *capacity - 1; i++) {
s[i] = s[i + 1]; // 移动指针,不移动结构体
}
(*capacity)--;
STU **news = realloc(s, (*capacity) * sizeof(STU*));
指针解析
-
先释放被删除学生的内存。
-
通过移动指针调整数组顺序:
-
只改指针指向,不移动结构体。
-
-
realloc缩小数组,返回新地址。 -
赋值给
news,避免悬空指针。
✅ 关键点:
-
二级指针让我们在删除或增加元素时可以安全操作数组本身。
-
指针数组操作灵活且高效,不需要拷贝整个结构体。
内存可以抽象为三层:
students (STU**) --------------------> +--------+ +--------+ +--------+
| s[0] | | s[1] | | s[2] |
+--------+ +--------+ +--------+
| | |
v v v
+-----------+ +-----------+ +-----------+
| STU struct| | STU struct| | STU struct|
| name ptr | | name ptr | | name ptr |
| age | | age | | age |
| gender | | gender | | gender |
| score | | score | | score |
+-----------+ +-----------+ +-----------+
|
v
+------------+
| char name[]|
+------------+
10.1 访问方式示例
-
students[i]→ 指向第 i 个STU的指针 -
students[i]->score→ 第 i 个学生的分数 -
students[i]->name→ 第 i 个学生的姓名指针 -
*(students[i]->name)→ 姓名的首字符
10.2 内存特点
-
数组部分:
students本身是一个数组,每个元素是STU*(指向结构体的指针)。 -
结构体部分:每个
STU*指向一个结构体,存储age,gender,score和name指针。 -
字符串部分:
name指向一块独立的字符数组,用来存储学生姓名。
-
✅ 注意:
-
内存释放顺序必须从最内层到最外层:
-
内部字符串
-
结构体
-
指针数组
-
-
如果顺序错了,会出现悬空指针或者内存泄漏。
为什么增加学生时要返回新数组指针
当我们给学生数组增加新元素时,程序通常会用 realloc 重新分配内存。
注意:
realloc可能会把原来的内存块移动到一个新的地址,这意味着数组的起始地址可能发生变化。
如果函数内部只是修改了二级指针的副本:
-
函数内部的指针会指向新的地址
-
但主函数里的原始指针仍指向旧地址
-
结果:主函数里的数组变成“野指针”,访问会出错
两种解决方案
-
使用三级指针
-
把主函数数组指针的地址传进去
-
函数内部可以直接修改主函数的指针变量
-
-
返回新地址(本程序采用的方法)
-
函数返回
realloc后的新指针 -
主函数接收返回值并更新自己的指针
-
为什么选择二级指针 + 返回值
-
二级指针:表示数组里存储的是学生结构体指针,每个元素指向一个学生
-
返回值:把可能变化的数组起始地址安全传回主函数
优点
-
不用三级指针,语法简单,易读性好
-
主函数明确知道数组可能被更新,需要接收返回值
-
内存管理逻辑清晰,减少指针混乱和潜在错误
总结:这种方式安全、简洁、清晰,非常适合动态数组的管理
1️⃣ 为什么数组里每个元素是 STU *
-
每个学生都是一个结构体
STU,里面有姓名、年龄、性别、成绩等信息。 -
学生的数量是 不固定 的,可能后续还要增加或删除。
-
如果数组直接存
STU(也就是STU students[capacity]),每次增加学生时可能需要 整体搬移整个数组,尤其当使用realloc调整大小时,会搬动所有学生的内容。 -
为了节省内存和提高灵活性,我们把数组里的元素存成 指向学生结构体的指针(
STU *)。
换句话说,数组存的不是学生本身,而是指向学生的地址,这样我们只需要搬指针,不搬整个结构体,效率更高。
2️⃣ 为什么用 STU **
-
数组本身是指针数组,存储的是
STU *,即每个元素都是指向学生结构体的指针。 -
数组首地址是一个指针,指向数组的第一个元素(即第一个
STU *)。 -
所以整个数组的类型就是
STU **:-
第一级
*表示这是一个指针 -
第二级
*表示指针指向的是STU *
-
-
简单理解:
-
STU→ 一个学生 -
STU *→ 一个学生的地址 -
STU **→ 学生指针数组的地址
-
3️⃣ 用二级指针的好处
-
动态管理数组长度:可以在函数里用
realloc扩展数组,不需要固定大小。 -
统一访问方式:无论是读取、排序、删除,都可以通过
students[i]->score或students[i]->name来访问结构体成员。 -
节省内存:结构体内容不搬动,只搬指针。
总结一句话:
因为我们有一个“数组”存学生指针,这个数组本身也是动态分配的,所以用
STU **就够了,每个元素存STU *,整个数组用指针管理,而不需要三级指针。
针对物联网(嵌入式)一定要保证!!!
-
可读性(Readability)
-
内存安全(Memory Safety)
-
可维护性(Maintainability)
-
健壮性(Robustness)
-
效率(Efficiency)
-
一致性(Consistency)
-
可扩展性(Extensibility)
-
低耦合(Low Coupling)
-
高内聚(High Cohesion)
-
模块化(Modularity)
-
可复用性(Reusability)
-
清晰的接口设计(Clear Interface Design)
-
错误处理规范(Error Handling)
-
代码风格一致性(Code Style Consistency)
-
注释和文档完善(Comments & Documentation)
861

被折叠的 条评论
为什么被折叠?



