C/C++ 基础 - 回调函数

C/C++ 基础 - 回调函数

目录

前言

回调函数预备知识

函数指针

什么是函数指针

函数指针的语法

如何用函数指针调用函数

函数指针作为函数的参数

函数指针作为函数返回类型

函数指针数组

回调函数

什么是回调函数

为什么要用回调函数

怎么使用回调函数

总结


前言

在写项目的时候,对于回调函数一知半解,这次将重新学习一下,重新理解一下 回调函数 的魅力所在

回调函数预备知识

在讲回调函数 回调函数 回调函数之前,我们需要了解函数指针

我们都知道,C语言的灵魂是指针,我们经常使用整型指针,字符串指针,结构体指针等

函数指针

int *p1; // p1是一个指向整数(int)类型的指针变量,可以存储一个int类型数据的地址 char *p2; // p2是一个指向字符(char)类型的指针变量,可以存储一个char类型数据的地址 STRUCT *p3; // p3是一个指向结构体类型STRUCT的指针变量,STRUCT是我们定义的结构体类型 

但是好像我们一般很少使用函数指针,我们一般使用函数都是直接使用函数调用

下面我们来了解一下函数指针的概念和使用方法。

什么是函数指针

函数指针也是个指针,但是和通常的指针不一样通常的指针指向的是整型、字符型或数组等变量

函数指针,指向的是函数

函数指针的语法

返回类型 (*指针变量名)(参数类型列表); 
  • 返回类型: 函数返回的数据类型(如 int double void 等)。
  • 指针变量名: 你给这个函数指针起的名字。
  • 参数类型列表: 函数接受的参数类型(如果没有参数,可以留空或写 void )。

这里需要注意的是(*指针变量名)两端的括号不能省略括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针。而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

那么怎么判断一个指针变量是指向变量的还是指向函数呢?
  • 首先看变量名前面有没有 “*如果有 “*” 说明是指针变量;
  • 其次看变量名有没有带 (),如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。

最后需要注意的是:指向函数的指针变量没有 ++ 和 – 运算。

一般为了方便使用,我们会选择用typedef进行函数指针类型的别名定义

// 定义一个函数指针类型 别名为:Fun1,它指向返回 int 类型、接受一个 int 参数的函数 typedef int (*Fun1)(int); // 定义一个函数指针类型 别名为:Fun2,它指向返回 int 类型、接受两个参数(int 和 int)的函数 typedef int (*Fun2)(int, int); // 定义一个函数指针类型 别名为:Fun3,它指向返回 void 类型、无参数的函数 typedef void (*Fun3)(void); // 定义一个函数指针类型 别名为:Fun4,它指向返回 void* 类型、接受一个 void* 参数的函数 typedef void* (*Fun4)(void*); 
为什么要使用typedef呢?

如何用函数指针调用函数

举个例子

int Func(int x); /*声明一个函数*/ int (*p) (int x); /*定义一个函数指针*/ p = Func; /*将Func函数的首地址赋给指针变量p*/ p = &Func; /*将Func函数的首地址赋给指针变量p*/ 

赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。

下面来写一个程序,看了这个程序你们就明白函数指针怎么使用了:

特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名也可以用&取函数的地址。

函数指针作为函数的参数

 我们见过,函数的形参是指针的

// 函数接受一个整数指针作为参数,并修改该值 void modifyValue(int *ptr) { *ptr = 20; // 修改指针指向的值 }

那么函数指针作为指针,肯定也能放到函数的形参中的

#include <stdio.h> // 定义一个函数类型:别名为operation,返回类型是int,参数类型是int和int typedef int (*operation)(int, int); // 一个加法函数 int add(int a, int b) { return a + b; } // 一个减法函数 int sub(int a, int b) { return a - b; } // 函数 modifyValue 接受一个函数指针作为参数,并调用它 void modifyValue(int *ptr, operation op) { *ptr = op(*ptr, 5); // 使用传入的函数指针 op 来修改 ptr 指向的值 } int main() { int num = 10; printf("Before: %d\n", num); // 输出修改前的值 // 传递加法函数的指针 modifyValue(&num, add); printf("After add: %d\n", num); // 输出加法操作后的值 // 传递减法函数的指针 modifyValue(&num, sub); printf("After sub: %d\n", num); // 输出减法操作后的值 return 0; } 

运行结果

为什么函数指针我们用的是别名operation,在下面调用modifyValue的时候,直接传入add,sub就能识别了呢?

在C语言中,函数名(如add或sub)在没有括号时会自动转换为指向该函数的指针。这与operation类型 的期望使用一个函数指针相匹配。

函数指针作为函数返回类型

