C语言链表从入门到精通:增删改查、内存管理与常见Bug全解析
2026/9/18 17:26:45 网站建设 项目流程

写这篇东西之前,先说说我自己的经历。当年学 C 语言的时候,我一度觉得链表是个“永远绕不过去但又永远学不明白”的东西。数组用得好好的,偏偏要搞个结构体,还要在里面塞一个指向自己的指针,然后在堆上 malloc 一块内存,手动串成一串珠子。我那时候想不明白:这不是脱裤子放屁吗?直到后来我做了一个课设,需要在循环里不断往一个动态增长的列表里插入数据,数组那种“要么提前给够空间、要么每次扩容都搬一次家”的写法彻底把我折磨疯了,我才意识到链表的价值。

这篇就好好把链表的增删改查讲透。目标读者是:正在学 C 语言、刚接触结构体和指针、准备数据结构课程设计、或者准备计算机二级、刷翁恺老师习题的同学。我会用“说人话”的方式,把底层逻辑、代码实现、边界条件、还有我踩过的坑全部摊开讲。学完你不仅能看懂,还能自己手写出来。

1. 为什么数组不够用,链表才更贴近真实世界的存储

1.1 数组的三个死穴:长度固定、插入拖家带口、内存碎片浪费

先别急着写代码,我们得先搞清楚链表到底解决了什么问题。数组不是你想象的“一劳永逸”的存储方案,它有三个很要命的毛病。

第一个毛病:长度固定。在 C 语言里声明数组,要么写死在代码里(int arr[100]),要么用变长数组(老编译器还不一定支持),要么自己 malloc 一片连续内存。问题在于:你根本不知道程序运行的时候到底会有多少个数据。学生管理系统,今天录入 30 个学生,明天来了 300 个,你的数组开多大?开大了浪费,开小了溢出。

第二个毛病:插入和删除的代价太高。数组是连续内存,这意味着你要在一个有序数组中插入一个新元素,得把这个位置及后面的所有元素全部往后挪一位。删除同理,后面所有元素往前挪。如果你在数组头部插入元素,整个数组的元素都要搬家,时间复杂度 O(n)。数据量小还好,数据量大了,这个搬迁成本非常可观。

第三个毛病:内存碎片问题。如果你频繁地 malloc 一块新数组、把旧数组内容拷过去、再 free 旧数组,堆上就会产生大量内存碎片。对于长期运行的程序,这会导致内存利用率下降,甚至分配失败。

1.2 链表的本质:每个节点都是“数据 + 线索”,像一场寻宝游戏

链表的思路完全不同。它不要求数据在内存中连续存放,每个数据项都是一个独立的节点,节点和节点之间靠指针“串”起来。每个节点里,数据域存真正的数据,指针域存“下一个节点在哪里”。

这就像是玩寻宝游戏:你手里有一张纸条,上面写着“金币在第 7 个房间的保险柜里,下一个纸条在 12 号房间”。你跑到 12 号房间,找到下一张纸条,上面又写着“下一个线索在 8 号房间”。你顺着线索一间一间跑,直到最后一张纸条写着“线索到此结束”。

链表增加和删除节点,只需要调整相邻节点的指针指向,不需要移动任何数据。这就是它跟数组最本质的区别。你插入一个新节点,只需要“通知”前一个节点:你的下家换人了,以前是 B,现在是新来的 X,X 的下家才是 B。整条链上其他节点一概不用动,这是数组完全做不到的。

动图很多教程里有,但光看图容易“眼睛会了,手不会”。真正的理解发生在你亲手操作指针变量的时候。

2. 先认识结构体:一个节点就是一块最小积木

2.1 定义节点的两种风格:typedef 大法 vs 裸写 struct

链表的节点是一个结构体,里面至少包含两部分:数据域和指针域。最经典的定义长这样:

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

这里有个小细节很多人第一遍会忽略:struct Node里面有一个struct Node *next,这是一个指向自身类型的指针。听起来有点绕——我举个不恰当但很形象的例子:你手里有一张照片,照片上是一个人举着一张照片,照片里那个人又举着一张照片,无限套娃。指针域就干这个事,指向下一个同类型的节点。

实际写代码的时候,我更推荐用 typedef 起个别名,不然每次定义一个指针变量都要写struct Node *p,很烦:

typedef struct Node { int data; struct Node *next; } Node;

