当前位置: 首页 > news >正文

C语言——深入理解指针(2)(数组与指针)

文章目录

  • 数组名的理解
  • 使用指针访问数组
  • 一维数组传参的本质
  • 冒泡排序
  • 二级指针
  • 指针数组
  • 指针数组模拟二维数组

数组名的理解

之前我们在使用指针访问数组内容时,有这样的代码:

int arr[10]={1,2,3,4,5,6,7,8,9,10};
int* p=&arr[0];

这里我们使用&arr[0]的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址。
我们来做个测试,看数组名到底是不是首元素的地址:
在这里插入图片描述
从打印出来的值来看,它们确实是一样的,所以数组名确实是首元素的地址。
那么数组名是首元素地址的话,我们去求地址长度,在32位平台下应该为4个字节(地址的长度与多少位的平台有关)
我们用sizeof计算下地址长度是多少:
在这里插入图片描述
这是为什么呢?
数组名就是数组首元素(第一个元素)的地址,但是有两个例外:

  • sizeof(数组名),sizeof中单独放数组名时,这里的数组名表示整个数组,不是首元素的地址,此时计算的是这整个数组的大小,单位是字节。
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)在这里插入图片描述
    从值的角度来看的话,取整个数组的地址与取首元素的地址打印出来的值一模一样。
    这是因为无论是取首元素地址,还是取整个数组的地址,它们的值都是从首元素的地址开始的。
    但是我们平时写代码的时候要知道,数组名前+取地址符号是取整个数组的地址。
    如果我们想看看它们的区别还是可以看到的,我们给首元素的地址、整个数组分别都+1:
    在这里插入图片描述
    在这里插入图片描述
    我们发现,前两个地址+1都只跳过了一个整形的大小,而第三个地址+1却跳过了一个数组的大小(40个字节)
    所以我们将三个地址打印出来只能看到打印出来的值是一样的,但是类型绝对是不同的,因为int*类型+1跳过的应该是一个整形,而不是40个字节。

除此之外,任何地方使用数组名,数组名都表示首元素的地址。


使用指针访问数组

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。
为什么在访问数组的时候可以使用指针呢?
1.因为数组在内存中是连续存放的,只要是连续存放的,只要找到第一个,就可以顺藤摸瓜找到其他的了。
2.指针±整数的运算,方便我们获得每一个元素的地址

接下来我们写个代码,实现用指针访问数组,给数组的每个元素输入值,再输出值。在这里插入图片描述
注意:
数组就是数组,是一块连续的空间(数组的大小与元素个数和元素类型都有关系)
而指针(变量)就是指针(变量),是一个变量(通常是4/8个字节)
那么它俩之间的联系是什么呢?
数组名是地址,是首元素的地址,可以使用指针来访问数组

拓展:
我们知道arr[i]*(arr+i)这两种写法是完全等价的,并且*(arr+i)里面的式子是加法,加法是支持交换律的。所以arr[i]==*(arr+i)==*(i+arr)
我们代入代码中看会不会错:
在这里插入图片描述
结果也是正确的。
既然这三个等价:arr[i]==*(arr+i)==*(i+arr)
*(arr+i)可以写成arr[i],那么*(i+arr)可不可以写成i[arr]呢?
我们带入代码试试:
在这里插入图片描述
我们发现即使是这样程序也没有问题。那么这说明了什么呢?
我们知道[]是个下标引用操作符,既然加号操作符+可以实现:1+2可以写成2+1;那么,arr[i]也就可以写成i[arr]
提示:这种方式虽然可行,但是不推荐。
因为这样写不利于阅读代码。


一维数组传参的本质

数组我们学过,数组是可以传递给函数的,现在我们讨论一下数组传参的本质。
首先从第一个问题开始:
写个函数实现数组元素的打印,我们之前都是在函数外部计算数组元素的个数:
在这里插入图片描述
那我们可以只把数组传给函数,少传一个参数。然后在函数内部求数组的元素个数再打印数组嘛?在这里插入图片描述
此时我们这样写却出现错误,数组里的元素只打印了一个,这是为什么呢?我们进行调试:

  • 按F11开始调试,创建数组执行完后打开监视,查看创建的数组有没有问题:
    在这里插入图片描述
  • 发现创建数组没有问题后按F11进入函数内,在监视里输入arr,10查看数组传参有没有问题:在这里插入图片描述
    此时数组的传参也没有问题,我们继续调试。
  • 当我们继续往下执行的时候却发现原本期望的值是10,这里sz却显示的是1在这里插入图片描述
    如果sz是1的话,在下面循环中只能循环一次,所以也就只能打印第一个元素的。
    此时就找到了问题所在,是sz出问题了。

