编程能力提升的一些编程技巧

李肖遥 2024-12-29 22:12
    关注、星标公众号,直达精彩内容

一、函数指针

在讲回调函数之前,我们需要了解函数指针。
我们都知道,C语言的灵魂是指针,我们经常使用整型指针,字符串指针,结构体指针等
int *p1;
char *p2;
STRUCT *p3; //STRUCT为我们定义的结构体
但是好像我们一般很少使用函数指针,我们一般使用函数都是直接使用函数调用。
下面我们来了解一下函数指针的概念和使用方法。

1.概念

函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
那么怎么判断一个指针变量是指向变量的指针变量还是指向函数的指针变量呢?首先看变量名前面有没有“”,如果有“”说明是指针变量;其次看变量名的后面有没有带有形参类型的圆括号,如果有就是指向函数的指针变量,即函数指针,如果没有就是指向变量的指针变量。
最后需要注意的是,指向函数的指针变量没有 ++ 和 – 运算。
一般为了方便使用,我们会选择
typedef 函数返回值类型 (* 指针变量名) (函数参数列表);
比如
typedef int (*Fun1)(int);//声明也可写成int (*Fun1)(int x),但习惯上一般不这样。
typedef int (*Fun2)(intint);//参数为两个整型,返回值为整型
typedef void (*Fun3)(void);//无参数和返回值
typedef void* (*Fun4)(void*);//参数和返回值都为void*指针

2,如何用函数指针调用函数

给大家举一个例子:
int Func(int x);   /*声明一个函数*/
int (*p) (int x);  /*定义一个函数指针*/
p = Func;          /*将Func函数的首地址赋给指针变量p*/
p = &Func;          /*将Func函数的首地址赋给指针变量p*/
赋值时函数 Func 不带括号,也不带参数。由于函数名 Func 代表函数的首地址,因此经过赋值以后,指针变量 p 就指向函数 Func() 代码的首地址了。
下面来写一个程序,看了这个程序你们就明白函数指针怎么使用了:
#include 
int Max(intint);  //函数声明
int main(void)
{
    int(*p)(intint);  //定义一个函数指针
    int a, b, c;
    p = Max;  //把函数Max赋给指针变量p, 使p指向Max函数
    printf("please enter a and b:");
    scanf("%d%d", &a, &b);
    c = (*p)(a, b);  //通过函数指针调用Max函数
    printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
    return 0;
}
int Max(int x, int y)  //定义Max函数
{
    int z;
    if (x > y)
    {
        z = x;
    }
    else
    {
        z = y;
    }
    return z;
}
特别注意的是,因为函数名本身就可以表示该函数地址(指针),因此在获取函数指针时,可以直接用函数名,也可以取函数的地址。
p = Max可以改成 p = &Max
c = (*p)(a, b) 可以改成 c = p(a, b)

3.函数指针作为某个函数的参数

既然函数指针变量是一个变量,当然也可以作为某个函数的参数来使用的。
示例:
#include 
#include 

typedef void(*FunType)(int);
//前加一个typedef关键字,这样就定义一个名为FunType函数指针类型,而不是一个FunType变量。
//形式同 typedef int* PINT;
void myFun(int x);
void hisFun(int x);
void herFun(int x);
void callFun(FunType fp,int x);
int main()
{
    callFun(myFun,100);//传入函数指针常量,作为回调函数
    callFun(hisFun,200);
    callFun(herFun,300);

    return 0;
}

void callFun(FunType fp,int x)
{
    fp(x);//通过fp的指针执行传递进来的函数,注意fp所指的函数有一个参数
}

void myFun(int x)
{
    printf("myFun: %d\n",x);
}
void hisFun(int x)
{
    printf("hisFun: %d\n",x);
}
void herFun(int x)
{
    printf("herFun: %d\n",x);
}
输出:

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

有了上面的基础,要写出返回类型为函数指针的函数应该不难了,下面这个例子就是返回类型为函数指针的函数:
void (* func5(intintfloat ))(intint)
{
    ...
}
在这里, func5 以 (int, int, float) 为参数,其返回类型为 void (\*)(int, int) 。在C语言中,变量或者函数的声明也是一个大学问

5.函数指针数组

在开始讲解回调函数前,最后介绍一下函数指针数组。既然函数指针也是指针,那我们就可以用数组来存放函数指针。下面我们看一个函数指针数组的例子:
/* 方法1 */
void (*func_array_1[5])(intintfloat);

