C语言结构体指针数组讲解笔记

1️⃣ 什么是结构体

结构体(struct)是 C 语言中用来组合不同类型数据的一种数据类型。
例如我们定义一个学生结构体:

typedef struct student {
    char *name;    // 姓名
    int age;       // 年龄
    char gender;   // 性别
    float score;   // 成绩
} STU;

1.1 分析字段

  • name 是指针类型 (char *),指向字符串。

  • agegenderscore 是普通变量。

  • 结构体可以把多个相关信息组合在一起,便于管理。


2️⃣ 结构体变量与结构体指针

2.1 普通结构体变量

STU s1;
s1.age = 18;
s1.score = 90.5;
  • s1 是一个完整的结构体变量,内存中直接存放 agegenderscore 的值。

  • 访问字段用 . 符号。

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;
}
解析
  1. int *capacity

    • 用指针传入,函数内部可以修改原来的学生数量。

  2. STU **s = malloc(*capacity * sizeof(STU *))

    • 分配一个结构体指针数组,数组长度为学生人数。

    • 每个元素 s[i]STU*,暂时还没有分配结构体内容。

  3. s[i] = malloc(sizeof(STU))

    • 为每个学生分配实际的结构体空间。

  4. s[i]->name = malloc(50)

    • 为姓名单独分配内存(因为 namechar*)。

  5. 返回值 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]->ages[i]->genders[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);
}
解析
  1. realloc

    • 二级指针允许我们扩展指针数组的长度。

    • realloc 返回新的数组地址,需要用 news 接收。

  2. s[*capacity - 1] = malloc(sizeof(STU))

    • 为新增学生分配结构体内存。

  3. 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 *));
解析
  1. free(s[index]->name); free(s[index]);

    • 先释放结构体内部分配的内存,再释放结构体本身。

  2. s[i] = s[i + 1];

    • 将后面的指针前移,保证数组连续。

  3. 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 调试技巧

  1. 打印地址

printf("students[%d] = %p, name = %p\n", i, (void*)students[i], (void*)students[i]->name);
  • 可以直观查看指针是否分配成功。

  1. 初始化为 NULL

STU **students = NULL;
  • 防止野指针。

  1. 释放顺序

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;
        }
    }
}
指针解析
  1. s[j] 是第 j 个学生的指针(STU*)。

  2. s[j]->score 访问第 j 个学生的分数。

  3. 交换两个学生:

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);
指针解析
  1. realloc 返回可能的新数组指针,所以用 news 接收。

  2. news[i] 依然是指向结构体的指针。

  3. 给新学生分配内存:

    • 结构体本身:malloc(sizeof(STU))

    • 内部 name 字符串:malloc(50)

  4. 更新数组容量:(*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*));
指针解析
  1. 先释放被删除学生的内存。

  2. 通过移动指针调整数组顺序:

    • 只改指针指向,不移动结构体。

  3. realloc 缩小数组,返回新地址。

  4. 赋值给 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 内存特点

  1. 数组部分students 本身是一个数组,每个元素是 STU*(指向结构体的指针)。

  2. 结构体部分:每个 STU* 指向一个结构体,存储 age, gender, scorename 指针。

  3. 字符串部分name 指向一块独立的字符数组,用来存储学生姓名。

  • 注意

  • 内存释放顺序必须从最内层到最外层:

    1. 内部字符串

    2. 结构体

    3. 指针数组

  • 如果顺序错了,会出现悬空指针或者内存泄漏。

为什么增加学生时要返回新数组指针

当我们给学生数组增加新元素时,程序通常会用 realloc 重新分配内存。

注意realloc 可能会把原来的内存块移动到一个新的地址,这意味着数组的起始地址可能发生变化。

如果函数内部只是修改了二级指针的副本:

  • 函数内部的指针会指向新的地址

  • 但主函数里的原始指针仍指向旧地址

  • 结果:主函数里的数组变成“野指针”,访问会出错


两种解决方案

  1. 使用三级指针

    • 把主函数数组指针的地址传进去

    • 函数内部可以直接修改主函数的指针变量

  2. 返回新地址(本程序采用的方法)

    • 函数返回 realloc 后的新指针

    • 主函数接收返回值并更新自己的指针


为什么选择二级指针 + 返回值

  • 二级指针:表示数组里存储的是学生结构体指针,每个元素指向一个学生

  • 返回值:把可能变化的数组起始地址安全传回主函数


优点

  1. 不用三级指针,语法简单,易读性好

  2. 主函数明确知道数组可能被更新,需要接收返回值

  3. 内存管理逻辑清晰,减少指针混乱和潜在错误

总结:这种方式安全、简洁、清晰,非常适合动态数组的管理

1️⃣ 为什么数组里每个元素是 STU *

  • 每个学生都是一个结构体 STU,里面有姓名、年龄、性别、成绩等信息。

  • 学生的数量是 不固定 的,可能后续还要增加或删除。

  • 如果数组直接存 STU(也就是 STU students[capacity]),每次增加学生时可能需要 整体搬移整个数组,尤其当使用 realloc 调整大小时,会搬动所有学生的内容。

  • 为了节省内存和提高灵活性,我们把数组里的元素存成 指向学生结构体的指针STU *)。

换句话说,数组存的不是学生本身,而是指向学生的地址,这样我们只需要搬指针,不搬整个结构体,效率更高。


2️⃣ 为什么用 STU **

  • 数组本身是指针数组,存储的是 STU *,即每个元素都是指向学生结构体的指针。

  • 数组首地址是一个指针,指向数组的第一个元素(即第一个 STU *)。

  • 所以整个数组的类型就是 STU **

    • 第一级 * 表示这是一个指针

    • 第二级 * 表示指针指向的是 STU *

  • 简单理解:

    • STU → 一个学生

    • STU * → 一个学生的地址

    • STU ** → 学生指针数组的地址


3️⃣ 用二级指针的好处

  • 动态管理数组长度:可以在函数里用 realloc 扩展数组,不需要固定大小。

  • 统一访问方式:无论是读取、排序、删除,都可以通过 students[i]->scorestudents[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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值