函数指针与回调函数详解

news2024/9/22 5:37:43

目录

  • 1.函数指针
  • 2.函数指针数组
  • 3.指向函数指针数组的指针
  • 4.回调函数

1.函数指针

前面我们学的:

  • 整形指针是指向整形的指针
  • 字符指针是指向字符的指针
  • 数组指针是指向数组的指针

所以函数指针就是指向函数的指针

假如有一个int类型变量a,要取它的地址就是&a,有一个字符类型变量c,要取它的地址就是&c,那么一个函数的地址是怎样取到的呢。

接下来,我们取一下一个函数的地址

#include<stdio.h>
int sum(int x,int y)
{
	return x + y;
}

int main()
{
	printf("%p\n", &sum);
	return 0;
}

取出的地址:
请添加图片描述

那么函数指针的类型怎么写呢?

int sum(int x,int y)函数为例:
因为函数指针是一个指针,所以*需要先与指针名pf结合为(*pf),sum函数有两个int类型的参数,返回值为int
所以指向这个sum函数的函数指针为:int (*pf)(int,int)

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

int main()
{
	int (*pf)(int, int) = &sum;  //用一个函数指针接受sum函数的地址
	return 0;
}

对于一个数组,它的数组名是数组首元素的地址,对数组名取地址,会得到数组的地址
那么函数名和对函数取地址都表示这什么?
其实这两种写法没有区别,都是函数的地址,对于·1函数名去不去地址都能得到函数的地址
int (*pf)(int, int) = &sum;int (*pf)(int, int) = sum;等价


函数指针的解引用:

对函数指针解引用,再对参数列表中传参:int ret = (*pf)(1,2),这样就是对函数指针的解引用

int (*pf)(int, int) = sum;这样的写法时,可以理解为sum的地址赋给了指针pf,这时sumpf其实表示一个意思,而在平常的函数调用时int ret = sum(1.2),这里直接写sum不用解引用。
所以在用函数指针的解引用时,也可以不用加*,即为int ret = pf(1,2)
并且这里的*其实为摆设,写不写或者写几个都是表达一个意思


接下来解读两个有意思的语句:

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

