指针之旅(4)—— 指针与函数:函数指针、转移表、回调函数

news2024/9/21 2:39:06

目录

1. 函数名的理解

1.1 “函数名”和“&函数名”的含义

1.2 函数(名)的数据类型

2. 函数指针(变量)

2.1 函数指针(变量)的创建格式

2.2 函数指针(变量)的使用格式

2.3 例子 · 判别

3. typedef 关键字

3.1 typedef的作用

3.2 typedef的运作逻辑 和 函数指针类型的重命名

4. 函数指针数组

5. 转移表

5.1 转移表的概念

5.2 转移表 与 加减乘除计算器

6. 回调函数

6.1 回调函数的概念

6.2 回调函数 与 加减乘除计算器


1. 函数名的理解

1.1 “函数名”和“&函数名”的含义

我们在《指针之旅(3)—— 指针与数组》中了解过“数组名”代表数组首元素的地址,“&数组名”代表整个数组的地址,共2种含义。但是在函数这可就不一样了:

“函数名”和“&函数名” 在数值上相同,含义上也相同,都表示函数的地址。

代码演示: 

int add(int a, int b)
{
	return a+b;
}
int main()
{
	printf("add = %p\n",add);
	printf("&add = %p\n", &add);
	return 0;
}

add与&add数值一样: 

它们的含义也相同,且都不能进行+-操作:

1.2 函数(名)的数据类型

函数的数据类型:

  • 在函数声明中,去掉函数名就是函数的数据类型了:
  • “ 返回类型 (参数表) ”

比如:

 这里的加法函数add的声明是“int add(int a, int b)”,它的数据类型是“int (int , int)”。(其实在函数声明时,参数表中的变量名可以省略)

2. 函数指针(变量)

2.1 函数指针(变量)的创建格式

我们类比推理一下:

  • 字符指针-->是指针变量-->存放的是字符变量的地址
  • 整型指针-->是指针变量-->存放的是整型变量的地址
  • 函数指针-->是指针变量-->存放的是函数的地址

所以我们知道了函数指针的定义:函数指针(变量)存放的是函数的地址

函数指针(变量)的2种初始化:

1. 返回类型  函数名 (参数表);                                                //先要有函数

2. ❶同返回类型  (* 函数指针名) (同参数表) = &函数名;      //再有函数指针

    ❷同返回类型  (* 函数指针名) (同参数表) = 函数名

(两种初始化都可以,因为“函数名”和“&函数名”代表的含义相同)

以加法函数“int add(int a,int b)”举例,函数指针的创建和初始化可以写成:

  • int (*pf)( int a, int b) = &add;
  • int (*pf)( int , int ) = add;

(参数表中的变量名可写可不写)


为什么创建方式是“ int (*pf)(int, int) = &add ”,而不是“ int(*)(int, int) pf = &add ”?

原因:“(参数表)”中的小括号()是函数调用操作符,函数调用操作符有两种功能,(1)是声明函数(2)是调用函数。无论哪种功能,它的结合规则都是自左向右结合

补充:函数指针创建时,函数调用操作符此时的作用是(1)声明函数。由于这里声明的对象是指针,所以使得该指针获得了函数的性质。

2.2 函数指针(变量)的使用格式

函数指针(变量)的2种使用格式

❶ (*函数指针名)(参数1,参数2,…);                //可理解为“ *&函数名(参数表)”

❷ 函数指针名 (参数1,参数2,…);                  //可理解为“ 函数名(参数表)”

这两种方式是一样的。

代码举例:

int add(int a, int b)
{
	return a + b;
}
int main()
{
	int (*pf)(int, int) = &add;
	printf("%d\n", (*pf)(60, 4));
	printf("%d\n", pf(60, 4));
	return 0;
}

可以看到,两种使用格式都能算出正确的结果。

2.3 例子 · 判别

例子1:

 ( *( void (*)( ) ) 0 )( );        //请解读该语句的意思,提示:切入点是viod(*)()

图解:

这句话的意思是:

(1)先将0强制转换成void(*)()型的函数指针

即:( void (*)() ) 0;

