🛡️

身份验证

答对题目即可进入,验证你是谁

剩余尝试次数:3

结构体与指针

C语言学习教程

结构体与指针 — 动态数据结构的基础

结构体和指针的结合是 C 语言最强大的特性之一。上一课我们用结构体数组管理了 4 个学生,但如果学生人数不确定——可能 30 人,也可能 300 人——数组就显得笨拙了。指针让数据结构可以动态增长、自由链接,这正是链表、树、图等高级数据结构的基石。

温习:指向结构体的指针

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

int main() {
    Point p  = {10, 20};   // 普通结构体变量
    Point *ptr = &p;       // 指向结构体的指针

    // 三种等价写法
    printf("x = %d\n", (*ptr).x);  // 解引用 + 点号:比较啰嗦
    printf("x = %d\n", ptr->x);    // 箭头运算符:推荐!
    // 永远不要写 *ptr.x,因为 . 优先级比 * 高,实际是 *(ptr.x)
    return 0;
}

-> 的本质

ptr->member 就是 (*ptr).member 的”语法糖”(写起来更甜的等价写法)。

ptr->name   ≡   (*ptr).name
ptr->age    ≡   (*ptr).age
ptr->score  ≡   (*ptr).score

为什么 *ptr.x 是错的?

记忆口诀:指针用箭头 ->,变量用点号 .。永远不会错。

传值 vs 传指针 —— 深入对比

上一课我们提过,这里完整展开。

#include <stdio.h>
#include <string.h>

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

// 方式 A:传值 —— 复制整个结构体
void print_student_value(Student s) {
    printf("  [传值] %s, %d岁, %.1f\n", s.name, s.age, s.score);
}

// 方式 B:传指针 —— 只传 8 字节地址
void print_student_ptr(const Student *s) {
    printf("  [传指针] %s, %d岁, %.1f\n", s->name, s->age, s->score);
}

// 方式 C:传值并修改 —— 不会影响原件!
void grow_up_wrong(Student s) {
    s.age += 1;  // 只改了副本
}

// 方式 D:传指针并修改 —— 真正修改原件
void grow_up_right(Student *s) {
    s->age += 1;  // 通过指针直接修改原件
}

int main() {
    Student s1 = {"张三", 18, 92.5};

    // 打印:两种方式都能正确打印
    print_student_value(s1);
    print_student_ptr(&s1);

    // 修改测试
    grow_up_wrong(s1);
    printf("错误方式后:%d岁 (没变)\n", s1.age);   // 仍是 18

    grow_up_right(&s1);
    printf("正确方式后:%d岁 (变了)\n", s1.age);    // 变为 19

    return 0;
}

什么时候传值、什么时候传指针?

情况建议原因
结构体很小(≤ 16 字节)传值也可以开销不大
结构体较大(> 16 字节)传指针 const只传 8 字节地址,快得多
需要修改原结构体必须传指针传值改的是副本
只读取、不修改const 指针安全 + 高效
函数需要返回修改后的结构体传指针直接改避免返回大结构体的开销

const Student *s 的含义s 是一个指针,指向的 Student 不能被修改。即”只读视角”。这样既能享受指针的轻量,又不会被意外修改数据。

动态分配结构体 —— malloc

数组的最大问题是”大小写死了”。malloc 让你在程序运行时按需创建结构体。

创建一个动态结构体

#include <stdio.h>
#include <stdlib.h>   // malloc, free
#include <string.h>

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

int main() {
    // 在堆上分配一个 Student 大小的内存
    Student *s = (Student *)malloc(sizeof(Student));

    // 永远检查 malloc 是否成功!
    if (s == NULL) {
        printf("内存分配失败!\n");
        return 1;
    }

    // 像普通结构体指针一样使用
    strcpy(s->name, "动态学生");
    s->age   = 20;
    s->score = 88.5;

    printf("姓名:%s, 年龄:%d, 分数:%.1f\n",
           s->name, s->age, s->score);

    // 用完了必须归还!
    free(s);      // 释放内存
    s = NULL;     // 好习惯:把悬空指针置为 NULL

    return 0;
}

