不允许你还不了解指针的那些事(二)(从入门到精通看这一篇就够了)(数组传参的本质+冒泡排序+数组指针+指针数组)

目录

数组名的理解

使用指针访问数组

一维数组传参的本质

冒泡排序

二级指针

指针数组

指针数组模拟二维数组

字符指针变量

数组指针变量

二维数组传参的本质

函数指针变量

函数指针变量的创建 

函数指针变量的使用

两段有趣的代码

代码一

代码二

typedef关键字 

函数指针数组

转移表


个人专栏:《零基础学C语言》

附赠:《数据结构世界》


不要划走!不要划走!这篇博客真的写了很久很久,呕心沥血,干货满满 ,能不能点个赞或者说一句鼓励的话来支持一下博主?

数组名的理解

先来看一段代码 

我们发现数组名和数组首元素的地址打印出的结果⼀模⼀样,数组名就是数组首元素(第⼀个元素)的地址 

但是下面这段代码怎么解释呢? 

 

如果arr代表首元素地址,那计算结果应该是4才对啊? 

其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有 两个例外
sizeof(数组名) ,sizeof中单独放数组名,这里的数组名表示整个数组, 计算的是整个数组的大小 ,单位是字节
&数组名 ,这里的数组名表示整个数组, 取出的是整个数组的地址 (整个数组的地址和数组首元素的地址是有区别的)

这里还看不出arr和&arr的区别,请看以下代码

这里我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是首元素的地址, +1就是跳过一个元素
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址, +1 操作是跳过整个数组 的。

使用指针访问数组

在这里数组名arr和指针p其实是等价的 

下列四种等价写法 

其实编译器在计算arr[i]时,就会把它转换为 *(arr+i),再进行计算  

所以下面展示一种奇特的写法

i[arr] <----> *(i+arr) <----> *(arr+i) <----> arr[i]  

一维数组传参的本质

首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给一个函数后,函数内部求数组的元素个数吗? 

这里为什么是1呢?因为前面学过,函数传参arr,数组名是首元素的地址 。函数形参arr实际上是一个整型指针,而x86环境下,其大小为4个字节,所以除以一个整型元素等于1 

那么再来看看这段代码,想想输出结果是什么呢?

形参arr是字符指针,还是4个字节,但是它一次只能访问1个字节,所以相除结果为4  

所以在函数内部,此时arr数组和指针没有区别,相互等价  

当然,上面这种写法还有缺陷,因为只能打印固定元素,一旦原数组改变,就没办法完整打印。所以,我们最好算好元素个数,传入函数。  

为什么传入函数就变成指针了呢?从C语言设计的角度考虑, 因为通过指针已经能访问整个数组,而且如果将整个数组都传入函数空间开销是非常大的,会造成空间浪费 

冒泡排序

 冒泡排序核心思想就是:两两相邻的元素进行比较 

先写一个基本框架  

再实现函数定义部分 ,先外层循环确定趟数,再内层循环确定每趟交换的对数,最后判断相邻元素大小,如果不满足顺序就交换 

这样就实现了冒泡排序。但是上述代码还可以再进行优化,试想一下,如果要排序的数组是

9,0,1,2,3,4,5,6,7,8  我们第一趟排序完便已经升序了 ,但是还在不停的循环判断。所以,我们可以这样改。

加入flag变量,表示数组当前是否有序。而判断有序的方法,则是如果一趟冒泡排序下来,没有一对交换,则证明有序。 反之,如果有交换,则flag置为0,表示无序,则继续下一趟冒泡排序。这样,就可以节省时间。 

二级指针

指针变量也是变量,是变量就有地址,那 指针变量的地址存放 在哪里?
这就是 二级指针

a的地址取出,放在一级指针p中;把p的地址取出,放在二级指针pp中 。二级指针类型有两个**,比如int**,前面的int*说明pp指向的对象类型,后面的*说明pp是指针变量 

*pp通过p的地址找到p,*(*pp)再对p解引用,通过p中存储a的地址找到a  

