【数据结构】链表中二级指针的应用

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境:Visual Studio 2022

(注:为方便演示本篇使用的x86系统,因此指针的大小为4个字节)


目录

📌形参的改变不影响实参!

1.调用函数更改整型时传值调用与传址调用的区别

🎏传值调用

🎏传址调用

2.调用函数更改指针的指向时传值调用和传址调用的区别

🎏传值调用

🎏传址调用

3.调用函数更改数组和结构体成员

🎏更改数组成员

🎏更改结构体成员

📌二级指针的作用

1.链表的头指针结构

2.空链表时的链表尾插

3.非空链表时的尾插逻辑

📌不使用二级指针操作链表的两种方法

1.使用带头结点的链表

2.在外部更改头指针的指向

结语


相信大家在初学链表时一定被下面这些函数的二级指针搞得晕头转向的,疑惑包括但不限于:

  • 什么是二级指针?
  • 为什么链表要用到二级指针?
  • 为什么同样是链表的函数,有的要用二级指针而有的只要用一级指针?
  • 为什么同样是链表,有的链表中使用了二级指针?而有的函数却只需要使用一级指针?

要搞清上面这些问题,我们就要先搞清楚二级指针在链表中的作用到底是什么,接下来我将带大家一起探究二级指针的"前世今生".


📌形参的改变不影响实参!

1.调用函数更改整型时传值调用与传址调用的区别

🎏传值调用

如下代码,我们在主函数创建了一个变量a,并给其赋值为5.然后我们通过传值调用函数test1,在函数内部a的值改为10.并在过程中打印出a的值:

void test1(int a)
{a = 10;printf("调用函数时a=%d\n", a);
}int main()
{int a = 5;printf("没有调用函数前a=%d\n", a);test1(a);printf("调用函数后a=%d\n", a);return 0;
}

在编译器中查看运行结果:

可以看到,传值调用虽然函数调用时将a的值改为了10,但是一旦出了函数之后a的值是完全没有改变的.

因此:形参的改变不影响实参!

        形参的改变不影响实参!

        形参的改变不影响实参!


🎏传址调用

如下代码,我们在主函数创建了一个变量a,并给其赋值为5.还创建了一个整型指针pa记录下了变量a地址.然后我们通过传址调用函数test2,在函数内部使用指针将a的值改为10.并在过程中打印出a的值:

void test2(int *pa)
{*pa = 10;printf("调用函数时a=%d\n", *pa);
}int main()
{int a = 5;int* pa = &a;printf("没有调用函数前a=%d\n", a);test2(pa);printf("调用函数后a=%d\n", a);return 0;
}

 在编译器中查看运行结果:

可以看到,传址调用的函数在内部修改a的值,出了函数依然是有效的.

这有些像快递送货上门时,如果按照人名派送快递,可能在这个小区有3个人都叫"张伟",这时派送给哪个"张伟"都有可能派送错,但是如果按照他下单时填写的地址派送快递,那就绝对不会出错,名字可能出错,但地址一定是唯一的.

传值调用和传址调用不同的核心原理:函数会对形参和中间变量重新分配空间 


2.调用函数更改指针的指向时传值调用和传址调用的区别

那么是否我们要改变形参时都传指针就一劳永逸了呢?再来看个例子:

🎏传值调用

如下代码,我们在主函数创建了两个变量a和b,并给其赋值为5和10.还创建了两个整型指针papb分别记录下了变量a和b的地址.然后我们通过传值调用函数test3,在函数内部将pb的值赋给pa.并在过程中打印出pa和pb的值:

