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

news2025/1/15 23:42:54

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境: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指向newnode
	phead = newnode;

	//返回新头phead
	return 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.coloradmin.cn/o/1239954.html

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

相关文章

【Pytorch】Visualization of Fature Maps(2)

学习参考来自 使用CNN在MNIST上实现简单的攻击样本https://github.com/wmn7/ML_Practice/blob/master/2019_06_03/CNN_MNIST%E5%8F%AF%E8%A7%86%E5%8C%96.ipynb 文章目录 在 MNIST 上实现简单的攻击样本1 训练一个数字分类网络2 控制输出的概率, 看输入是什么3 让正确的图片分…

表格制作软件排行榜,热门做表格的软件推荐

在数字化时代&#xff0c;表格不仅仅是企业管理和数据整理的重要工具&#xff0c;更是学术研究、项目规划以及日常生活中必不可少的一部分。为了更高效地进行表格制作&#xff0c;选择一款优秀的表格制作软件是至关重要的。在众多的软件中&#xff0c;我们特别推荐一款备受好评…

【电路笔记】-电流源

电流源 文章目录 电流源1、概述1.1 理想电流源1.2 实际电流源1.3 连接规则 2、依赖电流2.1 压控电流源2.2 电流控制电流源 3、总结 本文为前面文章 电压源的延续&#xff0c;我们将在本文介绍电流源。 与电压源的情况类似&#xff0c;我们将首先介绍理想电流源的概念&#xff…

基于springboot实现摄影跟拍预定管理系统【项目源码+论文说明】计算机毕业设计

基于springboot实现摄影跟拍预定管理系统演示 摘要 首先,论文一开始便是清楚的论述了系统的研究内容。其次,剖析系统需求分析,弄明白“做什么”,分析包括业务分析和业务流程的分析以及用例分析,更进一步明确系统的需求。然后在明白了系统的需求基础上需要进一步地设计系统,主要…

【Redis】持久化-RDBAOF混合持久化

文章目录 前置知识RDB&#xff08;定期备份&#xff09;触发机制流程说明RDB文件的处理RDB 的优缺点 AOF&#xff08;实时备份&#xff09;使用AOF命令写入AOF工作流程文件同步重写机制重写触发机制AOF进制重写流程 混合持久化启动时数据恢复 总结 前置知识 回顾MySQL MySQL的事…

UVM项目笔记——通过寄存器模型实现AHB接口的寄存器读写(内含源码)

目录 1.前言 2.DRIVER实现 2.1 AHB二级流水时序 2.2 “队列错位法”实现driver 2.3 driver代码 2.4 仿真log与波形 2.5 多级流水拓展方法 1.前言 UVM driver在接口协议的实现中起着非常重要的作用&#xff0c;因为它一端处理基于类的事务级sequence&#xff0c;另一端处…

Wagtail-基于Python Django的内容管理系统CMS如何实现公网访问

Wagtail-基于Python Django的内容管理系统CMS实现公网访问 文章目录 Wagtail-基于Python Django的内容管理系统CMS实现公网访问前言1. 安装并运行Wagtail1.1 创建并激活虚拟环境 2. 安装cpolar内网穿透工具3. 实现Wagtail公网访问4. 固定的Wagtail公网地址 前言 Wagtail是一个…

数字化转型过程中的RPA+X与RPA+B

当前&#xff0c;RPA已成为企业数字转型初始阶段里最受欢迎的自动化解决方案&#xff0c;越来越多的企业开始引入RPA来协助员工&#xff0c;开展各类业务场景的自动化应用。很多企业也都将RPA列为重要流程优化技术&#xff0c;不少公司甚至直接放弃了传统的IT自动化方案&#x…

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

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

sap系统连接其它系统

本文来自博客园&#xff0c;作者&#xff1a;Lovemywx2&#xff0c;转载请注明原文链接&#xff1a;https://www.cnblogs.com/1187163927ch/p/8669859.html JAVA连接ORACLE数据库 1&#xff0c;首先需要在Oracle安装完成之后新建一个用户 --新建用户 create user chenh iden…