依此类推,***就是三级指针……,不过三级指针及以上就用得很少了  

指针数组

指针数组是 指针还是数组
我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。
那指针数组呢?是 存放指针的数组

我们先来做一下类比

那么,希望有一个数组,有5个元素,每个元素是整型指针,应该怎么写呢? 

应该怎么理解呢?arr先与[ ]结合为数组,有5个元素 ,每个元素是int*(整形指针类型)。指针数组的每个元素是地址,又可以指向一块区域 

指针数组模拟二维数组

那可能有同学会疑惑,这个指针数组有什么用呢?下面我们来演示用指针数组模拟二维数组 

存储了3个元素的指针数组每一个元素就是一个指针指向对应数组首元素的地址(数组名的理解)  

 

arr[ i ][ j ] ---->*( *(arr+i) + j ),两种等价写法 

字符指针变量

在指针的类型中我们知道有⼀种指针类型为字符指针 char* 

这里是把一个字符串放到pstr指针变量里了吗? 

不是把字符串abcdef\0存放在p中,而是把第一个字符的地址存放在p中  

1. 你可以把字符串想象为一个字符数组,但是这个数组是不能修改的
2. 当常量字符串出现在表达式中的时候,它的值是第一个字符的地址  

那我们就可以来看看一些奇特的写法 

数组名,一般就是首元素地址,那么这里常量字符串和字符指针p都存储的是第一个字符的地址,那么也能用数组的方式进行打印访问。  

 但因为常量字符串是不能修改的,所以最好在p前用const进行修饰 

 我们再来看一道有趣的题目,请分析打印的结果:

有的同学可能会惊讶,这是为什么呢?因为,str1和str2是两个数组,因此有不同的地址,而str3和str4都是字符指针,指向相同的常量字符串 ,根据C语言的规则,相同的常量字符串只会保存一份(为了节省内存空间) 

数组指针变量

之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
数组指针变量是 指针变量?还是数组?
答案是: 指针变量

 

我们已经熟悉:
整形指针变量: int * pi; 存放的是 整形变量 地址 ,能够指向整形数据的指针。
浮点型指针变量: float * pf; 存放 浮点型变量 地址 ,能够指向浮点型数据的指针。
数组指针变量 应该是:存放的应该是 数组的地址 ,能够指向数组的指针变量。

那我们来判断一下,下面的两段代码分别代表什么?

解释:p 先和*结合 ,说明p是⼀个 指针变量 ,然后指着指向的是一个大小为10个整型的数组。所以
p是一个指针,指向一个数组,叫 数组指针
注意: []的优先级要高于*号的,所以必须 加上()来保证p先和*结合

那有同学就会问了,数组指针变量怎么初始化?其实很简单,数组指针中存放的是整个数组的地址,那么只要&arr将数组的地址取出,放入数组指针中即可 

 这里再对比一下,普通整型指针都是存放数组arr首元素的地址+1跳过一个元素;而数组指针是存放数组arr整体的地址+1跳过整个数组 

 

二维数组传参的本质

有了数组指针的理解,我们就能够讲一下二维数组传参的本质了。 

让我们继续类比,过去我们讨论一维数组传参本质, 形参可以是数组,也可以是指针

为什么呢?

1.写成数组,更加直观,为了方便理解

2.写成指针,是因为数组传参,传过去的是数组首元素的地址

在之前扫雷项目的实现中,我们已经用过了二维数组传参,当时写的是数组的形式。所以,二维数组传参,写成数组是可以的,更加直观,方便理解,但是能写成指针的形式吗? 

可以的!二维数组,其实是元素为一维数组的数组。对于二维数组,首元素是第一行,首元素的地址,就是第一行的地址。那么根据数组名的理解,二维数组数组名就代表第一行的地址。 

二维数组传参本质 上也是传递了地址, 传递的是第一行这个一维数组的地址

函数指针变量