所以这为什么会算错呢?
我们之前讲过,数组传参有两个例外:sizeof(数组名)&数组名
只有这两种例外表示的是整个数组,而现在这段代码中的Print(arr)并不算这两种情况,所以此时传的是首元素的地址。

那我们将首元素的地址传过去,Print()函数接收不应该用指针嘛?在这里插入图片描述
这个函数接收的正确写法就应该是int* p这样的指针来接收。

那为什么我们之前用int arr[10]这样数组的形式去接收呢?
因为数组传参的时候,形参的部分是可以写成数组的形式的。对于我们之前初学来说,传的是数组,就用数组来接收,这样讲法对于我们初学接受度很好。
形参的部分虽然可以写成数组的形式,但是本质上还是指针变量(地址)。就相当于这个地方是int* arr在这里插入图片描述
既然Print()不属于那两种例外,所以传的就是个地址,形参也就是个指针变量。
那么下面的sizeof(arr)就不是求一个数组的大小了,而是求一个指针变量的大小:在这里插入图片描述
x86的环境下,无论什么类型的指针,都是4个字节。

所以数组传参的时候,形参是可以写成数组的形式的,但是本质上还是一个指针变量(地址),下面要求大小的时候sizeof(arr)求的就不是一整个数组的大小,而是首元素地址的大小。
所以int sz = sizeof(arr) / sizeof(arr[0])是得不到元素个数的

还有一点要说明:
我们说数组传参的时候传的是首元素的地址(假设是0x0012ff40),所以传给函数的地址也是0x0012ff40
所以我们在这个函数里使用的都是0x0012ff40这个地址在这里插入图片描述
既然实参使用的是0x0012ff40,形参也是使用0x0012ff40,那么实参跟形参使用的数组不就是一样的嘛?
所以我们得到以下结论:

  • 数组传参的本质是传递了数组首元素的地址,所以形参使用的数组跟实参使用的数组一定是同一个数组。
  • 既然形参使用的数组跟实参使用的数组是同一个数组,所以形参的数组是不会单独再创建数组空间的,形参的数组是可以省略掉数组大小的:在这里插入图片描述
    在这里插入图片描述
    注意:无论形参部分写不写10,都不影响下面int sz = sizeof(arr) / sizeof(arr[0])这个表达式(正确写法时这个表达式一定要放在函数外面!)

接下来我们将这段错误代码改过来:
1.既然数组传的是地址,函数的形参就写成指针
2.求数组元素的表达式放在函数外面
正确代码:
在这里插入图片描述


冒泡排序

冒泡排序解决的是排序的问题。
冒泡排序的核心思想就是:两两相邻的元素进行比较。
假设我们这里有一串降序的数字:9 8 7 6 5 4 3 2 1 0
我们要排成升序:0 1 2 3 4 5 6 7 8 9 ,此时该怎么用冒泡排序排成升序呢?
在这里插入图片描述
在这里插入图片描述
就这样一对一对的比较下去,最后9会到最后:
在这里插入图片描述
因为9是最大的一个元素,所以它无论与谁进行比较,都来到最后一位。
这样将9放在最后,我们叫做一趟冒泡排序
所以剩下该解决9前面的数字
在这里插入图片描述
所以,一趟冒泡排序解决一个数字。第一趟解决了最大的,第二趟解决了次大的…
那么这个地方有10个元素,要有几趟冒泡排序呢?
9趟。因为前9个数字在进行冒泡排序后,最后一个数字应该已经在它们应该在的位置上了。
所以是n个元素的时候,我们需要进行n-1趟冒泡排序。