1.(*(void (*)())0)();
分析:(void (*)()是一个函数指针,该函数指针指向一个无返回值,且无参数的函数,将(void (*)()用括号括起来后面跟着0,就是将0强制类型转换成函数指针类型,最后用*解引用强转后的函数指针,以因为参数列表中无参数,所以后面的括号中无参数。

结论:该代码是一次函数调用,首先先将代码中的0强制类型转圜为void (*)()类型的函数指针,然后解引用调用。

2.void (*signal(int , void(*)(int)))(int);
分析:首先从名字signal开始下手,可以看出signal是一个函数,有两个参数,第一个是int类型的,第二个是函数指针,该函数指针指向一个返回值为void,参数为int的函数,为了方便看,把分析过的部分取出,剩下的部分是void(* )(int),所以signal函数的返回类型就是一个函数指针,该指针指向一个返回值为void参数为int类型的函数

这里对于signal函数返回值类型理解有些困难,为什么把里边函数名那部分取出来后剩下的部分就是返回类型呢?
这其实是C语言语法的锅,在不考虑语法错误的情况下,完全可以写成这样:
void(*)(int) signal(int,void(*)(int)),这样十分容易理解返回值类型,但是这样写语法是错误的,只是易于理解而已

但是如void (*signal(int , void(*)(int)))(int);这样写,一层层的括号让人很不容易理解,这里就可以使用typedef简化,因为signal的第二个参数和返回类型都是同一类,所以将void (*)(int)简化

typedef void(*)(int) pf_t,这样写很容易看明白就是用pf_t名替换void (*)(int),但这样写是错误的,由于语法,只能写成typedef void(*pf_t)(int),下面的函数声明就可以写成pf_t signal(int,pf_t)

typedef void(*pf_t)(int);

	pf_t  signal(int, pf_t);

2.函数指针数组

函数指针数组——存放函数指针的数组

接下来我们写一个函数指针数组,函数指针数组顾名思义,就是有函数指针和指针数组演变而来的

int sum(int x,int y)//一个函数
{
	return x + y;
}
int(*p)(int,int) = &sum; //指向sum函数的一个函数指针

int(*p)(int,int),里面的*是与p结合的,此时还是一个指针,要将它改成一个数组,就应将名字先于[]结合,所以改为:int(*p[10])(int,int)

这里的int(*p[10])(int,int)就是函数指针数组

函数指针有什么用呢

例如要写一个计算器程序


#include <stdio.h>

void menu()
{
	printf("*******************************\n");
	printf("****** 1. add   2. sub    *****\n");
	printf("****** 3. mul   4. div    *****\n");
	printf("****** 0. exit            *****\n");
	printf("*******************************\n");
}

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 input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	do 
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Add(x, y);
			printf("%d\n", ret);
			break;
		case 2:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Sub(x, y);
			printf("%d\n", ret);
			break;
		case 3:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Mul(x, y);
			printf("%d\n", ret);
			break;
		case 4:
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = Div(x, y);
			printf("%d\n", ret);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

从上面的程序可以看出:case语句比较长,如果有更多的计算函数,则case语句还会变长
其实可以用函数指针数组简化

因为前面的加减乘除函数的返回值一致,参数列表一样,所以完全可以将这四个函数的地址存到一个函数指针数组里

void menu()
{
	printf("*******************************\n");
	printf("****** 1. add   2. sub    *****\n");
	printf("****** 3. mul   4. div    *****\n");
	printf("****** 0. exit            *****\n");
	printf("*******************************\n");
}

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 input = 0;
	int x = 0;
	int y = 0;
	int ret = 0;
	int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		if (input == 0)
		{
			printf("退出计算器\n");
			break;
		}
		else if (input>0&&input<=4)
		{
			printf("请输入两个操作数:>");
			scanf("%d %d", &x, &y);
			ret = p[input](x, y);
			printf("%d\n", ret);
		}
		else
		{
			printf("选择错误\n");
		}
	} while (input);
	return 0;
}

这里最主要的代码就是int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };将四个函数的地址存到一个数组里

用户会在菜单界面后输入一个整形数字,这个整形数字是用来供用户选择功能的,同时也可以称为函数指针函数的下标,通过下标取出数组中的函数指针,进而调用相应的函数。

这样写就一定程度上使代码精短

通过这个示例,这么写函数指针数组有一种跳转的感觉,给一个下标,就能通过数组下标访问到函数的地址,再去调用函数
所以像int (*p[5])(int, int) = { 0,Add,Sub,Mul,Div };这样的叫做转移表


3.指向函数指针数组的指针

前面学习了指向数组的指针

int arr[10];
int(*pa)[10] = &arr;

那么将函数指针数组的地址取出来,放到上什么类型的指针变量里呢?

这个类型是可以通过函数指针推出来的:

int (*pf[5])(int,int);这是一个函数指针数组,这里的名是先和[]结合的,所以是数组,想要变成指针,就要是名先与*结合,所以就得出:int (*(*ppf)[5])(int,int)

//ppf是指向函数指针数组的指针
int (*(*ppf)[5])(int,int) = &pf

4.回调函数

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

在上面的模拟计算器程序里,由于运用case语句,其中有大量重复的语句在这里插入图片描述

所以就可以使用回调函数的方法去解决这个问题:

我们发现,在各个case语句中,只有调用函数语句不同,其他语句都相同。所以先新建一个函数,将那些重复语句都封装在这个函数里,我们可以通过函数传参将不同的函数指针传过去


void cale(int (*p)(int,int)) //函数参数为一个函数指针
{
	int x = 0,y = 0;
	int ret = 0;
	int input = 0;
	printf("请输入两个操作数:>");
	scanf("%d %d", &x, &y);
	ret = p(x, y);
	printf("%d\n", ret);
}

int main()
{
	int input = 0;
	do 
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			calc(Add);
			break;
		case 2:
			calc(Sub);
			break;
		case 3:
			calc(Mul);
			break;
		case 4:
			calc(Div);
			break;
		case 0:
			printf("退出计算器\n");
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);

	return 0;
}