什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们的 类比关系 ,我们不难得出结论:
函数指针变量 应该是用来 存放函数地址的 ,未来通过地址能够调用函数的。

但是,数组名和函数名还是有所不同的 ,我们发现数组名是首元素的地址,&数组名才是整个数组的地址;但是函数名和&函数名都是函数的地址 

 

函数指针变量的创建 

那么,函数指针应该怎样表示呢?我们来类比一下: 

解释:*先与pf结合,表示它是一个指针变量,后面跟()表示函数调用,括号内表示函数参数,最左边表示函数返回类型  

再举一个例子

 

函数指针变量的使用

那函数指针怎么使用呢? 

 

我们平时调用函数,写的都是ret的形式,那么函数指针就可以替换函数名的部分,先对指针解引用,后面在输入参数  

 前面说过,函数名和&函数名,都是函数的地址。那么,在创建函数指针的时候,右侧可以不写&。同时,函数指针代表的也是函数地址,那么也可以不写*。 

上述四种写法都是等价的  

两段有趣的代码

请大家尝试思考一下下面两段代码表达的是什么意思?

代码一

首先,void(*)()是刚刚学过的函数指针类型参数为空,返回类型为void

其次,0前面的括号,表示强制类型转换,就比如  (int)3.14

最后,外层的  (*)( ),是一次对函数指针的调用,参数为空

 综上,这段代码是一次函数调用。先将0(数值)强制类型转换成函数指针类型(地址),再对它进行调用 

代码二

这段代码是一次函数声明signal是函数名。 

signal参数有两个,第一个是整型(int),第二个是函数指针类型,该指针指向的函数参数为int,返回类型为void  

signal返回类型,也是void (*)(int)函数指针类型,该指针指向的函数参数为int,返回类型为void  

typedef关键字 

是不是感觉上述函数声明太抽象,那我们就可以使用typedef关键字进行重定义

typedef 是用来类型重命名的,可以将复杂的类型,简单化

注意 : 数组指针和函数指针类型重定义时,重新定义的函数名要写在内部,不能写在最右侧 

这样,该代码是不是就好理解很多了?  

 

两段代码均出自:《C陷阱和缺陷》这本书  

函数指针数组

数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,

那要把函数的地址存到一个数组中,
那这个数组就叫 函数指针数组 ,那函数指针的数组如何定义呢?

parr1 先和 [] 结合 ,说明 parr1是数组,数组的内容是什么呢?
int (*)() 类型的 函数指针

 

函数指针数组小小的运用 

数组中每个地址都指向一个函数 

依此类推,那么数组指针数组又怎么表示呢?比如:int(*parr1[ 3 ])[ 3 ] 

上述例子,表示parr1是数组,数组的每个元素是int (*) [3]类型的数组指针,每个指针指向存储3个int(整型)的数组

转移表

前面的运用其实并不是函数指针数组的真正用途,下面来介绍它真正的好处。

函数指针数组 的用途: 转移表
举例:计算器的一般实现:

 我们想写一个加减乘除的计算器,先写一个菜单

主体框架用do-while循环和switch语句构建

紧接着是每条switch语句代表一种算法(加、减、乘、除) ,但是我们发现相似的代码出现了多份,显得有些冗余,这应该怎么办呢?

此时函数指针数组就派上用场了!这里的函数指针数组,我们称为转移表。这样,代码就简洁了不少,相同的代码只出现一份。

看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注

💛 💙 💜 ❤️ 💚💓 💗 💕 💞 💘 💖
拜托拜托这个真的很重要!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。  

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

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

相关文章

整理笔记——MOS管、三极管、IGBT

一、MOS管 在实际生活要控制点亮一个灯&#xff0c;例如家里的照明能&#xff0c;灯和电源之间就需要一个开关需要人为的打开和关闭。 再设计电路板时&#xff0c;如果要使用MCU来控制一个灯的开关&#xff0c;通常会用mos管或是三极管来做这个开关元件。这样就可以通过MCU的信…