我们开始写代码进行实现:

  • 先将元素和排序的函数创建出来在这里插入图片描述

  • 当我们把排序函数名字写好之后就是传参了,因为我们要排的是这个数组,所以数组需要传进函数。
    并且冒泡排序的趟数得依据元素的个数,而函数内部不能求元素个数,所以得在主函数里面求元素的个数在这里插入图片描述

  • 接下里写冒泡排序里的内容

    • 写形参:指针接收数组(用数组形式也行),int sz接收元素个数。
      因为这个函数只用排好序就行了,所以返回类型写个void就可以了。在这里插入图片描述

    • 因为冒泡排序是一趟解决一个元素,所以首先要考虑趟数在这里插入图片描述
      上面我们说过,当有n个元素的时候,我们需要进行n-1趟冒泡排序,所以i<sz-1

    • 趟数确定后就思考一趟的排序过程:
      一趟排序的过程就是两个相邻元素之间相比较,我们创建一个变量j视为元素下标,使得相邻两元素之间相比较。在这里插入图片描述

    • 接下来我们分析:一趟冒泡排序要进行多少对比较。
      以上面的例子来说,当我们进行第一趟冒泡排序的时候,待排序的元素有10个,需要进行9对比较在这里插入图片描述
      所以代码循环次数可以写成这样,控制9次相邻元素进行比较
      在这里插入图片描述
      但是我们发现:当我们进行第二趟冒泡排序的时候,待排序的元素只有9个,需要进行的是8对比较在这里插入图片描述
      所以比较次数也与趟数有关:
      当第一趟冒泡排序时有10个待排元素,我们要进行9对比较;
      当第二趟冒泡排序时有9个待排元素,我们要进行8对比较;
      即第三趟冒泡排序时有8个待排元素,我们要进行7对比较…
      所以我们将循环次数改为:j<sz-1-i(因为i控制了趟数)
      在这里插入图片描述
      此时当第一趟冒泡排序的时候,就可以进行9对比较;第二趟冒泡排序的时候,就可以进行8对比较了…

  • 在冒泡排序函数执行完之后,我们写个函数将这个被排完序的数组打印出来:
    在这里插入图片描述

完整的代码为:

void bubble_sort(int arr[], int sz)
{//趟数int i = 0;for (i = 0; i < sz - 1; i++){//一趟排序的过程int j = 0;for (j = 0; j <sz-1-i ; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ",arr[i]);}
}int main()
{int arr[] = {9,8,7,6,5,4,3,2,1,0};//排序int sz = sizeof(arr) / sizeof(arr[0]);//元素个数bubble_sort(arr,sz);//打印print_arr(arr,sz);return 0;
}

打印出的结果:
在这里插入图片描述
注意:这段代码是可以优化的。
当需要排序的数组本身是接近有序的情况时:9 0 1 2 3 4 5 6 7 8,此时只需要进行一趟冒泡排序就可以变成:0 1 2 3 4 5 6 7 8 9 在这里插入图片描述
那我们该怎么进行优化呢?

  • 我们定义一个变量flag,在进行每趟冒泡排序之前都假设这趟元素已经有序:在这里插入图片描述

  • 如果这趟数组不是有序的,将flag改为0:
    例如9 0 1 2 3 4 5 6 7 8这个数组第一趟冒泡排序时9与0交换了,所以这趟并不是有序的,就将flag改为0,进行下一趟的比较。
    在这里插入图片描述

  • 下一趟全部比较完后发现并没有进入到if语句中(0 1 2 3 4 5 6 7 8 98次比较全部不符合if中的表达式,没进入if语句中),所以flag还是为1,此时就可以跳出循环,不再进行第3趟、第4趟的比较排序了。
    在这里插入图片描述
    注意:这个break跳的是这个循环,break是在这个循环里的
    在这里插入图片描述

所以,当任何一对元素交换了,就说明这一趟的元素不是有序的,就得进行下一趟的元素比较、交换。
若是下次没有元素的交换,就说明这一趟的元素就是有序的,不用再进行下一趟了,所以直接跳出循环。


二级指针

我们写段常见的代码:
在这里插入图片描述
这就是我们之前用的一级指针。
那么什么是二级指针呢?
我们根据上面这段代码画出图:
在这里插入图片描述
既然p也有地址,那么我们就可以通过&p拿到p的地址,然后放进变量pp里。此时pp就存放着一个一级指针变量的地址,我们叫pp二级指针
在这里插入图片描述在这里插入图片描述

那么pp的类型该怎么写呢?
pp的类型为int**,完整写法:int** pp=&p;
对于这个类型int**我们该怎么去理解呢?
首先我们要知道,int**是二级指针的类型。在这里插入图片描述
所以一级指针与二级指针的类型理解思路是一样的,分两部分理解:

  • 最后一个*说明该变量是指针
  • 前面一部分说明该变量指向的对象类型

那么,此时p+1pp+1各自跳过几个字节呢?
我们知道,指针±整数与指针的类型有关。因为p是整形指针,指向的对象为整形,所以p+1跳过4个字节。
那么pp+1呢?
因为pp指向的是int*类型的变量,也就是指针变量。
所以我们就要看指针变量的大小为多少,就跳过几个字节。有可能是4个字节,也有可能是8个字节。
在这里插入图片描述
如果我们想取出pp的地址可以吗?
当然是可以的,此时得写成:int*** ppp=&pp;
同样的:int**说明ppp指向的对象(pp)是int**类型的,最后一个*说明ppp是指针变量。此时ppp就是一个三级指针。在这里插入图片描述
可以一直往下推,但是不建议。

