IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 还搞不懂指针?点进来!手把手带你进阶指针(数组指针、指针数组、指针传参、指向函数指针数组的指针等等一切关于复杂指针的问题,统统搞定!) -> 正文阅读

[C++知识库]还搞不懂指针?点进来!手把手带你进阶指针(数组指针、指针数组、指针传参、指向函数指针数组的指针等等一切关于复杂指针的问题,统统搞定!)

前面我们已经对指针进行了一个初步的讲解,相信大家对于指针都已经有了一个基本的认识,那么今天,我们就把指针拿出来,更加深入全面地扒开指针的皮,看看指针的内涵。

首先我们先回顾一下初阶指针中的内容。(具体可以看这篇看完这篇文章,别再说指针难了!手把手带你入门指针的基本使用

  1. 指针是一个用来存放地址的变量,这个地址可且仅可标识一块内存空间。
  2. 指针的大小是4(32位平台上)或者8(64位平台上)个字节
  3. 指针是有类型的,指针的类型决定了指针加减整数时可以跳过多少个字节,以及指针解引用操作时候的权限。
  4. 指针是可以进行运算的,指针的运算有指针加减整数运算,指针减指针以及指针的关系运算。

正文开始之前,别忘了先给文章点个赞噢!
在这里插入图片描述

字符指针

在指针的类型中我们知道有一种指针类型为字符指针char* 。

我们一般这么使用它:
在这里插入图片描述

但是实际上,它还有以下这种使用方法:

在这里插入图片描述
我们知道,这里的“hello world”是一个常量字符串,而这里我们并非把整个常量字符串的地址赋给p,而是将常量字符串中的第一个字符的地址“h”赋给p。

在这里,我们可以对p解引用,看看p的地址到底指向谁。
在这里插入图片描述

那么如果我们想打印整个字符串,我们只需要给出第一个字符的地址(即p)就可以了。(不需要解引用)

在这里插入图片描述

所以我们说,字符指针除了可以指向一个字符,还可以指向一个常量字符串。但是在本质上它指向的是字符串的第一个字符。

虽然字符指针可以指向常量字符串,但是有时候在一些编译器中也可能会报错或警告。

因为常量字符串是存在内存的常量中,不可修改的,而字符指针是可以修改的,如果我们想通过字符指针对常量字符串进行修改,则编译器会报错。

在这里插入图片描述
所以,在使用字符指针指向常量字符串的时候,我们最好在前面加上const,让我们的代码显得更合理。

在这里插入图片描述

明白了以上,我们来看下面这段代码(出自《剑指offer》),它的输出结果是什么呢?

在这里插入图片描述
在这里插入图片描述

答案如下:

在这里插入图片描述
你答对了吗?

那么为什么str3和str4一样,str1和str2却不一样呢?

因为str1和str2是我们创建的两个字符数组,它们是两块独立的内存空间,互相之间是不影响的。而数组名是首元素地址,str1和str2分别指向的是各自数组首元素的地址,所以它们不一样。

而str3和str4指向的是一个常量字符串,常量字符串是开辟在内存的静态区的,它是不能修改的,所以我们在内存中只需要存一份,没必要再开辟一块空间存放它,所以str3和str4指向的是同样内容、同一块空间的常量字符串,so,str3 and str4 are same。

指针数组

我们知道数组有整型数组、字符数组等等,数组中存放的是什么类型的元素,这个数组就叫什么数组。
在这里插入图片描述
所以如果这个数组中存放的是指针,那么我们称这个数组为指针数组。

对应每种指针的类型,则又分为相应的指针类型的数组。
在这里插入图片描述

指针数组的意义

那么指针数组有什么意义呢?

当我们有多个相同类型变量时,如果我们要分别取出它们的地址存起来,就需要每个地址都创建一个指针变量存起来,比较麻烦。
在这里插入图片描述
但是如果我们把它存到一个数组中,则方便得多。
在这里插入图片描述

并且可以直接通过数组下标访问每一块对应的空间。
在这里插入图片描述

*p和p[ ]

我们再看下面的代码:代码中有三个整型数组,把它们的数组名都存放在一个指针数组parr。
在这里插入图片描述
下面我们画个图来更直观的表现parr和三个整型数组之间的关系。
在这里插入图片描述
由图可知,我们可以通过parr中的元素分别找到对应的arr数组。

而parr中的元素是一个地址,这个地址就是对应的数组的首元素的地址,如果我们拿到这个地址,并对它进行解引用,就可以拿到整个数组。

在这里插入图片描述

打印结果如下:
在这里插入图片描述
所以,从这我们可以看到对*和[ ]这两个符号之间的一个转换。

即*(p+i) = p[i],特别地,当i=0时,*p=p[0]。

所以上面代码中在对parr中的元素解引用时,我们直接写成了p[i][j],这看起来貌似和二维数组有点像,但是大家要注意,这可不是二维数组哦,它们之间还是有区别的!

但是我们可以把通过对[ ]对数组访问和通过*对指针进行解引用进行一个联系,因为数组名在一般的情况下表示的就是首元素的地址。

字符串在指针数组中

我们已经知道,字符串放到数组中时,数组名的首元素就是字符串中第一个字符的地址。

那么,我们来看看下面这段代码。
在这里插入图片描述
那么这个数组arr中存放是是什么呢?是三个字符串吗?

其实和之前用字符串初始化数组同理,我们看似把整个字符串放到数组中去了,但是实际上存的是字符串第一个字符的地址,所以上面的arr中存的分别是‘a’,‘b’,‘h’的地址。

在这里插入图片描述

可以看到,数组arr的类型是char*,说明这是一个存放字符指针的数组。

我们可以通过对数组进行解引用,访问每一个字符串。
在这里插入图片描述

数组指针

定义

前面我们了解了什么是指针数组,那么下面我们再来看看数组指针。

首先我们要明确,数组指针指的到底是数组还是指针?

上面已经说过了,指针数组本质是一个数组,数组中的每个元素的类型为指针。那么轮到数组指针了,这一次它的本质应该是一个指针了。

我们知道,指针分为整型指针、字符指针等类型:

int* p 整型指针 - 能够指向整型的指针
char* ch 字符指针 - 能够指向字符类型的指针
float* pf 浮点型指针 - 能够指向浮点型的指针
等等

那么同理,如果一个指针能够指向数组,那么我们就称它为数组指针。

那么指针数组应该长什么样呢?

在这里插入图片描述
parr和*结合,说明parr是一个指针。

去掉指针的名字,剩下的就是指针的类型,即int(*)[10],所以这是一个指向数组的指针,数组包含10各元素,每个元素的类型是int。

这里大家要注意和int * parr[10]区分开,因为[ ]的优先级更高,所以当没有()时,parr先和[ ]结合,表示一个数组。
所以这时的parr就是一个包含十个元素的数组,每个元素的类型是int*。

所以这里我们也可以获得一个判断指针和数组类型的方法:

先找到名字,看它是先和*结合还是和[ ]结合,如果是前者,则这是一个指针,如果是后者,则这是一个数组。
确定是指针(或数组)之后,我们把名字去掉,剩下的就是这个指针(或数组)的类型了。

数组名和&数组名

我们之前说过,数组名在通常情况下是数组首元素地址,但是&数组名时,取出的是整个数组的地址。

在这里插入图片描述
它们在打印的时候,地址是一样的,但是当我们分别给它们+1,arr和&arr跳过的大小是不一样的。

通过对指针的学习,我们现在可以从本质上来理解为什么它们不一样了。

因为arr是数组首元素的地址,这个数组的整型数组,所以arr是一个指向整型的指针,所以当我们给它+1时,跳过的是一个整型的大小(4)。

而&arr是整个数组的地址,所以指针的类型应该是数组指针,指向的是一个包含10个元素的整型数组,所以如果我们给它+1,跳过的就是整个数组的大小(40)。

那么如果让p=&arr,我们试一下写出p的类型。
在这里插入图片描述
你写的是不是这样的呢?

数组指针的使用

那么数组指针到底应该怎么使用呢?

之前我们都用数组名和下标引用操作符对数组进行访问,如下:
在这里插入图片描述

但是当我们知道arr的本质之后,我们就可以这样写:
在这里插入图片描述
我们能看到,我们把参数的类型写成int*时,把arr看做一个指针,在访问时对它解引用,同样能够在屏幕上打印出数组的元素。

提问:那么如果我们传的不是arr,而是&arr呢?我们还能访问数组中的每一个元素吗?

我们知道,&arr取出的是整个数组的元素,所以如果我直接对它+1再解引用,就直接跳过了整个数组,因此是这样做的不能访问到数组中的元素的。

但是,这并不代表用&arr我们就无法访问数组的元素了。

这里博主讲一个比较别扭的方法,虽然它可行,但是重在理解这种方法的思考方式,在平常我们使用访问数组元素时,最常用的还是第一种,因为比较好理解,且不容易出错。

这里我们把数组arr看成一个只有一行的二维数组,则我们把这里的&arr就看成是第一行的地址。

根据二维数组的使用,我们先访问第一行,再访问第一行的每一个元素。

如果p=&arr。则*(p+0)相当于拿到了这个二维数组的第一行。

然后我们再访问第一行中的元素,即*(p+0)[i]。

也可以写成parr[0][i]。

在这里插入图片描述
我们可以注意到,这里的(*parr)就相当于parr所指向的数组的数组名。

讲完了一维数组的访问,接下来我们再看看二维数组的访问。

一般我们用以下这种方式访问二维数组的元素:
在这里插入图片描述
但是我们也可以这样写:

在这里插入图片描述

由上我们其实可以直接知道,在一般情况下,数组名表示数组首元素的地址,所以二维数组的数组名是二维数组第一行的地址,而第一行相当于一个一维数组,所以此时二维数组的数组名本质上就是一个数组指针。

数组传参和指针传参

我们在使用函数进行传参时,有时候也需要把数组或者指针传给函数,那么这时函数的参数应该怎么设计呢?

接下来我们就来看看。

一维数组传参

如果我们把一个数组传过去,我们可以用一个数组来接收,因为数组名就是数组首元素的地址,所以我们也可以用一个地址来接收。

在这里插入图片描述

二维数组传参

同理,二维数组传参时,我们既可以选择以数组的形式传参,也可以选择以指针的形式传参。

但是我们要注意两点:

二维数组以数组的形式传参时,不能省略二维数组的列数。因为对于一个二维数组来说,可以不知道数组有多少行,但是必须知道每一行有多少个元素。
数组名是首元素的地址,而二维数组可以看做是一个大的一维数组,每个元素就是每一行,所以二维数组的首元素是第一行。在以指针的形式传参时,是二维数组第一行的地址,即一个数组的地址。

在这里插入图片描述

一级指针传参

如果我们传给函数的是一个一级指针,那么我们就用一个一级指针来接收。
在这里插入图片描述

那么反过来思考一下,一个一级指针的参数可以接收什么类型的值呢?

在这里插入图片描述
此外,我们还可以传一个空指针(NULL)过去,但是要慎重,因为空指针是不能解引用的,在使用时要避免出错。

ps:我们一般在time函数(获取系统时间,返回时间戳)中会传空指针。time函数的参数实际上也是一个一级指针,而它的返回值是一个时间戳,我们既可以通过传一个时间戳的地址给time函数,让它通过传址调用把时间戳带回来,也可以通过它的返回值(time_t)把时间戳返回来。
而我们如果使用返回值获取函数的结果,就不需要再传一个特定的地址给time函数了,所以我们可以直接传一个空指针给它。

二级指针传参

二级指针的传参其实和一级指针一样,用二级指针传参,就对应地用二级指针接收即可。

在这里插入图片描述

同样地,我们也可以反过来思考,一个类型为二级指针的参数可以接收什么样的值呢?

在这里插入图片描述

函数指针

定义

我们前面已经认识了字符指针、整型指针、数组指针等等,那么函数有地址吗?存放函数的地址是不是就是函数指针呢?

答案是肯定的。用来存放函数地址的变量就是函数指针变量。

而函数名就是函数的地址。
在这里插入图片描述

我们可以看到函数名和&函数名打印出来的地址都是一样的。

这里要声明一点,与数组不同,数组名是数组首元素的地址,而函数的数组名就是函数的地址,函数名和&函数名在本质上是一样的。

那么函数的地址应该如何保存呢?
函数指针的类型应该如何写呢?

在这里插入图片描述

我们再用下面这段代码来进一步分析:
在这里插入图片描述
首先确定变量名是pf,pf和结合表明这是一个指针,确定这是指针之后,把pf去掉,剩下的就是指针的类型。所以上面这个指针指向的一个函数,函数的参数有两个,类型分别为int、int。函数的返回值也是int。

我们要注意区别下面两个变量:

int *pf(int,int);

()的优先级高于*,所以pf先和()结合,所以这是一个函数的声明,声明有一个pf函数,参数为int、int,返回类型是int。
int (*pf)(int, int);

pf和*结合,所以pf是一个指针,指向的是一个参数为int、int,返回类型为int的函数。

意义

那么函数指针有什么意义呢?

要知道,我们可能并不总是能直接拿到一个函数的,有时候可能我们需要通过函数的地址来对函数进行调用,这个在后面会讲到回调函数,这种函数就是利用函数指针在进行调用的。

在这里插入图片描述
在调用时我们可以发现:
在这里插入图片描述

更深的理解

下面我们看《C陷阱和缺陷》中的两段代码:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

我们看代码1:

突破口是0,实际上我们是把0强制转换成一个类型为void (*)()的函数指针,然后对这个函数指针进行解引用,找到这个函数之后再对它进行调用。

所以代码1实际上是一次函数调用。

代码2:

突破口是signal,signal是一个名字,和后面的()结合,表明这是一个函数,函数的参数有两个,参数的类型分别为int和void()(int)函数指针。
把signal(int , void(
)(int))去掉之后,剩下的就是函数的返回类型void (*)(int) - 函数指针。

所以代码2实际上是一个函数声明,声明有一个名为signal的函数,函数有两参数,参数类型分别为int和void()(int),返回类型是void ()(int)。

上面的代码那么复杂,我们可不可以按照我们平常的思路这样写呢?

void (*)(int) signal(int, void(*)(int));

答案是不行的,这样的写法是错误的!!

包括前面我们写定义一个函数指针时,不能把变量名和指针类型完全独立开来写。

void (*)(intint) pf=add;//err

虽然这样看起来更容易理解,但是它是错误的噢!

那么上面你的代码2可不可以有简单一点的写法呢?

我们可以利用typdef对类型进行重定义(即对它重新起个名字)。

typdef void(* pfun_t)(int);//把返回类型void(*)(int)从定义为pfun_t
pfun_t signal(int, void(*)(int));

函数指针数组

我们刚刚学了指针数组,有学了函数指针,那么我们能不能把函数指针都放到一个数组中去呢?

答案当然是可以的。

函数指针数组,顾名思义,这是一个存放函数指针的数组。

在这里插入图片描述

ps:由于&函数名和函数名本质上是一样的,所以我们也可以把&省略。

那么像上面这样一段代码,我们可以用一个它来实现一个简单能实现加减乘除功能的计算器。
在这里插入图片描述
完整代码如下:

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

int Sub(int x, int y)
{
	return x - y;
}

int Mul(int x, int y)
{
	return x * y;
}

int Div(int x, int y)
{
	return x / y;
}

void menu()
{
	printf("欢迎使用计算器\n");

	printf("*****1.Add****\n");
	printf("*****2.Sub****\n");
	printf("*****3.Mul****\n");
	printf("*****4.Div****\n");
	printf("*****0.exit****\n");

	printf("请选择:>");

}

int main()
{
	int n = 0;
	int x = 0;
	int y = 0;
	int(*pfArr[])(int, int) = { 0,Add,Sub,Mul,Div };
	do
	{
		menu();
		scanf("%d", &n);
		if (0 == n)
		{
			printf("退出计算器\n");
		}
		else if (n >= 1 && n <= 4)
		{
			printf("请输入两个操作数:>");
			scanf("%d%d", &x, &y);
			printf("%d\n", pfArr[n](x, y));
		}
		else
			printf("输入错误,请重新输入");
	} while (n);
	return 0;
}

我们可以发现,这样写我们就能很容易地实现一个计算器啦!

在《C和指针》中,把这样的函数指针数组称为转移表,我们可以利用数组的下标找到相应的函数,然后转移到该函数中去。

除此之外,我们还可以使用下面这种方法:
在这里插入图片描述
上面这种方法其实就是把函数的地址作为参数传给另一个函数,由另一个函数来调用,这样的做法本质上依赖于函数指针的存在。

回调函数

这里我们再介绍一个概念:回调函数。

回调函数就是一个通过函数指针调用的函数

如果你把函数的指针(地址)作为参数传递给另一
个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

注意!回调函数不是通过函数指针调用其他函数的函数,而是指被函数指针调用的那个函数噢~大家可千万不要搞错了!
在这里插入图片描述

在上面计算器的实现中,我们就利用了函数指针调用了Add、Sub、Mul、Div这几个函数,所以Add、Sub、Mul、Div就是回调函数。

指向函数指针数组的指针

说完了函数指针之后,我们再来看看指向函数指针数组的指针。

是不是看起来听起来读起来都特别拗口呢?虽然它看着很绕,但是只要我们把它“肢解”了,就能很好地弄懂它咯。

首先看中间的部分“函数指针数组”,说明的一个数组,数组的元素是指针,每个指针指向的是一个函数。

然后再看外面部分“指向XXX的指针”,说明这本质上是一个指针,指向的是我们刚才分析的中间部分。

所以总的来说,这是一个指针,指向的是一个数组,数组中的每个元素都是函数指针。

那么我们应该如何定义一个指向函数指针数组的指针呢?

在前面,我们已经讲了函数指针数组,那么函数指针数组的类型是怎么写的呢?

还是以上面的例子来说~
(PS:这里把上面代码中的第一个元素0去掉,pfArr变为包含4个元素的数组)

int(*pfArr[4])(int, int) = { Add,Sub,Mul,Div };

pfArr是变量名,与[4]先结合,所以这是一个数组。
去掉pfArr[4]再向外看,剩下的是int(*)(int, int),是一个函数指针的类型。

说明这是一个存放函数指针的数组。

那么如果我们要写出指向这个数组的指针(即指向函数指针数组的指针),应该如何做呢?

首先我们写出变量名:ppfArr。
变量名先和*结合,表明这是一个指针:(*ppfArr)。
指针指向的是一个数组,数组包含4个元素。所以(*ppfArr)和[4]结合:(*ppfArr)[4]。

那么最后剩下的就是数组元素的类型,前面已经知道,数组元素的类型为int(*)(int, int)。

所以,我们只要把(*ppfArr)[4]和数组元素类型放在一起就好啦!

在这里插入图片描述
这时候,上图中的ppfArr就是一个指向函数指针数组的指针。

int(*(*ppfArr)[4])(int, int) = &pfArr;

当然我们还可以无限往下套:函数指针数组指针的数组、指向函数指针数组指针的数组的指针等等等等……
在这里插入图片描述

但是这就太复杂啦!!!我们一般探讨到指向函数指针数组的指针就不再套了,不过如果你实在有兴趣,可以自己继续深入“研究”一下噢~

关于指针的进阶,就讲到这里啦!

如果你觉得文章对你有帮助,记得点赞收藏评论关注一波噢!

关注我,一起精进C语言!

在这里插入图片描述

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-22 14:29:00  更:2021-09-22 14:29:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 21:21:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码