C语言的灵魂——指针(3)

前言:上期我们介绍了const修饰指针,saaert断言都是针对指针本身的,文章后面我们用指针与数组建立了联系,这种联系或者是关系就是这篇文章所要介绍的。上一篇文章的传送门:指针2

指针3

一,数组名的含义及理解

谈到数组想必大家都知道一个点就是:数组名就是数组的首地址,也是首元素的地址。这就是数组名的含义我=我们不妨写一个代码来验证一下:

c 复制代码
#include<stdio.h>
int main()
{
	int arr[10]={1,2,3,4,5,6,7,8,9,10};
	printf("%p\n",arr);//%p打印地址的格式占位符
	printf("%p\n",&arr[0]);//打印首元素的地址
	return 0;
}


通过运行结果我们能很明显的看到数组名就是数组首元素的地址。 既然已经知道它的含义了那我们为什么还要重点来讲它呢?如果没有例外那就不会在这里讲它了,下面我们来看代码:

c 复制代码
#include<stdio.h>
int main()
{
	int arr[10]={1,2,3,4,5,6,7,8,9,10};
	printf("%zd\n",sizeof(arr));
	return 0;
}

这段代码的运行结果会是什么呢?如果我们认为arr是首元素的地址,而数组元素的类型是int类型那么打印数来的大小应该就是4个字节或8个字节才对(4个字节还是8个字节由平台大小决定)。下面我们来看结果:

结果明显与我们所想的不符,那为什么会出现这种情况呢?其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有两个例外:

1.sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小, 单位是字节。

2.&数组名,这⾥的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的。

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


那我们再来看一段代码:

c 复制代码
#include <stdio.h> 
int main() 
{ 
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
	printf("&arr[0] = %p\n", &arr[0]); 
	printf("arr = %p\n", arr); 
	printf("&arr = %p\n", &arr); 
	return 0; 
}

看到这里有些人可能又会懵了,前面不是说 **&数组名** 取出的是整个数组的地址吗?那为什么这里 &arrarr &arr[0] 打印出来的地址是一样的呢?难道整个数组的地址与首地址是一样的吗?

这里就要引用我在指针(1)指针变量和地址中介绍&这个操作符所说的了:

但有一点需要特别注意取地址取出来的是较小的地址,因为我们知道数据类型占几个字节又知道较小的地址剩下的顺藤摸瓜直接往后推算即可。

要想探究 &arr 我们只需要加上偏移量就可以很明显的看出来了:

c 复制代码
#include <stdio.h> 
int main() 
{ 
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
	printf("&arr[0]   = %p\n", &arr[0]); 
	printf("&arr[0]+1 = %p\n", &arr[0]+1); 
	printf("arr       = %p\n", arr); 
	printf("arr+1     = %p\n", arr+1); 
	printf("\n");
	printf("&arr      = %p\n", &arr); 
	printf("&arr+1    = %p\n", &arr+1); 
	return 0; 
}


这⾥我们发现&arr0和&arr0+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr0 和 arr 都是 ⾸元素的地址,+1就是跳过⼀个元素。

但我们看到&arr------>&arr+116进制从A8变成了D0,我们用D0减去A8看看是不是40个字节就能验证&arr是不是整个数组的地址了。

我们发现&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。

看到这里大家应该搞清楚数组名的意义了吧。

二,使用指针访问数组

弄个清楚了数组名,这就让我们有了使用指针来访问数组的基础。我们来看一个常规访问数组的代码:

c 复制代码
#include<stdio.h>
int main()
{
	int arr[10]={0};
	int i=0;
	int sz=sizeof(arr)/sizeof(arr[0]);
	for(i=0;i<sz;i++)
	{
		scanf("%d",&arr[i]);
	}
	int j=0;
	for(j=0;j<sz;j++)
	{
		printf("%d\n",arr[j]);
	}
	return 0;
}

这是我们最原始的访问数组的方法,不知大家有没有注意到一个点,其实我们以前求数组元素长度用的就是 sizeof(arr)/sizeof(arr[0]) 只是当时我们不知道。这也说明了上面说所的点sizeof(数组名)求的是整个数组的大小。

言归正传,既然数组名就是地址,而地址就是指针那么能不能使用一个指针变量指向数组,然后使用指针来访问数组呢?当然可以:

c 复制代码
#include<stdio.h>
int main()
{
	int arr[10] = {0};
	int* p = arr;
	int i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0;i < sz;i++)
	{
		scanf("%d", p+i);
		//替换scanf("%d",&arr[i]);
	}
	int j = 0;
	for (j = 0;j < sz;j++)
	{
		printf("%d ", *(p + j));
		//替换printf("%d\n",arr[j]);
	}	
	return 0;
}

这个代码搞明⽩后,我们再试⼀下,如果我们再分析⼀下,数组名arr是数组⾸元素的地址,可以赋值 给p,其实数组名arr和p在这⾥是等价的。那我们可以使⽤arri可以访问数组的元素,那pi是否也可 以访问数组呢

c 复制代码
#include<stdio.h>
void test(int arr[], int sz1)//参数本质上就是int *arr 
{                    //要想求数组元素个数需要传sz1过来
	int sz2 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz2);
	printf("%d\n", sz1);
}
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz1 = sizeof(arr) / sizeof(arr[0]);
	printf("%d\n", sz1);
	test(arr,sz1);

	
	return 0;
}

