不要划走!不要划走!这篇博客真的写了很久很久,呕心沥血,干货满满 ,能不能点个赞或者说一句鼓励的话来支持一下博主?
先来看一段代码
我们发现数组名和数组首元素的地址打印出的结果⼀模⼀样,数组名就是数组首元素(第⼀个元素)的地址。
但是下面这段代码怎么解释呢?
如果arr代表首元素地址,那计算结果应该是4才对啊?
这里还看不出arr和&arr的区别,请看以下代码
在这里数组名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语言的规则,相同的常量字符串只会保存一份(为了节省内存空间)
那我们来判断一下,下面的两段代码分别代表什么?
那有同学就会问了,数组指针变量怎么初始化?其实很简单,数组指针中存放的是整个数组的地址,那么只要&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 是用来类型重命名的,可以将复杂的类型,简单化。
注意 : 数组指针和函数指针类型重定义时,重新定义的函数名要写在内部,不能写在最右侧
这样,该代码是不是就好理解很多了?
两段代码均出自:《C陷阱和缺陷》这本书
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,
函数指针数组小小的运用
数组中每个地址都指向一个函数
依此类推,那么数组指针数组又怎么表示呢?比如:int(*parr1[ 3 ])[ 3 ]
上述例子,表示parr1是数组,数组的每个元素是int (*) [3]类型的数组指针,每个指针指向存储3个int(整型)的数组
前面的运用其实并不是函数指针数组的真正用途,下面来介绍它真正的好处。
函数指针数组 的用途: 转移表举例:计算器的一般实现:
我们想写一个加减乘除的计算器,先写一个菜单
主体框架用do-while循环和switch语句构建
紧接着是每条switch语句代表一种算法(加、减、乘、除) ,但是我们发现相似的代码出现了多份,显得有些冗余,这应该怎么办呢?
此时函数指针数组就派上用场了!这里的函数指针数组,我们称为转移表。这样,代码就简洁了不少,相同的代码只出现一份。
看到这里了还不给博主扣个:
⛳️ 点赞
☀️收藏
⭐️ 关注
!
💛 💙 💜 ❤️ 💚💓 💗 💕 💞 💘 💖
拜托拜托这个真的很重要!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。