C语言中函数指针和回调函数的详解「建议收藏」

C语言中函数指针和回调函数的详解「建议收藏」函数指针:指向函数的指针变量。因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数…

大家好,又见面了,我是你们的朋友全栈君。

函数指针:指向函数的指针变量。 因此“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。

我们首先来看一个函数指针的例子:

#include <stdio.h>
#include <stdlib.h>
void (*pfun)(int data);
void myfun(int data)
{
	printf("get data:%d\n",data);
}
int main(int argc,char *argv[])
{
	pfun = myfun;
	(*pfun)(100);
	return 0;
}
从这个例子可以看到,我们首先定义了一个函数指针pfun ,这个函数指针的返回值为void型,然后我们给函数指针赋值,赋值为myfun,也就是myfun函数的首地址,在C99中myfun函数名就是myfun函数的首地址,此时pfun获得了myfun的地址,pfun的地址等于myfun的地址,所以最终调用pfun();也就相当于调用了myfun();

第二种用法:typedef 原变量类型 别名
也可以用typedef来定义一个指针函数这样使在大型代码中更加简洁
#include <stdio.h>
#include <stdlib.h>
typedef void (*pfun)(int data);
/*typedef的功能是定义新的类型。第一句就是定义了一种pfun的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回void类型。*/
void myfun(int data)
{
	printf("get data:%d\n",data);
}
int main(int argc,char *argv[])
{
	pfun p= myfun;      //函数指针指向执行函数的地址
	p(100);
	return 0;
}
这里面的pfun代表的是函数的类型,通过pfun来代表void (*)(int)函数类型即pfun是指针函数的别名,pfun p相当于定义了一个
void (*p)(int)函数指针。p = myfun可以理解为将函数指针p指向myfun函数的地址,p(100);相当于执行myfun(100);

第三种用结构体函数指针的方法
#include <stdio.h>
#include <stdlib.h>
typedef struct gfun{
	void (*pfun)(int);	
}gfun;
void myfun(int data)
{
	printf("get data:%d\n",data);
}
int main(int argc,char *argv[])
{
	gfun gcode={
		.pfun = myfun,   //将函数指针指向要调用函数的地址
	};
	gcode.pfun(100);
	return 0;
} 

这三种方法运行的结果一样
在这里插入图片描述
回调函数:通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
下面是一个回调函数的例子:

#include <stdio.h>
#include <stdlib.h>
typedef struct gfun{
	int (*pfun)(int);	
}gfun;

int myfun(int data)
{
	printf("get data:%d\n",data);
	return (data*2);
}
int rt_data(int data,int (*tr_fun)())
{
	return ((*tr_fun)(data));
}  
int main(int argc,char *argv[])
{
	int ret;
	gfun gf;
	gf.pfun = myfun;
	ret = rt_data(100,gf.pfun);
	printf("return data:%d\n",ret);
	return 0;
}

运行的结果如下:
在这里插入图片描述
通过上面的例子我们可以看到将结构体中的函数指针指向了myfun函数地址,在回调函数中我们将函数指针gf.pfun作为rt_data(int data,int (*tr_fun)())函数的参数即为int (*tr_fun)();回调函数中的return (*tr_fun)(data)相当于对指针进行了简引用,返回这个指针指向地址的内容值。

回调函数的意义
可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
回调函数在实际中有什么作用?先假设有这样一种情况:我们要编写一个库,它提供了某些排序算法的实现(如冒泡排序、快速排序、shell排序、shake排序等等),为了能让库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,能让库可用于多种数据类型(int、float、string),此时,该怎么办呢?可以使用函数指针,并进行回调。
回调可用于通知机制。例如,有时要在A程序中设置一个计时器,每到一定时间,A程序会得到相应的通知,但通知机制的实现者对A程序一无所知。那么,就需一个具有特定原型的函数指针进行回调,通知A程序事件已经发生。实际上,API使用一个回调函数SetTimer()来通知计时器。如果没有提供回调函数,它还会把一个消息发往程序的消息队列。
谈完回调函数的意义,我们就有了用户和开发者之间的概念,举个例子,用户是实现myfun这个函数,开发者是实现rt_data函数,根据需求用户将myfun函数以参数的形式传入开发者的rt_data函数中,rt_data函数就能返回给相应的数据给用户,开发者不用告诉用户它实现了什么,用户也并不知道开发者怎么实现,用户只用传入自己的函数,便可以得到开发者实现的函数返回值,开发者可以将内容封装起来,将头文件以及库文件提供给用户。
下面看个封装的例子
main.c是上层用户开发的
fun.c fun.h是开发者开发的

mian.c代码如下

#include "fun.h"
#include<stdio.h>
#include<stdlib.h>

