一文读懂函数指针

2023-11-04

前言:本篇是关于函数指针的保姆级教程

一、函数指针的定义和修饰

函数指针广泛应用于嵌入式软件开发中,其常用的两个用途:调用函数做函数的参数

void (*fptr)();

//把函数的地址赋值给函数指针,一般采用如下形式:
fptr=Function;

//如果是函数调用,还必须包含一个圆括号括起来的参数表。可以采用如下方式来通过指针调用函数:
x=(*fptr)();

使用typedef来“修饰”一个函数指针

typedef void (*fptr)();

fptr func;
fptr func1;
fptr func2;

在刚开始学C语言的时候,总认为typedef取别名的一般形式为

typedef 旧名字 新名字;

但遇到给函数指针类型、数组类型等定义别名的时候就要特别区分了。如:

typedefchar ARRAY20[20];
ARRAY20 a1,a2; /* 等价于char a1[20],a2[20]; */

再论typedef

typedef int size;  // typedef行
int  i;            // 原型行
size  i;           // 应用行
//同理:
typedef char Line[81];  // typedef 行
char  t[81];            // 原型行
Line  t;                // 应用行
//再引申下:
typedef  int  (*fun_ptr)(int,int);  // typedef 行
int (*fp)(int,int);                 // 原型行
fun_ptr  fp;                        // 应用行

以上三个例子都有以下几个共同点。
首先,“typedef 行”和 “原型行”相比,“typedef 行”仅仅多个 typedef 而已。就函数指针的例子来说,“typedef 行”和 “原型行”的根本区别在于,fun_ptr 是类的别名,fp 是该类的变量。

其次,“原型行”和“应用行”的编译结果是一样的。就函数指针的例子来说,它们都是创建了一个类型为 int(*)(int,int) 的函数指针 fp。只是 fun_ptr fp(应用行)比 int(*fp)(int,int)(原型行)这种形式更加简洁,便于书写和理解。形式越复杂,typedef 的优势就越明显。

二、函数指针的具体示例 

(一)示例1:指针函数与函数指针的类型

int *pf(int *, int);        // int *(int *, int)类型
int (*pf)(int, int);        // int (*)(int, int)类型

指针函数的类型:int *(int *, int)

函数指针的类型:int (*)(int, int)

 (二)示例2:一个极简的函数指针

//返回值类型 (*指针名称)();
int (*)()
//定义了一个指向返回值为int,无参数的函数的指针;

定义了一个指向返回值为int,无参数的函数的指针。

(三)示例3:函数指针的跳转

我们在真实的项目开发过程中,可能需要直接跳转到函数的某个地址去指针。

void (*function_p)(void);  //定义函数指针function_p,无返回值,无参数
function_p = my_func;  //函数指针指向function函数
(*function_p)();  //采用函数指针运行函数

这个等同于直接调用my_func函数,那么这个有什么意义呢?其实这样提出了一个思路,就是可以根据函数的地址,跳转到函数的地址。(具体可参照示例4)

(四)示例4:使用函数指针调用特定内存地址的函数

首先看一个例子:

(*(void(*) ())0)()
  1. void (*)()  是一个函数指针,只是把p去掉了而已。

  2. 把上面的void (*)()PN代替,上面的表达式变成(*(PN)0)();

  3. PN后面有一个0,这个是让人咋舌的地方,然后想一下 (char)a这样的表达式;因此所以*(PN)0就是把0当成一个地址,强制转换为PN类型,用*这个钥匙取出这个地址区域的值

  4. (*(PN)0)()替换成PM,原来的表达式变成PM(),这样看起来就明了了,就是正常的函数调用。

(五)示例5:bootloader中经典的内存跳转

比如我们在bootloader中,当把二进制文件加载到内存中后,如何去执行这个kernel程序呢?也就是实现一个bootloader到kernel的跳转。

((void(*)())0x80000)();

这里就是说0x80000处的地址是函数类型,并且没有返回值。当我们的kernel地址为0x80000时程序跳转过去,不再返回。这就是一个比较经典的例子。实际上此示例同示例4。

 const修饰指针

此处插入一下,如果指针所指向内存中的数据或指针指向不想被修改,可以参考const用法。

const int *p;//指针变量p所指向的空间内容不可修改即*p不可修改,但p可修改
int const *q;//指针变量q所指向的空间内容不可修改即*q不可修改,但q可修改
int *const r;//指针变量r不可修改,但*r所指向的空间内容可修改
const int * const x;//指针变量x自身不可修改,x所指向的空间内容亦不可修改

(六)示例6:函数指针数组形式的调用

void (*f[])(char *)

f 是个什么what?

[] 的优先级 比 *的优先级高,所以 f首先是修饰了数组,然后跟后面的 *组合,就说明这个数组里面住的都是指针,这些指针是什么呢,再出来看看就看到了,这个指针是 一个函数,这个函数的 参数是 char *返回值是void

因此这是一个函数指针数组!!!

函数指针数组1:

#include <stdio.h>

void (*f[3])(char *);

void efunction(char * s)
{
 printf("%s\n",s);
}

int main()
{
 f[0] = efunction;
 //void (*f[])(char *) = {efunction}; 
 (*f[0])("hello code");
    return 0;
}

函数指针数组2:

#include <stdio.h>
#include <string.h>

char * fun1(char * p)
{
    printf("%s\n", p);
    return p;
}

char * fun2(char * p)
{
    printf("%s\n", p);
    return p;
}

char * fun3(char * p)
{
    printf("%s\n", p);
    return p;
}

int main()
{
    char * (*pf[3])(char * p);
    pf[0] = fun1; // 可以直接用函数名
    pf[1] = &fun2; // 可以用函数名加上取地址符,等价于pf[1] = fun2
    pf[2] = &fun3;//    等价于pf[2] = fun3

    pf[0]("fun1");
    pf[0]("fun2");
    pf[0]("fun3");

    getchar();
    return 0;
}

三、函数指针的调用

(一)示例1:用typedef申请一个简单的函数指针

//声明一个函数指针,一般的方法是
int (*pfunc)(int a,int b)
//当命名很多个函数指针的时候,用上面的方法显得非常不方便,可以这样做
typedef int (*PF) (int a,int b)
PF pfunc;
/********************************************************************/
//例程
#include "stdio.h"

typedef int(*PF)(int, int);

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

int reduce(int a, int b)
{
 return a - b;
}

int main()
{
 PF pfunc = NULL;
 pfunc = add;
 printf("add:%d\n",pfunc(3, 4));
 pfunc = reduce;
 printf("reduce:%d\n", pfunc(3, 4));
 /*getchar是用VS编写方便查看输出*/
 getchar();
 return 0;
}

 (二)示例2:一种隐藏式的函数指针调用

#include<stdio.h>
#include <assert.h>

double getMin(double *dbData, int iSize)  // 求最小值

{
    double dbMin; 
    assert((dbData != NULL) && (iSize > 0));
    dbMin = dbData[0]; 
    for (int i = 1; i < iSize; i++)
    { 

        if (dbMin > dbData[i])
        {
            dbMin = dbData[i];
        }
    }
    return dbMin;
}

double getMax(double *dbData, int iSize)   // 求最大值
{
    double dbMax;
    assert((dbData != NULL) && (iSize > 0)); 
    dbMax = dbData[0]; 
    for (int i = 1; i < iSize; i++)
    {
        if (dbMax < dbData[i])
        { 
            dbMax = dbData[i];
        }
    }
    return dbMax;
}

double getAverage(double *dbData, int iSize)  // 求平均值
{
    double dbSum = 0;
    assert((dbData != NULL) && (iSize > 0)); 
    for (int i = 0; i < iSize; i++)
    {
        dbSum += dbData[i]; 
    }
    return dbSum/iSize;
} 

double unKnown(double *dbData, int iSize)   // 未知算法
{ 
    return 0;
}

/*******************************************
定义函数指针类型: double(*)(double* , int)
输入参数:double指针和int
返回值:指向double的函数
********************************************/
typede double (*PF)(double *dbData, int iSize); 

PF getOperation(char c)     // 根据字符得到操作类型,返回函数指针;注意返回值“PF”隐式调用
{
    switch (c)
    { 
        case 'd':
            return getMax; //隐式调用;若显示则为“PF = getMax”,且getOperation定义为void
        case 'x': 
            return getMin; //隐式调用,返回函数指针
        case 'p': 
            return getAverage;//同上
        default:
            return unKnown;       
    }
} 


int main(void) 
{
    double dbData[] = {3.1415926, 1.4142, -0.5, 999, -313, 365}; 
    int iSize = sizeof(dbData) / sizeof(dbData[0]); 
    char c; 
    printf("Please input the Operation :\n");
    c = getchar(); 
    PF pf = getOperation(c);
    printf("result is %lf\n", pf(dbData, iSize));//!!!使用函数指针
    return 0;
}

使用这个函数的主要步骤为:

1. 定义与PF类型匹配的函数,如getMax、getMin等。 

2. 确定要进行的操作对应的字符,如'x'表示getMin。

3. 调用getOperation()函数,传入操作字符,如getOperation('x')。

4. 函数会返回对应操作的函数指针,如返回getMin函数指针。 

5. 我们可以直接使用返回的函数指针调用getMin()函数。

6. 所以通过这个函数,可以根据简单的输入选择调用不同的函数,增加了程序的灵活性。

使用这个根据输入返回函数指针的函数需要注意:

1. 定义的函数必须严格匹配PF的类型定义,否则编译无法检查,运行时会出错。

2. 输入的操作字符必须在函数支持的选项范围内,否则返回未定义函数指针。

3. 返回的函数指针可以直接调用,但函数的定义和指针的有效范围必须正确。

4. 该函数隐藏了具体的函数调用,增加了抽象性但也增加了难度。如果出现问题,追踪会更加困难。

具体的显示调用可参考下一篇文章。

 (三)示例3:强制类型转换将函数地址赋值给函数指针以实现调用

void (*p)(); //指向返回void的,参数为空的函数指针

/*********************************/
#include "stdio.h"

void Function()
{
    printf("Call    Function!\n");
}

int main()
{
    void(*p)();//类型为:void(*)()
    *(int*)&p = (int)Function;//将Function函数的地址强制转换为int类型,赋值给p;
    (*p)();//通过(*p)()调用p指向的函数,也就是调用Function函数
    getchar();
    return 0;
}

这个程序的主要步骤为:

1. 定义一个void Function()函数,用于打印信息。 

2. 在main函数中声明一个void(*)()类型的函数指针p。 

3. 将Function函数的地址强制转换为int类型,赋值给p。 

4. 这实际上是将Function的地址保存到p这个函数指针。 

5. 通过(*p)()调用p指向的函数,也就是调用Function函数。

 6. 所以最终这个程序会打印出"Call Function!"。

 7. getchar()用于阻塞,等待用户输入以防窗口自动关闭。所以,这个程序展示了如何通过强制类型转换的方式,将一个函数的地址赋值给函数指针,并通过函数指针调用该函数。这属于函数指针的高级用法。

使用这个方法需要注意:

1. 必须确保p的类型与Function的类型严格匹配,否则无法正确调用Function。

2. 将Function的地址赋值给p时,需要通过强制类型转换方式,这会导致编译器无法进行安全检查。如果类型不匹配,运行时会导致问题。 

3. 如果Function的类型发生变化,p调用Function的语句也需要相应修改,否则会出现问题。这增加了代码的维护难度。

4. 这种通过 address 直接获取函数地址的方式是实现函数指针的基础,但实际使用中,通常通过函数名直接为函数指针赋值更加简单与安全

5. 这种通过强制类型转换实现函数调用的方式,增加了代码的抽象度,但也增加了调试的难度。需要慎用。

(四)示例4:函数指针作为函数的返回值

void sCal(void *param,void *fuc)
{
    ((void (*)(void*))fuc)(param);
}

1、这个sCal函数主要是使用void*实现一个公共的接口,包括数据和方法,所以函数内部对fuc指针进行了一个强制类型转化,并且把param作为函数参数传入。
2、使用注意:
1)fuc作为void*类型,实际上是个函数指针,但类型不定。
2)将fuc强制转换为void (*)(void)类型,即指向参数为void*,返回void的函数指针;
3)利用转换后的函数指针进行函数调用,传入param作为参数
4)作为参数传递的param可以在所调用的函数中使用;【示例2的一种“显示”调用】
5)因为返回类型为void,所以不返回任何结果。
3、void (*) 表示返回值为void的函数指针(void*)表示该函数指针接收参数的类型

同样使用typedef进行简化,其真面目就很明显了:

((void (*)(void*))fuc)( param );
-->
( (pFuc *       ) fuc)( param );

(五)函数指针的三大要素

当看到复杂的函数指针形势时,准确区分三大要素就能把握函数指针。

1、函数指针的返回值;

2、函数指针的参数;

3、函数指针所指向函数的参数;

(六)函数指针作为返回值,实现回调函数的注册

 函数指针的形式如此复杂的主要原因是 : 函数指针既可以作为函数的参数,也可以作为函数的返回值;而作为函数参数的函数指针又可以带有函数指针参数和函数指针返回值,层层嵌套,一旦展开那还是非常恐怖的。

void (   *signal(int signum,void(*handler)(int))  )(int)

pFuc1 *signal(int signum,void(*handler)(int))
pFuc1 *signal(int signum,void(  *handler  )(int))

pFuc2  void(*)(int)

pFuc1 *signal(int signum,pFuc2  *handler        )

void (signal(int , void()(int)))(int) 是一个复杂的函数原型,它的作用是:

signal() 函数接收两个参数:第一个参数是中断信号,第二个参数是中断信号对应的处理函数

signal() 函数返回值是中断信号之前的处理函数。也就是说,这个函数原型定义了 signal() 函数,它可以用来注册中断信号对应的处理函数,并返回中断信号之前的处理函数。

这个函数原型的具体语法为:

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

它返回一个指向参数为int,返回值为void的函数指针。

使用这个函数的步骤为:

1. 定义中断信号处理函数,其原型为 void fun(int)。

2. 调用 signal() 函数,传入要注册的中断信号编号和处理函数名。如:

void (*old_handler)(int);  
old_handler = signal(SIGINT, fun); 

3. signal() 函数会注册 fun() 函数为 SIGINT 信号的处理函数。

4. signal() 返回之前的 SIGINT 信号处理函数,存储在 old_handler 中。

5. 当产生中断时,fun() 函数会被调用,用于处理该中断。

6. 如果需要恢复老的处理函数,可以调用:signal(SIGINT, old_handler)。

具体的回调函数和回调注册机制将在写一篇文章中阐述。

以上signal函数的简化写法

void (*signal(int , void(*)(int)))(int);
/*
1、signal 是一个函数声明;
2、这个函数的参数有 2 个:
   第一个是 int 类型;
   第二个是函数指针,该指针类型为“返回void,函数参数 int”;
3、signal 函数的返回类型也是函数指针,该指针指向的函数参数 int,返回类型是 void 。
*/

typedef void(*pfun_t)(int);
//原函数
void (*signal(int , void(*)(int)))(int);
//简化后
pfun_t signal(int , pfun_t);

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一文读懂函数指针 的相关文章

随机推荐

  • 如何在PHP中以编程方式将Excel文件转换为PDF?试试Aspose

    Excel电子表格被广泛用于以行和列的形式存储和分析数据 但是 在各种情况下 需要先将Excel文件转换为PDF 然后才能通过Internet共享它们 为了自动将Excel转换为PDF 本文介绍了 如何使用PHP通过编程将Excel文件转换
  • 你们要的终极指南! 交易API接口

    股票API接口可谓是量化交易和金融工具中最重要的组成部分 一个高质量的交易API接口对于量化交易程序员而言可谓是福星 因其可使得程序员们进行后向测试 得到可靠的测试结果 验证自己制定的交易策略是否有效 甚至构建自己的财富主页 有可能成为下一
  • 主从架构&lua脚本-Redis(四)

    上篇文章介绍了rdb aof持久化 持久化RDB AOF Redis 三 https blog csdn net ke1ying article details 131148269 redis数据备份策略 写job每小时copy一份到其他目
  • Ubuntu22.04更换国内镜像源(阿里、网易163、清华、中科大)

    更换方法 Ubuntu采用apt作为软件安装工具 其镜像源列表记录在 etc apt source list文件中 首先将source list复制为source list bak备份 然后将source list内容改为需要的镜像源列表即
  • unity 在pc平台 重启应用程序

    void ReStart 延迟5秒启动 string strs new string echo off echo wscript sleep 5000 gt sleep vbs start wait sleep vbs start d 0
  • 深度学习训练营Resnet之鸟类识别

    深度学习训练营之鸟类识别 原文链接 理论知识储备 为什么会提出ResNet ResNet 环境介绍 前置工作 设置GPU 导入数据并进行查找 数据处理 可视化数据 配置数据集 残差网络的介绍 构建残差网络 模型训练 开始编译 结果可视化 训
  • jupyter虚拟环境连接内核失败

    当你的Jupyter一直显示 连接内核中 或者是404 那么不妨考虑是否是防火墙的问题 首先使用conda install ipykernel 这样可以在jupyter上看到内核 依次把对应的虚拟环境的目录下的python exe pyth
  • java工具之解析yaml文件

    工具使用背景 很多配置项都是使用yaml的格式进行配置的 按一定的格式进行缩进 一眼看上去 清晰明了 如Springboot工程下图所示 如 k8s的Deploy文件 本次写这个yaml工具解析是想解析k8s的config文件 然后封装一个
  • JAVA解析Json并输出所有属性值,多层次

    import com alibaba fastjson JSON import com alibaba fastjson JSONArray import com alibaba fastjson JSONObject import org
  • 关于opencv通道的C++转化实现

    首先我是在visual2019上使用C 因此测试之前要安装opencv的依赖包 可以上官网自行下载安装 1 图像通道 1 单通道图 每个像素点只有一个值 0 255 即我们常见的黑白图 称作灰度图 2 三通道图 每个像素点都有 3 个值表示
  • Quartusii 链接Altera-Modelsim进行功能仿真

    下文介绍利用Altera Modelsim来进行功能仿真的步骤 quartus ii 版本 17 0 altera modelsim版本 ModelSim Intel FPGA Starter Edition 10 5b Quartus P
  • 如何使用scrcpy管理和控制你的Android设备

    关于scrcpy scrcpy是一款针对Android设备的管理和控制工具 该工具可以通过USB或TCP IP来帮助广大研究人员显示 管理和控制Android设备 该工具不需要root访问权限 支持GNU Linux Windows和mac
  • 蚁群算法(Ant Colony Optimization,ACO)

    1 算法基本思想 在自然界中 蚂蚁群体在寻找食物的过程中 无论是蚂蚁与蚂蚁之间的协作还是蚂蚁与环境之间的交互均依赖于一种被称为信息素 Pheromone 的物质实现蚁群的间接通信 从而通过合作发现从蚁穴到食物源的最短路径 蚂蚁在寻找食物的过
  • 2019.9最新JRebel激活方式

    原文链接 最近JRebel离线方式到期 idea报无法激活JRebel了 找了很多以前的方式都无法生效 ip或域名都已经失效了 好在找到了大神有效的激活方式 以下是激活步骤 1 下载反向代理软件 下载地址 https github com
  • Linux安装python3

    1 获取安装包 第一种方式 通过官网下载 登录 https www python org downloads source 下载所需安装包并上传至服务器 第二种方式 通过命令行的下载工具 以python3 6 1为例 wget https
  • ViewModel 使用及原理解析

    本文是基于 androidx lifecycle lifecycle extensions 2 0 0 的源码进行分析 ViewModel旨在以生命周期意识的方式存储和管理用户界面相关的数据 它可以用来管理Activity和Fragment
  • WDA学习笔记(二)通过页面跳转理解WDA开发流程

    在进行开发之前先简单介绍一下WDA的控制器 WDA控制器包括组件控制器 定制控制器 视图控制器和窗口控制器 组件控制器 每个 Web Dynpro 组件只有一个组件控制器 该控制器是全局控制 器 对所有其它控制器可见 组件控制器可以控制整个
  • 服务器重装

    搜索自己的品牌看如何进入bios 这里是在最后进入前按del bios的命令 在boot里面主要有 Boot Settings Configuration 启动选项设定 Boot Device Priority 启动顺序设置 Hard Di
  • Vue项目Vite配置代理解决跨域问题

    Vite 一个Vue作者开发的Web开发工具 它具有快速的冷启动 及时的模块热更新 真正的按需加载 Vite基于浏览器原生 ES imports 的开发服务器 利用浏览器去解析 imports 在服务器端按需编译返回 完全跳过了打包这个概念
  • 一文读懂函数指针

    前言 本篇是关于函数指针的保姆级教程 一 函数指针的定义和修饰 函数指针广泛应用于嵌入式软件开发中 其常用的两个用途 调用函数和做函数的参数 void fptr 把函数的地址赋值给函数指针 一般采用如下形式 fptr Function 如果