那么二级指针到底是怎么样用的呢?
如果我们要通过pp找到p有什么办法呢?
解引用ppp的地址打印出来:
在这里插入图片描述

*pp——访问pp里存的地址,找到这个地址后拿该地址里的数据(对pp里的地址进行解引用)

我们也将a的地址打印出来,看看*pp的值是不是a的地址
在这里插入图片描述
发现值确实一样。
我们通过对二级指针解引用,可以找到一级指针内存的数据,再进行解引用,就可以找到10了.

*pp==p==&a;
//通过对pp里的地址解引用找到p(a的地址)
//对*pp再次解引用:
**pp==*p=10

在这里插入图片描述
所以,对二级指针变量两次解引用可以间接的找到10.


指针数组

指针数组是指针还是数组呢?
答案是数组。
我们类比一下:整型数组,是存放整型的数组;字符数组,是存放字符的数组。在这里插入图片描述

所以指针数组,是存放指针的数组。
在这里插入图片描述
指针数组的每个元素都是地址,可以指向⼀块区域

那么指针数组有什么用呢?
我们可以用指针数组模拟二维数组。


指针数组模拟二维数组

举例:
在这里插入图片描述
当我们写下这样的代码的时候,我们知道,这三个数组各自是一块内存空间,在内存里也并不是连续存放的,可能离得很远。

如果我们想把这三块空间弄成二维数组:将这三个数组分别当成二维数组的第一行、第二行、第三行。

我们该怎么办呢?
因为数组名是首元素的地址,我们将这三个数组首元素的地址放入到一个指针数组arr中,通过访问这个指针数组,再访问到这三个数组。
这样就可以模拟出一个二维数组了:
在这里插入图片描述
当我们写上arr[0]的时候,就是访问arr数组中arr1这个元素,而这个元素是arr1数组首元素的地址,所以我们就可以在这个地址的基础上进行+整数,进行arr1数组元素的遍历。
同理,当我们写上arr[1]的时候,就是访问arr数组中arr2这个元素,而这个元素是arr2数组首元素的地址,所以我们也就可以找到arr2数组中的每个元素了…

//可以看作访问第一行所有元素
arr[0][j];  j:0~4//访问第二行所有元素:
arr[1][j];  j:0~4//访问第三行所有元素:
arr[2][j];  j:0~4

这样就实现了指针数组模拟二维数组

因为arr1共有5个元素,首元素地址+0为元素1的地址,首元素+1为元素2的地址…首元素+4就为元素5的地址了,所以整数j的取值范围就为0~4

我们继续写代码:
在这里插入图片描述
实现了每个元素的打印。

注意:这种写法并不是真的二维数组,真的二维数组在内存中是连续存放的,而这种写法只是通过地址将三个分散的数组看为三行,再进行打印,这里的arr并不是真的二维数组。

那么既然arr并不是真的二维数组,那么代码中的打印为什么要写成二维数组的形式呢?
在这里插入图片描述
因为:在这里插入图片描述


http://www.mrgr.cn/news/91463.html

相关文章:

  • Arm64架构CentOS7服务器搭建Fabric环境
  • Django 5实用指南(二)项目结构与管理
  • 【MySQL安装】
  • TMS320F28335二次bootloader在线IAP升级
  • 云计算架构学习之Ansible-playbook实战、Ansible-流程控制、Ansible-字典循环-roles角色
  • Docker安装Minio对象存储
  • 天翼云910B部署DeepSeek蒸馏70B LLaMA模型实践总结
  • 如何使用 vxe-table grid 全配置式给单元格字段格式化内容,格式化下拉选项内容
  • 小米电视维修记录 2025/2/18
  • Ubuntu学习备忘
  • 【TOT】Tree-of-Thought Prompting
  • python进阶篇-面向对象
  • 23种设计模式 - 模板方法
  • cesium视频投影
  • 前端VUE+后端uwsgi 环境搭建
  • Breakout Tool
  • 9.PG数据库层权限管理(pg系列课程)第2遍
  • ubuntu22.04离线安装K8S
  • DeepSeek部署到本地(解决ollama模型下载失败问题)
  • 0.1 量海航行:量化因子列表汇总(持续更新)