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



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

    数组名的理解

    先来看一段代码 

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

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

     

    如果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语句代表一种算法(加、减、乘、除) ,但是我们发现相似的代码出现了多份,显得有些冗余,这应该怎么办呢?

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

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

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

  • 相关阅读:
    控制基础学习(1)-干扰观测器
    【Android】App开发-控件篇
    Java计算机毕业设计视频网站的设计与实现源码+系统+数据库+lw文档
    Redis:hash类型底层数据结构剖析
    树莓派烧录系统
    【快速学习系列】Mybatis缓存和使用SpringBoot开启MyBatis缓存+ehcache
    Flutter 打印日志封装及创建Live Templates快捷打印日志
    最新ChatGPT/GPT4科研应用与AI绘图及论文高效写作
    【Jetson】使用 Jetson 控制无人车常用指令
    UI自动化测试是什么?什么项目适合做UI自动化测试
  • 原文地址:https://blog.csdn.net/2301_79188764/article/details/134357160