这是我看过最全面讲解嵌入式C语言回调函数和函数指针的教程_单片机中使用void( )作回调函数什么意思(2)

上面的例子定义func_ptr是一个函数指针, 函数类型是不带形参, 返回参数是uint8_t

要定义的类型是uint8_t (*)(void),没有输入参数,返回值为uint8_t 的函数指针,定义的别名是func_ptr

在分析这种形式的定义的时候可以这样看:先去掉typedef别名, 剩下的就是原变量的类型。去掉typedeffunc_ptr以后就剩:uint8_t (*)(void)

2.为啥使用typedef定义函数指针

答:typedef定义的函数指针类型是比较方便和明了的,因为typedef实际上就是定义一个新的数据类型,typedef有这样的一个作用,就可以用它来定义函数指针类型,这个定义的函数指针类型是能够指向返回值是uint8_t的,并且函数的参数是void类型。

这里定义的typedef uint8_t (*func_ptr) (void);;就相当于把uint8_t (*) (void); 定义成了另一个别名 func_ptr了。这个func_ptr就表示了函数指针类型。

注意:这里的uint8_t (*) (void);实际上不存在这样的写法,只是为了方便理解,这样的写法是不允许的,也是错误的!这样的写法并不代表是一个类型!

C语言真是博大精深!

3.函数指针常规定义

如果不使用typedef就应该这样定义:

#include “sys.h”
#include “led.h”
#include “delay.h”
#include “usart.h”

uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}

int main(void)
{
delay_init();
uart_init(9600);

printf(“www.zhiguoxin.cn\r\n”);
printf(“微信公众号:果果小师弟\r\n”);

uint8_t a = 10;
uint8_t b = 8;

/*定义一个函数指针*/
uint8_t (*func_ptr)(uint8_t, uint8_t);
/*将函数名赋值给函数指针*/
func_ptr = cal_sum;

printf(“%d + %d = %d\r\n”, a, b, func_ptr(a, b));

while(1)
{
}
}

在keil中测试:

4.函数指针typedef定义

如果使用typedef就应该这样定义:

#include “sys.h”
#include “led.h”
#include “delay.h”
#include “usart.h”

uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}

int main(void)
{
delay_init();
uart_init(9600);

printf(“www.zhiguoxin.cn\r\n”);
printf(“微信公众号:果果小师弟\r\n”);

uint8_t a = 10;
uint8_t b = 8;

/*定义一个函数指针*/
typedef uint8_t (*func_ptr)(uint8_t, uint8_t);
/*声明了一个函数指针变量 pFun*/
func_ptr pFun;
/*将这个pFun指向了cal_sum函数*/
pFun = cal_sum;

printf(“%d + %d = %d\r\n”, a, b, pFun(a, b));

while(1)
{
}
}

为啥要这样?为啥要使用typedef来定义函数指针?其实这个也是类比结构体的操作,在结构体中我们也常常给结构体起别名。

综上所述:定义函数指针就有了两种方法

/* 方法1 */
uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL;
/* 方法2 */
typedef uint8_t (*func_ptr)(uint8_t, uint8_t);;
func_ptr pFun = NULL;

函数指针也有两种赋值方法:

uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL;
/* 方法1 */
func_ptr= &cal_sum;
/* 方法2 */
func_ptr= cal_sum;

上面两种方法都是合法的。其实函数名就是函数的地址,你将函数名cal_sum赋值给函数指针func_ptr,与将函数的地址&cal_sum赋值给函数指针func_ptr是一样的。

同样调用函数也有两种方法:

/* 方法1 */
func_ptr(a,b)

/* 方法2 */
(*func_ptr)(a,b)

二、回调函数

既然函数指针和去普通的指针一样,普通的指针可以作为函数的形参,那么函数指针是不是也可以作为函数的形参呢?

答:是的,肯定可以

那么函数指针作为函数的形参我们把这个函数指针叫啥呢?

答:回调函数

回调函数原来是这样得来的啊,学到了!

能不能举一个简单的例子呢?

uint8_t compute_func(uint8_t, uint8_t);

首先我们这样写一个函数是没有问题的,但是我们现在要将函数指针作为函数的形参,这样合法吗?

uint8_t compute_func(uint8_t (*func_ptr)(uint8_t, uint8_t),uint8_t, uint8_t);

编译一下

发现没有错误也没有警告,说明我们把函数指针当做函数的形参是没有任何问题的。

在这个函数当中,通过该函数指针调用的函数被称为回调函数。这种开发方式的用途非常广泛。具体来说,在回调函数的应用场景当中,会出现两个角色。分别是某功能函数的开发者以及该功能函数的使用者。compute_func函数就是开发者写的函数,是非常牛逼的写库和底层的那一类人写的函数,我们每一个单片机的使用者,需要写出各种各样的具体的功能函数,只要我们写得功能函数的形参和返回值和函数指针的类型相同就可以了。