(2)然后去调用0地址处的函数。

即:( *0 )( );

例子2:

  void ( * signal(int , void(*)(int) ) )(int);        //请解读该语句的意思

提示:

(1)切入点是 signal、void(*)(int)和最后一个(int)。

(2)以 “函数名 (参数表)” 的形式组合。

图解:

写成这样让大家好理解一点:(但在编译器中不能写成这样,是错的)

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

      |                   |              |

      |                   |     这是参数表,调用该函数时要输入这两种类型的变量            

      |        signal是函数名

void(*)(int)是函数的返回类型                                          

3. typedef 关键字

3.1 typedef的作用

typedef作用的对象只有数据类型,用来给类型进行重命名(定义新的别名)。可以将复杂的类型,简单化。

最基础的使用方式:

1.    typedef    要被重命名的类型    新的别名;

⽐如,你觉得 unsigned int 写起来不⽅便,如果能写成 uint 就⽅便多了,那么我们可以使⽤:

  • typedef  unsigned int  uint;     

//虽然unsigned与int直接有空格,但是unsigned int是一种计算机中的内置类型,是合起来看的

可以看到,旧的类型重命名后依然能被使用。

3.2 typedef的运作逻辑 和 函数指针类型的重命名

typedef的运作逻辑是:

在正常创建变量(指针、数组、函数)的格式基础上,原本书写变量名(指针名、数组名、函数名)的地方typedef把该地方的内容当作是旧类型的别名

比如,我们要创建一个数组int arr[10],但是要求用typedef的方式创建。

这个arr数组的类型是int [10],我们可以把它重命名为int10。

正确的重名方式是:typedef  int int10[10];

而不是:typedef  int [10]  int10;

如下:

函数指针类型的重命名规则也是如此:

比如,我们把 指向加法函数的指针类型 重命名成PADD,应该写成:typedef int(*PADD)(int , int);

代码如下:

int add(int a, int b)
{
	return a + b;
}

typedef int (*PADD)(int a, int b);	//参数表中的变量名可不写
int main()
{
	//创建函数指针padd
	PADD padd = &add;
	//以函数指针的方式调用函数add
	int ret = padd(4, 10);
	printf("4与10之和为%d\n", ret);
	return 0;
}

结果:

4. 函数指针数组

函数指针数组中的每个元素,它的数据类型都是函数指针类型。

函数指针数组的创建格式:

  • 在函数指针名后面加上下标引用操作符[ ],并写上数组元素的个数:
  • 即“ 返回类型 ( *函数指针数组名[数组元素个数] )( 参数表 ) ”

补充:谁最先与名字结合,该名称的本质就是什么。[ ]的优先级比*的优先级高,所以下标引用操作符[ ]最先与名字结合,所以函数指针数组的本质是一个数组

比如我们要创建一个有3个元素的数组parr,每个元素的类型都是int (*)( )。

正确的格式:

  • int (*parr[3])( ) = {0};

常见错误格式:

  • int *parr[3]( );
  • int (*)( ) parr[3];

5. 转移表

5.1 转移表的概念

转移表的本质是一个函数指针数组。传统的条件选择语句(如switch)在处理大量操作时会变得冗长,而转移表通过将操作映射到函数指针数组中,可以显著减少代码的重复性

5.2 转移表 与 加减乘除计算器

我们来做一个只有加减乘除的整型计算器,一开始我们会这样想:

头文件counter.h

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
void menu();

函数文件counter.c(包含计算器函数与菜单函数)

#include"counter.h"
int add(int a, int b)	//1.加法
{
	return a + b;
}

int sub(int a, int b)	//2.减法a-b
{
	return a - b;
}

int mul(int a, int b)	//3.乘法
{
	return a * b;
}

int div(int a, int b)	//4.除法a/b
{
	return a / b;
}

void menu()		//菜单页面
{
	printf("*************************\n");
	printf("    1:加法\t2:减法 \n");
	printf("    3:乘法\t4:除法 \n");
	printf("-> 0:退出 \n");
	printf("*************************\n");
	printf("请选择并输入对应的数字:");
}

最后我们写成一个简单的计算器程序: test1.c 

版本1——普通版:
#include"counter.h"
int main()	
{
	int input;	  //input对应菜单的选择
	int x, y;	 //x和y分别对应运算的数值a和b
	int ret;	//ret接收计算器运算后结果
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("输入2个操作数:");
			scanf("%d %d", &x, &y);
			ret = add(x, y);
			printf("计算结果是:%d\n\n", ret);
			break;
		case 2:
			printf("输入2个操作数:");
			scanf("%d %d", &x, &y);
			ret = sub(x, y);
			printf("计算结果是:%d\n\n", ret);
			break;
		case 3:
			printf("输入2个操作数:");
			scanf("%d %d", &x, &y);
			ret = mul(x, y);
			printf("计算结果是:%d\n\n", ret);
			break;
		case 4:
			printf("输入2个操作数:");
			scanf("%d %d", &x, &y);
			ret = div(x, y);
			printf("计算结果是:%d\n\n", ret);
			break;
		case 0:
			printf("程序已成功退出\a\n");
			break;
		default:
			printf("选择错误,请重新选择:\n");
			break;	//这个break可以不写
		}
	} while (input);	//input等于0就退出程序
	return 0;
}

我们可以发现,这个基础的版本有很多重复的部分。比如:

  1. printf("输入2个操作数:");
  2. scanf("%d %d", &x, &y);
  3. ret = add(x, y);
  4. printf("计算结果是:%d\n\n", ret);

这4句相似语句重复出现了4次

思考一下,能不能让这4句话只出现一次?

  1. 这4句相似的话唯一不同的地方在于“ret = 函数返回值”
  2. 加减乘除这4个函数,他们都是int (int, int)型,我们可以用一个函数指针数组来对这4个函数进行映射。

于是我们有了下面这个版本: test2.c 

版本2——转移表法:
int main()	
{
	int input;	int x, y;	int ret;		 
	//创建转移表
	int (*pfun[5])(int, int) = { 0,add,sub,mul,div }; //pfun[0]=0后,使得函数选择与下标序号一一对应。
	do
	{
		menu();
		scanf("%d", &input);
		if (input >= 1 && input <= 4)
		{
			printf("输入2个操作数:");
			scanf("%d %d", &x, &y);
			ret = pfun[input](x, y);//通过下标引用操作符[]和函数调用操作符()来使用对应的函数
			printf("计算结果是:%d\n", ret);
		}
		else if (input == 0)
		{
			printf("程序已成功退出\a\n");
		}
		else
		{
			printf("选择错误,请重新选择:\n");
		}
	} while (input);
	return 0;
}

6. 回调函数

6.1 回调函数的概念

回调函数的概念:

如果你把函数的指针(地址)作为参数传递给另⼀个函数,当这个指针被⽤来调⽤其所指向的函数时,被调⽤的函数就是回调函数

回调函数不是由该函数的实现⽅直接调⽤,⽽是在特定的事件或条件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。

举例说明:

假设现在有一个叫blog的函数,它的函数声明是这样的:int  blog( int (*pfun)(int , int)  , int n);

  • 那么pfun所指向的函数就是回调函数,函数指针pfun接收着回调函数的地址。
  • blog函数是调用回调函数的函数。(也叫主调函数 或者 回调触发函数)

6.2 回调函数 与 加减乘除计算器

对于版本1的重复的4句话,我们可以通过函数回调的方式使其变成1句话: test3.c 

版本3——函数回调版
//调用回调函数的汇合函数
void calculator(int (*pfun)(int, int)) //输入的参数是计算器函数的地址,形参pfun是函数指针
{
	int x, y;
	printf("输入2个操作数:");
	scanf("%d %d", &x, &y);
	//使用计算器函数
	int ret = pfun(x, y);
	printf("计算结果是:%d\n", ret);
}

int main()
{
	int input;
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calculator(add);//case1的回调函数是add
			break;
		case 2:
			calculator(sub);//case2的回调函数是sub
			break;
		case 3:
			calculator(mul);//case3的回调函数是mul
			break;
		case 4:
			calculator(div);//case4的回调函数是div
			break;
		case 0:
			printf("程序已成功退出\a\n");
			break;
		default:
			printf("选择错误,请重新选择:\n");
		}
	} while (input);
	return 0;
}