中国最常用的制作报表的软件有哪些?

​随着信息化时代的浪潮席卷而来&#xff0c;报表制作软件已经成为了企业管理中的得力助手。在中国的大地上&#xff0c;有许多优秀的报表制作软件&#xff0c;而本文将为您揭示五位佼佼者&#xff0c;其中更以VeryReport报表软件为首选。 编辑搜图 请点击输入图片描述&#x…

centos 安装k8s教程(一键安装k8s)

第一步 准备几台机器 第二步 K8s Manager 服务器中添加docker支持 安装教程请查看这个博客 docker 安装详细教程 点我 第三步安装 KuboardSpray 教程在这里 第四步 下载k8s资源包 第五步 安装k8s 点击安装后 显示如下&#xff1a;等待完成

MAX/MSP SDK学习07:list传递

实现自定义Obejct&#xff0c;要求将传入的一组数据100后传出。 #include "ext.h" #include "ext_obex.h" typedef struct _listTrans {t_object ob;void* outLet;t_atom* fArr;long listNum;} t_listTrans;void* listTrans_new(t_symbol* s, long arg…

【GUI】-- 13 贪吃蛇小游戏之食物及成绩判断

GUI编程 04 贪吃蛇小游戏 4.4 第四步&#xff1a;食物及成绩判断 首先&#xff0c;添加食物与分数的数据定义&#xff1a; //食物的坐标int foodX;int foodY;Random random new Random();//积分面板数据结构int score;在初始化方法中&#xff0c;添加(画出)食物与分数&…

层次分析法--可以帮助你做决策的简单算法

作用 层次分析法是一个多指标的评价算法&#xff0c;主要用来在做决策时&#xff0c;给目标的多个影响因子做权重评分。特别是那些需要主观决策的、或者需要用经验判断的决策方案&#xff0c;例如&#xff1a; 买房子&#xff08;主观决策&#xff09;选择旅游地&#xff08;…

Redis事务的理解与使用

文章目录 Redis 事务1)基本认识2)事务操作1.MULTI2.EXEC3.错误处理4.DISCARD5.WATCH6.SCRIPT Redis 事务 官方文档&#xff0c;永远是你学习的第一手资料&#xff1a;Redis 事务 1)基本认识 谈到事务&#xff0c;大家首先都会联想到 mysql 中复杂但又功能强大的“事务”&…

基于element-plus定义表单配置化扩展表单按钮

文章目录 前言一、新增btn.vue组件二、使用总结如有启发&#xff0c;可点赞收藏哟~ 前言 在后台管理系统一般都存在列表查询&#xff0c;且可输入数据进行查询 基于element-plus定义表单配置化 新增按钮配置化 一、新增btn.vue组件 <template><template v-for&qu…

如何做好性能压测?压测环境设计和搭建的7个步骤你知道吗?

简介&#xff1a;一般来说&#xff0c;保证执行性能压测的环境和生产环境高度一致是执行一次有效性能压测的首要原则。有时候&#xff0c;即便是压测环境和生产环境有很细微的差别&#xff0c;都有可能导致整个压测活动评测出来的结果不准确。 1. 性能环境要考虑的要素 1.1 系…

5种社交媒体策略提升你的SEO

你希望你的网站在多大程度上在搜索结果中被更多的受众看到&#xff1f;如果你想增加你网站的曝光率和流量&#xff0c;社交媒体整合是提高你的SEO的一个好方法。为了帮助公司和网站所有者提高他们的搜索引擎优化 &#xff08;SEO&#xff09;&#xff0c;这篇文章将介绍五种社交…

易点易动设备管理系统:提升企业设备保养效率的最佳选择

在现代企业中&#xff0c;设备的正常运行和保养对于业务的顺利进行至关重要。然而&#xff0c;传统的手动设备管理方式往往效率低下、容易出错&#xff0c;给企业带来不必要的成本和风险。为了解决这一问题&#xff0c;易点易动设备管理系统应运而生。本文将介绍易点易动设备管…