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

news2025/1/24 1:36:35

我们不会每天都使用函数指针。但是,它们的确有用武之地,最常见的两个用途是转换表(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;
    else
        return 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://www.coloradmin.cn/o/1446338.html

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

相关文章

Linux基础I/O(三)——缓冲区和文件系统

文章目录 什么是C语言的缓冲区理解文件系统理解软硬链接 什么是C语言的缓冲区 C语言的缓冲区其实就是一部分内存 那么它的作用是什么&#xff1f; 下面有一个例子&#xff1a; 你在陕西&#xff0c;你远在山东的同学要过生日了&#xff0c;你打算送给他一份生日礼物。你有两种方…

视觉开发板—K210自学笔记(五)

本期我们来遵循其他单片机的学习路线开始去用板子上的按键控制点亮LED。那么第一步还是先知道K210里面的硬件电路是怎么连接的&#xff0c;需要查看第二节的文档&#xff0c;看看开发板原理图到底是按键是跟哪个IO连在一起。然后再建立输入按键和GPIO的映射就可以开始变成了。 …

VTK 常用坐标系 坐标系 转换

1.VTK 常用坐标系 计算机图形学里常用的坐标系统主要有四种&#xff0c;分别是&#xff1a;Model坐标系统、World坐标系统、View坐标系统和Display坐标系统 在VTK里&#xff0c;Model坐标系统用得比较少&#xff0c;其他三种坐标系统经常使用。它们之间的变换则是由类vtkCoord…

Docker容器输入汉字触发自动补全

一、描述 输入汉字自动触发补全&#xff1a; Display all 952 possibilities? (y or n)是因为容器中没有中文字符集和中文字体导致的&#xff0c;安装中文字体&#xff0c;并设置字符集即可。 二、解决 1、安装字符集 &#xff08;1&#xff09;查看系统支持的字符集 lo…

甘肃旅游服务平台:技术驱动的创新实践

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【Java程序设计】【C00260】基于Springboot的企业客户信息反馈平台(有论文)

基于Springboot的企业客户信息反馈平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的企业客户信息反馈平台 本系统分为平台功能模块、管理员功能模块以及客户功能模块。 平台功能模块&#xff1a;在平台首页可…

计算机网络——网络安全

计算机网络——网络安全 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家&#xff0c; [跳转到网站](https://www.captainbed.cn/qianqiu) 小程一言专栏链接: [link](http://t.csdnimg.cn/ZUTXU) 网络安全何…

【AI绘图】初见·小白入门stable diffusion的初体验

首先&#xff0c;感谢赛博菩萨秋葉aaaki的整合包 上手 stable diffusion还是挺好上手的&#xff08;如果使用整合包的话&#xff09;&#xff0c;看看界面功能介绍简单写几个prompt就能生成图片了。 尝试 我在网上找了一张赛博朋克边缘行者Lucy的cos图&#xff0c;可能会侵…

黄金交易策略(Nerve Nnife.mql4):趋势平仓按钮的作用

当觉得行情不太对路&#xff0c;可以点击右下角按钮&#xff0c;实现趋势单的移动止盈&#xff08;止损&#xff09;。点了这个按钮回撤多少平仓是可以在参数里设定的。 完整EA&#xff1a;Nerve Knife.ex4黄金交易策略_黄金趋势ea-CSDN博客

深入理解梯度加权类激活热图(Grad-CAM)

深入理解梯度加权类激活热图&#xff08;Grad-CAM&#xff09; 项目背景与意义 在深度学习领域&#xff0c;模型的预测能力往往是黑盒子&#xff0c;难以解释。梯度加权类激活热图&#xff08;Grad-CAM&#xff09;作为一种可解释性技术&#xff0c;能够帮助模型开发者更好地…

使用R语言建立回归模型并分割训练集和测试集

通过简单的回归实例&#xff0c;可以说明数据分割为训练集和测试集的必要性。以下先建立示例数据: set.seed(123) #设置随机种子 x <- rnorm(100, 2, 1) # 生成100个正态分布的随机数&#xff0c;均值为2&#xff0c;标准差为1 y exp(x) rnorm(5, 0, 2) # 生成一个新的变…

Ubuntu Desktop - Disks

Ubuntu Desktop - Disks 1. Search your computer -> DisksReferences 1. Search your computer -> Disks ​ References [1] Yongqiang Cheng, https://yongqiang.blog.csdn.net/

[神奇代码岛】制作一个单开门(有过渡动画)

前言 神岛PRO不是有一个可以制作动画的功能吗&#xff1f;但是有些岛民到现在还不知道怎么操控这个动画&#xff0c;那我今天就来教学&#xff0c;咋们哪一个简单的例子吧------单开门&#xff08;有过渡动画&#xff09; 材料准备 建模 简单建模的一个门&#xff0c;此模型…

qt “美颜”

要想成为一名优秀的qt工程师 学会使用qss编程也是重要的 不可获缺的一部分 qss 简介和优势 QSS&#xff08;Qt Style Sheets&#xff09;是一种用于定义Qt应用程序界面外观和样式的样式表语言。它类似于CSS&#xff08;层叠样式表&#xff09;&#xff0c;但针对Qt框架进行了定…

2023爱分析·大模型厂商全景报告|爱分析报告

01 研究范围定义 研究范围 大模型是指通过在海量数据上依托强大算力资源进行训练后能完成大量不同下游任务的模型。2023年以来&#xff0c;ChatGPT引爆全球大模型市场。国内众多大模型先后公测&#xff0c;众多互联网领军者投身大模型事业&#xff0c;使得大模型市场进入“百团…

基于Zigbee的智能温室大棚系统(附详细使用教程+完整代码+原理图+完整课设报告)

🎊项目专栏:【Zigbee课程设计系列文章】(附详细使用教程+完整代码+原理图+完整课设报告) 前言 👑由于无线传感器网络(也即是Zigbee)作为🌐物联网工程的一门必修专业课,具有很强的实用性,因此很多院校都开设了zigbee的实训课程;👑同时最近很多使用了我的单片机课…

【开源】SpringBoot框架开发农家乐订餐系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 用户2.2 管理员 三、系统展示四、核心代码4.1 查询菜品类型4.2 查询菜品4.3 加购菜品4.4 新增菜品收藏4.5 新增菜品留言 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的农家乐订餐系统&#xff0c…

博客系统-SpringBoot版本

相比于之前使用Servlet来完成的博客系统&#xff0c;SpringBoot版本的博客系统功能更完善&#xff0c;使用到的技术更接近企业级&#xff0c;快来看看吧~ 目录 1.项目介绍 2.数据库准备 3.实体化类 4.返回格式 5.登录和注册功能 6.登出&#xff08;注销&#xff09;功能…

【Rust】使用Rust实现一个简单的shell

一、Rust Rust是一门系统编程语言&#xff0c;由Mozilla开发并开源&#xff0c;专注于安全、速度和并发性。它的主要目标是解决传统系统编程语言&#xff08;如C和C&#xff09;中常见的内存安全和并发问题&#xff0c;同时保持高性能和底层控制能力。 Rust的特点包括&#x…

一周学会Django5 Python Web开发-Django5操作命令

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计11条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…