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 }
以下介绍几种比較easy混淆的指针概念:
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定义为一个全局函数或静态成员函数,但是这两个都不方便訪问类的非静态成员。之所以郑重其事地写这篇文章,就是曾经静态回调用起来很不爽。
回调函数是非静态成员函数呢?我们可不能简单地设为这样:
{
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);
}
这样编译就不会通过的,由于非静态的成员函数必须通过对象来訪问。好。我们稍稍改进一下:
{
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多传个对象进去,好吧,貌似问题攻克了。但是,调用者(如库的提供商)仅仅知道回调函数接口长这样而已,事先完全不知客户的类是怎样定义,最终模板登上场了:
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++不同意,当然我们能够强制转换,只是要在十分清楚类型的情况下才干这么做,否则由于不是类型安全的非常easy引起程序错误甚至崩溃。
所以要支持各种參数,多个參数,还得模板,嗯嗯。努力尚未成功。同志还需革命!
多态回调
甭管什么名词。总之我们的目的是:产生某个事件时,调用某个待客户实现的行为,调用者什么时候调用确定了。关键是客户依照规定接口实现这个行为,这听起来有点像多态了。是的,有时候被调用者与调用者是继承关系,这就不须要其他理论了,就多态呗。只是多态不一定非得用虚函数来实现,就像MFC一样,考虑到每一个类背负一个庞大的虚函数表会带来非常大的性能损失,换做用几个结构体和强大的宏而实现消息映射。
在wincore.cpp中。CWnd::OnWndMsg源代码里,当来了消息。在事先建立的链表中从派生类依次向上查找第一个实现了这个消息的类的AFX_MSGMAP结构体,再取得它的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进行类型转换:
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 slotkeyword,不是C++标准的啊。其他编译器不能随便编译(好像先经过qmake生成标准的代码就能够了)。直接上源代码不妥得搞清楚为什么,一切从最简单的入手。我们先来用标准C++实现一个简易的signal/slots,怎样实现呢,说白了,就是想方设法把回调函数信息保存起来。必要时利用它就OK了,回调函数信息就两个,类对象指针与成员函数地址,我们将这对信息存储到名叫slot的类中,而在signal类中,维护多个slot就可以。仍然用带一个int參数,返回值为void的函数接口:
#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 <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将它们连接起来就可以,已经非常组件化了。但是离真正的project应用尚有一段距离,如它不能接收全局函数或静态成员函数或仿函数为回调函数。不能带两个或很多其它的函数參数,最后一步了。
c/c++:回调函数的更多相关文章
- 小兔JS教程(三)-- 彻底攻略JS回调函数
这一讲来谈谈回调函数. 其实一句话就能概括这个东西: 回调函数就是把一个函数当做参数,传入另一个函数中.传进去的目的仅仅是为了在某个时刻去执行它. 如果不执行,那么你传一个函数进去干嘛呢? 就比如说对 ...
- 嵌入式&iOS:回调函数(C)与block(OC)传 参/函数 对比
C的回调函数: callBack.h 1).声明一个doSomeThingCount函数,参数为一个(无返回值,1个int参数的)函数. void DSTCount(void(*CallBack)(i ...
- 嵌入式&iOS:回调函数(C)与block(OC)回调对比
学了OC的block,再写C的回调函数有点别扭,对比下区别,回忆记录下. C的回调函数: callBack.h 1).定义一个回调函数的参数数量.类型. typedef void (*CallBack ...
- 理解 JavaScript 回调函数并使用
JavaScript中,函数是一等(first-class)对象:也就是说,函数是 Object 类型并且可以像其他一等对象(String,Array,Number等)一样使用.它们可以"保 ...
- 关于js的回调函数的一点看法
算了一下又有好几个月没写博客了,最近在忙公司android的项目,所以也就很少抽时间来写些东西了.刚闲下来,我就翻了翻之前看的东西.做了android之后更加感觉到手机端开发的重要性,现在做nativ ...
- JS学习:第二周——NO.1回调函数
[回调函数] 定义:把一个函数的定义阶段,作为参数,传给另一个函数: 回调函数调用次数,取决于条件: 回调函数可以传参: 回调函数可以给变this指向,默认是window: 回调函数没有返回值,for ...
- 【java回调】java两个类之间的回调函数传递
背景交代:熟悉用js开发的cordovaAPP:对java一窍不通的我,老师让做一个监测用户拍照事件的功能,无奈没有找到现成的库,无奈自己动手开发java插件~~0基础java GreenHand,祝 ...
- Java|今天起,别再扯订阅和回调函数
编程史上有两个令人匪夷所思的说辞,一个是订阅,一个是回调函数. 我想应该还有很多同学为“事件的订阅”和“回调函数”所困扰,因为事情本来就不应该按这个套路来解释. 多直白,所谓的“回调函数”你完全可以线 ...
- C++ 回调函数的定义与用法
一回调函数 我们经常在C++设计时通过使用回调函数可以使有些应用(如定时器事件回调处理.用回调函数记录某操作进度等)变得非常方便和符合逻辑,那么它的内在机制如何呢,怎么定义呢?它和其它函数(比如钩子函 ...
- 通过修改i8042prt端口驱动中类驱动Kbdclass的回调函数地址,达到过滤键盘操作的例子
同样也是寒江独钓的例子,但只给了思路,现贴出实现代码 原理是通过改变端口驱动中本该调用类驱动回调函数的地方下手 //替换分发函数 来实现过滤 #include <wdm.h> #inclu ...
随机推荐
- FreeBSD查看带宽占用情况,CPU,硬盘IO 虚拟内存等命令
FreeBSD查看带宽占用情况,CPU,硬盘IO 虚拟内存等命令 来源 https://www.liurongxing.com/freebsd-tips.html 来源 http://blog.51c ...
- Codeforces Round #364 (Div. 2) A 水
A. Cards time limit per test 1 second memory limit per test 256 megabytes input standard input outpu ...
- Gerrit使用简介
Gerrit,一种免费.开放源代码的代码审查软件,使用网页界面. Gerrit,一种免费.开放源代码的代码审查软件,使用网页界面.利用网页浏览器,同一个团队的软件程序员,可以相互审阅彼此修改后的程序代 ...
- AngularJS实现TodoMVC
一个小的to do list,界面如下 首先安装angular js,出现了无非安装到桌面的问题,安装到D盘了 npm install angular 文件结构: index.html: <!d ...
- python之装饰器【转】
装饰器 先来看一个例子,一个函数f1接收另一个函数f2作为参数,并将该参数f2返回给f2: def deco(func): print("before myfunc() called.&qu ...
- xCode控制台的使用-应用闪退原因跟踪
xCode控制台的使用-应用闪退原因跟踪 今天遇到这个一个问题,使用xCode version = 5.0 编译一个程序,刷到IOS7设备后,应用运行正常,但是刷新到IOS<7,打开饮用后就会出 ...
- pylint & jenkins
利用pylint 检测 python源码. Pylint 提供了简单的方式来分析 Python 代码,其高可配置性很容易使一个部门的人员使用统一的代码风格. Pylint 是什么 Pylint 是一个 ...
- Android 中利用ViewFlipper 滑动屏幕切换页面,ListView展示数据
首先新建一个Android项目,命名为ViewFlipperTest 如图:项目机构,本项目主要操作图中红色箭头标注的文件 1.HgroupAdapter.java文件代码↓主要实现listview数 ...
- (4)ASP.NET内置对象1
一.Response 把数据从服务端发送到客户端 Response.Write() 在页面上输出数据 Response.WriteFile(@"F:\WriteFile.txt") ...
- SecureCRT的帮助文档
[Help]->[Help Topics] Ubuntu:file:///usr/share/doc/scrt/SecureCRTHelp/SecureCRT.htm