void test3(int* pa,int* pb)
{pa = pb;printf("调用函数时:\n");printf("pa指针中存储的内容:%p\n", pa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");
}int main()
{int a = 5;int b = 10;int* pa = &a;int* pb = &b;printf("调用函数前:\n");printf("pa指针中存储的内容:%p\n", pa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");test3(pa,pb);printf("调用函数后:\n");printf("pa指针中存储的内容:%p\n", pa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");return 0;
}

在编译器中查看运行结果:

(注:为方便演示使用的x86系统,因此指针的大小为4个字节)

可以看到,传值调用虽然在函数调用时将pa的指向改为了pb,但是一旦出了函数之后pa的指向是完全没有改变.

因此:在改变指针变量时形参的改变同样不影响实参!


🎏传址调用

既然改指针的时候给函数传指针本身没有用,那么要传什么呢?没错,要传"指针的指针",即二级指针.

如下代码,我们在主函数创建了两个变量a和b,并给其赋值为5和10.还创建了两个整型指针pa和pb分别记录下了变量a和b的地址.又创建了一个二级整型指针ppa用来记录指针pa的地址,然后我们通过传址调用函数test4,在函数内部将pb的值赋给解引用的ppa.并在过程中打印出pa和pb的值:

void test4(int** ppa, int* pb)
{*ppa = pb;printf("调用函数时:\n");printf("pa指针中存储的内容:%p\n", *ppa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");
}int main()
{int a = 5;int b = 10;int* pa = &a;int* pb = &b;int** ppa = &pa;printf("调用函数前:\n");printf("pa指针中存储的内容:%p\n", pa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");test4(ppa, pb);printf("调用函数后:\n");printf("pa指针中存储的内容:%p\n", pa);printf("pb指针中存储的内容:%p\n", pb);printf("\n");return 0;
}

在编译器中查看运行结果:

可以看到,传址调用的函数在内部修改指针pa的值,出了函数依然是有效的.

因此当我们想要在函数内修改指针的指向时,我们应该给函数传入二级指针.


3.调用函数更改数组和结构体成员

🎏更改数组成员

如下代码,我们在主函数创建了一个5个成员的数组arr,并给其初始化为0.然后我们通过调用函数test5,在函数内部将arr的成员赋为0,1,2,3,4.并在过程中打印出arr数组的成员值:

void test5(int arr[])
{//修改arr数组成员的值for (int i = 0; i < 5; i++){arr[i] = i;}printf("调用函数时arr数组的成员:\n");for (int i = 0; i < 5; i++){printf("%d ", arr[i]);}printf("\n");
}int main()
{int arr[5] = { 0 };printf("调用函数前arr数组的成员:\n");for (int i = 0; i < 5; i++){printf("%d ", arr[i]);}printf("\n");test5(arr);printf("调用函数后arr数组的成员:\n");for (int i = 0; i < 5; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}

在编译器中查看运行结果:

可以看到,test5函数成功修改了arr数组的成员值,但我们好像并没有传给函数arr数组的地址,为什么修改成功了呢?

这是因为在C语言中,数组名就是数组首元素的地址,因此我们看似给test5函数传入的是arr的名字,但实际上test5函数接收到的却是arr数组的地址,因此该函数同样可以写为:

void test5(int* arr)
{//修改arr数组成员的值for (int i = 0; i < 5; i++){*(arr+i) = i;}printf("调用函数时arr数组的成员:\n");for (int i = 0; i < 5; i++){printf("%d ", *(arr + i));}printf("\n");
}

测试运行结果和上面没有任何差别:


🎏更改结构体成员

如下代码,我们在主函数中创建了一个结构体变量stu,并给其赋值"张三",20,1006.

然后我们通过传址调用函数test6,在函数内部将stu的成员赋为"李四",30,1024.并在过程中打印出stu结构体的成员值:

typedef struct Student
{char name[5];int age;int idea;
}Stu;void test6(Stu* stu)
{strcpy(stu->name, "李四");stu->age = 30;stu->idea = 1024;printf("调用函数时stu结构体的成员:\n");printf("%s ", stu->name);printf("%d ", stu->age);printf("%d ", stu->idea);printf("\n");
}int main()
{Stu stu = { "张三",20,1006 };printf("调用函数前stu结构体的成员:\n");printf("%s ", stu.name);printf("%d ", stu.age);printf("%d ", stu.idea);printf("\n");test6(&stu);printf("调用函数后stu结构体的成员:\n");printf("%s ", stu.name);printf("%d ", stu.age);printf("%d ", stu.idea);printf("\n");return 0;
}

 在编译器中查看运行结果:

可以看到,要更改结构体的值,需要给函数传入结构体的指针才可以完成修改.


📌二级指针的作用

1.链表的头指针结构

我们在单链表程序的最开始曾经写过这样一句代码:

这句代码的作用创建了一个链表的头指针,其逻辑图示如下:

在计算机的栈上的物理结构(以下简称物理结构)图示如下:


2.空链表时的链表尾插

尾插操作我们已经在之前单链表详解中详细介绍过了,

因此这里只演示其逻辑图示:(紫色线条代表操作)

物理图示:(紫色线条代表操作)

可以看到,在空链表时的链表尾插操作中,我们更改了头指针plist的指向,因此在函数中要使用到二级指针.


3.非空链表时的尾插逻辑

逻辑图示:(紫色线条代表操作)

物理图示:(紫色线条代表操作)

可以看到,在非空链表时的尾插中我们更改的是d2结点结构体的指针域的存储内容,因此这时我们操作只需要d2结构体的地址,即一级指针.


综上可得:

链表中传入二级指针的原因是我们会遇到需要更改头指针plist的指向的情况.

如果我们仅是在不改变头指针plist的指向的情况下对链表进行操作(如非空链表的尾删,尾插,对非首结点(FirstNode)的结点的插入/删除操作等),则不需要用到二级指针.


📌不使用二级指针操作链表的两种方法

那么我们在写链表程序时就必须要使用二级指针吗?答案是否定的,下面给大家提供了两种不使用二级指针就可以完成链表所有操作的方法,大家可以结合自身情况选择合适的方法完成链表程序.

1.使用带头结点的链表

原理:如果我们为单链表设置一个哨兵位的头结点,那么plist的指向就固定了.即:

带头结点空链表示意图:

这时我们想改变链表的首结点(firstNode),如头删,头插等操作就只需要改变头结点的指针域即可.而plist只需要固定存储头结点(headNode)的地址,既然函数不需要改变plist的指向,也就不需要用到二级指针了.

带头结点空链表头插逻辑示意图:(紫色线条为操作)

带头结点空链表头插逻辑物理示意图:(紫色线条为操作)

可以看到,在带头结点空链表的头插操作中,plist的值没有被改变,我们通过改变头结点指针域的值实现了链表的头插,因此使用带头结点的链表就可以不使用二级指针操作链表.


2.在外部更改头指针的指向

原理:既然我们在函数内部给plist赋值不会影响到函数外的plist的指向,那么我们直接将更改指向这步操作放在函数外即可.其实类似的操作我们在获取新结点函数中就已经应用过了:

单链表中的BuySLTNode()函数:

为了防止newnode指针记录的动态开辟的空间的地址出了函数就被销毁,我们将新结点的地址通过返回值返回到函数外并用一个指针接收,这样虽然出了空间newnode被销毁,但我们已经在函数外部使用指针记录了下函数返回的它的地址,因此出了函数还可以正常使用这块空间.

同理,函数中更改了头指针的指向,我们将新的头指针的地址记录下来并返回给主函数,然后在主函数中重新使用plist指针接收这个头即可更新头指针的指向:

该思路代码示例如下(仅展示头插部分主函数与头插函数逻辑) :

//单链表头插
SLTNode* SLTPushFront(SLTNode* phead, int x)
{//创建新结点SLTNode* newnode = BuySLTNode(x);//BuySLTNode函数的实现参照上文//先将newnode的next指向首结点newnode->next = phead;//再将phead指向newnodephead = newnode;//返回新头pheadreturn phead;
}int main()
{SLTNode* plist=NULL;printf("请输入要头插的数据:>");int pushfront_data = 0;scanf("%d", &pushfront_data);plist=SLTPushFront(plist, pushfront_data);//把SLTPushFront函数返回的新头的地址赋给plist,这样plist就重新指向新头了return 0;
}

经过测试,这种方法同样可以不使用二级指针就能够完成链表的一系列相关操作,但缺点只要调用了有可能改变plist的函数,都必须在外面使用plist接收返回值以便更新新的头结点.有时一旦忘了就会导致程序出错,比较麻烦且容易出错.


结语

希望这篇链表中二级指针的应用能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【数据结构】什么是线性表?

【数据结构】线性表的链式存储结构

【数据结构】链表的八种形态

【数据结构】C语言实现单链表万字详解(附完整运行代码)

【数据结构】C语言实现带头双向循环链表万字详解(附完整运行代码)

【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)



数据结构线性篇思维导图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/160578.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

微服务学习|初识Docker、使用Docker、自定义镜像、DockerCompose、Docker镜像仓库

初识Docker 项目部署的问题 大型项目组件较多&#xff0c;运行环境也较为复杂&#xff0c;部署时会碰到一些问题 依赖关系复杂&#xff0c;容易出现兼容性问题 开发、测试、生产环境有差异 Docker如何解决依赖的兼容问题的? 将应用的Libs (函数库)、Deps (依赖)配置与应用…

线性回归的正则方法:岭回归和Lasso

线性回归的正则方法包括岭回归&#xff08;Ridge Regression&#xff09;和Lasso回归&#xff08;Least Absolute Shrinkage and Selection Operator Regression&#xff09;。这两种方法都是为了解决线性回归中可能存在的过拟合问题而提出的。 选择使用岭回归还是Lasso回归通常…

3d标签云实现过程(tagcloud.js)同步原生和 vue

写在前面 本来是没有准备写这个知识点&#xff0c;但是下载这个 js 的时候发现很多都是要钱或者是积分的&#xff0c;我就不明白了一个开源了这么久的 js 怎么还有人拿来挣钱的&#xff0c;同时还有一些只有原生 html 的例子&#xff0c;但是现在都是 框架主导的一些项目&#…

【LeetCode二叉树进阶题目】606,102,107

二叉树进阶题目 606. 根据二叉树创建字符串解题思路及实现 102. 二叉树的层序遍历解题思路及实现 107. 二叉树的层序遍历 II解题思路及实现 606. 根据二叉树创建字符串 描述 给你二叉树的根节点 root &#xff0c;请你采用前序遍历的方式&#xff0c;将二叉树转化为一个由括号…

晶振有哪几种?晶振旁边的两个电容起什么作用?

晶振可以分为普通晶振、温补晶振、压控晶振、恒温晶振、差分晶振。 普通晶振通常用作微处理器的时钟器件&#xff0c;主要应用于那些稳定度要求不要的设备中&#xff0c;例如电视机、微波炉。 温补晶振&#xff0c;在晶振内部采取了对晶体频率、温度特性进行补偿&#xff0c;已…

软文推广有什么作用?媒介盒子分享

数字时代&#xff0c;品牌方以往的营销打法可能需要应时而变&#xff0c;传统的广告模式很难将品牌推广出去&#xff0c;原因就在于传统广告的成本高昂并且针对性较弱&#xff0c;而软文推广能够通过较低的成本将产品或品牌信息送到消费者面前&#xff0c;今天媒介盒子就来分享…

vue3.0 + qiankun遇到的问题

进入子应用再回到主应用切换动态路由时 TypeError: Cannot read properties of undefined (reading ‘appWrapperGetter’) application ‘plat’ died in status UNMOUNTING: instance.$destroy is not a function 第一个报错是因为子应用切走时没有销毁 vue的实例&#xff0…

TCP/IP

分层模型 TCP 传输控制协议 UDP 用户数据包协议 四层 应用层 负责发送/接收消息 传输层 负责拆分和组装 .期间会有编号 网络层 TCP/UDP 属于网络层, 不会判断和处理编号 数据链路层 以太网 ,网络设备 TCP 连接 TCP连接需要端口,进行通信 Java 通过Socket 接收消息 发送 …

基于SpringBoot+Vue的体检预约管理系统

基于SpringBootVue的体检预约管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 管理员界面 用户界面 摘要 体检预约管理系统是一种基于Spring Boot…

Vue3常用操作

一、Vue3项目构建 1、安装最新版本vue npm create vuelatest 2、选择需要的配置 3、进入项目 cd 项目名称 4、下载依赖 npm install 5、启动项目 npm run dev

chatGLM3微调

文章目录 一、问答数据集生成器使用设置问题启动使用产出效果 二、进行微调第一步&#xff1a;下载模型第二步&#xff1a;项目准备2.1 下载项目2.2 然后使用 pip 安装依赖2.3 开始 第三步进行微调3.1安装相关依赖3.2准备数据集&#xff0c;并且上传3.3对数据集进行预处理3.4 进…

如何使用技术SEO来优化评论

你在网上购买吗&#xff1f;我的意思是&#xff0c;在当今时代&#xff0c;谁不这样做&#xff1f;作为买家&#xff0c;无论您想购买什么&#xff0c;您都了解全面和高质量评论的价值。这是您在决定是否购买产品时考虑的重要因素。 这就是为什么许多人在网上购物之前使用评论…

【开源】基于Vue和SpringBoot的康复中心管理系统

项目编号&#xff1a; S 056 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S056&#xff0c;文末获取源码。} 项目编号&#xff1a;S056&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 普通用户模块2.2 护工模块2.3 管理员…

基于WEB的停车场管理系统的设计和实现【附源码】

基于WEB的停车场管理系统的设计和实现 摘 要 随着现代社会的快速发展&#xff0c;人民生活水平快速提高&#xff0c;汽车的数量飞速增加&#xff0c;与此同时停车问题也越来越受到人们的关注&#xff0c;为了实现对停车场进行有效的管理&#xff0c;结合一些停车场的模式和现状…

【LeetCode刷题】--40.组合总和II

40.组合总和II 本题详解&#xff1a;回溯算法 class Solution {public List<List<Integer>> combinationSum2(int[] candidates, int target) {int len candidates.length;List<List<Integer>> res new ArrayList<>();if (len 0) {return re…

深度学习之基于YoloV5车辆和行人目标检测系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介YOLOv5 简介YOLOv5 特点 车辆和行人目标检测系统 二、功能三、系统四. 总结 一项目简介 # 深度学习之基于 YOLOv5 车辆和行人目标检测系统介绍 深度学习在…

2023 年 亚太赛 APMCM 国际大学生数学建模挑战赛 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 cs数模团队在亚太赛 APMCM前为大家提供了许多资料的内容呀&…

【西行纪年番】孙悟空对战阴界王,素衣奄奄一息,巨灵拳霸气一击

Hello,小伙伴们&#xff0c;我是拾荒君。 《西行纪年番》第20集已更新。为了救回素衣&#xff0c;孙悟空想尽办法&#xff0c;最后他拜托沙悟净帮忙&#xff0c;终于成功把自己传送到阴界。原来&#xff0c;素衣的魂魄被阴界王藏在了他制造的人偶之中。沙悟净提醒孙悟空必须在…

8.2 Windows驱动开发:内核解锁与强删文件

在某些时候我们的系统中会出现一些无法被正常删除的文件&#xff0c;如果想要强制删除则需要在驱动层面对其进行解锁后才可删掉&#xff0c;而所谓的解锁其实就是释放掉文件描述符&#xff08;句柄表&#xff09;占用&#xff0c;文件解锁的核心原理是通过调用ObSetHandleAttri…

Axios使用方式

ajax是JQUERY封装的XMLHttprequest用来发送http请求 Axios简单点说它就是一个js库,支持ajax请求,发送axios请求功能更加丰富,丰富在哪不知道 1.npm使用方式 vue项目中 npm install axios 2.cdn方式 <script src"https://unpkg.com/axios/dist/axios.min.js">…