这样后面直接用Node *p就可以了。在 C 语言里,typedef struct Node { ... } Node;中,struct Node是结构体标签,第二个Node是类型别名。注意:在结构体声明内部,因为类型别名还没定义完,所以next字段必须写成struct Node *next,不能直接写Node *next,否则编译会报错。这个坑我也踩过,当时编译报unknown type name 'Node',排查了半天。

2.2 堆内存分配三件套:malloc、检查 NULL、free 闭环

链表的好处是可以随时动态分配节点,但你得记住:每个节点都是你从堆上“借”来的。C 语言没有垃圾回收,借了要还。

分配节点用malloc。但很多人刚学的时候只写这样:

Node *p = (Node *)malloc(sizeof(Node));

然后呢?然后直接p->data = 10; p->next = NULL;。如果是刷题,这种写法没问题;但是在真实工程里,如果堆内存已经耗尽,malloc 会返回 NULL,你在 NULL 指针上写数据,程序直接崩溃。

正确写法是:

Node *createNode(int data) { Node *newNode = (Node *)malloc(sizeof(Node)); if (newNode == NULL) { printf("内存分配失败\n"); exit(1); } newNode->data = data; newNode->next = NULL; return newNode; }

这个createNode函数你会在后面的所有增删改查操作中反复用到。把“分配内存 + 初始化”封装成一个函数,是链表代码整洁的重要习惯。

2.3 只建一个节点的小实验:先跑通,再谈链

我们来做个最小实验,只创建一个节点,把它打印出来,再释放掉:

#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; int main() { Node *head = (Node *)malloc(sizeof(Node)); if (head == NULL) return 1; head->data = 42; head->next = NULL; printf("节点数据: %d\n", head->data); printf("节点地址: %p\n", (void *)head); free(head); return 0; }

这段代码你跑一遍,体会一下“节点是内存里的一个真实对象”这种感觉。你打印出来的那个地址,就是你向操作系统申请的一块堆内存。链表的所有操作,本质上都是在这个地址空间上改来改去。这个基础感觉有了,后面就好学很多。

3. 增加节点:头插、尾插、中间插,三种姿势一次讲透

插入是整个链表操作里最核心、也最容易出错的部分。很多人学链表学不明白,就是栽在插入上。其实插入分三种情况,每种情况的指针操作逻辑都不太一样,我们逐个拆开讲。

3.1 核心难点:每插入一个节点,就是告诉上一家“你的下家换人了”

先理解这句话,链表插入的本质就是一句话:让前驱节点的 next 指向新节点,让新节点的 next 指向原来的后继节点。就这么简单。关键是谁是谁的前驱、谁是谁的后继,以及操作的顺序。

为什么顺序重要?我举个例子。假如你要在 A 和 B 之间插入一个新节点 X,链表原来长这样:

A -> B -> NULL

目标长这样:

A -> X -> B -> NULL

直觉做法是:A->next = X; X->next = B;先改 A 的 next 指向 X,再设置 X 的 next 指向 B。这个顺序有问题吗?有。如果你执行A->next = X;,那么 A 到 B 之间的链就断了,B 节点从此“流浪”了,你还找得到 B 吗?找不到了,因为整个链里没有任何一个指针还指向 B。这时候你再执行X->next = B;,但 B 已经丢了。