typedef struct gfun{
	int (*pfun)(int);	
}gfun;

int myfun(int data)
{
	printf("get data:%d\n",data);
	return (data*2);
}
 
int main(int argc,char *argv[])
{
	int ret;
	gfun gf;
	gf.pfun = myfun;
	ret = rt_data(100,gf.pfun);
	printf("return data:%d\n",ret);
	return 0;
}

fun.h代码:

#ifndef _FUN_H_
#define _FUN_H_
int rt_data(int data,int (*tr_fun)());

#endif

fun.c代码:

#include "fun.h"
int rt_data(int data,int (*tr_fun)())
{
	return ((*tr_fun)(data));
}  

最后用gcc main.c fun.c -o main编译完成后生成mian执行文件
将执行文件执行后的结果如下:
在这里插入图片描述
在linux下制作动态链接库,将fun.c和fun.h打包成一个动态链接库

先明白以下几个命令是什么意思:

生成动态库:

gcc -shared -fPIC fun.c -o fun.so

-shared : 生成动态库;

-fPIC : 生成与位置无关代码;

-o :指定生成的目标文件;

使用动态库:

gcc main.c -L . –lfun -o main

-L : 指定库的路径(编译时); 不指定就使用默认路径(/usr/lib/lib)

-lfun : 指定需要动态链接的库是谁;

代码运行时需要加载动态库:

./main 加载动态库 (默认加载路径:/usr/lib /lib ./ …)

./main
我们将编译动态生成的libfun.so拷贝到/usr/lib后,现在就不需要fun.c了,此时我们将fun.c移除也可以正常的编译并执行main函数的结果。
下面是我制作动态链接库的过程:
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/152192.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • kubeadm 常用命令

    kubeadm 常用命令kubeadm 概述 Kubeadm 是一个工具 它提供了 kubeadminit 以及 kubeadmjoin 这两个命令作为快速创建 kubernetes 集群的最佳实践 安装官方参考 kuadmin 安装任务 kubeadminit 启动引导一个 Kubernetes 主节点 kubeadmjoin 启动引导一个 Kubernetes 工作节点并且将其加入到集群 kubeadmupgra 更新 Kubernetes 集群到新版本 kubeadmconfi 如果你使用 kube

    2025年9月17日
    2
  • python3.9多线程_python多线程没用

    python3.9多线程_python多线程没用什么是线程?线程也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其

    2022年7月28日
    16
  • linux开机自启动python脚本_python3执行linux命令

    linux开机自启动python脚本_python3执行linux命令linux重启服务命令重启:service服务名restart或systemctlrestart服务名service和systemctl1.service命令service命令其实是去/etc/init.d目录下,去执行相关程序#service命令启动redis脚本serviceredisstart#直接启动redis脚本/etc/init.d/redisstart#开机自启动…

    2022年10月9日
    3
  • 2021.7goland激活码【2021.7最新】

    (2021.7goland激活码)2021最新分享一个能用的的激活码出来,希望能帮到需要激活的朋友。目前这个是能用的,但是用的人多了之后也会失效,会不定时更新的,大家持续关注此网站~IntelliJ2021最新激活注册码,破解教程可免费永久激活,亲测有效,下面是详细链接哦~https://javaforall.net/100143.html…

    2022年3月21日
    40
  • 关于Loadrunner11激活成功教程的各种问题。。。泪奔。。。[通俗易懂]

    关于Loadrunner11激活成功教程的各种问题。。。泪奔。。。[通俗易懂]loadrunner11的激活成功教程步骤:(1)打开LoadRunner8.0文件夹,用LR8.0中的mlr5lprg.dll、lm70.dll覆盖LR11安装目录下“bin”文件夹中的对应文件4.手动修改注册表,删除下面内容,也就是Licesen2目录(不删除的话,在添加licence时,会提示“Licensesecurityviolation……”):[HKEY_LO

    2022年7月22日
    11
  • 服务端稳定性测试_web端性能测试怎么做

    服务端稳定性测试_web端性能测试怎么做1概述1.1背景系统的稳定性是系统长期稳定运行能力,需要时间累积才能度量。平台的某些问题需要达到一定时间、一定的使用量后才会暴露出来。如内存泄漏,系统运行过程中发现部分服务的部分接口会发生服务不可达的情况。从而团队提出对平台进行稳定性分析,通过给系统施加一定业务压力大情况下,使系统持续运行一段时间,以此来检测系统是否稳定运行(下统称稳定性测试或测试)。1.2服务说明平台运行的服务包括系统服务和业务服务,系统服务包括Consul、Redis、Cap、RabbitMQ、Exceptionless

    2025年9月16日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号