0基础C语言自学教程——第八节 函数指针数组的各种关系

写在前面:

各位小伙伴还在为C语言的学习而苦恼嘛?

还在为没有知识体系而烦心嘛?

别急。因为~~~~

接下来的时间里,我会持续推出C语言的有关知识内容

都是满满的干货,从零基础开始哦~,循序渐进😀,直至将C中知识基本全部学完🐂。

欢迎关注我♥,订阅专栏 0基础C语言保姆教学

就可以持续读到我的文章啦😀🐕~~~~

本文为第8节——函数指针数组的各种关系(文末附前7章的链接呦👉)

编者按:

目录

1、const int*        int const*         int* const      const int* const       int const* const 的区别和联系

2、指针数组

3、数组指针

4、数组名和&数组名所表示的地址区别

5、数组元素访问的两种形式

6、*p++和(*p)++的区别

7、指针和数组的关系表述

8、函数传参的传址调用

 9、函数传参传数组

一维数组传参

多维数组传参(以二维数组为例)

10、函数指针

11、返回指针的函数——注意要避免野指针

12、函数指针数组

13、指向函数指针数组的指针

14、回调函数


1、const int*        int const*         int* const      const int* const       int const* const 的区别和联系

const int* 是指向一个常量整数的指针,所以说,const int*所修饰的指针变量,其指针变量本身(即指向元素的地址)是可以被修改的,但是其指针所指向的值是不允许被修改的

这里的const 不会限制指针变量本身,也就是说,其指针变量是允许指向其他的地址的。

const的位置可以放在int 的前面,也可以放在int  的后面,不会产生影响。

也就是说,const int* 和int const* 等价。

int* const 是一个指向可修改的整型的常量指针,也就是说,其指针变量本身所指向的地址是不可以被改变的,但是可以改变其指针所修饰的值。

也就是说,这里的指针变量所指向的地址是不允许修改的,但是其地址所对应的值是可以被修改的。

const int* const 是指向一个常量整数的常量指针。也就是说,其指针变量所指向的地址和其地址所对应的整型值都是不允许改变的。

第一个const的位置可以放在int 的前面,也可以放在int 的后面。

也就是说,const int* const 和 int const* const 等价。

 

2、指针数组

我们先来看这么一个例子:

int* p[20] = {0};

这里的数组p,实际上就是一个指针数组

什么意思呢?

就是一个数组p,里面有20个元素,每一个元素的类型都是int*。

这里的int*表示的是元素的类型。

就好像我们当初我们定义数组一样,之前我们会写

int a[20] = {0};

这样的代码,这里的int表示数组里有20个类型为int的元素。

那么如果现在是int*类型,依此类推,其数组里就是含有20个int*类型的指针(变量)元素

而这个数组p,就是 叫指针数组

 我们来举一个例子,便于读者理解:

 OK,上面实际上就是其一个实例,仅做理解该知识点使用,实际应用比这稍复杂。

3、数组指针

数组指针,本质上为一个指针。实际上,它就是一个指向数组的指针

我们还是来通过之前的类型来引入。

我们如果是这样

int* p1 = NULL;

这是的定义了一个指向整型的指针p1

如果是

char* p2 = NULL;

这是定义了一个指向字符类型的指针p2

依此类推...

那么,我如果想定义一个指向数组的指针,我该怎么写?

比如,我有这么个数组

int a[3] = {0,1,2};
___  p = &a;

那么问题来了,我该怎么去定义这个p呢?

这个时候,我们就引进了数组指针这样一个概念:

它的定义方式是这个样子的:

int (*p)[3] = &a;

为什么要加那一个小括号呢?

理由很简单,因为[]的优先级是高于*的,会首先与p结合。如果不加括号,那么就变成了我们上面所说的指针数组了。

所以总结一下:数组指针是用来存放数组的地址的,它的定义方式为 type_name (*__)[num_arr]

按照上面的例子来看, 就是int (*p)[3]

我们在下面讲解函数传参的时候会详细地讲解这个东西的实际作用。

4、数组名和&数组名所表示的地址区别

数组名实际上就是数组的首元素地址。

但是有两种情况例外:

一种是用sizeof()去求数组的大小的时候;

还有一种情况是在&整个数组的时候(就是取整个数组的地址)

这两种情况下,数组名代表的是整个数组的地址。

当然,也许在你看来数组首元素的地址和整个数组的地址并没有什么区别。如图:

但是,其表示的含义不一样。何以见得?

我们来继续看:

 为什么会这样?

我们知道,指针加减一个常数表示的是下一个元素。

那么,直接一个a表示的是数组首元素的首地址;其下一个元素就是数组a的第二个元素。

