c/c++:回调函数

1:函数名为指针

首先,在C语言中函数是一种function-to-pointer的方式,即对于一个函数,会将其自动转换成指针的类型.如:

 1 #include<stdio.h>
 2
 3 void fun()
 4 {
 5 }
 6
 7 int main()
 8 {
 9    printf("%p %p %p\n", &fun, fun, *fun);
10    return 0;
11 }

这三个值的结果是一样的. 其实对于最后的那个*fun, 即使前面加上很多个*号, 其结果也不变, 即**fun, ***fun的结果都是一样的. 对于这个问题, 因为之前讲过函数是一种function-to-pointer方式, 其会自动转换成指针的类型, &fun是该函数的地址, 为指针类型, fun是一个函数, 会转换成其指针类型, 而对于*fun, 由于fun已经变成了指针类型,
指向这个函数, 所以*fun就是取这个地址的函数, 而又根据function-to-pointer, 该函数也转变成了一个指针, 所以以此类推, 这三个值的结果是相同的.

2:回调函数

通过将回调函数的地址传给调用者从而实现动态调用不同的函数。因此当我们想通过一个统一接口实现不同的内容,这时用回掉函数非常合适。

若要实现回调函数,最关键的是要把调用函数的参数定义为函数指针类型。函数指针的定义这里稍

微提一下。比如:

int (*ptr)(void); 这里ptr是一个函数指针,其中(*ptr)的括号不能省略,因为括号的优先级高于星号,那样就成了一个返回类型为整型的函数声明了。int为返回类型,括号内为函数的参数。

下面通过一个例子来解释回调函数的用法:

 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 int Test1(int num)
 4 {
 5   printf("i am test1,the data is %d \n",num);
 6   return 0;
 7 }
 8 int Test2(int num)
 9 {
10   printf("i am test2,the data is %d\n",num);
11   return 0;
12 }
13
14 int Caller(int (*ptr)(int n),int n)//指向函数的指针作函数参数,这里第二个参数是函数指针的参数
15 {                                               //不能写成void Caller2(int (*ptr)(int n)),这样的定义语法错误。
16   int a=(*ptr)(n);
17   return a;
18 }
19 int main()
20 {
21
22    Caller(Test1,20);
23    printf("************************\n");
24    Caller(Test2,10);
25
26   return 0;
27 } 

下面介绍几种比较容易混淆的指针概念:

1:函数指针

1:函数指针的定义方式:

  返回值类型  (* 指针变量名)(形参列表);

返回值为指针的函数定义: 返回指针类型 * 函数名(形参列表);

2:函数指针的赋值:

   在赋值时,可以直接将函数指针指向函数名(函数名即代表该段代码的首地址),但是前提是:函数指针和它指向的函数的参数个数以及类型必须一致。函数指针的返回值类型与函数的返回值类型必须一致。

3:通过函数指针调用函数:

  加上指针f指向函数func。(*f ) 和 func代表同一函数。

  使用方法如下:

  声明函数指针:int (*f)(int x);

  函数指针赋值: f=func   ( int func(int x));

  函数指针调用函数:  (*f)(x)  (x为整型变量)

2:函数指针数组

函数指针数组是一个其元素是函数指针的数组。即,此数据结构是是一个数组,且其元素是一个指向函数入口地址的指针。

定义方式:  返回值   ( *数组名[个数]) (参数列表)

3:指向数组的指针

类型 (*变量名)[元素个数]

4:  指针数组

  类型 *变量名[元素个数]

  因为[] 比*具有更好的优先级。所以如果是变量a先和*结合则表示其为一个指针,如果a先和[]结合,则表示是一个数组。

带参数的回调函数:

//定义带参回调函数
void PrintfText(char* s)
{
    printf(s);
}

//定义实现带参回调函数的"调用函数"
void CallPrintfText(void (*callfuct)(char*),char* s)
{
    callfuct(s);
}

