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

news2025/5/15 10:27:34

文章目录

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

数组名的理解

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

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.coloradmin.cn/o/2300642.html

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

相关文章

Open-WebUI官方部署文档

Github地址&#xff1a;GitHub - open-webui/open-webui: User-friendly AI Interface (Supports Ollama, OpenAI API, ...) 打开 WebUI &#x1f44b; 如果你是零基础的小白&#xff0c;不知道什么是DeepSeek的话&#xff1f;不知道如何本地化部署&#xff0c;我强烈建议先看…

爬虫破解网页禁止F12

右击页面显示如下 先点击f12再输入网址&#xff0c;回车后没有加载任何数据 目前的一种解决方法&#xff1a; 先 AltD &#xff0c;再 CtrlShifti

Xshell连接虚拟机ubuntu,报错(port 22): Connection failed.

Connecting to 192.168.37.131:22... Could not connect to 192.168.37.131 (port 22): Connection failed. 虚拟机ubuntu 可以ping通&#xff0c;但就是连接不上。 先后排查了&#xff0c; 1. 网络适配器是否被禁用 2.设置虚拟机网络适配器的网络连接模式为桥接模式&#xf…

浏览器报错:无法访问此网站 无法找到xxx.xxx.net的DNS地址。正在诊断该问题。尝试运行Windows网络诊断。DNS_PROBE_STARTED

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;希望我的文章能帮到您&#x1f7ea;如有兴趣可点关注了解更多内容 &#x1f4d8;博主信息 点击标题&#x1f446;有惊喜 &#x1f4c3;文章前言 &#x1f537;文章均为学习和工作中整理的笔记&#xff0c;分享记录…

2024年国赛高教杯数学建模C题农作物的种植策略解题全过程文档及程序

2024年国赛高教杯数学建模 C题 农作物的种植策略 原题再现 根据乡村的实际情况&#xff0c;充分利用有限的耕地资源&#xff0c;因地制宜&#xff0c;发展有机种植产业&#xff0c;对乡村经济的可持续发展具有重要的现实意义。选择适宜的农作物&#xff0c;优化种植策略&…

CentOS/RHEL如何更换国内Yum源

在国内使用CentOS或RHEL系统时&#xff0c;默认的Yum源是国外的&#xff0c;这可能导致软件包的下载速度慢&#xff0c;甚至出现连接超时的问题。为了解决这个问题&#xff0c;我们可以将Yum源切换到国内的镜像源&#xff0c;从而大大提高软件包的下载速度和稳定性。 本文将详…

A9. Jenkins Pipeline自动化构建,飞书通知

怎么收集服务部署信息?【前置】首先Python如何操作JSON数据如何记录部署信息,什么时机统计?如何下发某一服务统计信息 ?那么怎么创建飞书通知机器人呢?编写飞书通知脚本总结下面我们接着上一篇文章《A8. Jenkins Pipeline自动化部署过程,多模块远程服务协调实战》继续往下…

利用 OpenCV 进行棋盘检测与透视变换

利用 OpenCV 进行棋盘检测与透视变换 1. 引言 在计算机视觉领域&#xff0c;棋盘检测与透视变换是一个常见的任务&#xff0c;广泛应用于 摄像机标定、文档扫描、增强现实&#xff08;AR&#xff09; 等场景。本篇文章将详细介绍如何使用 OpenCV 进行 棋盘检测&#xff0c;并…

DeepMind发布Matryoshka(套娃)量化:利用嵌套表示实现多精度LLM的低比特深度学习

本文将介绍 Google DeepMind 提出的 Matryoshka 量化技术&#xff0c;该技术通过从单个大型语言模型 (LLM) 实现多精度模型部署&#xff0c;从而革新深度学习。我们将深入研究这项创新技术如何提高 LLM 的效率和准确性。 随着深度学习模型在规模和复杂度上持续增长&#xff0c…

Java-数据结构-(HashMap HashSet)