创建动态结构体数组

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

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

int main() {
    int n;
    printf("请输入学生人数:");
    scanf("%d", &n);

    // 一次性分配 n 个 Student
    Student *class = (Student *)malloc(n * sizeof(Student));
    if (class == NULL) {
        printf("内存分配失败!可能是人数太多了。\n");
        return 1;
    }

    // 填充数据
    for (int i = 0; i < n; i++) {
        printf("第%d个学生 姓名:", i + 1);
        scanf("%s", class[i].name);   // class[i] 等价于 *(class + i)
        printf("        年龄:");
        scanf("%d", &class[i].age);
        printf("        分数:");
        scanf("%f", &class[i].score);
    }

    // 打印并计算平均
    printf("\n===== 全班学生 =====\n");
    float total = 0.0;
    for (int i = 0; i < n; i++) {
        printf("  %d. %s, %d岁, %.1f\n",
               i + 1, class[i].name, class[i].age, class[i].score);
        total += class[i].score;
    }
    printf("全班平均分:%.1f\n", total / n);

    free(class);
    class = NULL;
    return 0;
}

关键点class[i] 是对 class + i 的解引用——底层是指针运算,但对使用者来说和普通数组一模一样。

动态结构体指针数组

有时你需要单独创建每个学生:

Student *roster[3];   // 3 个指针,每个可以指向一个学生

for (int i = 0; i < 3; i++) {
    roster[i] = (Student *)malloc(sizeof(Student));
    sprintf(roster[i]->name, "学生%d", i + 1);
    roster[i]->age   = 18 + i;
    roster[i]->score = 80.0 + i * 5.0;
}

// 使用时
for (int i = 0; i < 3; i++) {
    printf("%s\n", roster[i]->name);
}

// 释放时必须逐个释放
for (int i = 0; i < 3; i++) {
    free(roster[i]);
}

自引用结构体 —— 链表的起点

结构体里能不能包含一个”和自己同类型”的成员?不能——那会无限递归:

struct Node {
    int data;
    struct Node next;  // 编译错误!!Node 里面又有一个 Node,里面又有一个……
};

但可以包含一个指向同类型的指针

typedef struct Node {
    int data;              // 数据域:存放"值"
    struct Node *next;     // 指针域:指向下一个节点
} Node;

这就是链表的”原子单位”——节点(Node)

想象一串火车车厢:
┌──────┬────┐    ┌──────┬────┐    ┌──────┬────┐
│ 数据 │ 钩子│───>│ 数据 │ 钩子│───>│ 数据 │ NULL│
└──────┴────┘    └──────┴────┘    └──────┴────┘
  节点1             节点2             节点3
  head

每个车厢(节点)有两个部分:

动手写一个单链表

创建链表

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct StudentNode {
    char  name[32];
    int   age;
    float score;
    struct StudentNode *next;   // 指向下一个学生节点
} StudentNode;

// 创建一个新学生节点
StudentNode *create_student(const char *name, int age, float score) {
    StudentNode *new_node = (StudentNode *)malloc(sizeof(StudentNode));
    if (new_node == NULL) {
        printf("内存分配失败!\n");
        return NULL;
    }
    strcpy(new_node->name, name);
    new_node->age   = age;
    new_node->score = score;
    new_node->next  = NULL;   // 新节点暂时没有后继
    return new_node;
}

int main() {
    // 创建三个学生节点
    StudentNode *head = create_student("张三", 18, 92.5);
    head->next        = create_student("李四", 19, 78.0);
    head->next->next  = create_student("王五", 18, 85.0);

    // 此时链表结构:
    // head ──> "张三" ──> "李四" ──> "王五" ──> NULL

    return 0;
}

遍历链表