计及源荷不确定性的综合能源生产单元运行调度与容量配置随机优化模型MATLAB

主要内容 本程序复现《计及源荷不确定性的综合能源生产单元运行调度与容量配置两阶段随机优化》模型&#xff0c;采用全年光伏、风电数据通过kmeans聚类得到6种场景&#xff0c;构建了随机优化模型&#xff0c;在研究融合P2G与CCS的IEPU系统框架基础上&#xff0c;建立了各关键…

linux关于cmake,makefile和gdb的使用

c文件的编译 安装环境(centos 7) 检查命令是否齐全 gcc --version g --version gdb–version 安装命令 yum -y install gcc-c安装g命令&#xff08;用于编译c/c文件&#xff09; yum -y install gcc安装gcc命令(用于编译c文件&#xff09; 每个都出现版本号&#xff0c;证明…

深度学习入门(第四天)——递归神经网络与词向量原理解读

一、RNN网络架构解读 常规神经网络并不能考虑时间序列的特征&#xff08;比如前天昨天今天或者带有前后关联的特征&#xff09;&#xff0c;现在每个特征都是独立考虑的&#xff0c;那么如果有这样的特征&#xff0c;网络应该怎么学呢 而递归递归网络hidden这里的转回箭头&…

C/C+=内存管理

C/C内存管理以及动态内存的申请_c动态内存的申请与释放_Demo Test的博客-CSDN博客 问题是&#xff0c;这个0x0804 8000 到0xC 0000 0000之间&#xff0c;不止3GB&#xff0c;应该有47GB&#xff0c;该怎么解释呢&#xff1f;

4.1 Windows驱动开发:内核中进程与句柄互转

在内核开发中&#xff0c;经常需要进行进程和句柄之间的互相转换。进程通常由一个唯一的进程标识符&#xff08;PID&#xff09;来标识&#xff0c;而句柄是指对内核对象的引用。在Windows内核中&#xff0c;EProcess结构表示一个进程&#xff0c;而HANDLE是一个句柄。 为了实…

Python 如何实现外观设计模式?什么是 Facade 外观设计模式?Python 设计模式示例代码

什么是&#xff08;Facade&#xff09;外观设计模式&#xff1f; 外观&#xff08;Facade&#xff09;设计模式是一种结构型设计模式&#xff0c;它提供了一个简化复杂系统接口的高级接口&#xff0c;使得系统更容易使用。外观模式通过定义一个高层接口&#xff0c;隐藏了系统…

2023年中国恒温蜡疗仪发展趋势分析:应用前景存有很大发展与探索空间[图]

恒温电蜡疗仪可将蜡熔化&#xff0c;利用蜡自身特点&#xff0c;能阻止热的传导、散热慢、气体和水分不易消失&#xff0c;保温性能优越。利用蜡能紧密贴于体表的可塑性&#xff0c;可加入其他药物协同进行治疗&#xff0c;也可将中药与蜡疗有机地结合在一起&#xff0c;产生柔…

【Linux】Linux进程间通信(二)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;Linux &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【Linux】…

《QT从基础到进阶·三十》QVariant的基础用法

很多时候&#xff0c;需要几种不同的数据类型需要传递&#xff0c;如果用结构体&#xff0c;又不大方便&#xff0c;容器保存的也只是一种数据类型&#xff0c;而QVariant则可以统统搞定。 QVariant可以保存QT和C常用类型&#xff0c;如果是自定义类型&#xff0c;比如struct,c…

什么是PWA(Progressive Web App)?它有哪些特点和优势?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

2023年中国逆流式冷却塔性能特点、应用领域及市场规模分析[图]

按冷却塔热交换时气流和水流方向不同的配置&#xff0c;机力通风冷却塔又可分为横流式冷却塔、逆流式冷却塔&#xff0c;目前主流的冷却塔型式为逆流式冷却塔&#xff0c;逆流式冷却塔&#xff08;counterflowcoolingtower&#xff09;是指水流在塔内垂直落下&#xff0c;气流方…