这样写就可以将case语句中的语句做到最简,想要使用哪个功能就将哪个函数的指针传过去

通过函数指针调用的函数是回调函数,所以在这个程序里Add,Sub,Mul,div是回调函数

还有一个回调函数的应用是qsort函数,具体的内容在另一篇文章中:点击跳转


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

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

相关文章

【Linux】Linux软件包管理器与Linux编辑器

文章目录&#x1f3aa; Linux软件包管理器&#x1f680; 1.yum基本介绍&#x1f680; 2.yum基本命令&#x1f680; 3.关于rzsz工具的安装与使用⭐3.1 rzsz工具介绍⭐3.2 rzsz工具安装⭐3.3 rzsz工具使用&#x1f3aa; Linux编辑器&#x1f680; 1.vim常用三种模式&#x1f680;…

事务管理-spring

什么是事务 - 事务是由N步数据库操作序列组成的逻辑执行单元&#xff0c;这系列操作要么全执行&#xff0c;要么全放弃执行。 • 事务的特性&#xff08;ACID&#xff09; - 原子性&#xff08;Atomicity&#xff09;&#xff1a;事务是应用中不可再分的最小执行体。 - 一致…

MySQL使用索引的最佳指南

MySQL使用索引的最佳指南1.选择合适的字段创建索引2.尽可能的考虑建立联合索引而不是单列索引3.注意避免冗余索引4.考虑在字符串类型的字段上使用前缀索引代替普通索引5.索引失效的情况1.选择合适的字段创建索引 不为 NULL 的字段 &#xff1a;索引字段的数据应该尽量不为 NUL…

java面向对象,全是对象,这么多对象2023015

面向对象&#xff08;一遍一遍的领悟&#xff09; Java支持面向对象的三大特征&#xff1a;封装、继承和多态&#xff0c; Java提供 了private、protected和public三个访问控制修饰符来实现良好的封装&#xff0c;提供了extends关键字来让子类继承父类&#xff0c;子类继承父类…

人工智能图像形状检测算法

博主简介 博主是一名大二学生&#xff0c;主攻人工智能研究。感谢让我们在CSDN相遇&#xff0c;博主致力于在这里分享关于人工智能&#xff0c;c&#xff0c;Python&#xff0c;爬虫等方面知识的分享。 如果有需要的小伙伴可以关注博主&#xff0c;博主会继续更新的&#xff0c…

Redis下载安装与配置(linux)

一、Redis下载与安装 1.下载安装包 官网下载地址&#xff1a;Download | Redis 点击"Download 7.0.7"&#xff0c;即可进行下载。 2.将安装包上传至服务器 2.1将安装包上传至/usr/local目录并解压 cd /usr/local lstar -zxvf redis-7.0.7.tar.gz2.2删除安装包 r…

第二天总结 之 商品类型管理界面的实现 之 添加和修改操作 的实现

添加和修改操作 页面跳转问题 点击修改按钮时 跳转的路径 如下 点击添加按钮时 跳转的路径如下 通过这两张图片 不难发现 跳转的是同一个jsp 但是添加操作 是不带id跳转 而修改操作是带着id跳转 所以在其 跳转的页面add_goods_type.jsp页面中 有一个这样的判断 如果没有id…

Linux常用命令——tmux命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) tmux Tmux是一个优秀的终端复用软件&#xff0c;类似GNU Screen&#xff0c;但来自于OpenBSD&#xff0c;采用BSD授权。 补充说明 使用它最直观的好处就是&#xff0c;通过一个终端登录远程主机并运行tmux后&a…

2022年HarmonyOS/OpenHarmony生态观察

一、鸿蒙生态世界快速构建升级中 HarmonyOS鸿蒙2019年正式面世&#xff0c;当时消费者只能在华为的智慧屏上体验&#xff1b;2020年&#xff0c;鸿蒙智联-华为面向智能硬件生态伙伴全新品牌和开放平台发布&#xff1b;2021年&#xff0c;智能手机等多种终端全面搭载HarmonyOS2…

UE4 RenderDoc笔记