本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2120883.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全球瞩目丨2024深圳秋季糖酒会火热招商中

第 111 届深圳秋季全国糖酒会 2024 年 10 月29-31日 将在深圳国际会展中心&#xff08;宝安新馆&#xff09;盛大举行。 这是一场备受瞩目的行业盛会&#xff0c; 为企业提供了一个展示产品、 拓展市场、加强合作的绝佳机会。 作为亚洲地区食品行业规模最大、最具影响力的…

stm32之硬件SPI读写W25Q64存储器应用案例

系列文章目录 1. stm32之SPI通信协议 2. stm32之软件SPI读写W25Q64存储器应用案例 3. stm32之SPI通信外设 文章目录 系列文章目录前言一、电路接线图二、应用案例代码三、应用案例代码分析3.1 基本思路3.2 相关库函数介绍3.3 MySPI模块3.3.1 模块初始化3.3.2 SPI基本时序单元模…

指挥中心操作台厂家哪家好?选择时需要注意哪些?

在构建高效、稳定的指挥中心过程中&#xff0c;操作台作为核心设备之一&#xff0c;其选择至关重要。面对市场上琳琅满目的指挥中心操作台厂家&#xff0c;如何挑选出既符合需求又品质卓越的合作伙伴&#xff0c;成为众多采购者关注的焦点。接下来就给大家从以下几个方面探讨指…

uniapp设置隐藏qiun-data-charts数据标签

隐藏前&#xff1a; 隐藏后&#xff1a; 具体代码实现&#xff1a; 在opts配置中传入 "dataLabel": false 即可

个性化推荐兴趣社交社交平台

1 项目介绍 社交兴趣平台是一个基于 spring boot、vue3 的社交平台&#xff0c;旨在为用户提供一个分享、交流和发现各种有趣内容的场所。 该平台的核心功能是让用户能够创建个人主页并发布自己的动态、经历、见解和创意。用户可以自由发表各种主题的内容&#xff0c;涵盖但不…

《向量数据库指南》——非结构化数据大爆发,向量数据库引领电商推荐新潮流

在当今数据驱动的时代,数据作为企业的核心资产,其价值挖掘的深度与广度直接关乎到企业的竞争力和创新能力。长久以来,结构化数据因其规整的格式和易于分析的特性,成为了数据科学家和工程师们研究的热点,其潜力在多个领域已被充分挖掘和应用。然而,随着互联网的飞速发展,…

C语言常见运算符

C语言提供了丰富的运算符&#xff0c;这些运算符用于执行各种类型的操作&#xff0c;比如算术运算、比较运算、逻辑运算、位运算等。下面是一些基本的C语言运算符分类及其示例&#xff1a; 1. 算术运算符 加法 (): a b 表示a和b的和。减法 (-): a - b 表示a和b的差。乘法 (*…

2024年Ai智能绘画Stable Diffusion软件+整合包+保姆式教程

前言 在2024年的科技浪潮中&#xff0c;一款名为Stable Diffusion的AI智能绘画软件吸引了全球的目光。它不仅为艺术家和设计师提供了无限创意的可能&#xff0c;也让我们每个人都能轻松体验绘画的乐趣。那么&#xff0c;Stable Diffusion究竟有何魅力&#xff1f;它又是如何工…

消息队列实现多人聊天

消息队列实现多人聊天 分析&#xff1a; 每个程序都有两个任务&#xff0c;一个任务是负责接收消息&#xff0c;一个任务是负责发送消息&#xff0c;通过 fork 创建子进程实现多任务。 一个进程负责接收信息&#xff0c;它只接收某种类型的消息&#xff0c;只要别的进程发送…

2024i春秋第四届长城杯网络安全大赛暨京津冀网络安全技能竞赛初赛wp-flowershop+easyre

