c语言:从指针到指针数组

一、地址的相关介绍

1、指针

指针变量是为了储存内存地址的一个变量,在我们创建一个变量的时候,就会在内存中申请一块区域,这块区域就是这个变量的地址。以整形变量为例,我们创建一个变量a,就会在内存空间申请四个字节的地址:

0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73

由于这个四个地址的序号都是连着一块的,当我们知道第一个字节的地址时,就可以顺便地推出另外三个字节的地址。所以为了方便,指针指向a时,实际上指向的是a的第一个字节。

我们如何去取出一个元素的地址呢?这里就要介绍一个操作符了:&,取地址操作符。

int main()
{
 int a = 10;
 &a;//取出a的地址
 printf("%p\n", &a);//%p专门用来打印地址
 return 0;
}

这样就会打印出a所占四个字节的第一个字节的地址,比如:0x006FFD70,我们一般使用一个指针变量来接收 ,指针变量也是⼀种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。

int *p=&a;

 这里的我们用了int * p=&a来定义一个p指针。

*表示p是一个指针,前面的int则表示这个指针指向的是一个int类型的对象,所以,当我们要存储的地址是一个char类型的数据时,我们就应该使用:char* 。

我们拿到了地址,又改如何去使用呢?这里也有一个操作符:*,解引用操作符

int main()
{
 int a = 100;
 int* pa = &a;
 *pa = 0;
 return 0;
}

我们把a的地址给了指针pa,指针变量就是指向的a的地址,于是我们用解引用操作符*来通过pa中存放的地址,找到指向的空间,也就是说,*pa就相当于是a了,我们这时将*pa赋值于0,就相当于把a的值改成了0 。

指针变量也是有类型的,比如int*,char*,甚至还有void*这一可以接收任意数据类型的指针,但不论是是什么类型的指针变量,在相同的平台,他们的大小也是一定的,在64位操作系统下是8,在32位操作系统下是4。

指针变量的大小既然在同一操作系统下是一样的,那为什么还要分这么多类型呢?

#include <stdio.h>

//代码1
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}

//代码2

int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 *pc = 0;
 return 0;
}
通过编译与监视我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0。
所以指针的类型决定了对指针解引用的时候有多⼤的权限(⼀次能操作几个字节)。
⽐如: char* 的指针解引用就只能访问⼀个字节,而  int* 的指针的解引用就能访问四个字节。

当然,指针类型也会影响指针进行加减操作时跳过的字节大小。

int main()
{
	int n = 1;
	int* pn = &n;
	char* pc = (char*)&n;
	printf("%p\n",&n);
	printf("%p\n",pn);
	printf("%p\n",pc);
	printf("%p\n",pn+1);
	printf("%p\n",pc+1);

	return 0;
}

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。
这就是指针变量的类型差异带来的变化。
 
指针中有一种野指针,它指向的位置是不可知的(随机的、不正确的、没有明确限制的),会对程序运行造成危害,所以我们要避免野指针的出现。野指针的成因通常有以下三种:
1、 指针未初始化,2、指针越界访问,3、指针所指向的空间被释放

2、传值调用与传址调用

在下面的的代码中:

int swap(int a, int b)
{
	int c = a;
	a = b;
	b = c;
}

int main()
{
	int a = 10;
	int b = 20;
	swap(a, b);
	printf("%d,%d\n", a, b);
	return 0;
}

我们再swap函数中对a,b的值进行了交换,但是打印出来的a,b的值仍然是原理的值,这是为什么呢?

这是因为形参与实参是两个互不影响的参数,实参与形参的地址不同,而当我们使用swap函数时i,我们只是将实参的数值拷贝到了形参上,并不是把实参的地址传过去,所以我们对形参进行交换,并不会找到实参的地址,从而对实参的数值进行交换。

那要如何才能让实参交换呢,这里就要运用指针的知识了:

void swap2(int* a, int* b)
{
	int num = *a;
	*a = *b;
	*b = num;
}

int main()
{
	int a = 10;
	int b = 20;
	swap1(a, b);
	printf("%d,%d\n", a, b);
	swap2(&a, &b);
	printf("%d,%d\n", a, b);
	return 0;
}

像这样,我们把a与b的地址当做是实参,传递到形参指针变量里面,通过对a,b地址上面存储的值进行交换,完成了交换a,b值的功能。

3、数组中的地址

数组中的变量也都有地址,例如

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

中,我们定义了一个arr数组,然后又定义了一个指针p来指向该数组。

我们使用  &arr[0] 的方式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,⽽且
是数组首元素的地址,我们来做个测试。
int main()
{
	int arr[3] = { 1,2,3 };
	printf("arr[0]=%p\n", &arr[0]);
	printf("arr=   %p", arr);
	return 0;
}

显然我们默认打印数组的地址的时候,打印出的是数组首元素的地址,但当我们用sizeof打印arr的大小时,却出现了意外:

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int ret = sizeof(arr);
	printf("%d\n", ret);
	return 0;
}
打印的结果是40而不是我们想象中的4,这是因为这里的arr指的是整个数组。
其实数组名在多数情况下是数组首元素的地址,但是有两个例外:
sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的⼤⼩, 单位是字节
&数组名,这例的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素的地址是有区别的)

4、数组的地址首元素地址的差别

我们之前说到,整个数组的地址与首元素的地址的有区别的,那么区别在于哪里呢?

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", arr + 1);
	printf("%p\n", &arr[0]);
	printf("%p\n", &arr[0] + 1);
	printf("%p\n", &arr);
	printf("%p\n", &arr + 1);
	return 0;
}

ji