// 打印整个链表
void print_all(StudentNode *head) {
    StudentNode *current = head;   // 从头节点开始
    int index = 1;

    printf("===== 学生名单 =====\n");
    while (current != NULL) {
        printf("%d. %s, %d岁, %.1f\n",
               index, current->name, current->age, current->score);
        current = current->next;   // 移动到下一个节点
        index++;
    }
    printf("===== 共 %d 人 =====\n", index - 1);
}

// 遍历示意图:
// 第1轮:current → ["张三", next→]  打印张三,current = next
// 第2轮:current → ["李四", next→]  打印李四,current = next
// 第3轮:current → ["王五", NULL ]  打印王五,current = NULL
// 第4轮:current == NULL,退出循环

在链表末尾添加节点

// 在链表末尾追加一个新学生
void append_student(StudentNode *head, const char *name, int age, float score) {
    // 1. 先走到最后一个节点
    StudentNode *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    // 2. 创建新节点并挂在最后
    current->next = create_student(name, age, score);
}

// 使用:
// append_student(head, "赵六", 20, 91.0);
// 链表变成:张三 → 李四 → 王五 → 赵六 → NULL

删除节点

// 删除第一个匹配名字的节点,返回新的头指针
StudentNode *delete_student(StudentNode *head, const char *name) {
    if (head == NULL) return NULL;

    // 特殊情况:要删除的是头节点
    if (strcmp(head->name, name) == 0) {
        StudentNode *new_head = head->next;  // 把头指针后移
        free(head);                          // 释放旧头节点
        return new_head;
    }

    // 一般情况:找到目标节点的前一个节点
    StudentNode *current = head;
    while (current->next != NULL) {
        if (strcmp(current->next->name, name) == 0) {
            StudentNode *to_delete = current->next;
            current->next = current->next->next;  // 跳过目标节点
            free(to_delete);
            return head;
        }
        current = current->next;
    }

    printf("没有找到 %s\n", name);
    return head;
}

释放整个链表

void free_all(StudentNode *head) {
    StudentNode *current = head;
    while (current != NULL) {
        StudentNode *next = current->next;  // 先记下下一个
        free(current);                      // 释放当前
        current = next;                     // 移到下一个
    }
}

// 为什么不能直接 free(head) 然后没事了?
// free(head) 只释放头节点本身,后面的节点全变成"孤儿内存"——没人管但还占着空间,这就是内存泄漏。

完整的链表程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct StudentNode {
    char  name[32];
    int   age;
    float score;
    struct StudentNode *next;
} StudentNode;

StudentNode *create_student(const char *name, int age, float score) {
    StudentNode *node = (StudentNode *)malloc(sizeof(StudentNode));
    if (node == NULL) return NULL;
    strcpy(node->name, name);
    node->age   = age;
    node->score = score;
    node->next  = NULL;
    return node;
}

void append_student(StudentNode *head, const char *name, int age, float score) {
    StudentNode *current = head;
    while (current->next != NULL) {
        current = current->next;
    }
    current->next = create_student(name, age, score);
}

void print_all(StudentNode *head) {
    StudentNode *current = head;
    int index = 1;
    printf("\n===== 学生名单 =====\n");
    while (current != NULL) {
        printf("%d. %s | %d岁 | %.1f\n",
               index++, current->name, current->age, current->score);
        current = current->next;
    }
    printf("=====================\n");
}

StudentNode *delete_student(StudentNode *head, const char *name) {
    if (head == NULL) return NULL;
    if (strcmp(head->name, name) == 0) {
        StudentNode *new_head = head->next;
        free(head);
        return new_head;
    }
    StudentNode *current = head;
    while (current->next != NULL) {
        if (strcmp(current->next->name, name) == 0) {
            StudentNode *to_delete = current->next;
            current->next = current->next->next;
            free(to_delete);
            return head;
        }
        current = current->next;
    }
    printf("未找到学生:%s\n", name);
    return head;
}

void free_all(StudentNode *head) {
    StudentNode *current = head;
    while (current != NULL) {
        StudentNode *next = current->next;
        free(current);
        current = next;
    }
}