这样也是可行的,将*(p+i)换成pi也是能够正常打印的,所以本质上pi 是等价于 *(p+i),实际上我们写出数组下标的形式来访问数组元素的时候,编译器在处理的时候还是转换成指针来处理。

那我们想象一下 *(i+arr) 是否能写成 i[arr] 呢?答案是可以的只不过代码的可读性不高,其实 i[arr] 也不难理解 这个操作符是下标引用操作符,i和arr只是它的两个两个操作数而已并不影响实际的结果。

同理arri 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移 量求出元素的地址,然后解引用来访问的。

接着我们来看看一维数组传参

三,一维数组传参的本质

前面在讲函数传参的时候我们也讲过数组传参,学完了指针后我们便可以来探索一下它的本质,先来看一段代码:

c 复制代码
#include<stdiio.h>
//一维数组传参的时候形参是可以写成数组的形式,但是即使写成数组的形式本质其实是指针变量。
void test(int arr[],int sz1)//参数本质上就是int *arr 
{                    //要想求数组元素个数需要传sz1过来
	int sz2=sizeof(arr)/sizeof(arr[0]);
	printf("%d",sz2);
	printf("%d",sz1);
}
int main()
{
	int arr[10]={1,2,3,4,5,6,7,8,9,10};
	int sz1=sizeof(arr)/sizeof(arr[0]);
	printf("%d",sz1);
	test(arr,sz1);
	return 0;
}

通过运行结果我们有个疑惑为什么sz2的结果是2呢?首先我们能够确定的一点是 sizeof(arr[0]) 的大小就是4个字节(一个整型类型)这点是毫无疑问的,那么谁/4等于2呢?小学生都会,答案是8。那为什么是8呢?
注意sz2的值也可能是1这而取决于平台大小!

还记得最开始举的第二个例子吗?
如果我们认为arr是首元素的地址,地址就是指针;而数组元素的类型是int类型那么打印数来的大小应该就是4个字节或8个字节才对(4个字节还是8个字节由平台大小决定)。

所以我们得出一个非常重要的结论,数组传参本质上传过去的就数组元素的首地址(是一个指针)!

所以函数形参的部分理论上应该使用指针变量来接收⾸元素的地址。那么在函数内部我们写 sizeof(arr) 计算的是⼀个地址的大小(单位字节)⽽不是数组的⼤⼩(单位字节)。正是因为函 数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

四,二级指针

前面我们介绍了一级指针变量,指针变量就是用来存放普通变量的地址;那以此类推指针变量的地址当然就是用二级指针来存储。

二级指针与一级指针一样无非就是对指针变量本身的使用以及解引用:

c 复制代码
#include<stdio.h>
int main()
{
//使用指针对象
	int a = 10;
	int* pa = &a;
	int** ppa = &pa;
//解引用
	*pa = 20;
	printf("*pa=%d\n", *pa);

	int m = 100;
	*ppa = &m;//pa=&m
	printf("%p\n", &m);
	printf("%p\n", pa);

	printf("%d\n", **ppa);//对Pa解//printf("*pa=%d\n,*pa");

	** ppa = 30;//直接对m进行操作
	printf("**ppa=%d\n",**ppa);
}

所以 *pa pa解引用就是改变a的值,改变 *ppa 对就是改变 *pa 的所存的地址,**ppa 对解引用就是改变a的值。就像有三个抽屉第一第二个抽屉都有锁,第三个抽屉放着第二个抽屉的钥匙,第二个抽屉放着第一个抽屉的钥匙。

五,指针数组

前面我们学过
字符数组------是数组,是用来存放字符的数组;char arr5
整型数组------是数组,是用来存放整型的数组;int arr5

以此类推:
指针数组------是数组,是用来存放指针的数组;int*arr5 (存放整型指针的数组)

c 复制代码
#include<stdio.h>
int main()
{
	int a=10;
	int b=20;
	int c=30;
	int d=40;
	int e=50;
	//与其一个个用指针变量来存放abcde的地址还不如直接用一个指针数组来存放
	int *arr[5]={&a,&b,&c,&d,&e};
	return 0;
}

指针数组的每个元素都是用来存放地址(指针)的.

而指针数组的每个元素是地址,⼜可以指向⼀块区域:

六,指针数组模拟二维数组

明白上面的基础知识后我们就可以用指针数组来模拟一下二维数组了:

c 复制代码
#include <stdio.h> 
int main() 
{ 
	int arr1[] = {1,2,3,4,5}; 
	int arr2[] = {2,3,4,5,6}; 
	int arr3[] = {3,4,5,6,7}; 
	//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 
	int* parr[3] = {arr1, arr2, arr3}; 
	int i = 0; 
	int j = 0; 
	for(i=0; i<3; i++) 
	{ 
		for(j=0; j<5; j++) 
		{ 
			printf("%d ", parr[i][j]); 
			//parr[i]其实就是代表arr1/arr2/arr3
			//parr[][j]其实就是输出每一个数组内的所有元素
		}
	printf("\n"); 
	}
	return 0;
}


parri是访问parr数组的元素,parri找到的数组元素指向了整型⼀维数组,parrij就是整型⼀维数 组中的元素。 上述的代码模拟出⼆维数组的效果,实际上并⾮完全是⼆维数组,因为每一行并非是连续的。

好了以上就是本章的全部内容啦!
感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!

相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区11 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜12 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa12 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio