指针之旅(4)—— 指针与函数:函数指针、转移表、回调函数
目录
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()
{//创建函数指针paddPADD padd = &add;//以函数指针的方式调用函数addint 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和bint 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;
}
我们可以发现,这个基础的版本有很多重复的部分。比如:
- printf("输入2个操作数:");
- scanf("%d %d", &x, &y);
- ret = add(x, y);
- printf("计算结果是:%d\n\n", ret);
这4句相似语句重复出现了4次
思考一下,能不能让这4句话只出现一次?
- 这4句相似的话唯一不同的地方在于“ret = 函数返回值”。
- 加减乘除这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的回调函数是addbreak;case 2:calculator(sub);//case2的回调函数是subbreak;case 3:calculator(mul);//case3的回调函数是mulbreak;case 4:calculator(div);//case4的回调函数是divbreak;case 0:printf("程序已成功退出\a\n");break;default:printf("选择错误,请重新选择:\n");}} while (input);return 0;
}
本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ
相关文章:
指针之旅(4)—— 指针与函数:函数指针、转移表、回调函数
目录 1. 函数名的理解 1.1 “函数名”和“&函数名”的含义 1.2 函数(名)的数据类型 2. 函数指针(变量) 2.1 函数指针(变量)的创建格式 2.2 函数指针(变量)的使用格式 2.3 例子 判别 3. typedef 关键字 3.1 typedef的作用 3.2 typedef的运作逻辑 和 函数指针类型…...
打造线上+线下相结合的O2O平台预约上门服务小程序源码系统 带完整的安装代码包以及搭建部署教程
系统概述 本系统采用前后端分离的设计架构,前端以微信小程序为载体,提供直观、易用的用户界面;后端则采用稳定的服务器架构,确保数据处理的高效与安全。系统主要包括用户端、商户端和管理员端三大模块,通过API接口实现…...
python sys模块
在Python中,sys模块提供了访问和使用解释器的许多功能的方法,包括命令行参数、环境变量、路径管理、标准输入输出流等。sys模块是Python的标准库的一部分,不需要额外安装即可使用。 常用的sys模块功能 1. sys.argv sys.argv是一个包含命令…...
【Linux 报错】SSH服务器拒绝了密码。请再试一次。(xshell)
出现该错误 可能的原因: 你写入的登录密码错误了,错误原因有: 1、本来输入就错误了 2、创建用户时,只创建了用户名,但密码没有重新设置 3、多人使用同一台服务器时,该服务器管理员(本体&#x…...
云计算实训43——部署k8s基础环境、配置内核模块、基本组件安装
一、前期系统环境准备 1、关闭防火墙与selinux [rootk8s-master ~]# systemctl stop firewalld[rootk8s-master ~]# systemctl disable firewalldRemoved symlink /etc/systemd/system/multi-user.target.wants/firewalld.service. Removed symlink /etc/systemd/system/dbus…...
TAbleau 可视化 干货分享 | 简单三步助你打造完美仪表板
只需单击几下,你将能轻松创建美观、信息丰富的可视化效果、节省时间并推动业务向前发展! 借助精心设计的仪表板,分析师可以更好地理解复杂数据背后的信息,更有效地向他人分享你的见解,从而做出更明智的决策。 值得思考…...
JVM性能调优之5种垃圾收集器
JDK垃圾收集器 一、Serial GC垃圾收集器Serial GC的工作原理Serial GC的特点Serial GC的配置参数Serial GC的适用场景Serial GC的优缺点优点:缺点: Serial GC的总结 二、Parallel GC垃圾收集器Parallel GC的工作原理Parallel GC的特点Parallel GC的配置参…...
基于单片机的仔猪喂饲系统设计
文章目录 前言资料获取设计介绍功能介绍设计清单具体实现截图参考文献设计获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师,一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设…...
Helm Deploy Online Rancher v2.9.1
文章目录 准备安装查看下载 准备 $ kubectl get node NAME STATUS ROLES AGE VERSION kube-master01 Ready control-plane 19d v1.29.5 kube-node01 Ready <none> 19d v1.29.5 kube-node02 Ready <none&…...
【办公效率】Axure会议室预订小程序原型图,含PRD需求文档和竞品分析
作品说明 作品页数:共50页 兼容版本:Axure RP 8/9/10 应用领域:中小型企业的会议室在线预订 作品申明:页面内容仅用于功能演示,无实际功能 作品特色 本作品为会议室预订小程序原型图,定位于拥有中大型…...
论文解析一: SuperPoint 一种自监督网络框架,能够同时提取特征点的位置以及描述子
目录 SuperPoint:一种自监督网络框架,能够同时提取特征点的位置以及描述子1.特征点预训练2.自监督标签3.整体网络结构3.1 先对图像进行卷积3.2 特征点提取部分(Interest Point Decoder)3.3 特征描述子提取部分(Descrip…...
【评估指标】Fβ-score
1. Fβ-score 概述 Fβ-score 是一种综合考量精确率(precision)和召回率(recall)的分类评估指标。其公式为: 1.1 Precision(精确率):预测为正类的样本中,实际为正类的比…...
1963Springboot个性化音乐推荐管理系统idea开发mysql数据库web结构java编程计算机网页源码maven项目
博主介绍:专注于Java .net php phython 小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设,从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不然下次找不到哟 我的博客空间发布了1000毕设题目 方便大家学习使用 感兴趣的可以…...
solidity从入门到精通(持续更新)
我一度觉得自己不知何时变成了一个浮躁的人,一度不想受外界干扰的我被干扰了,再无法平静的去看一本书,但我仍旧希望我能够克服这些,压抑着自己直到所有的冲动和奇怪的思想都无法再左右我行动。 自律会让你更加自律,放纵…...
UEFI入门(二):edk2项目编译流程
UEFI入门(二):edk2项目编译流程 一、编译构建流程:1. 安装依赖工具2. 初始化构建环境3. 配置工具链和目标4. 定义平台配置5. 构建并编译 二、uefi-tools编译edk2实践:1. 克隆EDK2 项目2. 构建并编译 参考文章ÿ…...
局域网一套键鼠控制两台电脑(台式机和笔记本)
服务端(有键盘和鼠标的电脑作为服务端) 下载软件 分享文件:BarrierSetup-2.3.3.exe 链接:https://pan.xunlei.com/s/VO66rAZkzxTxVm-0QRCJ33mMA1?pwd4jde# 配置服务端 一, 二, 客户端屏幕名称一定要和…...
最新Nessus2024.9.8版本主机漏洞扫描/探测工具下载Windows版
Nessus号称是世界上最流行的漏洞扫描程序,全世界有超过75000个组织在使用它。该工具提供完整的电脑漏洞扫描服务,并随时更新其漏洞数据库。Nessus不同于传统的漏洞扫描软件,Nessus可同时在本机或远端上遥控,进行系统的漏洞分析扫描…...
关于使用 @iconify/vue2图标库组件的离线使用
Iconify 是最通用的图标框架,将各种图标库的图标集中在这里的一个组件库,例如ant-design,element-ui等 网站地址如下 https://iconify.design/getting-started/ 1.安装依赖 这两个包提供了图标组件和离线图标数据的支持。 npm install iconify/vue2 i…...
pdfmake生成pdf的使用
实际项目中有时会有根据填写的表单数据或者其他格式的数据,将数据自动填充到pdf文件中根据固定模板生成pdf文件的需求 文章目录 利用pdfmake生成pdf文件1.下载安装pdfmake第三方包2.封装生成pdf文件的共用配置3.生成pdf文件的文件模板内容4.调用方法生成pdf 利用pdf…...
PLM系统有哪些品牌推荐?国内不错的PLM厂商有哪些?
在当今快速变化的商业环境中,产品生命周期管理PLM系统已成为企业技术创新和管理创新的重要工具。PLM系统涵盖了产品从概念设计到市场推出、使用维护直至最终报废的整个生命周期,通过整合与产品相关的所有信息,助力企业实现高效、协同的产品开…...
ES6从入门到精通:前言
ES6简介 ES6(ECMAScript 2015)是JavaScript语言的重大更新,引入了许多新特性,包括语法糖、新数据类型、模块化支持等,显著提升了开发效率和代码可维护性。 核心知识点概览 变量声明 let 和 const 取代 var…...
CTF show Web 红包题第六弹
提示 1.不是SQL注入 2.需要找关键源码 思路 进入页面发现是一个登录框,很难让人不联想到SQL注入,但提示都说了不是SQL注入,所以就不往这方面想了 先查看一下网页源码,发现一段JavaScript代码,有一个关键类ctfs…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
Web后端基础(基础知识)
BS架构:Browser/Server,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端。 优点:维护方便缺点:体验一般 CS架构:Client/Server,客户端/服务器架构模式。需要单独…...