int main() {
    // 构建初始链表
    StudentNode *head = create_student("张三", 18, 92.5);
    head->next = create_student("李四", 19, 78.0);
    head->next->next = create_student("王五", 18, 85.0);

    print_all(head);

    // 追加一个
    append_student(head, "赵六", 20, 91.0);
    printf("追加赵六后:\n");
    print_all(head);

    // 删除一个
    head = delete_student(head, "李四");
    printf("删除李四后:\n");
    print_all(head);

    // 删除头节点
    head = delete_student(head, "张三");
    printf("删除张三(头节点)后:\n");
    print_all(head);

    // 清理
    free_all(head);
    head = NULL;
    return 0;
}

输出:

===== 学生名单 =====
1. 张三 | 18岁 | 92.5分
2. 李四 | 19岁 | 78.0分
3. 王五 | 18岁 | 85.0分
=====================
追加赵六后:

===== 学生名单 =====
1. 张三 | 18岁 | 92.5分
2. 李四 | 19岁 | 78.0分
3. 王五 | 18岁 | 85.0分
4. 赵六 | 20岁 | 91.0分
=====================
删除李四后:

===== 学生名单 =====
1. 张三 | 18岁 | 92.5分
2. 王五 | 18岁 | 85.0分
3. 赵六 | 20岁 | 91.0分
=====================
删除张三(头节点)后:

===== 学生名单 =====
1. 王五 | 18岁 | 85.0分
2. 赵六 | 20岁 | 91.0分
=====================

链表 vs 数组

特性数组链表
大小固定(静态)或一次分配(动态)动态增长,随时加节点
访问第 i 个元素O(1) 直接跳O(n) 从头数
在中间插入/删除O(n) 需要移动元素O(1) 改一下指针就行
内存连续的一块,可能浪费或不够分散的,用多少占多少
缓存友好度高(连续存储,CPU 预读)低(分散存储,缓存未命中多)

什么时候用链表?

什么时候用数组?

常见错误与调试

1. 忘记 free —— 内存泄漏

void bad_function() {
    StudentNode *node = create_student("小明", 10, 100);
    // 函数结束,node 指针消失,但 malloc 的内存还在堆里!
    // 没有人再能找到这块内存并释放它 → 内存泄漏
}

规则:每个 malloc 都要有一个对应的 free

2. 释放后继续使用(悬空指针)

free(head);
head->next = xxx;  // 未定义行为!head 指向的内存已经不属于你了

原则free 之后,把指针设为 NULL。这样再使用时会立刻崩溃(比你悄悄读/写野内存容易排查)。

3. 遍历时忘了移动指针 —— 死循环

while (current != NULL) {
    printf("%s\n", current->name);
    // 忘了写 current = current->next;
    // → 永远停在第一个节点,死循环!
}

4. 删除节点时丢失链表尾部

// 错误示范:
StudentNode *to_delete = current->next;
free(to_delete);
// 没把 current->next 指向 to_delete->next!
// 链表断了。

要点速查

概念说明
struct 指针声明:Student *ptr = &s;
-> 运算符通过指针访问成员:ptr->name 等价于 (*ptr).name
const 指针const Student *s — 只读,防止意外修改
malloc + 结构体Student *s = malloc(sizeof(Student));
free释放 malloc 分配的内存,每个 malloc 对应一个 free
自引用结构体包含指向同类型的指针:struct Node *next;
链表一系列通过指针串联的节点,可动态增减
头节点 head链表的入口指针,丢失了就找不到整个链表了

结构体和指针结合之后,你就拥有了构建”动态数据结构”的能力。链表只是第一个——后续还会学到栈、队列、二叉树、哈希表,它们的底层都一样:一个结构体,里面有一根/两根指向同类型的指针,把无数个这样的结构体串起来。

下一步我们转向另一个重要的应用场景:把数据存到磁盘上,让它断电也不丢——文件操作。