/* 方法2 */
typedef void (*p_func_array)(intintfloat);
p_func_array func_array_2[5];
上面两种方法都可以用来定义函数指针数组,它们定义了一个元素个数为5,类型是 *void (\*)(int, int, float)*的函数指针数组。

6.函数指针总结

  1. 函数指针常量 :Max;函数指针变量:p;
  2. 数名调用如果都得如(*myFun)(10)这样,那书写与读起来都是不方便和不习惯的。所以C语言的设计者们才会设计成又可允许myFun(10)这种形式地调用(这样方便多了,并与数学中的函数形式一样)。
  3. 函数指针变量也可以存入一个数组内。数组的声明方法:int (*fArray[10]) ( int );

二、回调函数

1.什么是回调函数

我们先来看看百度百科是如何定义回调函数的:
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
这段话比较长,也比较绕口。下面我通过一幅图来说明什么是回调:
假设我们要使用一个排序函数来对数组进行排序,那么在主程序(Main program)中,我们先通过库,选择一个库排序函数(Library function)。但排序算法有很多,有冒泡排序,选择排序,快速排序,归并排序。同时,我们也可能需要对特殊的对象进行排序,比如特定的结构体等。库函数会根据我们的需要选择一种排序算法,然后调用实现该算法的函数来完成排序工作。这个被调用的排序函数就是回调函数(Callback function)。
结合这幅图和上面对回调函数的解释,我们可以发现,要实现回调函数,最关键的一点就是要将函数的指针传递给一个函数(上图中是库函数),然后这个函数就可以通过这个指针来调用回调函数了。注意,回调函数并不是C语言特有的,几乎任何语言都有回调函数。在C语言中,我们通过使用函数指针来实现回调函数。
我的理解是:把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,这就叫做回调
如果代码立即被执行就称为同步回调,如果过后再执行,则称之为异步回调
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

2 为什么要用回调函数?

因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。
简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
int Callback()    ///< 回调函数
{
    // TODO
    return 0;
}
int main()     ///<  主函数
{
    // TODO
    Library(Callback);  ///< 库函数通过函数指针进行回调
    // TODO
    return 0;
}
回调似乎只是函数间的调用,和普通函数调用没啥区别。
但仔细看,可以发现两者之间的一个关键的不同:在回调中,主程序把回调函数像参数一样传入库函数。
这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,这样有没有觉得很灵活?并且当库函数很复杂或者不可见的时候利用回调函数就显得十分优秀。

3 怎么使用回调函数?

int Callback_1(int a)   ///< 回调函数1
{
    printf("Hello, this is Callback_1: a = %d ", a);
    return 0;
}

int Callback_2(int b)  ///< 回调函数2
{
    printf("Hello, this is Callback_2: b = %d ", b);
    return 0;
}

int Callback_3(int c)   ///< 回调函数3
{
    printf("Hello, this is Callback_3: c = %d ", c);
    return 0;
}

int Handle(int x, int (*Callback)(int)) ///< 注意这里用到的函数指针定义
{
    Callback(x);
}

int main()
{
    Handle(4, Callback_1);
    Handle(5, Callback_2);
    Handle(6, Callback_3);
    return 0;
}
如上述代码:可以看到,Handle()函数里面的参数是一个指针,在main()函数里调用Handle()函数的时候,给它传入了函数Callback_1()/Callback_2()/Callback_3()的函数名,这时候的函数名就是对应函数的指针,也就是说,回调函数其实就是函数指针的一种用法。

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

#include 
#include 

/****************************************
 * 函数指针结构体
 ***************************************/

typedef struct _OP {
    float (*p_add)(floatfloat); 
    float (*p_sub)(floatfloat); 
    float (*p_mul)(floatfloat); 
    float (*p_div)(floatfloat); 
} OP; 

/****************************************
 * 加减乘除函数
 ***************************************/

float ADD(float a, float b) 
{
    return a + b;
}

float SUB(float a, float b) 
{
    return a - b;
}

float MUL(float a, float b) 
{
    return a * b;
}

float DIV(float a, float b) 
{
    return a / b;
}

/****************************************
 * 初始化函数指针
 ***************************************/

void init_op(OP *op)
{
    op->p_add = ADD;
    op->p_sub = SUB;
    op->p_mul = &MUL;
    op->p_div = &DIV;
}

/****************************************
 * 库函数
 ***************************************/

float add_sub_mul_div(float a, float b, float (*op_func)(floatfloat))
{
    return (*op_func)(a, b);
}

int main(int argc, char *argv[]) 
{
    OP *op = (OP *)malloc(sizeof(OP)); 
    init_op(op);
    
    /* 直接使用函数指针调用函数 */ 
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", (op->p_add)(1.32.2), (*op->p_sub)(1.32.2), 
            (op->p_mul)(1.32.2), (*op->p_div)(1.32.2));
     
    /* 调用回调函数 */ 
    printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n"
            add_sub_mul_div(1.32.2, ADD), 
            add_sub_mul_div(1.32.2, SUB), 
            add_sub_mul_div(1.32.2, MUL), 
            add_sub_mul_div(1.32.2, DIV));

    return 0
}

5. 回调函数实例(很有用)

一个GPRS模块联网的小项目,使用过的同学大概知道2G、4G、NB等模块要想实现无线联网功能都需要经历模块上电初始化、注册网络、查询网络信息质量、连接服务器等步骤,这里的的例子就是,利用一个状态机函数(根据不同状态依次调用不同实现方法的函数),通过回调函数的方式依次调用不同的函数,实现模块联网功能,如下:
/*********  工作状态处理  *********/
typedef struct
{

 uint8_t mStatus;
 uint8_t (* Funtion)(void); //函数指针的形式
} M26_WorkStatus_TypeDef;  //M26的工作状态集合调用函数


/**********************************************
** >M26工作状态集合函数
***********************************************/

M26_WorkStatus_TypeDef M26_WorkStatus_Tab[] =
{    
    {GPRS_NETWORK_CLOSE,  M26_PWRKEY_Off  }, //模块关机
    {GPRS_NETWORK_OPEN,  M26_PWRKEY_On  }, //模块开机
    {GPRS_NETWORK_Start,   M26_Work_Init  }, //管脚初始化
    {GPRS_NETWORK_CONF,  M26_NET_Config  }, /AT指令配置
    {GPRS_NETWORK_LINK_CTC,  M26_LINK_CTC  }, //连接调度中心  
    {GPRS_NETWORK_WAIT_CTC, M26_WAIT_CTC  },  //等待调度中心回复 
    {GPRS_NETWORK_LINK_FEM, M26_LINK_FEM  }, //连接前置机
    {GPRS_NETWORK_WAIT_FEM, M26_WAIT_FEM  }, //等待前置机回复
    {GPRS_NETWORK_COMM,  M26_COMM   }, //正常工作    
    {GPRS_NETWORK_WAIT_Sig,  M26_WAIT_Sig  },  //等待信号回复
    {GPRS_NETWORK_GetSignal,  M26_GetSignal  }, //获取信号值
    {GPRS_NETWORK_RESTART,  M26_RESET   }, //模块重启
}
/**********************************************
** >M26模块工作状态机,依次调用里面的12个函数   
***********************************************/

uint8_t M26_WorkStatus_Call(uint8_t Start)
{
    uint8_t i = 0;
    for(i = 0; i < 12; i++)
    {
        if(Start == M26_WorkStatus_Tab[i].mStatus)
        {          
      return M26_WorkStatus_Tab[i].Funtion();
        }
    }
    return 0;
}
所以,如果有人想做个NB模块联网项目,可以copy上面的框架,只需要修改回调函数内部的具体实现,或者增加、减少回调函数,就可以很简洁快速的实现模块联网。


版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。


‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“星球”加入知识星球,有问必答。



点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。

李肖遥 公众号“技术让梦想更伟大”,作者:李肖遥,专注嵌入式,只推荐适合你的博文,干货,技术心得,与君共勉。
评论
  • 近日,紫光展锐正式推出高性能4G 智能穿戴平台W377E。该产品面向不同的应用场景,拥有丰富特性和超低功耗,进一步壮大紫光展锐的智能穿戴产品组合。紫光展锐面向中高端和海量的智能穿戴市场,持续提供技术先进、高效安全、高质可靠的解决方案。 紫光展锐W377E采用四核A53架构,支持13M摄像头,video 1080P 30fps,集成了Wi-Fi、蓝牙和GPS功能。W377E搭载Android 8.1系统,提供更加丰富多样的APP体验,支持在线好友聊天、支付宝等功能,且支持64位系统的AP
    紫光展锐 2024-12-31 16:26 122浏览
  • 从简单的照明工具到会说话的艺术品。认知总是不断地被刷新、升级。曾经,汽车内部的按键灯、内饰灯仿佛一盏盏小夜灯,那些光点,虽不起眼,却总能在黑夜中给予驾驶员和乘客陪伴和指引。但时代在变,人们对美好生活,包括车生活的追求也在升级。那些传统的光点,渐渐地,已经满足不了大众对驾驶体验的期待。让“上车”等于“回家”。至此,一场关于内饰照明的革命悄然兴起。1、从简单的照明工具到会说话的艺术品“内饰氛围灯的主要应用方向就是营造轻松舒适的氛围。”艾迈斯欧司朗高级系统方案工程师钟文帅还因此总结了内饰氛围灯的四大发
    艾迈斯欧司朗 2024-12-30 15:55 104浏览
  •         在之前的文章中,我们介绍了IEEE 802.3cz[1]协议,MultiGABSE-AU物理层中XMII、PCS子层以及两个可选功能的相关内容,本篇将介绍MultiGABSE-AU物理层PMA子层及PMD子层的相关机制。PMA子层        PMA子层位于PCS子层和PMD子层之间,规范中定义了PMA子层的三个功能:PMA Transmit、PMA Receive及PHY Control。&nbs
    经纬恒润 2024-12-30 18:16 104浏览
  • “新时代,共享未来”2024年11月5日-10日,第七届中国国际进口博览会在上海圆满落幕。其中,3万平方米的汽车展区展览面积,也让观众笑谈进博会再次进入“车展”时间。全球15大整车品牌,40多家企业参展,其中不乏耳熟能详,七届进博会“全勤生”的国际知名OEM,也会有首次参加的“新面孔”,它们共同构成了今年汽车展区的“全家福”。近年来,中国汽车进口均超百万辆。因此,对跨国车企来说,中国市场的重要性不言而喻。同时,在电动化和智能化赛道中弯道超车的中国汽车市场,也是全球汽车行业竞争最为激烈的市场,在这
    艾迈斯欧司朗 2024-12-30 17:05 59浏览
  • 2024年12月30日 调研咨询机构环洋市场咨询出版的《全球CPU渲染行业总体规模、主要厂商及IPO上市调研报告,2024-2030》主要调研全球CPU渲染总体规模、主要地区及国家的市场规模、主要企业规模和份额、主要细分市场规模、下游主要应用规模以及未来发展前景预测。统计维度包括收入、市场份额以及增速。同时也重点分析全球市场主要厂商(品牌)包括企业简介、总部、产地、CPU渲染产品介绍、规格/型号、收入、毛利率及市场份额、最新发展动态、优势与不足。历史数据为2019至2023年,预测数据为2024
    GIRtina 2024-12-30 16:36 127浏览
  • 时源芯微EMC(电磁兼容)领域,充斥着众多专业术语,令人眼花缭乱。1 电磁兼容(EMC)定义:指电气装置或系统在共同的电磁环境条件下,既能保持正常功能,又不会对周围环境产生不良影响。2  电磁环境Electromagnetic Environment定义:指存在于某一特定场所的所有电磁现象的总和。3半电波暗室Semi-anechoic Chamber定义:一种屏蔽室,除地面安装反射接地平板外,其余内表面均覆盖有吸波材料。4 远场Far Field定义:在天线辐射的场域中,功率密度与距离
    时源芯微 2024-12-31 10:52 111浏览
  • 感光百科:4000万片出货量背后的“技术经”目前,仅单点dToF,艾迈斯欧司朗的累积出货量就已超过了4000万片。大量采用的背后必然代表无数用户对这一技术的认可和信赖。究竟是什么魔力,让dToF传感器拥有如此强大的吸引力?又是怎样的know-how积累让艾迈斯欧司朗的dToF产品如此受到青睐?让我们再次回到底层原理来探究dToF 4000万片出货量背后的“技术经”。01、底层原理决定应用上限dToF传感器,顾名思义,直接飞行时间测量,基于光速不变,测量光子从发射端到接收端的光程差,从而直接计算二
    艾迈斯欧司朗 2024-12-30 18:21 94浏览
  • 产品概述MG600Q2YMS3 是一款基于硅基碳化物(SiC)技术的高功率N沟道MOSFET模块,适用于高功率开关和电机控制应用,如轨道牵引系统。其设计旨在满足高效能和快速切换需求,为工业和能源领域提供可靠解决方案。主要特性1. 高电压和电流能力   耐压 (VDSS):1200 V   漏极电流 (ID):600 A2. 高效率与低损耗   碳化硅材料降低导通损耗和开关损耗,实现更高的转换效率。3. 快速切换性能 支持高频操作,适用于
    东芝铠侠代理 2024-12-31 10:33 74浏览
  • 本文介绍Linux系统查看硬件配置及常用调试命令,方便开发者快速了解开发板硬件信息及进行相关调试。触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。查看DDR获取内存信息下面数字以MB为单位。root@ido:/# free -mtotalused free shared buff/cache availableMem:
    Industio_触觉智能 2024-12-31 10:01 70浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球CAN-Bus总线电缆产值达到2287百万美元,2024-2030年期间年复合增长率CAGR为3.6%。一、行业概述CAN-Bus总线电缆是一种专门用于CAN总线(Controller Area Network,控制器局域网络)通信系统中的电缆。CAN总线是一种广泛应用于汽车、工业自动化、机器人等领域的串行通信协议,主要用于微控制器和设备之间的数据交换。CAN-Bus电缆负责在多个设备间传输数据,保证信息的稳定传输和
    GIRtina 2024-12-31 11:00 102浏览
  • 高精度,抗干扰,设计灵活……一句话总结:打铁还需自身硬!2040年,人形机器人的数量或将超过人类,达到100亿个。 10月29日,沙特投资倡议论坛,在谈及AI和人形机器人赛道时,马斯克再一次大胆预测,描绘出上述令人震撼的未来景象,且预估每台机器人的价格将稳定在2万-2.5万美元之间。当下,技术飞速发展,众多机器人公司如雨后春笋般涌现。余建华,艾迈斯欧司朗资深应用工程师,深感对这个市场的期待,他表示每一个机器人关节的背后,都离不开位置传感器的精准掌控。这片市场的潜力,让人憧憬不已。在工业及消费中
    艾迈斯欧司朗 2024-12-30 17:25 55浏览
  •  随着电子技术的进步,电路中的隔离需求日益增加。晶体管光耦作为一种非接触式信号传输器件,因其独特的隔离特性和可靠性,成为了现代电子设备和工业控制中不可或缺的元件。本文将带您深入了解晶体管光耦的结构、工作原理和核心特点。晶体管光耦的基本结构晶体管光耦,通常由两个主要部分组成:发光二极管(LED)和光敏晶体管。其物理结构简单却极具创新性:发光二极管(LED):电流通过时,LED会发出红外光。光敏晶体管:接收来自LED的红外光信号,从而引导电流在输出端导通。这种结构的最大特点在于输入与输出端
    晶台光耦 2024-12-30 16:15 105浏览
  • 全球领先的光学解决方案供应商艾迈斯欧司朗(瑞士证券交易所股票代码:AMS)近日宣布,于2024年6月26-28日携汽车智能照明和智能座舱解决方案及相关产品和技术,亮相位于昆山市花桥国际博览中心的第十九届汽车灯具产业发展技术论坛暨上海国际汽车灯具展览会(以下简称:ALE),展示艾迈斯欧司朗优质的光发射器、光学元件、微型模组、传感器等产品在汽车领域的创新应用,展位号B馆T202。乘着车灯产业盛会的东风,全国汽车标准化技术委员会灯具及灯光分技术委员会秘书长卜伟理, 蔚来汽车内外车灯团队专家及电气工程副
    艾迈斯欧司朗 2024-12-30 16:55 69浏览
  • 在当今数字化时代,嵌入式系统无处不在,从我们手中的智能手机、智能手表,到汽车中的电子控制系统、工业自动化设备,嵌入式开发技术的发展历程宛如一部波澜壮阔的科技史诗,深刻地改变了我们的生活和社会的运作方式。萌芽阶段(20 世纪 60 - 70 年代)嵌入式开发的起源可以追溯到 20 世纪 60 年代。当时,计算机技术刚刚兴起,体积庞大且价格昂贵。为了满足特定设备的控制需求,工程师们开始尝试将计算机技术应用于一些专用系统中。这一时期,嵌入式系统的概念逐渐萌芽。1965 年,美国数字设备公司(DEC)推
    Jeffreyzhang123 2024-12-31 10:08 128浏览
  • 先临三维普及化手持3D扫描仪Einstar搭载艾迈斯欧司朗SFH 4726AS红外LED,打造真彩扫描、人眼安全、超高性价比的照明解决方案;得益于双堆叠发射器技术和透明硅树脂封装,OSLON® Black系列的SFH 4726AS实现小尺寸、高功率、高效率和优化的热管理,有效减小散热设计的压力;采用3.75×3.75mm的透明硅树脂封装,内置1mm2堆叠红外芯片,单颗光功率可达2W以上,为手持扫描仪这样小巧空间的应用提供更多光源设计空间。全球领先的光学解决方案供应商艾迈斯欧司朗(瑞士证券交易所股
    艾迈斯欧司朗 2024-12-30 16:50 61浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