C语言—指针进阶

Posted Zheng"Rui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言—指针进阶相关的知识,希望对你有一定的参考价值。

指针

首先看一下指针最基本的用法

#include<stdio.h>
int main(){
	char c = 'w';
	char* p = &c;
	printf("%c\\n", *p);

	*p = 'c';
	printf("%c\\n", *p);
	return 0;
}

这是上面代码的运行结果
看这段代码,这是对指针一个非常简单的应用,让指针p存放字符变量c的地址,通过对指针p解引用来访问变量c中存放的值。

指针数组和数组指针

首先搞清楚一个概念
指针数组指针 还是 数组??
答:是数组

数组指针数组 还是 指针??
答:是指针

要分清楚这个概念,其实只要关注这词最后的定语是什么,后面是数组,它就是一个数组,后面是指针,它就是一个指针。

下面来具体看一看,他们该如何定义和使用。

指针数组

指针数组这个概念比较好理解,我们已经学过整型数组,字符型数组。指针数组本质上就是一个数组,只不过,它的每个单元格里面存放的都是一个指针。
下面来看指针数组的定义

#include<stdio.h>
int main(){
	char* ch[3] = { "hello", "world", "!" };
	printf("%s ", ch[0]);
	printf("%s ", ch[1]);
	printf("%s ", ch[2]);
	return 0;
}

在这个指针数组中,有三个单元,分别存放了指向常量字符串"hello", “world”, "!"的指针,通过ch[数组下标]的方式,可以去调用其中的值。
下面看它的运行结果:
在这里插入图片描述

数组指针

我们说指针数组,是一个存放指针类型变量的数组。那数组指针又是什么含意呢?
定义:数组指针是一个指向整个数组的指针。
是的,你没有看错,它指向的是整个数组。
这里可能会有一个小误区,又可能会不理解什么叫做 指向整个数组,我们知道,数组名代表的只是数组首元素的地址,那我们该如何给这个数组指针赋上整个数组的地址呢? 而且一个数组有那么多的元素,那对这个指针解引用的时候,它会调用哪个元素的值呢?
下面我们来看实际代码:

比较 &数组名 和 数组名 之间的区别

	int num[5] = { 1, 2, 3, 4, 5 };
	printf("%p\\n", num);
	printf("%p\\n", &num);

我们首先定义一个一维数组,然后分别以地址的形式,打印num 和 &num 的址
在这里插入图片描述
我们发现,这两个值 ,居然是一样的,这是否表明 num 和 &num 是一个意思呢?
下面再来看:

	int num[5] = { 1, 2, 3, 4, 5 };
	printf("%p\\n", num);
	printf("%p\\n", &num);
	printf("------------\\n");
	printf("%p\\n", num + 1);
	printf("%p\\n", &num + 1);

在这里,我们保留了之前的代码,然后,输出num 和 &num 加1的结果,与之前进行对比

在这里插入图片描述
(注意:这里读者可能发现,num 和 &num 的值 与之前所得到的值不同,这是因为,我们在这里输出的是数组的地址,每次程序运行结束,都会销毁其中的数据,然后,下次运行时,再创建,所以系统为这个数组分配的地址可能与上次分配的不在一个地方,但是,num 和 &num 的值仍然相等,这个是不会变的。)

这里我们看到,num 和 &num 本来值是相同的,但在加1操作之后,居然指向了不同的地址,这是为什么?
为了了解这个问题,首先要清楚,地址加1代表了什么?是代表数值上的加1吗?显然不是,实际上地址加1,代表的是加上一个类型的大小。
明白了这个,就不难理解,num+1 为什么和 &num+1 的值不同了,因为,num代表的是数组首元素的地址,一个int型的元素,加1之后,加了四个字节,而&num代表的是整个数组的地址,&num+1,加的是整个数组的大小,也就是20个字节。所以我们看到,后面的两个数,之间差了16;
(注意:004FFB6C 和 004FFB7C之间差的是16,不是10,因为这是16进制表示的。)

明白了这个原理,我们就可以为我们的数组指针进行赋值,当然,赋的就是 (&数组名)这种形式了。
下面举一个例子:

#include<stdio.h>
int main(){
	int num[5] = { 1, 2, 3, 4, 5 };
	int(*p)[5] = &num;
	
	printf("%p\\n", &num);
	printf("%p\\n", *p);
	
	return 0;
}

这里我们定义了一个数组指针p,让它接收 数组num 的地址,在赋值完之后,我们输出&num 和 *p 的值,看看变量p 是否成功接收。
在这里插入图片描述
我们看到,p已经接收了这个数组的地址。
接下来,我们分析,如果对整个数组的地址进行解引用的话,会出现那个元素的值?

#include<stdio.h>
int main(){
	int num[5] = { 1, 2, 3, 4, 5 };
	int(*p)[5] = &num;
	
	printf("%p\\n", &num);
	printf("%p\\n", *p);
	printf("----------\\n");
	
	printf("%d\\n", **p);
	return 0;
}

在这里插入图片描述

这里我们打印 **p 来访问其中的值 发现给出的不是 数组中随机一个的值,也不是整个数组的值都打印一遍,而是打印了数组首个元素的值,所以,得出结论,对数组的地址解引用得到的是数组首个元素的值,这也不难理解,因为数组的地址,本来就等于数组首元素的地址(在值上相等,但意义不同)

下面来看一个关于数组指针的实际应用:

设计一个函数,打印二维数组的值

#include<stdio.h>

void print(int(*p)[3], int row, int col)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			printf("%d ", p[i][j]);
		}
		printf("\\n");
	}
}

int main()
{
	int num[2][3] = { 1, 2, 3, 4, 5, 6 };
	
	print(num, 2, 3);
	return 0;
}