而&a表示的是整个数组的首地址;其下一个元素是数组后面的地址。

如图:

至此,我们就解释清楚了这两者的关系

5、数组元素访问的两种形式

有两种形式,它们本质上是一致的:

一种是用数组的下标访问,还有一种是用指针访问。

我们来看下面这一段代码:

#include<stdio.h>

int main()
{
	char arr[3] = { 'a','b','c' };
	char* p = arr;
	for (int i = 0; i < 3; i++)
	{
		printf("%c ", arr[i]);
	}
	printf("\n");
	for (int i = 0; i < 3; i++)
	{
		printf("%c ", *(p + i));
	}
	return 0;
}

 可以看到,这两种方式所得到的结果是一样的。

不仅一维数组这样,二维甚至多维数组也可以这样。

我们再来举一个二维数组的例子:

#include<stdio.h>
int main()
{
	int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", a[i][j]);
		}
		printf("\n");
	}
	printf("\n");
	printf("\n");
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 4; j++)
		{
			printf("%d ", *(*(a + i) + j));
		}
		printf("\n");
	}
	return 0;
}

 在这个例子当中,我们主要对比了a[ i ][ j ]和 *(*(a + i) + j)

它们都是访问数组的形式,用到了不同的操作符。但代表的含义是一样的都是下标为i,j的元素。

6、*p++和(*p)++的区别

其实这个很简单,需要注意的是这两个不是同一个东西。

*p++的含义是对p进行解引用,然后指针 p自增。

(*p)++的含义是先对p进行解引用,然后对其解引用的值自增。

7、指针和数组的关系表述

指针提供一种以符号形式是以哦那个地址的方法,因为计算机的硬件指令非常依赖地址,指针某种程度上把程序员想要传达的指令以更接近机器的方式表达。使用指针能够更有效率,并且,指针能够有效地处理数组,数组表示法实际上是变相地使用指针。

8、函数传参的传址调用

这个详见之前的博客,说的已经很详细了,我比较懒,不想再重复了,直接拿来用了。

0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客

在什么位置我给标注一下

 9、函数传参传数组

对于一个函数,想要传参传数组,

我们在实参中,就直接是传一个数组名就可以了。

但是,在形参中,我们有很多种写法,其中一些写法是对的,还有一些写法注意是错误的。

一维数组传参

我们来看这么写写法:

 (这里的test(a)可以被修改为test1(a),test2(a),test3(a))

int test1(int a[])
{}
int test2(int a[3])
{}
int test3(int* a)
{}

这三种方法到底可不可以呢?

答案是:都是可以的。(只考虑传参,不考虑返回值以及函数的实现方式什么的)

因为这里int a[ ]就是等价于int* a。

a[ ]里的数可以为任何数(必须是大于0的整数)。

这里的a[ ]只是用来便于理解,并不具有实际意义,其本质就是一个指针。

就是说,其本质就是第三种形式。一个指针。

那下面的可不可以呢?

 先看第四个:我们看到,a是一个指针数组,那么其调用函数的形参也是一个指针数组的形式。肯定是可以的。

再看第五个:由于在传参的过程中,传递的是数组a首元素的地址,而数组a是一个指针数组,所以其首元素的地址就是一个指针的地址。而指针的地址实际上就是一个二级指针。所以,第五种写法也是对的。

多维数组传参(以二维数组为例)

#include<stdio.h>
void test(int arr[3][5])//ok?
{}
void test(int arr[][])//ok?
{}
void test(int arr[][5])//ok?
{}
void test(int *arr)//ok?
{}
void test(int* arr[5])//ok?
{}
void test(int (*arr)[5])//ok?
{}
void test(int **arr)//ok
{}
int main()
{
	int* a[3][4] = { 0 };
	test(a);
	return 0;
}

先来看第一个:void test(int arr[3][5]){}

这个肯定是可以的。我们之前在写形参的时候都是这样写的。它的形式与二维数组一致,所以是可以的。

再看第二个:void test(int arr[][]){}

这个看上图都已经挂红了,肯定是不行的。原因很简单,和我们在定义二维数组的时候是一样的,就是说,必须要指出其列数。可以省略其行数,但是不可以省略其列数。

再看第三个:void test(int arr[][5]){} 同理第二个,这个是可以的。

我们4、5、6、7一起来看

void test(int *arr){}
void test(int* arr[5]){}
void test(int (*arr)[5]){}
void test(int **arr){}

在弄清楚这个问题之前,我们需要来搞清楚一个问题:二维数组所传的首元素的地址到底是谁的地址?

我们知道,二维数组长这样:

 如图,我们表示一个四行四列的二维数组。

那么,我们所传的二维数组a到底是谁的地址?是第一行的1?还是什么?