一、Tree和Hash的区别 在上一篇文章中&#xff0c;我们讲到了"TreeMap"和"TreeSet"&#xff0c;但当我们刷题的时候却会发现&#xff0c;实际应用Map和Set时&#xff0c;却常常都只会用"HashMap"和"HashSet"&#xff0c;这是为什么呢…

el-table 结合 slot 具名插槽遍历封装列表模板

基础效果 要实现以上效果&#xff0c;可对 el-table 进行封装&#xff0c;将列配置视为数组&#xff0c;循环 el-table-column 标签模板组件 <div class"tableSlot"><el-table :data"dataList" border><el-table-columnv-for"(item, …

游戏引擎学习第108天

仓库:https://gitee.com/mrxiao_com/2d_game_2 看一下我们现在的进展 今天的工作重点是处理游戏中的Z轴问题&#xff0c;特别是如何在不同的层级之间进行移动&#xff0c;并确保Z轴的处理方式与真实世界中的透视效果一致。当前&#xff0c;游戏中的Z轴运动存在问题&#xff0…

遵循规则:利用大语言模型进行视频异常检测的推理

文章目录 速览摘要01 引言02 相关工作视频异常检测大语言模型 03 归纳3.1 视觉感知3.2 规则生成Normal and Anomaly &#xff08;正常与异常&#xff09;Abstract and Concrete &#xff08;抽象与具体&#xff09;Human and Environment &#xff08;人类与环境&#xff09; 3…

网页制作01-html,css,javascript初认识のhtml的基本标记

一、 Html简介 英文全称是 hyper text markup language,超文本标记语言,是全球广域网上描述网页内容和外观的标准. Html作为一款标记语言,本身不能显示在浏览器中.标记语言经过浏览器的解释和编译,才能正确地反映html标记语言的内容. 1.html 的基本标记 1&#xff09;头部标…

linux常用命令大全(包括抓包、网络检测、路由等,做项目一点点总结而来!)

文章目录 常用命令**apt相关****ls**&#xff1a;**cd****cp****ls -l | grep ssh**&#xff1a;会列出当前目录中包含 “ssh” 的文件或目录的详细信息。**系统资源**linux路由相关抓包工具和命令tcpdumpwiresharktshark iperf 常用命令 通过上下方向键 ↑ ↓ 来调取过往执行过…

Linux学习笔记之虚拟地址空间

1.示例引入 运行如下代码那么运行结果如下图。 #include<stdio.h> #include<unistd.h>int main() {pid_t id fork();if(id-1){printf("创建进程错误&#xff01;\n");return 1;}int size0;if(id0){//子进程while(1){printf("我是子进程&#xff0c…

HTTP的“对话”逻辑:请求与响应如何构建数据桥梁?

一、前言 作为现代互联网通信的基石&#xff0c;HTTP协议定义了客户端与服务器之间的“对话规则”。每一次网页加载、API调用或文件传输的背后&#xff0c;都离不开精心构造的HTTP请求与响应。请求中封装了用户的意图——从请求方法、资源路径到提交的数据&#xff1b;响应则承…

[论文阅读] SeeSR: Towards Semantics-Aware Real-World Image Super-Resolution

文章目录 一、前言二、主要贡献三、Introduction四、Methodology4.1 Motivation &#xff1a;4.2Framework Overview.** 一、前言 通信作者是香港理工大学 & OPPO研究所的张磊教授&#xff0c;也是图像超分ISR的一个大牛了。 论文如下 SeeSR: Towards Semantics-Aware Rea…

LM Studio笔记

一、什么是 LM Studio&#xff1f; LM Studio 是一款功能强大、易于使用的桌面应用程序&#xff0c;用于在本地机器上实验和评估大型语言模型&#xff08;LLMs&#xff09;。它允许用户轻松地比较不同的模型&#xff0c;并支持使用 NVIDIA/AMD GPU 加速计算。 功能集&#xff1…