flowershop 如图所示&#xff0c;v8是钱的数量&#xff0c;strncpy从src复制三个字符给dest的数组里&#xff0c;最后一个元素是0&#xff0c;相当于字符串截断&#xff0c;strcmp是比较c和dest中的内容&#xff0c;如果相等就不会exit(0)&#xff1b;双击c进去看&#xff0c;c…

Pr:新建序列 - 设置

在“新建序列”对话框中&#xff0c;“设置” Settings选项卡的主要用途是选择预设或手动配置序列的参数&#xff0c;以确保与用户的素材和输出需求相匹配。 可以根据项目的具体要求自定义序列中的视频和音频格式&#xff0c;包括帧速率、分辨率、工作色彩空间、音频采样率等以…

nvm install 16.14.1报错“Node.js v16.14.1 is not yet released or available.”

原因&#xff1a; 使用命令nvm ls available&#xff0c;结果列出的可供下载的版本列表为空&#xff0c;这就是原因了 解决办法&#xff1a;修改镜像 nvm node_mirror https://npmmirror.com/mirrors/node/ nvm npm_mirror https://npmmirror.com/mirrors/npm/ 结果&#xf…

基于SpringBoot+Vue+MySQL的校园生活服务平台

系统展示 用户前台界面 管理员后台界面 系统背景 二十一世纪互联网的出现&#xff0c;改变了几千年以来人们的生活&#xff0c;不仅仅是生活物资的丰富&#xff0c;还有精神层次的丰富。在互联网诞生之前&#xff0c;地域位置往往是人们思想上不可跨域的鸿沟&#xff0c;信息的…

springboot优雅停机无法关闭进程,kill无法停止springboot必须kill -9,springboot线程池使用

背景最近项目在jenkins部署的时候发现部署很慢&#xff0c;查看部署日志发现kill命令执行后应用pid还存在&#xff0c;导致必须在60秒等待期后kill -9杀死springboot进程 应用环境 springboot <dependency><groupId>org.springframework.boot</groupId>&l…

u盘怎么制作win10启动盘_win10启动盘制作详细教程

u盘怎么制作win10启动盘&#xff1f;制作win10启动盘方法有很多&#xff0c;有官方制作方法&#xff0c;有第三方u盘启动盘制作方法&#xff0c;下面小编就教大家制作win10启动盘详细教程。 u盘怎么制作win10启动盘&#xff1f; 制作win10启动通常有两种方法&#xff1a;直接安…

带有HSE组件的S32系列芯片中各子系统如何依次启动?

《S32系列芯片——Boot详解》系列——带有HSE组件的S32系列芯片中各子系统如何依次启动&#xff1f; 一、各子系统的重置释放顺序二、启动流程2.1 安装启动过程2.2 正常启动流程 博主已开通同名公众号&#xff0c;通过文末或主页二维码关注博主&#xff0c;将为你推送最新、最细…

音乐网站-前后台登录注册搜索试听下载评论音乐分计算机毕业设计/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序

1. 前台功能模块 首页&#xff1a; 展示热门音乐、推荐音乐、最新发布。搜索框&#xff1a;支持音乐、专辑、艺人等的搜索。用户登录/注册入口。 用户注册和登录&#xff1a; 用户注册&#xff1a;输入用户名、密码、邮箱等信息。用户登录&#xff1a;输入用户名和密码。密码找…

如何在NXP源码基础上适配ELF 1开发板的PWM功能

本次源码适配项目是在NXP i.MX6ULL EVK评估板所搭载的Linux内核源码&#xff08;特定版本为Linux-imx_4.1.15&#xff09;基础上进行的&#xff0c;主要目标是通过调整功能接口引脚配置&#xff0c;使其适应ELF 1开发板。为了深入阐述这一适配过程&#xff0c;我们将以PWM功能的…

浏览器百科:网页存储篇-IndexedDB应用实例(十二)

1.引言 在现代Web开发中&#xff0c;IndexedDB作为一种强大的客户端存储技术&#xff0c;越来越受到开发者的青睐。它不仅能够存储大量结构化数据&#xff0c;还提供了高性能的查询和事务支持。在前面的文章中&#xff0c;我们已经详细介绍了IndexedDB的基本概念、使用方法以及…