这里笔者就不浪费时间了,直接给出答案。(也是经历了千辛万苦才得到的答案....呜呜呜~~~)

其传递的是第一行的地址。

为什么是这样呢?

我们可以这样来试着理解一下:

类比一维数组,如果把二维数组想象成一个一维数组,那么二维数组就是一个数组元素为数组的一维数组。

举个例子:

int a[4][4] = {{1,0,1,0},{0,1,0,1},{1,0,1,0},{0,1,0,1}};

 里层的{ }看成是外层{ }的一个元素。那么a就是一个以数组为元素的数组。

这个时候,传递时,所传的就是首元素的地址;而其首元素就是{1,0,1,0}

所以,其传递的就是数组{1,0,1,0}的地址。

所以说,其传递时一个数组的地址!

那么,就必须要用一个接收数组地址的变量来去接收。而谁可以来做这个事情?

就是我们刚刚说的数组指针。

所以,4,5,6,7里只有6是正确的。

提醒一下:千万不要受到一维数组的影响,以为一维数组是用一级指针接收,那么二维数组就是用二级指针接收!如果用二级指针,编译器会报警告(严格的会报错),并且会让你的程序的执行结果超出你的掌控。

10、函数指针

我们同上面所说的数组指针,

数组指针是指一个指向数组的指针;

而函数指针就是一个指向函数的指针。

怎么去写呢?

int Add(int x, int y)
{
	return x + y;
}
int main()
{
	int (*p)(int, int) = &Add;
	return 0;
}

如图,这里的p就是一个函数指针。

我们的写法是:

当然,这里的参数可以不止有两个。

而这里的指针p, 存放的就是函数的地址。

我们可以通过函数的地址来找到这个函数,从而是实现调用。

那么,函数指针如何使用呢?

就像这样。p是存储着函数的地址,对p进行解引用,就相当于这个函数Add,后面跟上参数。

也就是说,(*p)(2,3)就相当于(Add)(2,3),从而实现函数的调用。

 而这里的*实际上可以省略,就是是说,实际上可以直接写p(2,3)。理由是什么呢?

理由是函数名本身就是一个地址。

函数名本身就是一个地址!

我们来看:

你不论怎么样对Add去取地址,结果都是这个地址。

同样的道理,不论你怎么对p去解引用,你得到的都还是函数的地址。就是说,哪怕你这样写: (******p)(2,3),还是相当于一个Add(2,3)

需要注意的是,如果我们这样去写:

int* p(int,int);

那么这个就是一个普通的函数声明,p表示的是一个函数。因为这里的p会优先于()结合。函数的返回值为int* 。也就是我们下面所要说的————返回值为指针的函数。

我们来看一看这两个实例:

//实例1(*(void (*)())0)();

//实例2void (*signal(int , void(*)(int)))(int);

先说实例1:

 (*(void (* )( ) ) 0 ) ( );

什么意思?

我们一步一步拆解分析:

void (* )( )是一个函数指针类型,指向的函数的返回类型为void。

0先与左边的括号结合,也就是先是(void (* )( ) ) 0,那么它的意思就是将数字0强转为void(*)()的函数指针。

然后*(void (* )( ) ) 0,表示对这个函数指针解引用。

然后 (*(void (* )( ) ) 0 ) ( );表示调用这个函数。

总结来说,就是将0先强转为类型为void (* )( )的函数指针,然后去调用这个函数。

实例2:

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

这个实际上是一个函数声明。

signal 是一个函数名,(int , void(*)(int))是其两个参数,一个是int ,一个是void(*)(int)(函数指针,参数为int,返回类型为void)其返回类型也是一个函数指针,类型为void (*)(int) (指向参数为int,返回值为void 的函数指针)

我们这里为了便于理解,再来解释一下:

我们如果typedef一下这个函数指针

我们能不能这样写?

typedef void(*)(int) fun_t;

 很遗憾,这样写法是不行的。

下面的写法是允许的:

typedef void(*pfun_t)(int);

 这两个是一个意思。这里的pfun_t就是类型名

有了这个typedef,我们就可以把这个代码改成:

pfun_t signal(int, pfun_t);

 这个代码与上述代码一个意思。

11、返回指针的函数——注意要避免野指针

一个函数如果返回值是一个指针类型,尤其需要避免野指针的出现。

为什么?

我们来通过一个例子来讲解:

#include<stdio.h>
int* test(int x, int y)
{
	int a[2] = { x,y };
	return a;
}
int main()
{
	int* p = test(1,2);
	printf("%d %d", *p, *(p + 1));
	return 0;
}

让上述代码执行,我们会发现:

这好像没有什么问题。