在这里插入图片描述

在这串代码中,我们实现了一个打印二维数组的函数,向print函数中,传入数组num,因为是一个二维数组,而num数组名代表了数组首元素的地址,num代表了第一行数组的地址,所以我们可以用一个指向数组的指针来接收这个地址。在函数中,我们仍然使用二维数组的形式去使用它,可以这么去理解
【p[1][2]就相当于,* ( (p+1)+2 ) p指向的是第一行数组,加1之后指向了第二行,也就是数组的第1行,然后解引用,(p+1)就相当于一个指向第二行首元素的地址,给这个地址,加2,也就是第二行的第三个元素,也就是num[1][2]。】

函数指针

函数指针,顾名思义,就是指向一个函数的指针。
这里我们引用一下函数指针的官方定义:
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

从这段定义中,我们可以知道,函数在编译的时候,系统会为其分配空间,而函数指针,就是存放这段空间地址的一个指针变量。

下面来看一下,如何去定义一个函数指针:

#include<stdio.h>

void print(int(*p)[3], int row, int col)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			printf("%d ", p[i][j]);
		}
		printf("\\n");
	}
}
int main()
{
	int num[2][3] = { 1, 2, 3, 4, 5, 6 };
	void(*pfunction)(int(*)[3], int, int) = &print;

	(*pfunction)(num, 2, 3);
	return 0;
}

在这里插入图片描述
可以看到,这段代码同样能够实现打印二维数组,也就说明,我们通过对函数指针的调用,同样可以去使用这个函数的功能。
但是,我们在实际使用函数指针的时候,并不是只是这样定义一个指针然后直接调用,一般,函数指针是用在 回调函数 当中的。

回调函数

什么是回调函数呢?
回调函数 英文名(call back function)当一个函数的参数也是一个函数的时候,这个函数就被称为,回调函数,那现在我们知道,要想传递一个函数的地址,就一定要使用到函数指针进行传参。

下面来看一个例子:

#include<stdio.h>
void print(int a)
{
	printf("%d\\n", a);

}
void test(void (*pfunction)(int ))
{
	(*pfunction)(3);

}
int main(){
	void(*pfunction)(int) = &print;
	
	test(pfunction);
	return 0;
}

在这里插入图片描述

这是一个简单的使用回调函数的例子,在这个例子中,我们的test函数中,并没有打印语句,打印语句在print函数中,所以我们首先定义了一个函数指针,让它指向函数print,然后调用test,向其中传递了函数print的地址,然后再test中调用print函数,从而达到打印数字3的目的。

这里有一个有意思的小问题,那就是,一个函数可以回调自身吗?也就是函数的参数,就是函数本身,这样的代码能实现吗?
答案是,不能。因为,函数再调用的时候,系统都会为函数分配一定的空间,如果函数回调自身的话,那么程序就会无休止的一直陷入函数调用中,系统一直为调用的函数分配空间,最终造成栈溢出,程序崩溃。

函数指针数组

函数指针数组与指针数组类似,本质上都是数组,其中的单元格存放的也都是地址,只不过函数指针数组当中存放的是各个函数的地址。
下面来看,函数指针数组该如何定义:

#include<stdio.h>
void add(int a, int b)
{
	printf("%d\\n", a + b);
}

void del(int a, int b)
{
	printf("%d\\n", a - b);
}
int main()
{
	void(*p[5])(int, int) = { &add ,&del};
	
	(*p[0])(1, 3);
	(*p[1])(3, 1);
	
	return 0;
}

在这里插入图片描述
在这段代码中,我们先定义了 add 和 del 两个函数,然后,在主函数中,我们没有直接调用它们,而是,通过定义了一个函数指针数组p,数组p的大小为5,其中存放的是指针,每个指针都是指向一个返回类型为 void ,并且有两个int型变量作为参数的函数。我们将函数add和del的地址放进去,然后通过访问数组的方法去访问它,看到仍然能够显示出正确的答案。

函数指针数组用途有它的局限性,也就是,放在函数指针数组当中的地址,对应的函数,必须有相同的返回类型,也必须有相同的参数,所以并不经常使用。

指向函数指针数组的指针

这种类型的指针使用频率更少,它也是一个指针,指向的是 存放函数指针的数组。
下面来看,借用上面的例子,如何定义一个指向函数指针数组 p 的指针:

#include<stdio.h>
void add(int a, int b)
{
	printf("%d\\n", a + b);
}

void del(int a, int b)
{
	printf("%d\\n", a - b);
}
int main(){
	void(*p[5])(int, int) = { &add ,&del};
	(*p[0])(1, 3);
	(*p[1])(3, 1);
	printf("----------\\n");
	
	void(*(*pp)[5])(int, int) = &p;
	(**pp[0])(1, 3);
	
	return 0;
}

在这里插入图片描述
在这里,我们定义了指向函数指针数组p的指针pp,然后通过pp对函数进行调用,也是可以的。但是,可以看出,我们嵌套了这么多的指针和数组,实现的仍然是本来简单的函数 add 和del的功能,但是,我们失去了原本代码非常重要的 可读性和可维护性。所以,在实际操作中,我们不需要写出,如此复杂难懂的代码,只需完成相应的功能即可。

以上是关于C语言—指针进阶的主要内容,如果未能解决你的问题,请参考以下文章

C语言进阶笔记深入了解进阶指针

C语言进阶笔记深入了解进阶指针

C语言进阶笔记深入了解进阶指针

C语言进阶学习笔记二指针的进阶(重点必看+代码演示+练习)

C语言进阶学习笔记二指针的进阶(重点必看+代码图解+练习)

C语言指针进阶第五站,函数指针