资讯详情

资讯详情

C语言链表入门:从指针基础到增删改查的完整实战教程

链表这东西只要是学C语言的人早晚都得正面撞上它一次。作为C语言里最经典、也最考验指针功底的数据结构链表几乎是所有高校《数据结构》课程的必讲内容也是技术面试里出现频率极高的基础题。很多人一听到链表就觉得头疼——指针绕来绕去增删改查一堆操作debug的时候更是折磨人。但你只要真正动手写过一遍把每个操作的指针变化在纸上画清楚就会发现链表其实比数组更符合人类对“一串数据”的直觉想象。这篇博文我尽量用大白话把链表的增删改查讲透。从节点的定义开始到创建、遍历、插入、删除、修改再到常见崩溃问题的排查技巧全部附上完整可运行的C语言代码。不管你是刚学完指针的初学者还是准备面试想快速捡起链表的开发者跟着这篇文章一步步实操下来你完全可以搞定链表的核心操作。1. 先从本质理解链表为什么它比数组更灵活1.1 数组的局限与链表的诞生背景先想一个问题数组在内存里是什么样子的它是一段连续的存储空间就像一排门牌号连在一起的公寓1号、2号、3号挨得紧紧的。这种连续存储的好处是访问效率极高通过下标直接算出内存地址时间复杂度O(1)。但坏处也很明显——一旦确定长度就很难扩展如果数组越界程序就直接崩溃如果要在中间插入一个元素得把后面的所有元素整体往后搬时间复杂度O(n)。链表就是为了解决这些问题而生的。它不再要求数据连续存放每个“节点”可以散落在内存的任何角落节点之间通过指针搭桥牵线像一条铁链一样串起来。正因为每个节点都带着指向下一个节点的指针所以它天然支持动态扩展想加就加想删就删只要内存够用链表的长度几乎没有上限。我用一个生活化的类比帮你看清楚数组就像电影院里一排固定编号的座位每一个座位都是提前焊死在地板上的。而链表就像一串用绳结拴在一起的灯笼每个灯笼知道自己后面跟着哪盏灯笼灯笼之间距离多远都无所谓想在哪两盏之间塞一个新灯笼只需解开绳子再重新系上。1.2 节点的自我指代结构体指针的奇妙用法链表的基本单位是“节点”Node。每个节点至少包含两个部分数据域和指针域。数据域用来存放真正要存储的数据指针域存放指向下一个节点的地址。实现这个结构靠的就是C语言中结构体里能够包含指向自身类型的指针这个特性typedef struct Node { int data; // 数据域这里先用int类型做演示 struct Node *next; // 指针域指向下一个节点 } Node;注意看这个定义struct Node *next指向的是一个尚未定义完整、但已经声明了标签struct Node的类型。这就像写信时地址栏写上“递给下一个人”虽然你还没见到那个人但你知道地址的格式是对的。C语言允许在结构体内部使用指向自身类型的指针因为指针只占固定大小比如64位系统上占8字节编译器不需要知道结构体的完整大小就能算出指针成员的位置。搞清楚这个基础定义后面的增删改查全部都是在跟next这个指针打交道。可以说链表的每一次操作本质都是一次“指针的重新指向”和“内存的分配释放”。2. 动手前的准备链表节点的定义与辅助函数2.1 创建节点的通用工具函数写链表操作之前我强烈建议先封装一个createNode函数。这样后面不管你在头插、尾插还是中间插入每次需要新节点的时候只要调用它就行了代码会清爽很多。#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node *next; } Node; // 分配一个节点data为初始值next默认为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; }这里有几个细节值得说一句。malloc(sizeof(Node))是根据节点结构体实际大小分配内存不要直接写死一个数字不同系统、不同对齐规则下sizeof(Node)可能不一样。还有一个容易犯的错误是分配后忘记检查返回值如果内存耗尽malloc会返回NULL这时候直接操作newNode-data就是解引用空指针程序必崩。所以检查返回值不是一句废话而是C程序员的保命习惯。2.2 头指针整个链表的“门牌号”链表的入口是一个头指针head它保存着第一个节点的地址。如果链表为空head就是NULL。所有对链表的遍历、插入、删除操作都要从head出发顺着next一路走下去。所以我们在测试程序里这样初始化一个空链表int main() { Node *head NULL; // 空链表头指针还没有指向任何节点 // 后续所有增删改查操作都围绕head展开 return 0; }有的教材会把链表设计成带有“头节点”也就是第一个节点不存数据只作为哨兵的形式。这种做法在某些场景下确实能简化边界判断但对初学者来说带哨兵和非哨兵两种写法混着看容易绕晕。所以这篇文章我统一采用不带头节点的写法即head直接指向第一个真正存储数据的节点。这种写法代码更直观缺点是某些操作的边界条件需要自己小心处理正好借机锻炼边界意识。2.3 为什么把打印封装成独立函数后面调试链表时我几乎每个操作完都会打印一遍链表看看结果对不对。所以封装一个printList函数是刚需void printList(Node *head) { Node *cur head; while (cur ! NULL) { printf(%d - , cur-data); cur cur-next; } printf(NULL\n); }这个函数的核心逻辑就一句永远不要修改head本身而是用一个临时指针cur去遍历。如果你直接写while (head ! NULL)然后head head-next打印完链表之后head就变成NULL了整个链表就再也找不到了。这种低级错误在真实开发中屡见不鲜而且特别难排查——表面上程序没崩但数据“神秘消失”了。记住这个原则所有需要遍历链表的函数都用局部指针变量去迭代不要动传入的头指针。3. 链表“查”与“读”遍历、定位、按值查找3.1 遍历打印从第一个节点走到 NULL遍历是链表最基础的操作前面的printList已经展示了框架。它的时间复杂度是 O(n)因为链表不支持随机访问必须从head开始一个节点接一个节点地走。这个和数组通过arr[i]一步到位有着本质区别。遍历的核心逻辑可以拆成三个关卡初始化一个指针cur指向头节点判断cur是否为NULL是就停止每次循环末尾更新cur cur-next让它向后移动。很多人写这个循环的问题在于搞不清cur cur-next和cur-next cur的区别。前者是“把下一个节点的地址交给cur”让cur向后走后者是“把cur的地址回填给当前节点的next”相当于把链表链接方向颠倒一旦执行后面的节点就丢了。遍历用的是前者千万别写反。3.2 按值查找与按位置查找实际开发中“查”往往有两种目标按值查找链表里有没有data等于某个数的节点以及按位置查找第几个节点是什么。按值查找的代码如下Node* findByValue(Node *head, int target) { Node *cur head; while (cur ! NULL) { if (cur-data target) { return cur; // 找到了返回该节点的地址 } cur cur-next; } return NULL; // 遍历完都没有找到返回NULL }按位置查找以0为起始下标的写法是Node* findByIndex(Node *head, int index) { Node *cur head; int pos 0; while (cur ! NULL pos index) { cur cur-next; pos; } return cur; // 如果index超过链表长度cur会变成NULL返回NULL即可 }注意while循环的退出条件里cur ! NULL和pos index是并列的不能只写pos index。万一index比链表总长度还大cur早早就走到NULL了再去取cur-data就是在访问空指针程序直接段错误。3.3 查的复杂度分析与实际应用场景链表查找的时间复杂度是 O(n)最坏情况下要遍历整条链表才能确定目标是否存在。有人会问既然数组随机访问是 O(1)为什么链表还能有一席之地原因很简单——链表的优势在增删不在查。插入和删除操作在已知前置节点的情况下链表的复杂度是 O(1)而数组是 O(n)因为数组需要搬移元素。实际项目中的常见搭配是“哈希表 链表”哈希表用来做O(1)查找链表用来维护插入顺序、处理缓存淘汰策略等。比如经典的LRU缓存淘汰算法内层就是个双向链表配合哈希表索引。你学链表的时候如果能往这个思路上靠一靠会对以后理解更复杂的数据结构有很大帮助。4. 链表“增”头插、尾插、任意位置插入4.1 头插法三步完成“抢首座”头插法在最前面插入一个新节点。它的核心步骤只有三步创建新节点newNode把newNode-next指向原来的第一个节点即head把头指针head更新为newNode。代码实现void insertAtHead(Node **head, int data) { Node *newNode createNode(data); newNode-next *head; // 新节点指向原来的第一个节点 *head newNode; // 头指针指向新节点 }这里有一个很重要的细节为什么参数是Node **head而不是Node *head因为我们要修改head本身的值让它指向新的节点。C语言函数参数是值传递如果你传入Node *head在函数内部修改head改的是形参的副本函数外面head还是老样子。因此需要传入指针的指针也就是Node **head这样在函数内部对*head的赋值才能反映到外部。如果你实在觉得二级指针绕还有一种妥协方案让插入函数返回新的头指针调用时重新赋值。比如Node* insertAtHeadV2(Node *head, int data) { Node *newNode createNode(data); newNode-next head; return newNode; } // main里这样调用 // head insertAtHeadV2(head, 42);两种方法都行但你要理解二级指针的用法因为后面很多链表操作都要靠它来解决“修改头指针”的问题。4.2 尾插法走到最后再接上尾插法需要先找到链表的最后一个节点把它的next指向新节点。void insertAtTail(Node **head, int data) { Node *newNode createNode(data); if (*head NULL) { // 空链表新节点就是第一个节点 *head newNode; return; } Node *cur *head; while (cur-next ! NULL) { cur cur-next; // 走到最后一个节点 } cur-next newNode; }注意循环条件写的是cur-next ! NULL而不是cur ! NULL。如果用cur ! NULL循环会直到cur为NULL才停此时cur已经不是最后一个节点了而是最后一个节点后面的空位你根本拿不到“最后一个节点”的地址。只有判断cur-next ! NULL循环停止时cur才是真正有数据、且没有后继节点的那个“末班车”。另外不要忘记单独处理*head NULL的情况。空链表没有“最后一个节点”此时头插和尾插的效果是一样的直接把*head指向新节点即可。这个边界条件写漏的话空链表状态下做尾插就会访问空指针的next程序直接崩溃。4.3 任意位置插入先定位再接线任意位置插入相对麻烦一点需要先找到插入位置的前一个节点然后进行指针重新接线。假设position从0开始计数我想在position位置插入一个新节点。void insertAtIndex(Node **head, int data, int position) { if (position 0) { printf(位置非法\n); return; } Node *newNode createNode(data); if (position 0) { // 插在头部等价于头插 newNode-next *head; *head newNode; return; } // 找到当前位于 position-1 位置的节点 Node *cur *head; int i 0; while (cur ! NULL i position - 1) { cur cur-next; i; } if (cur NULL) { printf(插入位置超出链表长度\n); free(newNode); // 别忘了释放刚才分配的内存 return; } // 接线新节点先指向cur的后继然后cur的next再指向新节点 newNode-next cur-next; cur-next newNode; }这段代码的核心在最后两句。为什么顺序必须是newNode-next cur-next; // 先让新节点指向后一个节点 cur-next newNode; // 再让前一个节点指向新节点不能颠倒因为一旦先执行cur-next newNode原来cur后面的整条子链表就断了此时再想访问cur-next得到的其实是newNode原来的后续节点全部“失联”相当于你把一根铁链从中间解开然后还没挂上新的一环就把原来的后半段扔了。所以顺序必须固定先动新节点的next再动前置节点的next。你可以想象成两车追尾后防抱死必须先插保险销再拔旧销子。5. 链表“删”删除指定节点5.1 删除的核心逻辑拆解删除操作的核心是四个字——“跳过节点”。如果我删掉的是节点B而它的前一个节点是A那么只要让 A-next 直接指向 B-next再把 B 所占用的内存释放掉链表就“无缝愈合”了。这就像两个人中间隔了一把椅子你把这把椅子拖走前后两人之间的距离就自然闭合了。要分两种情况理解情况一删除头节点。此时要把head更新为原来第二个节点。if (*head target) { *head (*head)-next; free(target); return; }情况二删除非头节点。需要先找到它的前驱节点prev然后执行prev-next target-next最后free(target)。写一个完整函数void deleteNode(Node **head, int targetValue) { if (*head NULL) { printf(链表为空无法删除\n); return; } // 如果要删除的是第一个节点 if ((*head)-data targetValue) { Node *tmp *head; *head (*head)-next; free(tmp); return; } // 否则遍历查找待删除节点 Node *prev *head; Node *cur (*head)-next; while (cur ! NULL cur-data ! targetValue) { prev cur; cur cur-next; } if (cur NULL) { printf(没找到要删除的值: %d\n, targetValue); return; } prev-next cur-next; // 跳过cur free(cur); // 释放cur }删除操作中我之前见过不少新手会用两个循环来找前驱先while (cur-data ! target)然后再用一个prev去记前一个节点。这样写虽然也能工作但相当绕而且容易写错。实际上维护prev和cur两个指针同步向后走一趟循环就够了——prev始终是cur前面的节点。5.2 释放内存与野指针的防范很多初学者写完free(cur)就以为自己完成了“删除”大功告成。其实还有一件事没做让 cur 这个局部指针不要继续被使用或者至少不要再解引用。free释放的是内存块并不是指针变量本身。释放之后cur里存的地址依然是一个地址但从这一刻起它指向的内存已经被归还给堆管理器了这块区域可能被后续代码重新分配给别的数据。这时如果你再写cur-data就是访问一块已经不属于你的内存——这叫野指针访问程序不会立刻报错但会读出随机值甚至触发段错误。有个简单的规矩释放完节点之后如果这个指针后面还用得上立即给它赋NULL。对于局部指针变量只要保证后面不去用它就行。另外删除整个链表也叫销毁链表时更是要想清楚。正确做法是void destroyList(Node **head) { Node *cur *head; while (cur ! NULL) { Node *tmp cur; // 先保存当前节点 cur cur-next; // 指针先移动到下一个节点 free(tmp); // 再释放当前节点 } *head NULL; // 置空头指针防止野指针 }这里的关键是先保存下一个节点地址再释放当前节点。如果先free(cur)再cur cur-nextcur-next这一句读的是一个已经释放的内存区域属于访问已经free的内存行为未定义几乎必然出问题。这个顺序要记牢先取next再free当前。5.3 删除时常见的边界情况总结场景处理方式常见错误链表为空直接提示并返回不判断空链表直接访问 head-data删除的节点是头节点头指针往后移释放原头节点忘记更新头指针导致链表入口丢失删除的节点在中间前驱 next 指向后继顺序颠倒先改 next 导致断链删除的节点是尾节点前驱 next 设为 NULL删除后尾节点 next 仍指向旧地址遍历越界找不到目标值打印提示返回然后去 free 一个空指针或者什么都不做造成“假删除”这五种场景在面试里简直就是送分题和送命题的区别。能答出“如果删除的是头节点怎么办”的人很多但能主动把五种情况都考虑到并写出代码的人才算真的理解了链表。6. 链表“改”节点值的修改与综合演练6.1 节点值的直接修改链表的修改相对简单只要找到目标节点直接更新它的data就好void updateNode(Node *head, int oldValue, int newValue) { Node *cur head; while (cur ! NULL) { if (cur-data oldValue) { cur-data newValue; printf(已将 %d 修改为 %d\n, oldValue, newValue); return; } cur cur-next; } printf(没有找到值 %d修改失败\n, oldValue); }这个函数不用二级指针因为我们不修改头指针也不改变链表的链接结构只修改节点内部的数据域。C语言按值传递传给函数的是头指针的副本但副本指向的目标还是原来那个节点所以通过副本去修改cur-data生效的是真正的节点数据。这类操作只需要一级指针就够了。6.2 综合演练用链表实现一个学生成绩管理系统光看单个函数的讲解可能还是有点飘我把这些操作串起来做一个极简的“学生成绩管理系统”。用链表存储若干学生的成绩支持增删改查四种操作。这个例子对应了很多人搜索的“学生管理系统”非常典型。#include stdio.h #include stdlib.h #include string.h typedef struct Student { char name[20]; int score; struct Student *next; } Student; Student* createStudent(const char *name, int score) { Student *s (Student*)malloc(sizeof(Student)); if (s NULL) { printf(内存分配失败\n); exit(1); } strcpy(s-name, name); s-score score; s-next NULL; return s; } // 按名字查找并打印 void searchStudent(Student *head, const char *name) { Student *cur head; while (cur ! NULL) { if (strcmp(cur-name, name) 0) { printf(找到学生%s成绩%d\n, cur-name, cur-score); return; } cur cur-next; } printf(查无此人%s\n, name); } // 按名字修改成绩 void updateScore(Student *head, const char *name, int newScore) { Student *cur head; while (cur ! NULL) { if (strcmp(cur-name, name) 0) { cur-score newScore; printf(已将 %s 的成绩更新为 %d\n, name, newScore); return; } cur cur-next; } printf(未找到学生%s\n, name); } // 按名字删除 void deleteStudent(Student **head, const char *name) { if (*head NULL) return; if (strcmp((*head)-name, name) 0) { Student *tmp *head; *head (*head)-next; free(tmp); printf(已删除学生%s\n, name); return; } Student *prev *head; Student *cur (*head)-next; while (cur ! NULL strcmp(cur-name, name) ! 0) { prev cur; cur cur-next; } if (cur NULL) { printf(未找到学生%s\n, name); return; } prev-next cur-next; free(cur); printf(已删除学生%s\n, name); } void printStudents(Student *head) { Student *cur head; while (cur ! NULL) { printf(姓名%s成绩%d\n, cur-name, cur-score); cur cur-next; } } int main() { Student *head NULL; // 模拟数据 head createStudent(张三, 85); head-next createStudent(李四, 92); head-next-next createStudent(王五, 78); printStudents(head); searchStudent(head, 李四); updateScore(head, 王五, 88); printStudents(head); deleteStudent(head, 张三); printStudents(head); return 0; }这个例子能跑通就说明你对链表增删改查的基本功已经过关了。你可以把它当成一个支点后续往两个方向扩展一是把数据域从单个成绩变成更多字段学号、年龄、班级等二是把查找条件从“按名字”扩展成“按学号”“按分数区间”等。每次改动都是一次对链表操作的强化训练。6.3 链表的销毁与内存管理习惯学习阶段很多人不在意内存释放反正程序一结束操作系统会回收全部内存。但在长期运行的服务端程序里内存泄漏是会积累的——每次操作泄漏一点点跑个几天几夜之后内存就被吃光了程序最终被系统杀掉。所以养成“谁分配谁释放不用就释放”的习惯非常重要。在测试链表程序时每次跑完增删改查最好都调用一次destroyList(head)确认链表被完整销毁再用工具比如Linux下的Valgrind检查有没有内存泄漏gcc -g -o test test.c valgrind --leak-checkfull ./test反正我自己的习惯是任何涉及动态内存的练习代码都会主动跑一遍Valgrind看到“All heap blocks were freed”才安心。这个习惯帮我提前拦截了无数潜在的内存错误。7. 常见问题与排查技巧实录7.1 一运行就崩溃段错误Segmentation Fault段错误是链表初学者遇到最多的问题频率高得惊人。它的本质是程序访问了没有权限访问的内存地址通常是解引用空指针或野指针导致的。常见的触发场景有链表为空时直接访问head-data此时head是NULL遍历时循环条件写错导致指针走到NULL之后还在解引用节点被free之后还被再次访问。排查这类问题我的经验是先加打印确定崩在哪一行。只要把每次访问指针前的关键值打印出来四五个printf下去基本就能锁定是哪个指针出了问题。用调试器gdb也可以但新手往往还不熟悉断点操作先用打印的方式上手更快。举个例子假设你的程序一执行就崩但你不知道崩在哪里可以在每个函数入口和关键循环前后加上printf(debug: 进入删除函数head%p\n, head);这样你能看到崩之前最后一个输出是什么自然就锁定到对应的代码段了。7.2 链表打印出来出现环或者死循环如果链表里某个节点的 next 指针错误地指回了前面的节点那么遍历就会陷入无限循环——程序会一直打印数字直到永不停止。这种情况经常出现在插入或删除操作中“接线”顺序写反的时候。判断链表是否有环的经典办法是“快慢指针”但调试阶段有一个更简单粗暴的方法在打印循环里加一个计数器如果打印节点数超过链表应有长度的某个阈值说明几乎可以肯定有环。比如链表总共3个节点却打印了100个还没停那必然是有环了。遇到这种情况立刻检查你的插入和删除代码重点看有没有“先用cur-next覆盖了后面链表的入口”。记住一条铁律断开任何链接之前必须先把后续节点的地址另存一份或者确保已经通过另一个指针记下了它。7.3 内存泄漏程序跑完内存没回来内存泄漏不像崩溃那么显眼程序能正常运行但每次运行都会丢失一小块内存。在链表场景里最常见的泄漏点就是删除节点时忘了free或者插入失败时没有释放已经malloc出来的新节点。比如我在 4.3 节代码里有一句if (cur NULL) { printf(插入位置超出链表长度\n); free(newNode); // 别忘了释放刚才分配的内存 return; }这里就属于一种“提前失败”的路径。新节点已经malloc好了但你要插入的位置不存在如果直接 return新节点就永远失去释放的机会。开发的时候这种“分支里有没有释放”最容易遗漏所以要养成分支审视的习惯凡是提前 return 的地方都要检查自己分配过的内存是否被妥善处理了。7.4 常见问题速查表现象可能原因排查思路一运行就段错误解引用空指针检查 head 是否为 NULL检查循环结束后 cur 是否为 NULL打印结果缺失前半段头指针被修改查看遍历函数里是否直接操作了 head而不是用临时变量插入后链表丢失改变了头指针却没有通过二级指针或返回值更新看看是不是直接传了一级指针头指针删除后链表断裂前驱节点的 next 没有正确指向后继检查删除时的接线顺序释放后程序崩溃悬空指针还指向已释放内存检查 free 后是否仍然继续使用该指针程序运行很久后内存暴涨内存泄漏用 Valgrind 检查每次 malloc 是否都有对应 free这张表你可以打印出来贴在电脑旁边每遇到一个问题就先对照一遍。我接触过的绝大多数链表bug最后都逃不出这几类。8. 链表还能怎么玩常用扩展与面试关注点8.1 链表的排序插入排序与归并排序既然有链表就有人会想在链表上做排序。比较简单的写法是插入排序——每次从未排序部分取出一个节点在已排序部分找到合适的位置插进去这个过程正好用上我们前面学的“任意位置插入”逻辑。但插入排序的时间复杂度是O(n²)数据量大了不太行。追求效率的话最适合链表的是归并排序。因为链表天然支持在中间切分通过快慢指针找中点然后递归合并两个有序链表时间复杂度是稳定O(n log n)而且不需要额外的数组空间。面试中如果碰到“链表排序”考官通常期待你写出归并排序而不是冒泡排序或者插入排序。8.2 链表的反转一道很经典的入门题链表反转也是高频考点。它的核心思想是准备三个指针prev、cur和nextTemp每次把cur-next指向prev三个指针集体向后移动一个位置。最终prev会变成新链表的头节点。一个很常见的坑是反转时先把cur-next改成prev然后却发现找不到原来的下一个节点了——这就是为什么反转代码里总要有一个nextTemp提前保存下一站。如果你把这条规则想清楚了就理解了链表里“先保存后修改”的通用法则其他后续操作也会一通百通。8.3 双向链表和循环链表一个顺手的扩展单链表的不足是只能从头往后走如果想找前一个节点只能再开一个循环从头遍历。双向链表就是在每个节点里多存一个prev指针代价是每个节点多占一份内存换来的是查找前驱的O(1)效率。循环链表则是把最后一个节点的next指回头节点形成一个环。这两种结构在很多项目的底层都有应用包括操作系统的任务队列、缓存的LRU链表等。我个人的建议是先把单链表练熟再花两天时间把双向链表和循环链表的增删查改写一遍。等你把三种链表都写过一轮C语言指针那块基本就没有能难倒你的内容了。8.4 为什么面试这么爱问链表不得不承认面试官喜欢链表是有理由的。链表题目代码量不大但对指针、内存管理、边界条件的考察力度极大。一段十几行的插入函数可以同时检验你懂不懂二级指针、会不会处理空链表、是否考虑尾插的特殊性这比很多大而全的项目更能体现基本功。所以不要觉得链表是“大学课上考完就忘了”的知识它就是打开后续数据结构大门的第一把钥匙。这里面最核心的能力其实不是背诵操作步骤而是能在脑子里面画出指针动态变化的图。如果你现在写链表操作还需要在纸上画半天才能理清那很正常练多了之后这些指针的移动会慢慢变成直觉。我在带新人时经常说一句话链表题画图十分钟写代码三分钟调试能花三小时。不是因为代码难而是因为很多人的思维还停留在“内存是连续空间”的定式里看到指针跳来跳去就发懵。等你真的把每一根“next线”都画顺了链表的增删改查对你来说就是顺水推舟的事。换一个角度说链表也是通向“内存管理”这个C语言核心主题的最佳实践场。malloc的每一块内存都有对应的free在等它每一个指针都有明确的指向归宿。这种“所有权”的意识如果你能从链表开始建立起来那以后写任何C项目都会受益很多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →