肯尼斯·里科《C和指针》第13章 高级指针话题(2)函数指针

我们不会每天都使用函数指针。但是,它们的确有用武之地,最常见的两个用途是转换表(jump table)和作为参数传递给另一个函数。本节将探索这两方面的一些技巧。但是,首先容我指出一个常见的错误,这是非常重要的。

简单声明一个函数指针并不意味着它马上就可以使用。和其他指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数。下面的代码段说明了一种初始化函数指针的方法

int        f( int );
int        (*pf)( int ) = &f;

第2个声明创建了函数指针pf,并把它初始化为指向函数f。函数指针的初始化也可以通过一条赋值语句来完成。在函数指针的初始化之前具有f的原型是很重要的,否则编译器将无法检查f的类型是否与pf所指向的类型一致。

初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针。&操作符只是显式地说明了编译器将隐式执行的任务

在函数指针被声明并且初始化之后,就可以使用3种方式调用函数:

int ans;ans = f(25) ;     /*第一种办法*/
ans = (*pf)(25);  /*第二种办法*/
ans = pf(25);     /*第三种办法*/

第1条语句简单地使用名字调用函数f,但它的执行过程可能和想象的不太一样。函数名f首先被转换为一个函数指针,该指针指定函数在内存中的位置。然后,函数调用操作符调用该函数,执行开始于这个地址的代码。

第2条语句对pf执行间接访问操作,它把函数指针转换为一个函数名。这个转换并不是真正需要的,因为编译器在执行函数调用操作符之前又会把它转换回去。不过,这条语句的效果和第1条语句是完全一样的。

第3条语句和前两条语句的效果是一样的。间接访问操作并非必需的,因为编译器需要的是一个函数指针。这个例子显示了函数指针通常是如何使用的。

什么时候应该使用函数指针呢?前面提到过,两个最常见的用途是把函数指针作为参数传递给函数以及用于转换表。让我们各看一个例子。

13.3.1 回调函数

这里有一个简单的函数,它用于在一个单链表中查找一个值。它的参数是一个指向链表第1个节点的指针以及那个需要查找的值。

Node* search_list(Node *node, int const value)
{while(node != NULL){if(node->value == value)break;node = node->link;} return node;   
}

这个函数看上去相当简单,但它只适用于值为整数的链表。如果需要在一个字符串链表中查找,则不得不另外编写一个函数。这个函数和上面那个函数的绝大部分代码相同,只是第2个参数的类型以及节点值的比较方法不同。

一种更为通用的方法是使查找函数与类型无关,这样它就能用于任何类型的值的链表。我们必须对函数的两个方面进行修改,使它与类型无关。首先,必须改变比较的执行方式,这样函数就可以对任何类型的值进行比较。这个目标听上去好像不可能,如果编写语句用于比较整型值,它怎么还可能用于其他类型(如字符串)的比较呢?解决方案就是使用函数指针。调用者编写一个函数,用于比较两个值,然后把一个指向这个函数的指针作为参数传递给查找函数。然后查找函数调用这个函数来执行值的比较。使用这种方法,任何类型的值都可以进行比较。

必须修改的第二个方面是向函数传递一个指向值的指针而不是值本身。函数有一个void *形参,用于接受这个参数。然后指向这个值的指针便传递给比较函数。这个修改使字符串和数组对象也可以被使用。字符串和数组无法作为参数传递给函数,但指向它们的指针却可以。

使用这种技巧的函数被称为回调函数(callback function),因为用户把一个函数指针作为参数传递给其他函数,后者将“回调”用户的函数。任何时候,如果所编写的函数必须能够在不同的时刻执行不同类型的工作,或者执行只能由函数调用者定义的工作,都可以使用这个技巧。许多窗口系统使用回调函数连接多个动作,如拖拽鼠标和点击按钮来指定用户程序中的某个特定函数。

我们无法在这个上下文环境中为回调函数编写一个准确的原型,因为并不知道进行比较的值的类型。事实上,我们需要查找函数能作用于任何类型的值。解决这个难题的方法是把参数类型声明为void *,表示“一个指向未知类型的指针”

在使用比较函数中的指针之前,它们必须被强制转换为正确的类型。因为强制类型转换能够躲过一般的类型检查,所以在使用时必须格外小心,确保函数的参数类型是正确的。

在这个例子里,回调函数比较两个值。查找函数向比较函数传递两个指向需要进行比较的值的指针,并检查比较函数的返回值。例如,零表示相等的值,非零值表示不相等的值。现在,查找函数就与类型无关,因为它本身并不执行实际的比较。确实,调用者必须编写必需的比较函数,但这样做是很容易的,因为调用者知道链表中所包含的值的类型。如果使用几个分别包含不同类型值的链表,为每种类型编写一个比较函数就允许单个查找函数作用于所有类型的链表。

程序13.1是类型无关查找函数的一种实现方法。注意,函数的第3个参数是一个函数指针。这个参数用一个完整的原型进行声明。同时注意,虽然函数绝不会修改参数node所指向的任何节点,但node并未被声明为const。如果node被声明为const,函数将不得不返回一个const结果,这将限制调用程序,它便无法修改查找函数所找到的节点。

/*
** 在一个单链表中查找一个指定值的函数。它的参数是一个指向链表第1个节点的
** 指针、一个指向需要查找的值的指针和一个函数指针,它所指向的函数用于比
** 较存储于链表中的类型的值。
*/
#include <stdio.h>
#include "node.h"
Node *
search_list( Node *node, void const *value,int (*compare)( void const *, void const * ) )
{while( node != NULL ){if( compare( &node->value, value ) == 0 )break;node = node->link;}return node;
}

程序13.1 类型无关的链表查找 search.c

指向值参数的指针和&node->value被传递给比较函数。后者是我们当前所检查的节点的值。在选择比较函数的返回值时,这里选择了与直觉相反的约定,就是相等返回零值,不相等返回非零值。它的目的是为了与标准库的一些函数所使用的比较函数规范兼容。在这个规范中,不相等操作数的报告方式更为明确——负值表示第1个参数小于第2个参数,正值表示第1个参数大于第2个参数。

在一个特定的链表中进行查找时,用户需要编写一个适当的比较函数,并把指向该函数的指针和指向需要查找的值的指针传递给查找函数。例如,下面是一个比较函数,它用于在一个整数链表中进行查找。

int compare_ints(void const *a, void const *b)
{if(*(int *)a == *(int *)b)return 0;elsereturn 1;}

这个函数将像下面这样使用:

desired_node = search_list( root, &desired_value,compare_ints );

注意强制类型转换:比较函数的参数必须声明为void *以匹配查找函数的原型,然后它们再强制转换为int *类型,用于比较整型值。

如果希望在一个字符串链表中进行查找,下面的代码可以完成这项任务:

#include<string.h>
...
desired_node = search_list(root,"desired_value",strcmp);

碰巧,库函数strcmp所执行的比较和我们需要的完全一样,不过有些编译器会发出警告信息,因为它的参数被声明为char *而不是void *。

13.3.2 转移表

转移表最好用个例子来解释。下面的代码段取自一个程序,它用于实现一个袖珍式计算器。程序的其他部分已经读入两个数(op1和op2)和一个操作符(oper)。下面的代码对操作符进行测试,然后决定调用哪个函数。

switch( oper ){
case ADD:result = add(op1, op2);break;case SUB:result = sub(op1, op2);break;case MUL:result = mul(op1, op2);break;case DIV:result = div(op1, op2);break;...

对于一个新奇的具有上百个操作符的计算器来说,这条switch语句将会非常之长。

为什么要调用函数来执行这些操作呢?把具体操作和选择操作的代码分开是一种良好的设计方案。更为复杂的操作将肯定以独立的函数来实现,因为它们的长度可能很长。但即使是简单的操作,也可能具有副作用,例如保存一个常量值用于以后的操作。

为了使用switch语句,表示操作符的代码必须是整数。如果它们是从零开始连续的整数,则可以使用转换表来实现相同的任务。转换表就是一个函数指针数组。

创建一个转换表需要两个步骤。声明并初始化一个函数指针数组。唯一需要留心之处就是确保这些函数的原型出现在这个数组的声明之前。

在初始化列表中,各个函数名的正确顺序取决于程序中用于表示每个操作符的整型代码。这个例子假定ADD是0,SUB是1,MUL是2;依此类推。

第二个步骤是用下面这条语句替换前面整条switch语句!

result = oper_func[ oper ]( op1, op2 );

oper从数组中选择正确的函数指针,而函数调用操作符将执行这个函数。

在转换表中,越界下标引用就像在其他任何数组中一样是不合法的。但一旦出现这种情况,把它诊断出来要困难得多。当这种错误发生时,程序有可能在3个地方终止。首先,如果下标值远远越过了数组的边界,它所标识的位置可能在分配给该程序的内存之外。有些操作系统能检测到这个错误并终止程序,但有些操作系统并不这样做。如果程序被终止,这个错误将在靠近转换表语句的地方被报告,问题相对而言较易诊断。

如果程序并未终止,非法下标所标识的值被提取,处理器跳到该位置。这个不可预测的值可能代表程序中一个有效的地址,但也可能不是。如果它不代表一个有效地址,程序此时也会终止,但错误所报告的地址从本质上说是一个随机数。此时,问题的调试就极为困难。

如果程序此时还未失败,机器将开始执行根据非法下标所获得的虚假地址的指令,此时要调试出问题根源就更为困难了。如果这个随机地址位于一块存储数据的内存中,程序通常会很快终止,这通常是由于非法指令或非法的操作数地址所致(尽管数据值有时也能代表有效的指令,但并不总是这样)。要想知道机器为什么会到达那个地方,唯一的线索是转移表调用函数时存储于堆栈中的返回地址。如果任何随机指令在执行时修改了堆栈或堆栈指针,那么连这个线索也消失了。

更糟的是,如果这个随机地址恰好位于一个函数的内部,那么该函数就会顺利地执行,修改谁也不知道的数据,直到它运行结束。但是,函数的返回地址并不是该函数所期望的保存于堆栈上的地址,而是另一个随机值。这个值就成为下一个指令的执行地址,计算机将在各个随机地址间跳转,执行位于那里的指令。

问题在于指令破坏了机器如何到达错误最后发生地点的线索。没有了这方面的信息,要查明问题的根源简直难如登天。如果怀疑转移表有问题,可以在那个函数调用之前和之后各打印一条信息。如果被调用函数不再返回,用这种方法就可以看得很清楚。但困难在于人们很难认识到程序某个部分的失败可以是由于程序中相隔甚远的且不相关部分的一个转移表错误所引起的。

一开始就保证转移表所使用的下标位于合法的范围是很容易做到的。在这个计算器例子里,用于读取操作符并把它转换为对应整数的函数应该核实该操作符是否有效。

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

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

相关文章

【MATLAB源码-第138期】基于matlab的D2D蜂窝通信仿真,对比启发式算法,最优化算法和随机算法的性能。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 D2D蜂窝通信介绍 D2D蜂窝通信允许在同一蜂窝网络覆盖区域内的终端设备直接相互通信&#xff0c;而无需数据经过基站或网络核心部分转发。这种通信模式具有几个显著优点&#xff1a;首先&#xff0c;它可以显著降低通信延迟&…

铱塔 (iita) 开源 IoT 物联网开发平台,基于 SpringBoot + TDEngine +Vue3

01 铱塔 (iita) 物联网平台 铱塔智联 (open-iita) 基于Java语言的开源物联网基础开发平台&#xff0c;提供了物联网及相关业务开发的常见基础功能, 能帮助你快速搭建自己的物联网相关业务平台。 铱塔智联平台包含了品类、物模型、消息转换、通讯组件&#xff08;mqtt/EMQX通讯组…

1、 快速上手 [代码级手把手解diffusers库析]

快速上手Pipeline 内部执行步骤后续更新计划 diffusers是Hugging Face推出的一个diffusion库&#xff0c;它提供了简单方便的diffusion推理训练pipe&#xff0c;同时拥有一个模型和数据社区&#xff0c;代码可以像torchhub一样直接从指定的仓库去调用别人上传的数据集和pretrai…

Linux中ps/kill/execl的使用

ps命令&#xff1a; ps -aus或者ps -ajx或者 ps -ef可以查看有哪些进程。加上 | grep "xxx" 可以查看名为”xxx"的进程。 ps -aus | grep "xxx" kill命令&#xff1a; kill -9 pid 杀死某个进程 kill -l 查看系统有哪些信号 execl函数&#…

RocketMQ(二):领域模型(生产者、消费者)

1 生产者&#xff08;Producer&#xff09; 本节介绍Apache RocketMQ 中生产者的定义、模型关系、内部属性、版本兼容和使用建议。 1.1 定义 生产者是Apache RocketMQ 系统中用来构建并传输消息到服务端的运行实体。 生产者通常被集成在业务系统中&#xff0c;将业务消息按照要…

C++基础入门之引用

目录 一.引用 1.1引用和取地址 1.2 别名和原名的区别 1.3 引用的用法 1.31 做参数 1.311 输出型参数&#xff1a;形参改变实参 1.312 可以减少拷贝&#xff0c;增加效率 1.32 引用的约定 1. 引用必须初始化 2. 引用定义后&#xff0c;不能改变指向 4. 给指针取别名 1.33…

【Linux环境基础开发工具的使用(yum、vim、gcc、g++、gdb、make/Makefile)】

Linux环境基础开发工具的使用yum、vim、gcc、g、gdb、make/Makefile Linux软件包管理器- yumLinux下安装软件的方式认识yum查找软件包安装软件如何实现本地机器和云服务器之间的文件互传卸载软件 Linux编辑器 - vimvim的基本概念vim下各模式的切换vim命令模式各命令汇总vim底行…

聊聊JIT优化技术

&#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是小徐&#x1f947;☁️博客首页&#xff1a;CSDN主页小徐的博客&#x1f304;每日一句&#xff1a;好学而不勤非真好学者 &#x1f4dc; 欢迎大家关注&#xff01; ❤️ 我们知道&#xff0c;想要把高级语言转变成计算…

《动手学深度学习(PyTorch版)》笔记7.7

注&#xff1a;书中对代码的讲解并不详细&#xff0c;本文对很多细节做了详细注释。另外&#xff0c;书上的源代码是在Jupyter Notebook上运行的&#xff0c;较为分散&#xff0c;本文将代码集中起来&#xff0c;并加以完善&#xff0c;全部用vscode在python 3.9.18下测试通过&…

Python中的嵌套字典访问与操作详解

前言 在Python编程中&#xff0c;嵌套字典是一种常见的数据结构&#xff0c;它可以以层次结构的方式组织和存储数据。嵌套字典通常包含字典内嵌套在其他字典中&#xff0c;创建了一种多层级的数据结构。本文将详细介绍如何在Python中访问和操作嵌套字典&#xff0c;包括访问、…

卷积层Conv1d包含的元素分别是什么,经过卷积层,数据的形状发生变化吗?

nn.Conv1d 是一个一维卷积层&#xff0c;它通常用于处理序列数据&#xff0c;如时间序列或文本数据。这个层包含以下主要元素&#xff1a; 输入通道数&#xff08;In_channels&#xff09;&#xff1a;这是输入数据的通道数。对于单通道数据&#xff08;如灰度图像或单变量时间…

Leetcode3021. Alice 和 Bob 玩鲜花游戏

Every day a Leetcode 题目来源&#xff1a;3021. Alice 和 Bob 玩鲜花游戏 解法1&#xff1a;数学 Alice 和 Bob 在一个长满鲜花的环形草地玩一个回合制游戏。环形的草地上有一些鲜花&#xff0c;Alice 到 Bob 之间顺时针有 x 朵鲜花&#xff0c;逆时针有 y 朵鲜花。 游戏…

Ubuntu环境下安装部署Nginx(有网)

本文档适用于在Ubuntu20.04系统下部署nginx 一、使用apt-get命令安装nginx 注&#xff1a;以下命令都是在root用户下使用 1. 检查是否存在apt命令 apt –version 说明&#xff1a;出现版本号就说明当前环境存在apt 2. 更新apt命令 apt update 3. 安装nginx apt-get in…

containerd中文翻译系列(十八)containerd支持NRI

节点资源接口 NRI 是节点资源接口&#xff08;Node Resource Interface&#xff09;&#xff0c;它是一个通用框架&#xff0c;用于将扩展功能插入兼容 OCI 的容器运行时。它提供了插件跟踪容器状态并对其配置进行有限的更改改的基本机制。 NRI 本身与任何容器运行时的内部实…

猫头虎分享已解决Bug || AJAX请求错误(AJAX Request Error):AJAX Error: 404 Not Found

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

SpringIOC之support模块ReloadableResourceBundleMessageSource

博主介绍&#xff1a;✌全网粉丝5W&#xff0c;全栈开发工程师&#xff0c;从事多年软件开发&#xff0c;在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战&#xff0c;博主也曾写过优秀论文&#xff0c;查重率极低&#xff0c;在这方面有丰富的经验…

分布式系统架构介绍

1、为什么需要分布式架构&#xff1f; 增大系统容量&#xff1a;单台系统的性能瓶颈&#xff0c;多台机器才能应对大规模的应用场景&#xff0c;所以就需要我们的应用支撑平台具备分布式架构。 加强系统的可用&#xff1a;为了满足业务的SLA要求&#xff0c;需要通过分布式架构…

uniapp的配置和使用

①安装环境和编辑器 注册小程序账号 微信开发者工具下载 uniapp 官网 HbuilderX 下载 首先先下载Hbuilder和微信开发者工具 &#xff08;都是傻瓜式安装&#xff09;&#xff0c;然后注册小程序账号&#xff1a; 拿到appid&#xff1a; ②简单通过demo使用微信开发者工具和…

Linux开发工具的使用 (gcc/g++ | gdb)

目录 一、gcc/g 1.关于gcc/g 2.gcc如何使用 gcc选项&#xff1a; 预处理&#xff1a; 编译: 汇编: 连接: 函数库是什么&#xff1a; 函数库分为动态库和静态库两种 二、调试器gdb 1.关于gdb 2. gdb的使用 gdb选项&#xff1a; Linux是一个广泛用于开发的操作系统&…

关于数字图像处理考试

我们学校这门科目是半学期就完结哦&#xff0c;同学们学习的时候要注意时间哦。 选择题不用管&#xff0c;到时候会有各种版本的复习资料的。 以下这些东西可能会是大题的重点&#xff1a; 我根据平时代码总结的&#xff0c;供参考 基本操作&#xff1a; 1.读图&#xff1a;…