但是,我们可以在底部看到一个警告:

 为什么不能这样做?

我们先来看看其危害吧:

我在函数和printf之间再加上一个printf

 这个时候,*p和 *(p+1)便不再是我们想要的1,2了。而是变成了一个个随机值。

为什么会这样?

原因很简单。在test函数里的a,是一个临时变量,临时变量在出函数test的时候,会被销毁。那么a所指向的内容也会被销毁。就是说,a[0]和a[1]的内存会还给操作系统。不再被用户所使用。

p确实接收到了a的地址的值,但是a所指向的内容,也就是其地址所指向的内容,就像我们刚刚说的那样,该地址以及后面的地址的内存会被还给操作系统。

这有什么危害呢?

当内存还给系统,不再被我们所使用的时候,在下一次函数调用其他函数的时候,就有可能会使用。

这也就是我加了一个printf后会什么会变成这个样子的原因:

在我直接用printf去打印的时候,其内容并未被覆盖或者使用,编译器并未改变其值。需要注意的就是虽然值是这个,但是已经不是用户的了。

当我再加上了一个printf的时候,由于printf也是函数,函数在调用的时候会创建和销毁栈帧,这部分的内存被系统使用,原来的值就会被覆盖。

所以说,这里的p是一个野指针。这也就是为什么会出现这种情况的原因。

因此,我们一定在书写的时候要避免这种情况。

 

12、函数指针数组

我们在函数指针的基础上再引申一下:

如果我们这样:

这个在上述函数指针基础上就加了一个[ ]。

我们来理解一下:我们知道,这里的[ ]会先与p结合,那么p就是一个数组。数组的每个元素都是指向int (*)(int , int)的函数指针

这个有什么用呢?计算器就可以通过这个来实现。可以来看一下这篇文章:

用函数数组指针来实现计算器的功能(简易模式)(不过稍微改改也能算比较复杂的)_jxwd的博客-CSDN博客

13、指向函数指针数组的指针

这个有点绕。它什么意思呢?

首先,有这么一个函数指针数组

int test(int x,int y)
{}
int main()
{
    int(*p)(int,int) = test;//这是一个函数指针
    int(*p1[2])(int,int);//这是一个函数指针数组
    p1[0] = test;
    int(*(*p2)[2])(int, int) = &p1;//这里的p2就是一个指向函数指针数组的指针。
}

上面的p2,就是一个指向函数指针数组的指针。

如果在p2后面再加[ ]变成数组,那么又变成了一个函数,就又变成了一个指向函数指针数组的指针数组.....无穷尽套娃下去,这就有点离谱了。。。。。。

14、回调函数

简单来说,回调函数就是一个通过函数指针调用的函数。

我们依照着刚刚用函数指针数组写的计算器程序来看,实际上,这就是一个回调函数的实例

#include <stdio.h>
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;
}
 
int main()
{
	int a = 0;
	int (*output[4])(int, int) = { add,sub,mul,div };
	printf("是否进入四则运算模式\n");
	printf("*****1       是*****\n");
	printf("*****0       否*****\n");
 
	scanf("%d", &a);
	int b = 0;
	int c = 0;
	int d = 0;
 
	while (a)
	{
		printf("**************************\n");
		printf("*1  add**********2  sub***\n");
		printf("*3  mul**********4  div***\n");
		printf("**************************\n");
		printf("请选择\n");
		scanf("%d", &b);
		printf("请输入计算的两个值\n");
		scanf("%d%d", &c, &d);
		int e = 0;
 
		e = (output[b - 1])(c, d);// 回调函数实例
		printf("%d\n", e);
		printf("是否继续\n");
		printf("*****1       是*****\n");
		printf("*****0       否*****\n");
		scanf("%d", &a);
	}
 
	return 0;
}

好啦,本节内容就到此结束啦。

欢迎各位看官关注我@jxwd,订阅专栏,就能持续看到我的文章啦😀😀

C语言自学保姆教程——第一节--编译准备与第一个C程序_jxwd的博客-CSDN博客

0基础C保姆自学 第二节——初步认识C语言的全部知识框架_jxwd的博客-CSDN博客_c语言全部框架
0基础C语言自学教程——第三节 分支与循环_jxwd的博客-CSDN博客

0基础C语言保姆教程——第4节 函数_jxwd的博客-CSDN博客

0基础C语言保姆教学——第五节 数组_jxwd的博客-CSDN博客

0基础C语言保姆教程——第六节 操作符、表达式和语句_jxwd的博客-CSDN博客

0基础C语言自学教程——第七节 初始指针_jxwd的博客-CSDN博客
 

  • 32
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

jxwd

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

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

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

打赏作者

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

抵扣说明:

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

余额充值