当前位置: 首页 > news >正文

C++链表学习心得

前言:为什么我会“怕”上链表?—— 初学者的初始困惑

在接触链表之前,我对C++的认知还停留在基础语法和简单数组的使用上。数组虽然好用,但固定大小、插入删除繁琐的短板,让我意识到必须学习更灵活的数据结构——链表。可当我第一次看到“指针指向节点”“动态分配内存”这些概念时,瞬间就慌了:指针本身就够抽象了,还要用它连接一个个节点,想想就觉得难。直到我一点点摸索,亲手敲出每一行代码,踩过无数次报错的坑,才慢慢从“怕链表”变成“能熟练运用链表”。这篇心得,就是我作为初学者学习链表的完整成长记录,也希望能帮到和我一样曾经迷茫的小伙伴。

第一章:初识链表——从“不知所云”到“略懂皮毛”

1.1 链表是什么?—— 打破“抽象恐惧”的第一步

刚开始学链表时,我最大的疑问就是:“有数组存数据就够了,为什么还要学链表?” 直到老师说“数组大小固定,想插入一个数据就要移动一串元素”,我才意识到链表的必要性,但还是看不懂它的结构。后来我试着手动画图:画一个小方框代表“节点”,里面分两部分,一部分写“数据”,另一部分画个箭头指向另一个方框——这就是“指针域”。慢慢我才明白,链表就是靠这个“箭头”(指针),把一个个节点串起来,无需连续的内存空间,想加节点就加,想删就删,这就是它的“动态”之处。

而我后来写的代码,也完美印证了这个结构。比如代码里的节点定义:

struct listnode { int val;//数值(数据域) listnode* nxt;//指向下一个listnode节点的地址(指针域) };

一开始我看不懂“listnode* nxt”,不知道为什么要加“*”,也不清楚它的作用。后来对照画图才明白,nxt就是那个“箭头”,存储着下一个节点的地址,这样才能把所有节点连接起来,形成完整的链表。这是我第一次真正理解“节点=数据+指针”的含义,也算彻底打破了对链表的抽象恐惧。

1.2 核心基础:节点的定义与内存分配——第一次真正“用指针”

节点定义好之后,下一步就是创建节点、连接节点。这时候我又遇到了新的难题:怎么创建一个新的节点?代码里用了“malloc”,我之前只见过“new”,完全不知道二者的区别,一开始抄代码时,甚至把“malloc”写成了“new”,结果报错一大堆。

后来查资料才知道,malloc是C语言里的动态内存分配函数,在C++里也能使用,它需要指定分配的内存大小(sizeof(listnode)就是获取一个节点的内存大小),而new是C++的关键字,会自动分配对应大小的内存。而且用malloc创建的节点,需要手动初始化它的val和nxt,不然会出现乱码或者野指针。

代码里的创建节点部分:

listnode* temp = (listnode*)malloc(sizeof(listnode)); cin >> temp->val; temp->nxt = NULL;

我一开始忘记写“temp->nxt = NULL”,导致这个节点的指针域是随机值,后续连接时经常报错。调试了很久我才发现,原来每个新节点创建后,都要把它的指针域设为NULL,这样才能避免野指针。另外,代码里还定义了head和tail两个指针,一开始我不懂它们的作用,以为只要有节点就行,后来才知道,head指向链表的开头,tail指向链表的结尾,用它们能大幅简化链表的创建和操作过程,这也是我后续学习的重点。

1.3 头指针、尾指针与节点——初学者最容易混淆的“拦路虎”

在学习创建链表时,我最困惑的就是head和tail这两个指针。一开始我觉得,直接创建节点,然后让前一个节点的nxt指向后一个节点就好,没必要单独定义head和tail。直到我自己尝试不使用tail指针创建链表,才发现问题:每次添加新节点,都要从head开始遍历,找到最后一个节点才能挂载新节点,不仅繁琐,还容易出错。

而代码里的createList函数,用head和tail双指针解决了这个问题:

void createList() { cin >> n; for (int i = 1; i <= n; i++) { listnode* temp = (listnode*)malloc(sizeof(listnode)); if (head == NULL)head = temp; // 第一个节点,让head指向它 cin >> temp->val; temp->nxt = NULL; if(tail!=NULL)tail->nxt = temp; // 不是第一个节点,让前一个节点(tail)指向当前节点 tail = temp; // 更新tail,让它始终指向最后一个节点 } }

我反复调试这段代码,观察head和tail的变化:第一次循环,head和tail同时指向第一个节点;第二次循环,tail的nxt指向第二个节点,随后tail更新为第二个节点……慢慢我就明白了,head负责“记住”链表的开头,tail负责“记住”链表的结尾,这样每次添加新节点,直接用tail->nxt就能完成挂载,无需再遍历整个链表。这一点让我茅塞顿开,也让我意识到,指针的灵活运用,正是链表的核心所在。

第二章:实践摸索——在报错中“摸爬滚打”,逐步入门

2.1 第一个操作:打印链表——从“崩溃报错”到“成功打印”

创建完链表,下一步就是打印链表,看看自己创建的链表是否正确。这是我写的第一个链表操作函数,本以为很简单,结果却踩了很多坑。

一开始我写的打印函数是这样的(错误版本):

void printList() { listnode* p = head; while (p->nxt != NULL) { // 错误:循环条件写错 cout << p->val << " "; p = p->nxt; } }

运行后我发现,要么少打印最后一个节点(因为最后一个节点的nxt是NULL,循环会提前结束),要么程序崩溃(如果链表为空,p是NULL,访问p->nxt就会报错)。后来我对照参考代码,才找到错误所在:循环条件应该是“while (p)”,也就是判断p不为空,而不是p->nxt不为空。

正确的打印函数就是代码里的样子:

void printList() { listnode* p = head; while (p) { // 只要p不为空,就继续遍历 cout << p->val << " "; p = p->nxt; // 指针后移 } }

我逐行调试,看着p从head开始,一步步后移,直到p变成NULL,循环结束。当第一次成功打印出链表中的所有数据时,我真的很有成就感——这是我第一次独立写出能正常运行的链表操作函数,也让我明白,调试报错并不可怕,只要逐行观察,就能找到问题所在。另外,我也总结出一个经验:所有指针操作前,一定要先判断指针是否为空,避免因访问野指针导致程序崩溃。

2.2 核心操作:增删改查——每一步都踩坑,每一次都成长

掌握了打印链表的方法后,我开始学习链表的核心操作:查询、删除、插入。这三个操作,每一个都让我踩了不少坑,也让我对链表的理解更进了一步。

2.2.1 查询节点:看似简单,实则藏着小细节

查询节点是最基础的操作,就是根据目标值,找到对应的节点并返回。代码里的findListNode函数:

listnode* findListNode(int target) { listnode* p = head; while (p) { if (p->val == target) { return p; } p = p->nxt; // 指针后移,避免死循环 } return NULL; // 没找到目标节点,返回NULL }

我一开始写这段代码时,忘记了“p = p->nxt”,导致循环一直停留在head节点,形成死循环,程序直接卡死。后来调试时,发现p始终没有变化,才意识到自己漏写了指针后移的代码。这也让我牢记:遍历链表时,一定要记得让指针后移,否则就会陷入死循环。另外,查询不到目标节点时,返回NULL也很重要,这样后续执行删除、插入操作时,就能判断目标节点是否存在,避免出现报错。

2.2.2 删除节点:最容易出错的操作,分情况处理是关键

删除节点是我踩坑最多的操作,一开始我以为“只要找到目标节点,让前一个节点指向后一个节点就好”,但实际操作起来,却发现有很多细节需要注意——比如删除头节点和删除普通节点、尾节点的逻辑不一样,还要记得释放内存。

代码里的deleteListNode函数,分了两种情况处理:

void deleteListNode(int target) { listnode* p = findListNode(target);//p是要删除的节点 if (p == NULL) { return; // 没找到目标节点,直接返回 } // 情况1:p为头节点 if (p == head) { head = head->nxt; // 让head指向第二个节点,相当于删除头节点 free(p); p = NULL;//释放内存并防止野指针 return; } // 情况2:p为中间节点或尾节点 listnode* pre = head; while (pre) { if (pre->nxt == p) { // 找到p的前一个节点pre pre->nxt = p->nxt; // 让pre指向p的后一个节点,跳过p free(p); p = NULL; if (p == tail)tail = pre; // 如果删除的是尾节点,更新tail return; } pre = pre->nxt; } }

我一开始没有分情况,直接用“pre->nxt = p->nxt”删除节点,结果当p是头节点时,pre就是head,pre->nxt虽然指向了p->nxt,但head仍然指向原来的p,导致后续遍历链表时,head变成了已被释放的野指针,程序直接崩溃。后来我才明白,删除头节点时,必须更新head指针,让它指向第二个节点,才能保证链表的完整性。

另外,我还忘记了用free释放内存,一开始删除节点后,只修改了指针指向,就以为完成了删除操作。后来查资料才知道,用malloc分配的内存,必须用free手动释放,否则会造成内存泄漏——虽然程序短期内能正常运行,但长期运行会占用越来越多的内存,最终导致程序崩溃。还有,释放内存后,要把p设为NULL,防止出现野指针,这也是初学者很容易忽略的细节。

2.2.3 插入节点:指针顺序是关键,避免节点丢失

插入节点和删除节点一样,也需要分情况处理,而且指针的操作顺序非常重要,一不小心就会导致节点丢失。代码里的insertLsitNode函数,分了三种情况:插入到开头(pos=1)、插入到结尾(pos=n)、插入到中间位置。

void insertLsitNode(int target, int pos) { listnode* p = (listnode*)malloc(sizeof(listnode));//要插入的节点 p->val = target; p->nxt = NULL;//初始化 if (pos == 1) { // 插入到开头 p->nxt = head; head = p; } else if (pos == n) { // 插入到结尾 tail->nxt = p; tail = p; } else { // 插入到中间位置 int cnt = 0; listnode* pre = head;//插入前项 while (pre) { cnt++; if (cnt + 1 == pos) {//找到插入位置pos的前一个节点pre break; } pre = pre->nxt; } listnode* pnxt = pre->nxt;//插入后项,防止丢失 pre->nxt = p; p->nxt = pnxt; } }

我一开始插入中间节点时,没有用pnxt暂存pre->nxt,直接写“pre->nxt = p; p->nxt = pre->nxt;”,结果pre->nxt已经指向p,p->nxt再指向pre->nxt,就相当于指向自己,导致链表断裂,后续节点全部丢失。调试了很久,我才找到问题所在:必须先把pre->nxt(插入位置的后一个节点)暂存起来,再让pre->nxt指向p,最后让p->nxt指向暂存的节点,这样才能保证链表的连续性。

还有插入到开头和结尾的情况,一开始我经常忘记更新head和tail指针。比如插入到开头时,只写“p->nxt = head”,没有写“head = p”,导致head仍然指向原来的节点,插入的新节点没有成为新的头节点;插入到结尾时,忘记更新tail,导致tail还是指向原来的尾节点,后续操作会出现错误。这些细节,都是我在反复调试中慢慢发现并改正的。

2.3 常见报错复盘——初学者最容易踩的4个坑

在实践过程中,我遇到的报错几乎都离不开这4种情况。比如一开始写创建链表函数时,没有初始化head和tail为NULL,导致head和tail成为野指针,访问head->nxt时直接报错;删除节点后忘记free,程序运行一段时间后会变得卡顿;遍历链表时忘记指针后移,导致死循环……

后来我养成了几个习惯,慢慢减少了报错:

  • ① 所有指针操作前,先判断指针是否为空;
  • ② 用malloc创建节点后,必初始化val和nxt;用free释放节点后,必把指针设为NULL;
  • ③ 遍历链表时,时刻记得让指针后移;
  • ④ 插入、删除头节点或尾节点时,第一时间更新head或tail指针。这些习惯,不仅让我少踩坑,也让我的代码更规范、更安全。

第三章:进阶提升——从“会用”到“理解”,打通任督二脉

3.1 吃透基础操作后:理解代码逻辑,不再机械抄代码

当我能熟练写出所有核心操作后,我开始思考:“我写的代码,每一步到底是为什么?” 比如,为什么创建链表要用head和tail双指针?为什么删除节点要分情况?为了找到答案,我做了一个尝试:把createList函数改成单指针(不用tail),结果发现,每次添加新节点,都要从head开始遍历,找到最后一个节点才能挂载,不仅代码更繁琐,运行效率也更低。这时候我才明白,head和tail双指针的作用,就是节省遍历时间、简化操作。

再比如删除节点,为什么要分“头节点”和“非头节点”?因为头节点没有前一个节点,无法通过“前一个节点指向后一个节点”的方式删除,只能通过更新head指针来完成删除。而普通节点有前一个节点,只要跳过它就能完成删除。理解了这些逻辑后,我不再机械抄代码,而是能根据自己的需求修改代码逻辑——比如我尝试修改deleteListNode函数,让它根据位置删除节点,而不是根据值删除,虽然过程中也出现了报错,但最终成功实现了,这让我很有成就感。

3.2 从“写代码”到“懂思想”——链表的核心价值

一开始我学习链表,只是为了完成作业,知道“链表能实现增删改查”就够了。但当我亲手写完整套代码,对比数组和链表的差异后,才真正理解链表的价值。比如数组,一旦定义大小就无法修改,想要插入一个元素,就要移动后面所有的元素,效率很低;而我写的链表,用malloc动态分配内存,想加多少节点就加多少,插入、删除时只需修改指针指向,无需移动大量数据,效率很高。

比如我在main函数里做的测试:创建链表后,删除一个节点、插入一个节点,整个过程非常流畅,不用考虑内存是否足够,也不用移动其他节点。这就是链表的优势——灵活、高效。而且,通过学习链表,我对指针和动态内存管理的理解也更深刻了。之前我对“malloc”“free”“野指针”这些概念一知半解,现在能熟练运用,也明白“手动管理内存”是C++的重点和难点,而链表,正是练习这些知识点的最佳载体。

3.3 小实战:运行完整代码,检验学习成果

当我写完所有函数后,就把它们整合到一起,加上main函数的测试代码:

int main() { head = NULL; taiL; // 初始化head和tail,避免野指针 createList(); cout << "修改前:" << endl; printList(); cout <<endl<< "删后:" << endl; deleteListNode(2); printList(); out << endl << "插入后:" << endl; insertLsitNode(100,3); printList(); return 0; }

一开始运行时,报错很多:比如忘记初始化head和tail为NULL,导致创建链表时head是野指针;把“insertLsitNode”误拼成“insertListNode”,编译器找不到函数;删除节点时,free的位置不当,导致出现野指针。我逐行排查,对照之前的笔记,一点点修改错误,花了将近一个小时,终于让代码成功运行。

当屏幕上成功打印出:

的时候,我真的很有成就感。这不仅意味着我掌握了链表的基础操作,更意味着我能独立完成从“写函数”到“整合代码”再到“测试运行”的完整过程,不再是那个只会抄代码的初学者了。

第四章:总结感悟——初学者如何高效掌握链表?

4.1 我的学习历程复盘

  • 阶段回顾:从“恐惧指针、看不懂链表”→“能写基础节点和打印函数”→“能独立实现增删改查”→“能整合代码、理解核心逻辑”的完整过程

  • 最大的变化:从“怕报错”到“主动调试报错”,从“被动接受知识”到“主动思考逻辑”,从“机械抄代码”到“能灵活修改代码”

回顾整个学习过程,我从一开始听到“链表”就退缩,到现在能独立写出完整的链表代码,中间踩了无数坑,也收获了很多。一开始,我连节点的定义都看不懂,不知道指针域的作用,写代码全靠抄,报错了就慌,不知道如何排查;后来,我开始手动画图、逐行调试,慢慢理解每一行代码的作用,学会了判断空指针、释放内存、更新头尾指针;最后,我能整合所有函数,独立完成测试,甚至能修改代码逻辑,这就是我的成长。

我最大的变化,就是不再害怕报错。一开始报错,我会手足无措,甚至想放弃;但后来我发现,每一次报错,都是一次学习的机会——通过排查报错,我能找到自己的错误,记住对应的知识点,下次就不会再犯。比如第一次遇到空指针报错,我花了很久才找到原因,后来我就养成了“指针操作前先判断空指针”的习惯,再也没有因为这个问题报错过。

4.2 给同是初学者的3条真心建议

  • 手动画图!这是最关键的一步,指针操作看不见摸不着,画图能让逻辑一目了然。比如创建链表、插入删除节点时,用笔画出每个节点的位置和指针指向,就能轻松理解代码逻辑,避免出错

  • 不要怕报错,不要抄代码:一定要自己动手敲,报错后逐行调试,比看10遍教程都管用。抄代码只能让你“看起来会了”,只有自己动手敲、自己排查报错,才能真正理解每一行代码的作用

  • 先吃透基础操作,再追求进阶:不要一开始就想着学双向链表、循环链表,先把单链表的增删改查、内存管理吃透,打好基础,后续学习其他类型的链表,就能举一反三

作为一个初学者,我知道学习链表的过程是痛苦的,会遇到很多困惑和报错,但请相信,只要坚持下去,多画图、多敲代码、多调试,就一定能掌握它。我一开始也觉得自己学不会,但每天花一点时间,一点点摸索,慢慢就有了头绪。另外,一定要重视基础,比如指针的基本操作、动态内存管理,这些都是学习链表的基础,基础打牢了,后续的学习就会轻松很多。

4.3 后续规划:从链表到更复杂的数据结构

  • 感悟:链表是数据结构的“敲门砖”,掌握它之后,我对栈、队列等高级结构更有信心了。因为栈和队列的底层实现,很多都用到了链表的思想,学好链表,能为后续学习打下坚实的基础

  • 规划:后续继续深耕,比如学习双向链表、循环链表,尝试用链表实现栈和队列;同时,多刷一些链表相关的算法题,提升自己的代码能力和逻辑思维,夯实C++基础

学习完单链表后,我知道这只是数据结构的起点。接下来,我打算学习双向链表和循环链表——它们只是在单链表的基础上,增加了一个指针(双向链表的prev指针)或修改了指针的指向(循环链表的尾节点nxt指向head),相信有了单链表的基础,我能很快掌握它们。另外,我也打算多刷一些LeetCode上的链表简单题,比如反转链表、合并两个有序链表,通过刷题巩固知识点,提升自己的代码能力和逻辑思维。

我明白,学习编程没有捷径,只能靠不断练习和积累。链表的学习,让我突破了指针和动态内存管理的瓶颈,也让我养成了良好的编程习惯。未来,我会继续深耕数据结构和算法,努力成为一个更优秀的程序员。

附录:初学者友好的完整可运行代码

  • 说明:代码注释详细,贴合初学者视角,包含节点定义、链表创建、打印、查询、删除、插入等所有基础操作,采用malloc/free管理内存,适合新手参考、测试

  • 备注:可直接复制运行,运行时输入链表节点个数和每个节点的值,即可看到修改前、删除后、插入后的链表效果,方便新手测试、修改,加深理解

    #include<iostream> using namespace std; int n; struct listnode { int val;//数值 listnode* nxt;//指向下一个listnode节点的地址 }; listnode* head; listnode* tail; //链表创建 void createList() { cin >> n; for (int i = 1; i <= n; i++) { listnode* temp = (listnode*)malloc(sizeof(listnode)); if (head == NULL)head = temp; cin >> temp->val; temp->nxt = NULL; if(tail!=NULL)tail->nxt = temp; tail = temp; } } //链表打印 void printList() { listnode* p = head; while (p) { cout << p->val << " "; p = p->nxt; } } //查询函数(寻址) listnode* findListNode(int target) { listnode* p = head; while (p) { if (p->val == target) { return p; } p = p->nxt; } return NULL; } //删除链表节点 void deleteListNode(int target) { listnode* p = findListNode(target);//p是要删除的节点 if (p == NULL) { return; } //|---------p为head节点的值,删头节点---------| if (p == head) { head = head->nxt; free(p); p = NULL;//释放内存并防止出现野指针 return; } //|---------p为中间节点或尾节点,删尾和中间删除---------| listnode* pre = head; while (pre) { if (pre->nxt == p) { pre->nxt = p->nxt; free(p); p = NULL;//释放内存并防止出现野指针 if (p == tail)tail = pre; return; } pre = pre->nxt; } } //插入链表节点 void insertLsitNode(int target, int pos) { listnode* p = (listnode*)malloc(sizeof(listnode));//要插入的节点 p->val = target; p->nxt = NULL;//初始化 if (pos == 1) { p->nxt = head; head = p; } else if (pos == n) { tail->nxt = p; tail = p; } else { int cnt = 0; listnode* pre = head;//插入前项 while (pre) { cnt++; if (cnt + 1 == pos) {//找到插入位置pos,pos-1即为插入前项pre break; } pre = pre->nxt; } listnode* pnxt = pre->nxt;//插入后项(pos所在位置为防止丢失使用中介pnxt) pre->nxt = p; p->nxt = pnxt; } } int main() { createList(); cout << "修改前:" << endl; printList(); cout <<endl<< "删后:" << endl; deleteListNode(2); printList(); cout << endl << "插入后:" << endl; insertLsitNode(100,3); printList(); return 0; }
http://www.jsqmd.com/news/763604/

相关文章:

  • 别再死记硬背了!用Multisim仿真带你直观理解运放负反馈的三大魔法(增益、带宽、阻抗)
  • JESD204B同步实战:在Vivado里配置Xilinx IP核时,这几个参数千万别设错
  • 终极窗口控制指南:如何用WindowResizer强制调整任意窗口尺寸
  • 【软考高级架构】论文范文06——论DDD领域驱动设计及其应用
  • Opus 4.7 + GPT-5.5“双核驱动”——2026最强AI编程工作流实测
  • 考研数学救命稻草:一阶和二阶微分方程的通解公式,我帮你整理好了(附880/660真题解法)
  • 数据分析新手福音:告别复杂spss安装,用快马ai轻松入门统计
  • AI编码助手安全技能集成:vt、gakido等工具实战指南
  • 大模型应用开发入门:收藏!Java开发者如何精准转型,HR眼中的认知误区与你的优势
  • 5分钟掌握网盘直链下载:告别限速与强制客户端的神器
  • BIT概率论考情分析
  • MXFP4量化技术提升LLM推理性能与精度
  • 第 3 周 Unit 1:Kotlin Hello World、生日卡与单位转换器
  • 知识蒸馏‘救场’记:当YOLOv5剪枝过头后,如何用教师模型把精度‘教’回来?
  • 从GB2312汉字到海明码:在Logisim里设计一个带中文编码的校验电路
  • 避坑指南:微调chinese-roberta-wwm-ext做情感分析时,这5个参数调优细节千万别忽略
  • Flutter 跨平台实战:OpenHarmony 健康管理应用 Day6|基于 SharedPreferences 的数据本地持久化实现
  • 拯救你的Minecraft世界:Region Fixer存档修复工具完全指南
  • 德州亚太风机厂家电话
  • 保姆级避坑指南:用PX4 v1.12.3 + Gazebo搞定Offboard模式,解决‘Vehicle armed’失败问题
  • Cursor Free VIP:5步解决Cursor AI试用限制的终极方案
  • 第八部分-周边生态与工具——38. 模型工具
  • 使用mybatis查询所有用户报错,JUnit版本冲突
  • 告别Pyinstaller默认羽毛图标:一个临时ICO文件搞定Python GUI打包三件套
  • Mac本地运行多模态大模型:mlx-vlm环境搭建与性能优化指南
  • 提升网盘开发效率:用快马AI一键生成分片上传与断点续传功能模块
  • 前端调试 - 获取下拉框元素 F12 延时断点操作记录 - 秒杀其他所谓的F8和手速快操作
  • 2026 饮料代理加盟口碑推荐榜|:阿尔卑斯饮品厂家优选指南,饮品批发招商渠道加盟合作怎么选更靠谱 - 海棠依旧大
  • 终极NS模拟器管理指南:如何用NsEmuTools一键搞定Switch游戏环境
  • 第八部分-周边生态与工具——39. 框架集成