正确顺序是:先让新节点 X 指向它的后继 B(X->next = B;),再把 A 的 next 改成指向 X(A->next = X;。这样 B 一直没丢。简单记:新人先认清自己的下一家,老人才放心把新家地址给你

3.2 头插法:新节点上位,头指针换人

头插法,就是每次把新节点插到链表头部。这个操作最简单,但有个容易遗漏的细节。

Node *insertAtHead(Node *head, int data) { Node *newNode = createNode(data); newNode->next = head; // 新节点指向原来的头节点 head = newNode; // 头指针指向新节点 return head; // 返回新的头指针 }

注意这两行代码的顺序,不能反。如果先执行head = newNode;,那原来的头节点就被“抛弃”了,你再也找不到它,整条链断了,后面的节点全部丢失。

还有一个细节:C 语言函数参数是值传递,你在函数内部修改 head 参数,并不会影响调用者的 head。所以这个函数必须把新的 head 返回出去,调用时这样写:

head = insertAtHead(head, 100);

如果你忘了接收返回值,新节点插不进去,链表原地不动。这是新手最常见的问题之一。

3.3 尾插法:要踩到最后一个节点,真正的 O(n) 体现在这里

尾插法,插到链表末尾。关键操作:从头开始遍历,一直走到最后一个节点(p->next == NULL的那个节点),然后让它的 next 指向新节点。

Node *insertAtTail(Node *head, int data) { Node *newNode = createNode(data); if (head == NULL) { return newNode; // 空链表,新节点就是头节点 } Node *p = head; while (p->next != NULL) { p = p->next; } p->next = newNode; return head; }

这里有个很重要的边界条件:空链表要单独处理。如果 head 本身是 NULL,你直接对 head 遍历,p 就是 NULL,p->next直接崩溃。所以要么特判,要么用“带头节点”(后面会讲)来规避。

尾插的时间复杂度是 O(n),因为每次都要从头遍历到尾部。如果你频繁尾插,更高效的做法是维护一个 tail 指针,每次尾插直接 O(1),但删除节点时需要处理 tail 指针,增加了复杂度。新手阶段建议先老老实实遍历,理解了再优化。

3.4 中间插:必须先绑新节点的后继,再改前驱的下一个

中间插入是指插入到某个特定位置,比如“在第 k 个节点后面插入”。这个操作相对复杂,因为你要先找到前驱节点。

先写一个“在指定节点后面插入”的代码:

void insertAfter(Node *prev, int data) { if (prev == NULL) return; Node *newNode = createNode(data); newNode->next = prev->next; // 先让新节点指向 prev 原来的后继 prev->next = newNode; // 再让 prev 指向新节点 }

如果是“在第 k 个位置插入”,需要先遍历到第 k-1 个节点:

Node *insertAtPos(Node *head, int data, int pos) { if (pos <= 0) return insertAtHead(head, data); Node *p = head; for (int i = 1; i < pos - 1 && p != NULL; i++) { p = p->next; } if (p == NULL) { printf("位置无效\n"); return head; } Node *newNode = createNode(data); newNode->next = p->next; p->next = newNode; return head; }

理解这段代码的关键是:循环结束后,p 指向的是第 pos-1 个节点,也就是新节点的前驱。如果 p 为 NULL,说明链表的长度不够,位置越界。

3.5 建议你加一个“哑节点”:带头节点真的会省很多事

学了一段时间链表之后,你会发现:每次都要特判空链表、特判头插、特判头删,烦不烦?烦。

工程上的主流做法是引入头节点(哑节点)。这个节点本身不存数据,它的 next 指向真正的第一个数据节点。这样一来,空链表不再是没有节点,而是只有一个哑节点(head->next == NULL)。

好处是巨大的:

  • 头插、尾插、中间插,都不需要特判“空链表”这个情况。
  • 删除操作也不需要特判“删除的是头节点”。
  • 链表的“起点”永远是 head,不会因为你插删而改变。

代价是浪费一个节点的内存。但这点代价完全值得。

Node head; // 栈上分配哑节点,不需要 malloc head.next = NULL; // 所有的插入都从 head.next 开始处理

当你后面总结回顾的时候会发现,带头节点的写法代码更短、逻辑更统一,非常推荐在课设里使用。

3.6 完整可运行示例:头插 + 尾插 + 遍历打印

下面给一个可以直接跑起来的完整代码,包含头插法和尾插法:

#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; Node *createNode(int data) { Node *newNode = (Node *)malloc(sizeof(Node)); if (newNode == NULL) exit(1); newNode->data = data; newNode->next = NULL; return newNode; } Node *insertAtHead(Node *head, int data) { Node *newNode = createNode(data); newNode->next = head; return newNode; } Node *insertAtTail(Node *head, int data) { Node *newNode = createNode(data); if (head == NULL) return newNode; Node *p = head; while (p->next != NULL) p = p->next; p->next = newNode; return head; } void printList(Node *head) { Node *p = head; while (p != NULL) { printf("%d -> ", p->data); p = p->next; } printf("NULL\n"); } int main() { Node *head = NULL; head = insertAtHead(head, 10); head = insertAtHead(head, 20); head = insertAtTail(head, 30); printList(head); // 输出: 20 -> 10 -> 30 -> NULL return 0; }

跑一遍,打印出来的顺序正好符合预期:头插法导致 20 在 10 前面,尾插法把 30 放在最后。

4. 删除节点:释放内存的顺序就是重点

4.1 删除的本质:断链 + 释放,顺序不能反

删除节点同样有两件事:断链释放内存。这两步的顺序非常关键。

先说正确逻辑,以删除“值为 data 的第一个节点”为例。你要删除的目标节点是cur,它的前驱是prev。正确的操作:

  1. 让前驱的 next 直接指向目标节点的后继:prev->next = cur->next;
  2. 释放目标节点:free(cur);

很多人会把顺序搞反,先free(cur)再操作prev->next,这就出大问题了。free(cur)意思是“这块内存我不用了,还给系统”,虽然 cur 这个指针变量还在,但它指向的内存已经不属于你了,再去读它的内容就是“野指针访问”,程序行为未定义,可能崩溃,可能输出垃圾值,可能正常——但绝对不推荐依赖这种随机行为。

正确姿势是:先断链,再 free。断链的本质是“把它从链上摘下来”,摘下来之后它就是一个孤零零的节点,这时候再 free 是安全的。

4.2 三种位置的删除:边界条件最容易翻车

删除操作的边界条件比插入更多,挨个说:

删除头节点。头节点没有前驱,所以直接让头指针指向第二个节点,然后释放原头节点。这里要特别注意赋值和释放的顺序:

Node *deleteHead(Node *head) { if (head == NULL) return NULL; Node *temp = head; // 先保存要释放的节点地址 head = head->next; // 头指针后移 free(temp); // 再释放原来头节点 return head; }

删除中间节点。需要同时保存前驱节点和目标节点。遍历的时候,用一个prev指针记录p的前一个,这是链表遍历的常用套路:

Node *deleteByValue(Node *head, int data) { if (head == NULL) return head; // 先检查头节点是不是目标 if (head->data == data) { Node *temp = head; head = head->next; free(temp); return head; } // 遍历找目标节点 Node *prev = head; Node *cur = head->next; while (cur != NULL && cur->data != data) { prev = cur; cur = cur->next; } if (cur != NULL) { prev->next = cur->next; // 断链 free(cur); // 释放 } return head; }

删除最后一个节点。实际上不需要单独写一套逻辑,因为在上面这个代码里,删除最后一个节点时,cur->next == NULLprev->next = cur->next就是把 prev 的 next 设置为 NULL,链自然就断了,处理好。这就是链表的好处。

4.3 经典野指针问题:free 之后怎么办

这是很多初学者最迷惑的地方。有人问:free(cur)之后要不要加一句cur = NULL;

我的看法是:如果没有其他地方还保留着 cur 的副本,就不需要cur这个变量马上就会消失(函数结束),你把它置 NULL 意义不大。但是,如果cur是被保存到了全局变量、或者被保存在其他结构体里,那就要注意避免通过旧指针再次访问这块内存。

真正容易出问题的是“释放节点之后,但野指针还留在链表里”。比如你在删除时搞错了顺序,free(cur)之后才让prev->next = cur->next,这一步读的是已经被释放的内存。为了安全,我的习惯是:先断链,再释放,立刻把局部 cur 置 NULL。如果你用的 IDE 有内存检测工具,跑一遍就会发现这类错误。

4.4 整体释放链表:删除所有节点的正确姿势

除了删除单个节点,有时候你要把整条链表都释放掉。很多人会写这样的代码:

void destroyList(Node *head) { Node *p = head; while (p != NULL) { free(p); // 错误示例! p = p->next; // p->next 已经是非法访问了 } }

这个问题够经典了吧。free(p)之后,p 指向的内存已经被释放,再去读p->next,属于典型的 use-after-free。正确做法是:先保存下一个节点的地址,再释放当前节点

void destroyList(Node *head) { Node *p = head; while (p != NULL) { Node *next = p->next; // 先把后继存下来 free(p); p = next; } }

这个 bug 我在实际写代码时亲眼见过不止一次。你只要记住“释放之前先留好下一位同志的地址”这一句话,就不会犯这个错误。

5. 查找与修改:遍历是万能钥匙

5.1 遍历的模板:临时指针往前走,头指针永远不动

说增删改查,前面我们把“增”和“删”讲了,接下来是“改”和“查”。别看名字是两个操作,它们的共同核心是遍历:从头到尾走一遍链表,在每个节点上做判断、做修改。

遍历的模板代码长这样:

Node *p = head; while (p != NULL) { // 处理当前节点 p p = p->next; }

这个模板有个非常关键的纪律:永远不要动 head 指针。很多新手遍历的时候会图省事直接用head = head->next来走链表,结果走完之后头指针没了,链表也没了。正确做法是:用一个临时指针 p 来遍历,head 始终保持不变。这也是下一步所有查找和修改操作的基础。

5.2 按值查找与修改:学生成绩链表实战

我们做一个贴近实际的例子:链表里存学生的成绩信息,要求查找到学号等于某个值的节点,修改它的成绩。

结构体稍微扩展一下:

typedef struct Student { int id; // 学号 int score; // 成绩 struct Student *next; } Student;

查找函数:

Student *findById(Student *head, int targetId) { Student *p = head; while (p != NULL) { if (p->id == targetId) { return p; // 找到,直接返回节点指针 } p = p->next; } return NULL; // 没找到 }

有了查找函数,修改就变得很简单:

void updateScore(Student *head, int targetId, int newScore) { Student *target = findById(head, targetId); if (target != NULL) { target->score = newScore; } else { printf("未找到学号为 %d 的学生\n", targetId); } }

看起来非常简单,对吧?这里我想提醒一个很多人没意识到的点:findById返回的是一个指针,这个指针指向链表里的真实节点,所以通过它修改成员,就直接改到了链表里的数据。这正是“通过指针修改结构体”的威力。如果这里返回的不是指针而是结构体副本,那就白忙活了。

5.3 按位置修改:改第几个节点,别把遍历写成死循环

有时候我们要按“位置”来找节点,比如“修改第 n 个节点的数据”。这里很容易犯一个边界错误,我写出来你看看:

Node *getByIndex(Node *head, int index) { if (index < 0) return NULL; Node *p = head; int count = 0; while (p != NULL && count < index) { p = p->next; count++; } return p; // 可能就是 NULL }

循环条件count < index,所以如果 index 等于 0,一次循环都不走,直接返回头节点;如果 index 超出链表长度,p 变成 NULL 退出循环,返回 NULL。这是标准的“偏移 index 步”的写法,不容易死循环。

但如果你把条件写反了,比如while (p->next != NULL),然后 p 从头走到尾,到了最后一个节点 p->next 是 NULL,循环退出,你会漏掉最后一个节点。这种 off-by-one 错误在链表中非常常见,做题的时候特别容易栽。

5.4 查找的变种:倒数第 n 个节点、快慢指针初体验

查找不只是找值、找位置,链表中还有一类经典问题,比如“找到倒数第 n 个节点”。这个用笨办法是两次遍历,第一次算出链表长度,第二次再走到 len-n 的位置。但更优雅的做法是快慢指针

Node *findFromEnd(Node *head, int n) { Node *fast = head; Node *slow = head; // 快指针先走 n 步 for (int i = 0; i < n; i++) { if (fast == NULL) return NULL; // n 超出链表长度 fast = fast->next; } // 快慢指针一起走,快指针到末尾时,慢指针就是倒数第 n 个 while (fast != NULL) { fast = fast->next; slow = slow->next; } return slow; }

快指针先走 n 步,然后快慢指针同步向前,当快指针走到 NULL 时,慢指针正好在倒数第 n 个节点。这个思路第一次接触会觉得有点绕,但理解了你会觉得非常巧妙。这类题做得多了,你会慢慢建立起“操作链表就是操作指针”的感觉。

同样的思路还可以用来找链表的中间节点:快指针每次走两步,慢指针每次走一步,快指针到末尾时,慢指针正好在中点。这就是“快慢指针”家族,链表面试题里的常客。

6. 手写链表最常见的五个 bug,以及我建议的调试方法

6.1 头插/尾插时忘记更新头指针

这个是我见过的第一大 bug,尤其刚学链表的人几乎都犯过。头插法里面函数内部改了 head 参数,但没把新 head 返回出去,调用者手里的 head 还是旧值,插入就像石子扔进了水里,啥也没发生。解决办法有两个:

一是返回新 head(函数式写法),就像我前面写的代码那样。二是用二级指针Node **head,函数内部直接*head = newNode。两个方案都可以,新手建议先从返回值开始,思路更直白。

6.2 删除节点时先 free 再断链

这个 bug 的后果非常惨烈。free 之后,那块内存可能被系统回收,数据被覆盖,你再读它的 next 字段,拿到的可能是垃圾值。而且这种问题很难稳定复现,有时跑得好好的,有时随机崩溃。调试时 gdb 会给你报Segmentation fault,但定位到哪一行往往已经晚了。

错误代码长这样:

// 错误示例 free(cur); prev->next = cur->next; // cur 已经被释放,cur->next 是野访问

正确的一定是:先把 cur 的 next 保存起来,或者先把 prev->next 改了,最后 free(cur)。这个顺序一定不要搞反。

6.3 遍历判空条件写错

遍历链表的时候,你经常会看到两种判空条件:while (p != NULL)while (p->next != NULL)。它们有本质区别:

  • while (p != NULL):会访问到最后一个节点。
  • while (p->next != NULL):会在最后一个节点停下,此时 p 指向的是“最后一个非空节点”。

很多场景(比如尾插)需要停在最后一个节点,所以用p->next != NULL;很多场景(比如打印、查找)需要访问每一个节点,所以用p != NULL。把这两个搞混,就会出现“少处理一个节点”或者“空指针访问”的问题。我建议你刚开始写代码时,每次写 while 都问自己一句:我这个循环结束后 p 应该在哪个位置。

6.4 忘记检查 malloc 的返回值

刷题的时候可以不检查,因为刷题内存充足;但课设、项目、甚至以后的嵌入式开发里,malloc 失败是真实存在的。如果 malloc 返回 NULL,你直接写p->data就是往 NULL 地址写数据,程序直接崩溃。养成每次 malloc 后检查 NULL 的习惯,不费事,但很重要。

提示:更好的实践是把“创建节点”封装成函数,在函数内部统一处理 malloc 失败的情况。这样调用方代码只需要一行createNode(data),不用每次 malloc 都写三行检查。这是代码质量提升非常实用的一步。

6.5 调试利器:画图 + 打印 + gdb

链表代码出 bug 时,最没用的事情是盯着代码干看。我调试链表的时候只用三板斧:

第一板斧,画图。在纸上画方框表示节点,箭头表示指针。把链表当前状态画出来,把每一步操作对应到图上,画出新来的箭头、擦掉不要的箭头。80% 的指针错误一画图就清楚了。

第二板斧,写一个打印函数。每操作一步就调用printList(head)看结果是否符合预期,这是最直观的手段。很多“逻辑问题”比如顺序不对、指针接错,一打印就暴露了。

第三板斧,gdb 启动。如果你怀疑哪里崩溃,编译时加-g参数,用 gdb 跑到崩溃的位置,用print查看相关指针的值。比如print p->next看看是不是野指针,print head看看地址是否合理。学会在 gdb 里查看结构体字段(print p->data),排查速度能提升好几倍。

调试链表还有一个原则:一次只验证一个操作。不要写完增删改查四个函数再一起测试,那样出了问题你不知道是哪一个函数惹的祸。我一般顺序是:先写创建和遍历,验证能存能读;再写头插,打印验证;再写尾插,打印验证;再写删除,分别测试删头、删中间、删尾、链表为空这四种情况;最后再写修改和查找。每一步都验证通过才继续下一步,这样写出来的代码几乎不会有难调的 bug。

最后,分享一点我个人的使用体验

链表这个东西,入门确实有点门槛,但一旦跨过去,你会发现它是你理解很多数据结构的钥匙。栈、队列、二叉树、图的邻接表,底层都是链式结构;你以后学数据库里的索引、内核里的链表、操作系统里的任务队列,本质上都在跟“节点 + 指针”打交道。语言只是语法差异,核心模型是通用的,你学会了 C 语言版本的增删改查,去看 C++、Python 的链表实现,会发现结构一样,只是写法不同。

最后分享一个我在实际课设里发现的细节:如果你用链表存数据,千万注意“释放链表”和“程序退出”之间的顺序。在课设这种小项目里,程序一结束操作系统会自动回收内存,很多人就偷懒不写 destroyList,这能跑,但会养成坏习惯。到了真实项目(嵌入式、服务端),长期运行的进程如果每次创建链表用完不释放,内存泄漏是累积的,跑几天就崩。所以每次写完增删改查,记得补一个 destroyList 把整条链清掉,再 valgrind 跑一遍,看到 zero bytes lost,你会觉得很安心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询