//在main函数中实现带参的函数回调
int main(int argc,char* argv[])
{
    CallPrintfText(PrintfText,"Hello World!\n");
    return 0;
}

c++回调机制:

非静态成员函数作回调函数

当然如果是静态成员函数就好办跟全局函数是类似,到此为止世界还没有变乱,如在VC编程中用AfxBeginThread开启一个线程,就经常将参数AFX_THREADPROC pfnThreadProc定义为一个全局函数或静态成员函数,可是这两个都不方便访问类的非静态成员,之所以郑重其事地写这篇文章,就是以前静态回调用起来非常不爽。

回调函数是非静态成员函数呢?我们可不能简单地设为这样:

class CCallback

{

public:

void Func(int a)

{

cout<<"member function callback called with para="<<a<<endl;

}

};

typedef void (CCallback::*pMemberFunc)(int);

void Caller(pMemberFunc p)

{

(*p)(1);

}

这样编译就不会通过的,因为非静态的成员函数必须通过对象来访问,好,我们稍稍改进一下:

class CCallback

{

public:

void Func(int a)

{

cout<<"member function callback called with para="<<a<<endl;

}

};

typedef void (CCallback::*pMemberFunc)(int);

void Caller(CCallback* pObj,pMemberFunc p)

{

(pObj->*p)(1);

}

int main(int argc, char* argv[])

{

CCallback obj;

Caller(&obj,&CCallback::Func);

}

即给Caller多传个对象进去,好吧,貌似问题解决了,可是,调用者(如库的提供商)只知道回调函数接口长这样而已,事先全然不知客户的类是如何定义,终于模板登上场了:

template<typename T>

void Caller(T* pObj,void (T::*p)(int))

{

(pObj->*p)(1);

}

其他不变的,把调用者这里换成模板就OK了,当然这个Caller也可以是成员函数,现在用这个方法写个小应用是没什么问题了,但是限制多多,如调用者一次只调用了一个实现,但现实情况往往是产生某个事件时,应该依次调用多个行为,即把挂在这个事件上的所有回调函数通通临幸一遍,还有回调是如此的重要,以至于C#不用库在语言本身层面就实现了它,我们也不可以到此草草了事,而是按照组件化的思维提供一套完善的回调机制,所谓完善,如上个例子中Caller只能接收一个参数为int,返回值为void的成员函数指针,等等,必须是这样的接口吗,想想参数为double行不行,如void (T::*p)(double)这样的函数传给它可以吗,int不是可自动转换为double吗,那这个函数指针也能自动转换吗,就像C#中的协变与逆变一样,不行,C++不允许,当然我们可以强制转换,不过要在十分清楚类型的情况下才能这么做,否则因为不是类型安全的很容易引起程序错误甚至崩溃。所以要支持各种参数,多个参数,还得模板,嗯嗯,努力尚未成功,同志还需革命!

多态回调

甭管什么名词,总之我们的目的是:产生某个事件时,调用某个待客户实现的行为,调用者什么时候调用确定了,关键是客户按照规定接口实现这个行为,这听起来有点像多态了,是的,有时候被调用者与调用者是继承关系,这就不需要其它理论了,就多态呗,不过多态不一定非得用虚函数来实现,就像MFC一样,考虑到每个类背负一个庞大的虚函数表会带来很大的性能损失,换做用几个结构体和强大的宏而实现消息映射。在wincore.cpp中,CWnd::OnWndMsg源码里,当来了消息,在事先建立的链表中从派生类依次向上查找第一个实现了这个消息的类的AFX_MSGMAP结构体,再取得它的AFX_MSGMAP_ENTRY成员,即真正的消息入口地址,

struct AFX_MSGMAP_ENTRY