怎么理解?

#include “sys.h”
#include “delay.h”
#include “usart.h”
/*使用者写的函数*/
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
/*开发者写的函数*/
uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b)
{
return func_ptr(a, b);
}
int main(void)
{
delay_init();
uart_init(9600);

printf(“www.zhiguoxin.cn\r\n”);
printf(“微信公众号:果果小师弟\r\n”);

uint8_t a = 10;
uint8_t b = 8;

printf(“compute_func(cal_sum,a,b) =%d\r\n”, compute_func(cal_sum, a, b));

while(1)
{
}
}

注意:这里要注意的是我们使用者写的函数的类型一定要于开发者写的回调函数类型一样,比如形参和返回值的类型要一样。不然肯定不能调用的。

换句话说就是,下面的这两个函数的形参和返回值都必须是相同的类型才可以,不能一个有返回值一个没有,明明函数指针有两个形参,你写的函数却只有一个形参也是不行的。

正确写法:
uint8_t cal_mul(uint8_t , uint8_t )
uint8_t (*func_ptr)(uint8_t, uint8_t)

错误写法:
void cal_mul(uint8_t , uint8_t ) //你写的函数却没有返回值
uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有返回值

错误写法:
uint8_t cal_mul(uint8_t) //你写的函数却只有一个形参
uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有两个形参

我们来验证一下:

#include “sys.h”
#include “led.h”
#include “delay.h”
#include “usart.h”

/*使用者写的函数*/
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
/*使用者写的函数*/
void cal_sub(uint8_t a, uint8_t b)
{
printf(“666”);
}
/*使用者写的函数*/
uint8_t cal_mul( uint8_t a)
{
return a;
}

/*开发者写的函数*/
uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b)
{
return func_ptr(a, b);
}
int main(void)
{
delay_init();
uart_init(9600);

printf(“www.zhiguoxin.cn\r\n”);
printf(“微信公众号:果果小师弟\r\n”);

uint8_t a = 10;
uint8_t b = 8;

printf(“%d\r\n”, compute_func(cal_sum, a, b));
printf(“%d\r\n”, compute_func(cal_sub, a, b));
printf(“%d\r\n”, compute_func(cal_mul, a, b));

while(1)
{
}
}

看到了在keil中编译器不会报错,但是会报警告。因为在keil中编译做了优化。那么如果我们gcc记事本编译一下会是啥样的呢?

会发现同样会有两个警告,但是还是可以运行。

如何理解回调函数

有时候会遇到这样一种情况,当上层人员将一个功能交给下层程序员完成时,上层程序员和下层程序员同步工作,这个时候该功能函数并未完成,这个时候上层程序员可以定义一个API来交给下层程序员,而上层程序员只要关心该API就可以了而无需关心具体实现,具体实现交给下层程序员完成即可(这里的上层和下层程序员不指等级关系,而是项目的分工关系)。这种情况下就会用到回调函数(Callback Function),现在假设程序员A需要一个FFT算法,这个时候程序员A将FFT算法交给程序员B来完成,现在来让实现这个过程:

#include <stdio.h>

int InputData[100]={0};
int OutputData[100]={0};

/*定义回调函数*/
void CallBack_FFT_Function(int *inputData,int *outputData,int num)
{
while(num–)
{
printf(“www.zhiguoxin.cn\r\n”);
}
}
/*用来注册回调函数的功能函数*/
void TaskA(void (*fft)(int*,int*,int))
{
fft(InputData,OutputData,5);
}

int main(void)
{
/*注册FFT_Function作为回调*/
TaskA(CallBack_FFT_Function);
return 0;
}

这个例子是不是跟上面的那个例子是相同的,只是我们在这里换了一种说法而已。也就是我们硬件层实现的某个功能,当然可以在应用层直接调用,但是这种做法太low了,一看就是小学生的水平,或者说硬件层的东西应用层根本不需要关心,这就是分层的思想。硬件的东西就给硬件工程师做,应用工程师只关心自己的需要实现的任务。这也就是驱动工程师和应用工程师的区别,我硬件工程师只需要写好对应的API函数,你应用层直接调用就好了,你不需要关心这个API函数的内部是怎么实现的。而这两者之间的桥梁就是回调函数。而回调函数的形参就是函数指针,所以本篇最开始讲的是函数指针,只要你函数指针明白了,你就会写回调函数,也就理解了这其中到底只一个什么原理。

果果小师弟

上面的代码中CallBack_FFT_Function是回调函数,该函数的形参为一个函数指针,TaskA是用来注册回调函数的功能函数。可以看到用来注册回调函数的功能函数中申明的函数指针必须和回调函数的类型完全相同

函数指针结构体

但是很多时候我们一般在结构体中定义函数指针用的比较多一点。下面再举一个简单的例子。

#include “sys.h”
#include “led.h”
#include “delay.h”
#include “usart.h”
/****************************************
* 函数指针结构体 开发者写的结构体
***************************************/
typedef struct
{
uint8_t (*p_sum)(uint8_t, uint8_t);
uint8_t (*p_sub)(uint8_t, uint8_t);
uint8_t (*p_mul)(uint8_t, uint8_t);
float (*p_div)(uint8_t, uint8_t);
} Operation_T;

/*声明结构体变量g_Operation*/
Operation_T g_Operation;

/*使用者写的回调函数*/
uint8_t cal_sum(uint8_t a, uint8_t b)
{
return a + b;
}
/*使用者写的回调函数*/
uint8_t cal_sub(uint8_t a, uint8_t b)
{
return a - b;

}
/*使用者写的回调函数*/
uint8_t cal_mul( uint8_t a, uint8_t b)
{
return a * b;

}
/*使用者写的回调函数*/
float cal_div(uint8_t a, uint8_t b)
{
return a / b;

}
/*结构体变量g_Operation初始化*/
Operation_T g_Operation = {cal_sum, cal_sub, cal_mul, cal_div};

int main(void)
{
delay_init();
uart_init(9600);

printf(“www.zhiguoxin.cn\r\n”);
printf(“微信公众号:果果小师弟\r\n”);

uint8_t a = 10;
uint8_t b = 8;
/*使用函数指针调用函数*/
printf(“%d\r\n”, g_Operation.p_sum(a, b));
printf(“%d\r\n”, g_Operation.p_sub(a, b));
printf(“%d\r\n”, g_Operation.p_mul(a, b));
printf(“%f\r\n”, g_Operation.p_div(a, b));

while(1)
{
}
}

三、回调在嵌入式系统中的实际使用

回调可用于多种情况,并广泛用于嵌入式固件开发。它们提供了更大的代码灵活性,并允许我们开发可由最终用户进行微调而无需更改代码的驱动程序。

在我们的代码中具有回调功能所需的元素是:

  • 将被调用的回调函数cal_sum
  • 将用于访问回调函数的函数指针p_sum
  • 将调用回调函数的调用函数compute_func

在stm32的HAL库中,是使用了大量的回调函数的,串口、定时器等外设都是有对应的回调函数的,回调机制可以更好地分离代码,应用层和驱动层完全分离,降低耦合性。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

1713085702944)]
[外链图片转存中…(img-9QVEPRhp-1713085702945)]
[外链图片转存中…(img-2sNckiVz-1713085702946)]
[外链图片转存中…(img-pm8s4Oll-1713085702947)]
[外链图片转存中…(img-s03Ce9Nb-1713085702947)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-pjqejD7v-1713085702948)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 13
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
嵌入回调函数面试题是指在嵌入软件开发,面试官可能会问到与回调函数相关的问题。回调函数是一种常见的编程技术,用于在某个条件满足时执行特定的操。 在嵌入回调函数面试,可能会涉及以下问题: 1. 什么是回调函数回调函数是一种通过函数指针传递给其他函数函数。当满足特定条件时,被调用的函数会执行相应的操。 2. 为什么要使用回调函数回调函数可以增加代码的灵活性和可扩展性。通过使用回调函数,可以将某些操的实现和调用方解耦,使得代码更加模块化和可重用。 3. 在嵌入开发回调函数的应用场景有哪些? 回调函数嵌入开发非常常见,特别是在事件驱动的系统。例如,当某个外部事件发生时(如按下按钮、接收到数据等),可以通过注册回调函数来执行相应的操。 4. 如何定义和使用回调函数回调函数的定义需要满足特定的函数签名(参数类型和返回类型)。在使用回调函数时,通常会将函数指针为参数传递给其他函数,以指定在特定事件发生时应该调用的函数。 5. 回调函数断处理函数的区别是什么? 回调函数断处理函数都是在特定事件发生时执行的函数。区别在于断处理函数是由硬件断触发,而回调函数是由软件事件触发。 以上是嵌入回调函数面试题的一些常见问题和回答。在面试,根据具体的职位和公司需求,可能还会涉及更深入和具体的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [嵌入软件开发就业面试题。2022最新,最全总结。](https://blog.csdn.net/BBA_Code/article/details/127140662)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [嵌入Linux设备驱动面试题汇总](https://blog.csdn.net/chbgoon/article/details/122936954)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值