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

C语言 — 指针的进阶

文章目录

  • 前言
  • 一、字符指针
  • 二、指针数组
  • 三、数组指针
    • 数组名 与 &数组名
  • 四、指针传参
    • 二维数组传参
  • 五、函数指针
  • 结语

前言

通过前面的关于指针的学习,我们了解了指针的一些个特性。本篇文章我们将深入指针,挖掘指针更深处的知识。
在开始之前,我们先来复习一下指针:

  1. 指针就是变量,是用来存放地址的。地址是用来标识数据在内存中存储的唯一内存空间
  2. 指针变量的大小为4/8个字节。取决是32位的还是64位的平台
  3. 指针是有类型的,不同的指针类型决定了指针+ - 整数的步长,指针解引用的权限

一、字符指针

顾名思义,就是存放字符的指针。例子如下:

char c = ‘a’;
char* pc = &c;

第一行代码就是把 a 赋值给变量c
第二行代码就是取地址c,得到c的地址,而c的地址指向字符a,所以打印的话就会打印 a。

二、指针数组

数组指针是数组,是用来存放指针的数组
我们来举点例子,帮助下理解:

一、 int arr[4];
二、 char ch[3];
三、 int* arr2[4];
四、 char* ch2[3];

“一、”,这里的代码代表了arr这个数组的每个元素的类型都是int
“二、”,同理,ch这个数组里存放的类型都是char类型
“三、”,这里面数组arr2里面存放的内容均是地址,其指针变量均是int*
“四、”,同“三、”。ch2存放的均是char* 类型的指针

那么,我们来看个指针数组模拟二维数组的例子:

int arr1[3] = {1, 2, 3};
int arr2[3] = {2, 3, 4};
int arr3[3] = {3, 4, 5};

int* parr[3] = {arr1, arr2, arr3};

这里 ,我们定义了三个数组,并且分别存放了不同的数据。
然后,我们使用指针数组存放这三个数组。

注意:这里,指针数组parr[3]里,存放的每个数组都是其首元素的地址。
那么,我们只需要遍历一遍这个指针数组,就能得到一个二维数组

int i = 0, j = 0;
for(i = 0; i < 3; i++){
	for(j = 0; j < 3; j++){
		printf(%d “, *(parr[i] + j));
	}
	printf(“\n”);
}

这里解释一下 *(parr[i] + j) 这个操作
首先parr[i]取到的是首元素地址,这里假设i = 0,那么取到的就是arr1这个数据的首元素地址,我们把首元素地址 + j,得到的地址就能得到arr1中的任何一个元素的值,arr2 与 arr3 同理

三、数组指针

数组指针,其实本质上是一个指针,就好比好孩子,本质上是一个孩子一样。
这里我们来看两个不同的例子:

  1. int *p1[3];
  2. int(*p2)[3];

第一个表示的是p1是一个int类型的指针(int *),指向这个数组的首元素的地址。所以这是一个指针数组
第二个就有点区别了。首先,这里的p2被括号括起来了,没法与前面的int *
相结合,所以这里的 p2 是一个数组指针,表示的是 p2 可以指向一个数组,而该数组有 3 个元素,每个元素都是int类型的。

int *p1的意思是:能够指向整形数据的指针
所以数组指针是:能够指向一个数组数组的指针
是不是要被绕进去了?那么我就再来说个结论:
指针数组指向的一个数组的首元素地址,而数组指针指向的就是一整个数组。意味着如果 * p1 + 1指向的是下一个数据的话,那么(*p1)+1指向的就是这个数组的末尾的下一位,也就是直接移动了3格

要理解数组指针,我们不得不来看看 arr 与 &arr 的区别。

数组名 与 &数组名

int arr[10] = 0;
int *p = arr;

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);

以上代码都会输出什么呢?
为了方便理解,我将它们分为三组,以换行进行区分。
首先第一组的第一个arr。前面我们不是说过指针数组指向的是数组的首元素地址吗,所以这里的arr输出的是arr这个数组的首元素的地址。
而arr + 1 就是输出arr这个数组的首元素地址往后+1,移动一位,也就是输出第二个元素的地址。

第二组的&arr[0] 跟第一组的 arr 虽然写法不同,但这俩是一个意思。第一组敢直接写arr实际上也是因为使用了第二组的这个写法的简化版。所以第二组第一行输出的也是首元素地址,&arr[0] + 1同理,也是将首元素+1后得到的第二个元素的地址并输出。