{

UINT nMessage;   // windows message

UINT nCode;      // control code or WM_NOTIFY code

UINT nID;        // control ID (or 0 for windows messages)

UINT nLastID;    // used for entries specifying a range of control id‘s

UINT nSig;       // signature type (action) or pointer to message #

AFX_PMSG pfn;    // routine to call (or special value)

};

就类似于写一个普通的链表结构:struct list_node{list_node* next; int data},只不过这里的链表的next不能再随便指,要指向基类的节点,根据next指针找到对应的节点后取出数据data成员即可,在这里,data就是AFX_MSGMAP_ENTRY,如上图,AFX_MSGMAP_ENTRY里定义了消息标号即各种附加参数,还有最关键的成员pfn,代表了事先派生类通过宏填充好的回调成员函数地址。但是pfn的类型即AFX_PMSG定义为typedef void (AFX_MSG_CALL
CCmdTarget::*AFX_PMSG)(void); 只能代表一种类型,而客户的派生类的为响应消息的回调函数的类型有很多种,在框架中如何保证以正确的形式调用呢?原来客户在填充消息标号和函数地址时,也顺便填充好了函数类型交给nSig成员保存,根据nSig,如前文所说,将pfn强制转换到相应的类型就OK了,不过这成员函数指针转换来转换去,代码非常难看啊可读性不强,于是使用union进行类型转换:

//afximpl.h

union MessageMapFunctions

{

AFX_PMSG pfn;   // generic member function pointer

// specific type safe variants for WM_COMMAND and WM_NOTIFY messages

void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND)();

BOOL (AFX_MSG_CALL CCmdTarget::*pfn_bCOMMAND)();

void (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_RANGE)(UINT);

BOOL (AFX_MSG_CALL CCmdTarget::*pfn_COMMAND_EX)(UINT);

...

}

//wincore.cpp  CWnd::OnWndMsg

union MessageMapFunctions mmf;

mmf.pfn = lpEntry->pfn;

nSig = lpEntry->nSig;

switch (nSig)

{

default:

ASSERT(FALSE);

break;

case AfxSig_bD:

lResult = (this->*mmf.pfn_bD)(CDC::FromHandle((HDC)wParam));

break;

case AfxSig_bb:     // AfxSig_bb, AfxSig_bw, AfxSig_bh

lResult = (this->*mmf.pfn_bb)((BOOL)wParam);

break;

case AfxSig_bWww:   // really AfxSig_bWiw

lResult = (this->*mmf.pfn_bWww)(CWnd::FromHandle((HWND)wParam),

(short)LOWORD(lParam), HIWORD(lParam));

break;

...

}

当然这里只是一个小插曲而已,它只是MFC为满足于自己应用设计这么一套机制,派生类的回调函数类型是有限的,再则要求与框架类是继承关系,如果没有继承关系怎么办,例如当产生串口或者网口收到数据的事件时,需要更新UI界面,UI界面与串口类可是没有丝毫继承关系的,呃...铁人王进喜说:有条件要上,没条件创造条件也要上,我们大不了专门定义一个回调抽象类,让UI界面继承自它,实现类里的回调函数,然后串口类通过抽象类型对象指针就可以多态地调用到UI的真正回调实现。COM/ATL的回调,Java的回调就是这么干。不过在C++中,情形有些不一样,这样实现很勉强,它需要多重继承,仍然不能直接实现同时调用多个行为,耦合性高,每个回调都需要单独定义一个类(只要接口不一样),效率也不够高,我们想直接调用到绑定好的回调,基于这些缺点,还得寻找更好的方法。

信号与槽(Signal/Slots)

说了这么多,终于来到正题了,在C++中,信号与槽才是回调的完美解决方案,其实本质上是一个观察者模式,包括其它的叫法:delegate,notifier/receiver,observer,C#中的delegate也是一个观察者的实现。Qt中提供了信号与槽的整套机制,任何对象的槽可以绑定到另一个对象的信号上,一个信号可以拥有多个槽,经典的图例如下:

可是qt中的实现用了signal slot关键字,不是C++标准的啊,其它编译器不能随便编译(好像先经过qmake生成标准的代码就可以了),直接上源码不妥得搞清楚为什么,一切从最简单的入手,我们先来用标准C++实现一个简易的signal/slots,如何实现呢,说白了,就是想方设法把回调函数信息保存起来,必要时利用它就OK了,回调函数信息就两个,类对象指针与成员函数地址,我们将这对信息存储到名叫slot的类中,而在signal类中,维护多个slot即可,仍然用带一个int参数,返回值为void的函数接口:

#include <vector>

#include <iostream>

using namespace std;

template<typename T, typename T1>

class slot

{

public:

slot(T* pObj,void (T::*pMemberFunc)(T1))

{

m_pObj=pObj;

m_pMemberFunc=pMemberFunc;

}

void Execute(T1 para)

{

(m_pObj->*m_pMemberFunc)(para);

}

private:

T* m_pObj;

void (T::*m_pMemberFunc)(T1);

};

template<typename T, typename T1>

class signal

{

public:

void bind(T* pObj,void (T::*pMemberFunc)(T1 para))

{

m_slots.push_back(new slot<T,T1>(pObj,pMemberFunc));

}

~signal()

{

vector<slot<T,T1>* >::iterator ite=m_slots.begin();

for (;ite!=m_slots.end();ite++)

{

delete *ite;

}

}

void operator()(T1 para)

{

vector<slot<T,T1>* >::iterator ite=m_slots.begin();

for (;ite!=m_slots.end();ite++)

{

(*ite)->Execute(para);

}

}

private:

vector<slot<T,T1>* > m_slots;

};

class receiver

{

public:

void callback1(int a)

{

cout<<"receiver1: "<<a<<endl;

}

void callback2(int a)

{

cout<<"receiver2: "<<a<<endl;

}

};

class sender

{

public:

sender(): m_value(0)  {}

int get_value()

{

return m_value;

}

void set_value(int new_value)

{

if (new_value!=m_value)

{

m_value=new_value;

m_sig(new_value);

}

}

signal<receiver,int> m_sig;

private:

int m_value;

};

int main(int argc,char** arg)

{

receiver r;

sender s;

s.m_sig.bind(&r,&receiver::callback1);

s.m_sig.bind(&r,&receiver::callback2);

s.set_value(1);

return 0;

}

程序在VC6下顺利通过,这个版本相比前面所说的继承手法耦合性低了,被调用者receiver与规定函数接口的slot类没有任何关系,但仔细以观察这个程序在概念上是有问题的,signal类有两个模板参数,一个是类的类型,一个是函数参数类型,如果把这个signal/slots组件提供出去,使用者如上面的sender类不免会有个疑虑:在实例化signal类型时,必须提供这两个模板参数,可是调用方事先哪就一定知道接收方(receiver)的类型呢,而且从概念上讲事件发送方与接收方只需遵循一个共同的函数接口就可以了,与类没什么关系,上个程序要求在实例化时就得填充receiver的类型,也就决定了它与receiver只能一对一,而不能一对多,于是作此改进:将signal的参数T去掉,将T类型的推导延迟到绑定(bind)时,signal没有参数T,signal的成员slot也就不能有,那slot的成员也就不能有,可是,参数T总得找个地方落脚啊,怎么办?有个窍门:让slot包含slotbase成员,slotbase没有参数T的,但slotbase只定义接口,真正的实现放到slotimpl中,slotimpl就可以挂上参数T了,boost中any、shared_ptr就是用此手法,改进后全部代码如下:

#include <vector>

#include <iostream>

using namespace std;

template<typename T1>

class slotbase

{

public:

virtual void Execute(T1 para)=0;

};

template<typename T,typename T1>

class slotimpl : public slotbase<T1>

{

public:

slotimpl(T* pObj,void (T::*pMemberFunc)(T1))

{

m_pObj=pObj;

m_pMemberFunc=pMemberFunc;

}

virtual void Execute(T1 para)

{

(m_pObj->*m_pMemberFunc)(para);

}

private:

T* m_pObj;

void (T::*m_pMemberFunc)(T1);

};

template<typename T1>

class slot

{

public:

template<typename T>

slot(T* pObj,void (T::*pMemberFunc)(T1))

{

m_pSlotbase=new slotimpl<T,T1>(pObj,pMemberFunc);

}

~slot()

{

delete m_pSlotbase;

}

void Execute(T1 para)

{

m_pSlotbase->Execute(para);

}

private:

slotbase<T1>* m_pSlotbase;

};

template<typename T1>

class signal

{

public:

template<typename T>

void bind(T* pObj,void (T::*pMemberFunc)(T1 para))

{

m_slots.push_back(new slot<T1>(pObj,pMemberFunc));

}

~signal()

{

vector<slot<T1>* >::iterator ite=m_slots.begin();

for (;ite!=m_slots.end();ite++)

{

delete *ite;

}

}

void operator()(T1 para)

{

vector<slot<T1>* >::iterator ite=m_slots.begin();

for (;ite!=m_slots.end();ite++)

{

(*ite)->Execute(para);

}

}

private:

vector<slot<T1>* > m_slots;

};

#define CONNECT(sender,signal,receiver,slot)  sender.signal.bind(receiver,slot)

class receiver

{

public:

void callback1(int a)

{

cout<<"receiver1: "<<a<<endl;

}

};

class receiver2

{

public:

void callback2(int a)

{

cout<<"receiver2: "<<a<<endl;

}

};

class sender

{

public:

sender(): m_value(0)  {}

int get_value()

{

return m_value;

}

void set_value(int new_value)

{

if (new_value!=m_value)

{

m_value=new_value;

m_valueChanged(new_value);

}

}

signal<int> m_valueChanged;

private:

int m_value;

};

int main(int argc,char** arg)

{

receiver r;

receiver2 r2;

sender s;

CONNECT(s,m_valueChanged,&r,&receiver::callback1);

CONNECT(s,m_valueChanged,&r2,&receiver2::callback2);

s.set_value(1);

return 0;

}

这个版本就比较像样了,一个signal可与多个slots连接,增加了类似QT的connect,用宏实现#define CONNECT(sender,signal,receiver,slot) sender.signal.bind(receiver,slot),这样使用者就非常方便,而且现在已完全解耦,sender只管定义自己的signal,在恰当时机用仿函数形式调用即可,而receiver只管实现callback,互不影响,可独立工作,如果需要再通过CONNECT将它们连接起来即可,已经很组件化了,可是离真正的工程应用尚有一段距离,如它不能接收全局函数或静态成员函数或仿函数为回调函数,不能带两个或更多的函数参数,最后一步了。

c/c++:回调函数,布布扣,bubuko.com

时间: 2024-10-12 20:42:42

c/c++:回调函数的相关文章

嵌入式&amp;iOS:回调函数(C)与block(OC)传 参/函数 对比

C的回调函数: callBack.h 1).声明一个doSomeThingCount函数,参数为一个(无返回值,1个int参数的)函数. void DSTCount(void(*CallBack)(int data_i32)); callBack.c 1).在doSomeThingCount函数,对运行次数自增,并调用参数--函数. void DSTCount(void(*CallBack)(int data_i32)) { static int numb = 0; numb++; (*Call

回调函数的本质,就是把函数当作参数(首先要定义函数类型)

//把一个方法当作另一个方法的参数, 就是回调方法, 大家习惯称作回调函数 type   TFunType = function(i: Integer): Integer; {声明一个方法类型} function MyFun(i: Integer): Integer;        {建立类型兼容的函数} begin   Result := i*2; end; {把函数当作参数, 再定义一个函数} function MyTest(x: Integer; F: TFunType): Integer

MFC 定时器 SetTimer 如何使用回调函数

创建工程名TestCallBack 自定义回调函数   定义为全局函数 在TestCallBackDlg.h文件开头定义 #pragma once void CALLBACK EXPORT TimerProc(HWND hWnd,UINT nMsg,UINT nTimerid,DWORD dwTime); 在TestCallBackDlg.cpp文件末尾实现函数 void CALLBACK EXPORT TimerProc(HWND hwnd,UINT message,UINT iTimerID

(转)回调函数

原文:http://blog.csdn.net/callmeback/article/details/4242260 其实回调就是一种利用函数指针进行函数调用的过程. 为什么要用回调呢?比如我要写一个子模块给你用,   来接收远程socket发来的命令.当我接收到命令后,   需要调用你的主模块的函数,   来进行相应的处理.但是我不知道你要用哪个函数来处理这个命令,     我也不知道你的主模块是什么.cpp或者.h,   或者说,   我根本不用关心你在主模块里怎么处理它,   也不应该关心

ajax返回的值有两种方法,一种是把async:true改为false。 另一种是回调函数。

function load_val(callback){//定义一个回调函数 $.getJSON('test.php' , function(dat){ callback(data);//将返回结果当作参数返回 }); } load_val(function(data){ alert(data);//这里可以得到值 }); //否则的话你需要这样用同步ajax来实现了 function load_val2(){ var result; $.ajax({ dataType:'json', url

Python 3 进程池与回调函数

Python 3 进程池与回调函数 一.进程池 在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间.多进程是实现并发的手段之一,需要注意的问题是: 很明显需要并发执行的任务通常要远大于核数 一个操作系统不可能无限开启进程,通常有几个核就开几个进程 进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行) 例如当被操作对象数目不大时,可以直接利用multiprocessing中的Proces

【Cocos2d-x 3.0 基础系列一】 各类回调函数写法汇总

一.button回调 1. Lambda 表达式,C++11 Lambda 赋予了Cocos2d-x 3.0创建回调函数的灵活性. auto itemNor = Sprite::create("CloseNormal.png"); auto menuItem = MenuItemSprite::create(itemNor,nullptr,nullptr,[](Ref* sender) { log("show this msg."); }); auto menu =

IT小鲜肉 Widgets Tree 单选、多选、相关回调函数、获取选中的节点功能

写一个树控件并没有想象中的那么容易,今天又花了我一个1个多小时,主要为IT小鲜肉 Widgets Tree控件添加了 单选.多选.选择前和选择后两个回调函数.获取选中节点的功能.后面会继续努力完善这个树控件. 1.通过设置初始化时候的选项{select:true}开启单选,通过设置初始化时候的选项{select:{type:'multiple'}}开启多选 使用实例代码如下: 运行效果如下: 2.添加了onBeforeSelect回调函数,用来实现自定义选择,如果该函数返回false会中断默认的

设计一个函数,它接受不定数量的参数,这是参数都是函数。这些函数都接受一个回调函数作为参数,按照回调函数被调用的顺序返回函数名

function acceptFuncs() { var fnNames = []; //定义数组字面量,用来保存函数名称 for (var i = 0; i < arguments.length; i++) { //for循环检测接收到的每个参数是否为函数,是则传递回调函数给它,最后所结果压入数组中 if (typeof arguments[i] === "function") { fnNames.push(arguments[i](callback)); } } for (v

cocos2d-x学习笔记(c++与lua交互回调函数的处理)

本文假设读者已经会使用tolua++进行C++与lua之间的通讯 1.在头文件中定义注册回调函数,定义在MyClass类中 void register(unsigned short cmdID, LUA_FUNCTION func);//LUA_FUNCTION其实就是一个int void unregister(); 2.实现 void MyClass::register(unsigned short cmdID, LUA_FUNCTION func) { m_luaFunction = fun