1.Meh Viewer&#xff1a;当前DrawCall的Mesh信息&#xff0c;可以查看每个点的输入和输出 可以看到该DrawCall的Mesh顶点数量为510&#xff08;该材质ID的Mesh三角面数&#xff09;170*3&#xff0c;第一个顶点ID为3637 2.Texture Viewer:查看该次事件所调用的输入、输出缓…

【进阶】Spring Boot创建和使用

努力经营当下&#xff0c;直至未来明朗&#xff01; 文章目录一、Spring Boot 概述二、Spring Boot优点三、Spring Boot项目创建1. 使用IDEA创建&#xff08;社区版&#xff09;2. 网页版创建&#xff08;了解&#xff09;四、项目目录介绍和运行1. 项目目录介绍2. 输出hello w…

Win10 Hyper-V 固定虚拟机IP地址的方法

Windows10系统Hyper-V中存在着一个名为“Default Switch”的缺省虚拟交换机&#xff0c;其本质上是一块虚拟网卡&#xff0c;其所连接的虚拟网络的类型为"Internal"&#xff08;有关Hyper-V三种网络类型的含义&#xff0c;可参看《Hyper-V三种虚拟网络类型的理解_bol…

“深度学习”学习日记。误差反向传播法--算法实现

2023.1.18 经过学习了计算图、链式法则、加法层、乘法层、激活函数层、Affine层、Softmax层的反向传播的实现。今天来学习反向传播法的算法实现&#xff0c;做一次总结&#xff1b; 实现的思路&#xff08;“学习”的步骤&#xff09;&#xff1a; 一&#xff0c;前提 神经…

4.Java的基础语法

小伙伴们,本篇内容让我们一起来总结学习Java的基础语法吧!&#x1f609; 文章目录一、注释二、关键字三、字面量(也被叫做:常量/字面值常量)四、一些特殊字面量的书写五、变量(1)变量的定义格式:(2)输出打印变量:(3)变量的基本用法:(4)变量的注意事项:(5)变量的练习总结一、注释…

【第二章 Excel数据格式】

Excel数据格式1.Excel数据格式2.更改单元格格式2.1数值型数据的更改2.2文本型数据的更改2.3日期型数据的更改2.4日期型数据、数值型数据->文本型数据1.Excel数据格式 数字、文本和日期是最常用的三种数据格式&#xff0c; 数字一般右对齐&#xff0c;方便观测数据位数&am…

mac ganache安装以及在metamask创建ganache网络和账户导入

在做区块链本地测试时&#xff0c;需要测试网络、测试账户以及测试币&#xff0c;可以使用ganache来启动本地网络以及生成账户进行测试。 一、下载及安装ganache 首先下载ganache&#xff0c; 网址是这个https://trufflesuite.com/ganache/ 下载好后进行安装。 安装好以后使用…

Allegro如何让BUS线以粗线形式显示操作指导

Allegro如何让BUS线以粗线形式显示操作指导 在评估PCB布线的时候,设置好Bus线对于评估非常有帮助,Allegro不仅可以支持设置Bus组,还可以让Bus线以粗线形式显示,如下图 具体操作如下 选择Edit-PropertyFind选择nets

【algorithm】算法基础课---二分查找算法(附笔记 | 建议收藏)

&#x1f680;write in front&#x1f680; &#x1f4dd;个人主页&#xff1a;认真写博客的夏目浅石. &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;AcWing算法学习笔记 &#x1f4ac;总结&#xff1a;希望你看完…

几种常用的权重初始化方法

来源&#xff1a;投稿 作者&#xff1a;175 编辑&#xff1a;学姐 在深度学习中&#xff0c;权重的初始值非常重要&#xff0c;权重初始化方法甚至关系到模型能否收敛。本文主要介绍两种权重初始化方法。 为什么需要随机初始值 我们知道&#xff0c;神经网络一般在初始化权重…

【EasyExcel】在Java中操作Excel 完成数据的导入导出

快速入门 引入依赖 构建实体类 数据导出 参数 WriteWorkbook WriteSheet WriteTable 测试 数据导入 测试 EasyExcel是阿里巴巴开源的一个excel处理框架&#xff0c;以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一…