有了上面的基础,要写出返回类型为函数指针的函数应该不难了,下面这个例子就是返回类型为函数指针的函数:

void (* func5(int, int, float ))(int, int) { ... } 

在这里, func5 以 (int, int, float) 为参数,其返回类型为 void (\*)(int, int) 。 

函数指针数组

在开始讲解回调函数前,最后介绍一下函数指针数组。既然函数指针也是指针,那我们就可以用数组来存放函数指针。下面我们看一个函数指针数组的例子:

/* 方法1 */ void (*func_array_1[5])(int, int, float); /* 方法2 */ typedef void (*p_func_array)(int, int, float); p_func_array func_array_2[5]; 

回调函数

什么是回调函数

我们先来看看百度百科是如何定义回调函数的:

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

这段话比较长,也比较绕口。下面我通过一幅图来说明什么是回调:

假设我们要使用一个排序函数来对数组进行排序,那么在 主程序(Main program) 中,我们先通过库,选择一个 库排序函数(Library function)。但排序算法有很多,有冒泡排序,选择排序,快速排序,归并排序。同时,我们也可能需要对特殊的对象进行排序,比如特定的结构体等。库函数会根据我们的需要选择一种排序算法,然后调用实现该算法的函数来完成排序工作。这个被调用的排序函数就是回调函数(Callback function)。

结合这幅图和上面对回调函数的解释,我们可以发现,要实现回调函数,最关键的一点就是要将函数的指针传递给一个函数(上图中是库函数),然后这个函数就可以通过这个指针来调用回调函数了。注意,回调函数并不是C语言特有的,几乎任何语言都有回调函数。在C语言中,我们通过使用函数指针来实现回调函数。

总结:一段可执行的代码(函数)像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫 回调 。如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调

回调函数 就是一个通过函数指针调用的函数如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是 回调函数 。

回调函数不是由该函数的实现方直接调用而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

为什么要用回调函数

因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。

简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。

int Callback() ///< 回调函数 { // TODO return 0; } int main() ///< 主函数 { // TODO Library(Callback); ///< 库函数通过函数指针进行回调 // TODO return 0; } 

回调似乎只是函数间的调用,和普通函数调用没啥区别。

但仔细看,可以发现两者之间的一个关键的不同:在回调中,主程序把回调函数像参数一样传入库函数。

这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且当库函数很复杂或者不可见的时候利用回调函数就显得十分优秀。如果还不明白。看一下下面的例子就会恍然大悟了

怎么使用回调函数

int Callback_1(int a) //回调函数1 { cout << "Hello, this is Callback_1。" << "a=" << a << endl; return 0; } int Callback_2(int b) //回调函数2 { cout << "Hello, this is Callback_2。" << "b=" << b << endl; return 0; } int Callback_3(int c) //回调函数3 { cout << "Hello, this is Callback_3。" << "c=" << c << endl; return 0; } int Handle(int x, int (*Callback)(int x)) { Callback(x); return 0; } int main() { Handle(10, Callback_1); Handle(20, Callback_2); Handle(30, Callback_3); return 0; }
可能同学会有一个疑问:Callback(x); 这里就一个函数是怎么做到识别Callback_1 2 3的呢?

 Handle(10, Callback_1);  Handle(20, Callback_2);  Handle(30, Callback_3);本质上是把Callback_1 2 3函数的地址传给Handle函数Handle函数里的Callback(x); 它存储的是具体回调函数的内存地址,比如说你传入的是 Handle(10, Callback_1);,Callback存储的是Callback_1的地址,存储的x变量是10,它就会找到Callback_1并且把10传给它

下面是一个四则运算的简单回调函数例子:

typedef float (*Operation)(float, float); float ADD(float a, float b) { cout << "a+b=" << a + b << endl; return a + b; } float SUB(float a, float b) { cout << "a-b=" << a - b << endl; return a - b; } float MUL(float a, float b) { cout << "a*b=" << a * b << endl; return a * b; } float DIV(float a, float b) { if (b == 0) { printf("Error: Division by zero!\n"); return 0; } cout << "a/b=" << a / b << endl; return a / b; } float add_sub_mul_div(float a, float b, Operation op) { op(a, b); return 0; } int main() { add_sub_mul_div(1.1, 2.2, ADD); add_sub_mul_div(1.1, 2.2, SUB); add_sub_mul_div(1.1, 2.2, MUL); add_sub_mul_div(1.1, 2.2, DIV); return 0; }

总结

这下对于回调函数是更加理解了,希望各位在今后的学习中能够看见回调函数不再迷惑!

Read more

旧电脑秒变 AI 员工:OpenClaw 本地部署教程(含环境配置 + 插件开发 + 常见坑)

旧电脑秒变 AI 员工:OpenClaw 本地部署教程(含环境配置 + 插件开发 + 常见坑)

前言 本文基于最新OpenClaw版本编写,适配电脑低配置场景(最低2vCPU+2GiB内存+40GiB SSD),兼容Windows 10/11(优先WSL2)、Ubuntu 20.04+系统,全程纯操作指令,覆盖环境配置、本地部署、插件开发、高频坑排查。核心解决部署卡顿、国内网络适配、插件开发无思路、报错无法排查四大痛点,全程适配国内网络(国内镜像源)、国内大模型(通义千问、阿里云百炼等),无需海外代理,可稳定运行实现自动化办公(文件处理、IM对接、任务调度等)。 一、前置准备(适配优化) 1.1 硬件要求(最低适配) * CPU:Intel i3 4代+/AMD Ryzen 3 2000+(支持虚拟化,

By Ne0inhk
基于飞算JavaAI的学生成绩综合统计分析系统

基于飞算JavaAI的学生成绩综合统计分析系统

第一章:项目概述与背景 1.1 项目背景与意义 在教育信息化飞速发展的今天,学生成绩管理已成为学校教学管理的核心环节。传统的学生成绩管理多依赖于手工操作或基础的信息管理系统,存在数据处理效率低、统计分析功能薄弱、数据可视化缺失等问题。随着大数据技术的发展,教育领域对数据驱动的决策支持需求日益增长,一个能够提供综合统计分析功能的学生成绩管理系统显得尤为重要。 学生成绩综合统计分析系统旨在通过对学生成绩数据的深度挖掘和多维度分析,为教师、学生和管理者提供全面的数据支持。系统不仅能够实现基础的成绩录入和查询,更重要的是能够识别学习趋势、发现教学问题、预测学业表现,从而为个性化教学和精准教育干预提供科学依据。 1.2 飞算JavaAI平台介绍 飞算JavaAI是一款智能代码生成平台,采用人工智能技术辅助Java项目开发。 飞算JavaAI的核心功能模块,紧密围绕“高效、智能、安全”的Java开发全流程展开:左侧聚焦智能交互,包含三大实用工具——编程智能体可自动调用工具执行编程任务(如自动生成基础代码、辅助调试),智能问答提供实时技术答疑(快速解决开发中的疑难问题),Java Cha

By Ne0inhk
Flutter 三方库 langchain_google 的鸿蒙化适配指南 - 链接 Gemini 智慧中枢、LangChain AI 实战、鸿蒙级智能应用专家

Flutter 三方库 langchain_google 的鸿蒙化适配指南 - 链接 Gemini 智慧中枢、LangChain AI 实战、鸿蒙级智能应用专家

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 langchain_google 的鸿蒙化适配指南 - 链接 Gemini 智慧中枢、LangChain AI 实战、鸿蒙级智能应用专家 在鸿蒙跨平台应用迈向“智能化”的今天,接入生成式 AI(AIGC)已不再是加分项,而是必选项。如果你想在鸿蒙端利用 Google Gemini 的强大推理能力打造智能助手、自动化翻译或垂直领域 RAG 系统。今天我们要深度解析的 langchain_google——一个通过 LangChain 标准协议封装的 Google AI 适配器,正是帮你构建“大模型大脑”的核心插件。 前言 langchain_google 是 LangChain.

By Ne0inhk
AI与单片机之:STM32上运行AI大模型的四种方案!(含案例,建议收藏)

AI与单片机之:STM32上运行AI大模型的四种方案!(含案例,建议收藏)

前几天小编写了2篇文章 “为什么AI会改变单片机的未来?” 单片机上如何运行AI?单片机如何“学会思考”之TinyML崛起!(含案例,建议收藏), 引起了非常多的留言、关注和加群讨论。但是,仍然有读者朋友给小编留言,能否整理一些关于比较常用芯片比如STM32实用AI大模型的案例。为了满足粉丝朋友的诉求,小编整理了“在STM32单片机上运行AI大模型的”真实案例。 从粉丝的一个问题引出本文的思考:AI 模型能跑在 STM32 上吗? 一:先说结论 先说结论:不仅能跑,还一共有四种方案。 方案一:STM32官方提供的 STM32Cube.AI(X-CUBE-AI) 其实原理是我们把在 PC 上训练好的神经网络自动转换成可在 MCU 上运行的 C 库;然后在自己的软件/代码工程中调用已经编译产生的C库。 方案二:直接用 TensorFlow Lite Micro(TFLM)+ CMSIS-NN 在 STM32

By Ne0inhk