第三组的 &arr 也是一样的道理,也是输出首元素的地址。
但是,第二行就有点不一样了。&arr取的确实是arr这个元素的地址,但是这是一个数组指针,它 + 1 并不是首元素地址往后 + 1位,而是直接将arr看做一个整体,+1就是跳过这个整体,而不是上面两组的跳过这个整体中的其中一个元素。所以输出会是这样的
在这里插入图片描述
之前的都是001CFB88 - 001CFB84 = 4,也就是跳过了一个位
而(&arr + 1 )- &arr 也就是001CFBAC - 001CFB84 =40,也就是跳过了一整个数组。
下面我画了一张图方便大家理解

在这里插入图片描述
所以:

整形指针是用来存放整形的地址
字符指针是用来存放字符的地址
数组指针是用来存放数组的地址

四、指针传参

讲完了指针数组与数组指针,我们下载来学习下指针的传参
先来看看数组的传参:

void test(int arr[]) {}			//第一组
void test(int arr[10]) {}		//第二组
void test(int *arr[]) {}		//第三组

int main() {
	int arr[10] = { 0 };
	test(arr);
	return 0;
}

第一组传参:实参arr在被传递时实际上传递的是一个指向arr这个数组的首元素地址的指针。在传递过去后形参arr[]接受了这个地址,所以是可以的。
第二组传参:虽然形参变成了arr[10],但是实际上这个 10 是可以忽略的。数组大小在这里不会影响函数参数,所以可行。
第三组传参:这个其实就是第一组传参更为标准的写法,前面说到过传递的值是一个指针,所以我们这里可以使用 * 号解引用这个指针,得到地址。

看完了数组,再来看指针数组:

void test(int *arr[10]) {}		//第一组
void test(int* *arr) {}		//第二组

int main() {
	int *arr[10] = { 0 };
	test(arr);
	return 0;
}

这是一个指针数组,我们定义了一个数组arr[10],并且把他变成了指针数组。
第一组:指针数组里存储的是指针,所以在传递后也是需要通过解引用操作来得到首元素地址对应的值。而10不会影响这个函数的参数
第二组:int *arr[10] = { 0 };这段代码里的 *arr[10] 里面存放的其实是 10 个 int *,arr是数组名, int *在被传递过后首元素地址就是是int * 的地址。而又因为二级指针是用来存放一级指针变量的地址,所以也是没有问题。

二维数组传参

说完了一维数组,我们来说下二维数组

void test(int arr[3][4]) {};	//第一组
void test(int arr[][]) {};		//第二组
void test(int arr[][4]) {};		//第三组
int main() {
	int arr[3][4] = {0};
	test(arr);
	return 0;
}

记住二维数组传参的一句话就行:可以省略行,但是不能省略列!
也可以理解为多维数组可以省略低纬,但是不能省略高纬。二维数组可以省略第一维(行),但是不能省略第二维(列)
所以第一第三组可以,第二组不行。

指针传参宗旨:形参与实参类型需一致

五、函数指针

数组指针是指向数组的指针
那么显而易见,函数指针就是指向函数的指针

int main() {
	int arr[5] = { 0 };
	int (*p)[5] = &arr;

	return 0;
}

前面我们说了数组指针(如上所示)那么函数指针其实也是一样的

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