我们发现&arr[0]和&arr[0]+1相差4个字节,arr和arr+1 相差4个字节,是因为&arr[0] 和 arr 都是
⾸元素的地址,+1就是跳过⼀个元素。
但是&arr 和 &arr+1相差40个字节,这就是因为&arr是数组的地址,+1 操作是跳过整个数组的。 

5、指针访问数组

结合数组,指针与地址的关系,我们就可以很轻松的使用指针来访问数组了

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;//让p指向数组arr
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", arr[0] + i);
		printf("%d\n", *(p + i));

	}

	return 0;
}

我们发现,arr[0]+i与*(p+i)是等效的,这就是因为其实两者的本职都是找到地址,然后在进行改变。

我们可以通过arr[i]找到地址,那也可以通过p(i)找到地址。

两者传参的本质都是差不多,都是传递的地址,所以⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

二、指针数组

1、二级指针与多级指针

我们学会了用指针变量指向一个变量的地址,那么我们如何指向一个指针的地址呢?

这里就要用到二级指针了:

int main()
{
	int a = 10;
	int* p = &a;
	int** pa = &pa;

	return 0;
}

这里的*pa就是一个指向指针p地址的二级指针,**pa 先通过 *p找到 a ,然后对 a 进⾏解引⽤操作: *pa ,那找到的是 a 。

依次类推,多级指针也是这样。

2、指针数组与数组指针

指针数组是数组,存放指针的数组。

我们通常这样定义:

int * a[5];
char* b[3];

int*代表数组的存放的元素类型是int*指针,【5】代表元素个数是5,a为数组名,同理,char*是b数组的元素类型元素个数为3.

而数组指针有所不同,定义方式是:

int (*a)[5];
char (*b)[3];

*a代表这是一个指针,指向了一个有5个元素,并且全是int类型的的数组。

对于指针数组来说,在某种情况下可以代替二维数组来使用,例如:

int main()
{
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 3,4,5,6,7 };
	int arr3[5] = { 6,7,8,9,10 };

	int* str[3] = { arr1,arr2,arr3 };
	//数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中
	//用指针数组中的指针接受上面三个数组的地址


	for (int i = 0; i < 3; i++)
	{
		int j = 0;
		for (j = 0; j < 5; j++)
		{
			printf("%d ", str[i][j]);
		}
		printf("\n");
	}
	return 0;
}

在这串代码中,我们创建的指针数组就模拟了二维数组的功能,但要记住,这实际上并非完全是⼆维数组,因为每⼀行并非是连续的。

而数组指针是用来存储数组地址的,我们需要用&数组名取出该数组的地址,并让指针指向该地址。

3、二维数组传参的本质

当我们调用函数,实参包含一个二维数组时,我们应该怎么传递该实参呢?

首先我们再次理解⼀下⼆维数组,二维数组起始可以看做是每个元素是⼀维数组的数组,也就是说二维数组的每个元素是⼀个⼀维数组。那么二维数组的首元素就是第⼀行,是个⼀维数组。
如下图:
由于数组的数组名指向的地址也是首元素的地址,所以二维数组名arr也 表示的就是第⼀行的一维数组的地址。第一行的一维数组的类型是int [5],那么第一行的地址的类型就是数组指针int(*)[ 5 ]。所以二维数组传参也是传递的地址,只不过这个地址是数组指针。
与一维数组传参形参用指针接收一样,那二维数组传参就也可以用数组指针来接收。

4、函数指针变量与函数指针数组

根据前面的一系列数组指针,整形指针,那我们不难推出,存储函数地址的指针,就是函数指针。

大家可能会好奇,函数也会有地址吗?

我们做个测试:

//函数指针地址的证明

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	printf("%p\n", Add);
	printf("%p\n", &Add);
	return 0;
}

运行代码后,确实发现打印出来了地址,说明函数也是有地址的。

如果我们要将函数的地址存放起来,自然就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:
void test()
{
	printf("hehe\n");
}

void (*pf1)() = &test;
void (*pf2)() = test;

int Add(int x, int y)
{
	return x + y;
}

int(*pf3)(int, int) = Add;//Add也代表地址
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

以上我们创建了test与Add两个不同类型的函数,创建函数指针变量的方式自然也不同,以*pf3为例:

int (*pf3) (int x, int y)
 | | ------------ 
 | | |
 | | pf3指向函数的参数类型和个数的交代
 | 函数指针变量名
 pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型

函数指针变量是一个指向函数的指针,它可以存储函数的地址。通过函数指针变量,我们可以调用相应的函数。下面是一个简单的例子:

int Add(int x, int y)
{
	return x + y;
}

int(*pf3)(int, int) = Add;//Add也代表地址
/*int(*pf3)(int x, int y) = &Add;*///x和y写上或者省略都是可以的

int main()
{
	printf("%d\n", pf3(2, 4));
	printf("%d\n", (*pf3)(2, 4));
	return 0;
}

最后都会打印出来6,第二个要使用括号将指针名与解引用操作符括起来,因为解引用优先级较低。

了解了函数指针类型,那么函数指针数组自然就是存放一系列函数指针的数组,但这个数组的命名方式有所不同,并不是把数组名放在后面,如:

//函数指针数组的声明:

int(*arr[5])();

arr[ 5 ]表示是一个数组,int(*)()表示储存的元素类型都是函数指针。

函数指针变量和函数指针数组是C语言中强大而有用的工具。它们允许我们动态地选择要执行的函数,并将函数作为参数传递给其他函数。通过理解和灵活运用函数指针变量和函数指针数组,我们可以写出更加灵活、可扩展的代码。希望本文能够帮助你理解并使用这些概念。

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渡我白衣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值