int main() {
	int arr[5] = { 0 };
	int (*p)[5] = &arr;

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

我们这里打印出这个函数的地址,发现是可以打印出来的
在这里插入图片描述

而且对于函数来说,函数名与取地址函数名取出来的地址没有区别
再来看看如何搞个指针给函数
在这里插入图片描述

我们只需要参照数组指针的形式就能写出函数指针:
这里我的写法为:int (*pf)(int, int) = &Add;
解释一下,(int, int)对应的是函数的两个参数类型,然后在函数前面的int对应函数的返回类型,然后让 *号与要定义的函数变量相结合即可。
那么,可能有的小伙伴就会好奇了。诶,函数的指针有什么意义呢?
我们知道,定义一个指针变量在后期就可以通过指针变量找到地址然后通过地址来修改内容,就比如

int a = 10;
int* pa = &a;
*pa = 20;

通过*pa我们可以找到a然后再修改a的值。
那么这里也是一样的
在这里插入图片描述

我们可以通过跟前面学习的方式一样,直接修改函数的参数。
并且,这里的:(*pa)(3, 5) 其实等价于 pa(3, 5)
这个 * 号其实是为了让我们更好的理解罢了,不写也是可以的。甚至你可以写好几个星星,都没有问题。

学完了函数指针,我们就来看一段特别特别“有意思”的代码:
在这里插入图片描述

可能看到这串代码会让你不寒而栗。不要怕,我们来简化一下(虽然我已经简化过一次了)
在这里插入图片描述

现在再看这串代码你会发现好像应该能看懂了。
我来解释一下这串“有趣”的代码:
首先我们把关注点看向 “0” ,0是一个 int 类型的数据。他前面用了红色的括号给扩了起来,说明 “0” 将要被强制类型转换。
然后我们来看红色括号里的内容。红色括号里又有两个绿色的小括号,绿色小括号前还有一个 void。那么显而易见,这跟我们刚学的函数指针很像…好吧这就是一个函数指针,返回值类型为void的函数指针。函数变量有两个。所以这串代码就是把 int 类型的 “0” 强制类型转换成函数指针。
最后我们来看粉色括号,粉色括号有两个,这时候我们就要警觉了,有没有可能是一个函数指针。显而易见这可不就还是函数指针嘛,第一个粉色括号是某个函数的第一个形参,并且跟第一个绿色括号一样,都是解引用后的数据,而第二个粉色括号就是这个函数的第二个形参。并且前面我们说过函数指针不需要特别声明(不像数组指针一样要用括号和*号来标识),所以至此,解读完毕。


我这里再简单解释一下函数指针的妙用。(这部分比较抽象,慎看)
假设我们要定义一个计算器,然后我们使用while来进行选择(看你要选加法还是减法),
case1意味着加法,然后以此类推。
然后我们再写四个函数(加减乘除),然后在case1里调用加法,case2里调减法(以此类推)
我们会发现,每个case里的内容清一色都是调用某个函数再printf输出
代码会有冗余。
这时候我们就可以考虑封装一下代码,用一串一样的代码完美判断我们是否进行加减乘除,并且代码简单不冗余。
方法是这样的:

void calc(int (*pf)(int, int))
{
	int x = 0, y = 0;
	int ret = 0;

	printf(“请输入两个数:>);
	scanf(%d %d”, &x, &y);
	ret(x, y);
	printf(&d”, ret);
}

这里我们的杀招就是ret(x, y)这一串代码
因为上面的pf会完美的接受传递过来的函数地址,通过函数地址可以找到函数的名称还有实现方法,我们再通过ret就可以直接输出了。
所以下面的每个case都直接调用calc这个函数就能直接实现计算器功能,并且没有冗余。
(调用函数例子:calc(Add))这里就会直接把Add的地址传递给函数calc
通过*pf的解析就能得到所传递的函数的内容,再通过两个int 就能实现函数调用函数,函数化作参数互相传递。


结语

以上就是本次指针进阶的内容了。文字虽多希望可以帮到你理解。我们下篇文章再见~


http://www.kler.cn/a/381897.html

相关文章:

  • 【Java SE】BigInteger
  • 基于STM32单片机太阳能充电循迹避障小车
  • 20241102在荣品PRO-RK3566开发板的预置Android13下适配宸芯的数传模块CX6603N
  • 小菜家教平台:基于SpringBoot+Vue打造一站式学习管理系统
  • MYSQL安装(ubuntu系统)
  • python基础学习笔记
  • c语言简单编程练习9
  • 剧本杀小程序,市场发展下的新机遇
  • 鸿蒙HarmonyOS NEXT应用层架构
  • SpringBoot源码解析(一):SpringApplication构造方法
  • npm入门教程5:package.json
  • 静态库、动态库、framework、xcframework、use_frameworks!的作用、关联核心SDK工程和测试(主)工程、设备CPU架构
  • 分布式光伏发电的投融资计算
  • OTFS基带通信系统(脉冲导频,信道估计,MP解调算法)
  • 零基础快速入门MATLAB
  • Nat Med病理AI系列|哈佛大学团队发表研究,探讨深度学习在病理诊断中的公平性问题及解决方案|顶刊精析·24-11-02
  • Webserver(3.2)锁
  • 基于CentOS 7.9上安装WebLogic
  • 【STL_list 模拟】——打造属于自己的高效链表容器
  • EasyExcel 学习之 导出 “文件编码问题”
  • 苍穹外卖 商家取消、派送、完成订单
  • HTB:PermX[WriteUP]
  • 附件商户,用户签到,uv统计功能(geo,bitmap,hyperloglog结构的使用)
  • 如何使用RabbitMQ和Python实现广播消息
  • 深度学习基础知识-Batch Normalization(BN)超详细解析
  • 第二节 管道符、重定向与环境变量