回调函数

来源:互联网 发布:编程 知乎 编辑:程序博客网 时间:2024/05/20 19:31

什么是回调函数?

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

  为什么要使用回调函数?

  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序、快速排序、shell排序、shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(intfloatstring),此时,该怎么办呢?可以使用函数指针,并进行回调。

  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。

  另一个使用回调机制的API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。

  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。

  一个简单的回调函数实现

  下面创建了一个sort.dll的动态链接库,它导出了一个名为CompareFunction的类型--typedef int (__stdcall *CompareFunction)(const byte*, constbyte*),它就是回调函数的类型。另外,它也导出了两个方法:Bubblesort()Quicksort(),这两个方法原型相同,但实现了不同的排序算法。

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

void DLLDIR __stdcall Quicksort(byte* array,int size,int elem_size,CompareFunction cmpFunc);


  这两个函数接受以下参数:

·byte * array:指向元素数组的指针(任意类型)。

·int size:数组中元素的个数。

·int elem_size:数组中一个元素的大小,以字节为单位。

·CompareFunction cmpFunc:带有上述原型的指向回调函数的指针。

  这两个函数的会对数组进行某种排序,但每次都需决定两个元素哪个排在前面,而函数中有一个回调函数,其地址是作为一个参数传递进来的。对编写者来说,不必介意函数在何处实现,或它怎样被实现的,所需在意的只是两个用于比较的元素的地址,并返回以下的某个值(库的编写者和使用者都必须遵守这个约定):

·-1:如果第一个元素较小,那它在已排序好的数组中,应该排在第二个元素前面。

·0:如果两个元素相等,那么它们的相对位置并不重要,在已排序好的数组中,谁在前面都无所谓。

 
·1:如果第一个元素较大,那在已排序好的数组中,它应该排第二个元素后面。

  基于以上约定,函数Bubblesort()的实现如下,Quicksort()就稍微复杂一点:


void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc)
{
 for(int i=0; i < size; i++)
 {
  for(int j=0; j < size-1; j++)
  {
   //回调比较函数
   if(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))
   {
    //两个相比较的元素相交换
    byte* temp = new byte[elem_size];
    memcpy(temp, array+j*elem_size, elem_size);
    memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);
    memcpy(array+(j+1)*elem_size, temp, elem_size);
    delete [] temp;
   }
  }
 }
}


  注意:因为实现中使用了memcpy(),所以函数在使用的数据类型方面,会有所局限。

  对使用者来说,必须有一个回调函数,其地址要传递给Bubblesort()函数。下面有二个简单的示例,一个比较两个整数,而另一个比较两个字符串:


int __stdcall CompareInts(const byte* velem1, const byte* velem2)
{
 int elem1 = *(int*)velem1;
 int elem2 = *(int*)velem2;

 if(elem1 < elem2)
  return -1;
 if(elem1 > elem2)
  return 1;

 return 0;
}

int __stdcall CompareStrings(const byte* velem1, const byte* velem2)
{
 const char* elem1 = (char*)velem1;
 const char* elem2 = (char*)velem2;
 return strcmp(elem1, elem2);
}


  下面另有一个程序,用于测试以上所有的代码,它传递了一个有5个元素的数组给Bubblesort()Quicksort(),同时还传递了一个指向回调函数的指针。


int main(int argc, char* argv[])
{
 int i;
 int array[] = {5432, 4321, 3210, 2109, 1098};

 cout << "Before sorting ints with Bubblesort/n";
 for(i=0; i < 5; i++)
  cout << array[i] << '/n';

 Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);

 cout << "After the sorting/n";
 for(i=0; i < 5; i++)
  cout << array[i] << '/n';

 const char str[5][10] = {"estella","danielle","crissy","bo","angie"};

 cout << "Before sorting strings with Quicksort/n";
 for(i=0; i < 5; i++)
  cout << str[i] << '/n';

 Quicksort((byte*)str, 5, 10, &CompareStrings);

 cout << "After the sorting/n";
 for(i=0; i < 5; i++)
  cout << str[i] << '/n';

 return 0;
}


  如果想进行降序排序(大元素在先),就只需修改回调函数的代码,或使用另一个回调函数,这样编程起来灵活性就比较大了。

调用约定

  上面的代码中,可在函数原型中找到__stdcall,因为它以双下划线打头,所以它是一个特定于编译器的扩展,说到底也就是微软的实现。任何支持开发基于Win32的程序都必须支持这个扩展或其等价物。以__stdcall标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用C/C++调用约定。

Windows操作系统采用了标准调用约定(Pascal约定),因为其可减小代码的体积。这点对早期的Windows来说非常重要,因为那时它运行在只有640KB内存的电脑上。

  如果你不喜欢__stdcall,还可以使用CALLBACK宏,它定义在windef.h中:

#define CALLBACK __stdcallor

#define CALLBACK PASCAL //而PASCAL在此被#defined成__stdcall


  作为回调函数的C++方法

  因为平时很可能会使用到C++编写代码,也许会想到把回调函数写成类中的一个方法,但先来看看以下的代码:

class CCallbackTester
{
 public:
 int CALLBACK CompareInts(const byte* velem1, const byte* velem2);
};

Bubblesort((byte*)array, 5, sizeof(array[0]),
&CCallbackTester::CompareInts);


  如果使用微软的编译器,将会得到下面这个编译错误:

error C2664: 'Bubblesort' : cannot convert parameter 4 from 'int (__stdcall CCallbackTester::*)(const unsigned char *,const unsigned char *)' to 'int (__stdcall *)(const unsigned char *,const unsigned char *)' There is no context in which this conversion is possible


  这是因为非静态成员函数有一个额外的参数:this指针,这将迫使你在成员函数前面加上static。当然,还有几种方法可以解决这个问题,但限于篇幅,就不再论述了 .

2补充BBS评论

回调到底层次的看法就是:

让函数去"自主"调用函数,而不是由你决定.

typedef void (*VP)(void);

void Task1()
{
         ...
}

void Task2()
{
         ...
}

void EX_CallBack()
{
         VP M = NULL;

         if (condition)
         {
             M = Task1;
         }
         else
         {
             M = Task2;
         }

         M();
}

短歌说:它算是一种动态绑定的技术,
主要用于对某一事件的正确响应.

3.声明函数指针并回调

程序员常常需要实现回调。本文将讨论函数指针的基本原则并说明如何使用函数指针实现回调。注意这里针对的是普通的函数,不包括完全依赖于不同语法和语义规则的类成员函数(类成员指针将在另文中讨论)。

声明函数指针

       
回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。要实现回调,必须首先定义函数指针。尽管定义的语法有点不可思议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似。请看下面的例子:

void f()
//函数原型

上面的语句声明了一个函数,没有输入参数并返回void。那么函数指针的声明方法如下:

void (*) ();

       
让我们来分析一下,左边圆括弧中的星号是函数指针声明的关键。另外两个元素是函数的返回类型(void)和由边圆括弧中的入口参数(本例中参数是空)。注意本例中还没有创建指针变量-只是声明了变量类型。目前可以用这个变量类型来创建类型定义名及用sizeof表达式获得函数指针的大小:

//
获得函数指针的大小
unsigned psize = sizeof (void (*) ());

//
为函数指针声明类型定义
typedef void (*pfv) ();

pfv
是一个函数指针,它指向的函数没有输入参数,返回类行为void。使用这个类型定义名可以隐藏复杂的函数指针语法。

指针变量应该有一个变量名:

void (*p) (); //p
是指向某函数的指针

        p
是指向某函数的指针,该函数无输入参数,返回值的类型为void。左边圆括弧里星号后的就是指针变量名。有了指针变量便可以赋值,值的内容是署名匹配的函数名和返回类型。例如:

void func()
{
/* do something */
}
p = func;

p
的赋值可以不同,但一定要是函数的地址,并且署名和返回类型相同。

传递回调函数的地址给调用者

       
现在可以将p传递给另一个函数(调用者)- caller(),它将调用p指向的函数,而此函数名是未知的:

void caller(void(*ptr)())
{
ptr(); /*
调用ptr指向的函数 */
}
void func();
int main()
{
p = func;
caller(p); /*
传递函数地址到调用者 */
}

       
如果赋了不同的值给p(不同函数地址),那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。

调用规范

       
到目前为止,我们只讨论了函数指针及回调而没有去注意ANSI C/C++的编译器规范。许多编译器有几种调用规范。如在Visual C++中,可以在函数类型前加_cdecl_stdcall或者_pascal来表示其调用规范(默认为_cdecl)。C++ Builder也支持_fastcall调用规范。调用规范影响编译器产生的给定函数名,参数传递的顺序(从右到左或从左到右),堆栈清理责任(调用者或者被调用者)以及参数传递机制(堆栈,CPU寄存器等)。

       
将调用规范看成是函数类型的一部分是很重要的;不能用不兼容的调用规范将地址赋值给函数指针。例如:

//
被调用函数是以int为参数,以int为返回值
__stdcall int callee(int);

//
调用函数以函数指针为参数
void caller( __cdecl int(*ptr)(int));

//
p中企图存储被调用函数地址的非法操作
__cdecl int(*p)(int) = callee; //
出错


       
指针pcallee()的类型不兼容,因为它们有不同的调用规范。因此不能将被调用者的地址赋值给指针p,尽管两者有相同的返回值和参数列

4。函数指针和回调函数

函数指针和回调函数

你不会每天都使用函数指针,但是,它们确有用武之地,两个最常见的用途是把函数指针作为参数传递给另一个函数以及用于转换表(jump table)。
      
【警告】简单声明一个函数指针并不意味着它马上就可以使用。和其它指针一样,对函数指针执行间接访问之前必须把它初始化为指向某个函数。下面的代码段说明了一种初始化函数指针的方法。
        int    f(int);
        int    (*pf)(int)=&f;

        第 2 个声明创建了函数指针 pf ,并把它初始化为指向函数 f 。函数指针的初始化也可以通过一条赋值语句来完成。 在函数指针的初始化之前具有 f 的原型是很重要的,否则编译器就无法检查 f 的类型是否与 pf 所指向的类型一致。

        初始化表达式中的 & 操作符是可选的,因为函数名被使用时总是由编译器把它转换为函数指针。 & 操作符只是显式地说明了编译器隐式执行的任务。

        在函数指针被声明并且初始化之后,我们就可以使用三种方式调用函数:
        int    ans;
   
        ans=f(25);
        ans=(*pf)(25);
        ans=pf(25);

        第 1 条语句简单地使用名字调用函数 f ,但它的执行过程可能和你想象的不太一样。 函数名 f 首先被转换为一个函数指针,该指针指定函数在内存中的位置。然后, 函数调用操作符调用该函数,执行开始于这个地址的代码。
        第 2 条语句对 pf 执行间接访问操作,它把函数指针转换为一个函数名。这个转换并不是真正需要的,因为编译器在执行函数调用操作符之前又会把它转换回去。不过,这条语句的效果和第1条是完全一样的。
        第 3 条语句和前两条的效果是一样的。间接访问并非必需,因为编译器需要的是一个函数指针。

        (一)回调函数
        这里有一个简单的函数,它用于在单链表中查找一个值。它的参数是一个指向链表第 1 个节点的指针以及那个需要查找的值。

        Node *
        search_list(Node   *node, int    const    value)
        {
            while(node!=NULL){
                if( node->value ==value )
                    break;
                node = node->link;
            }
            return node;
        }

        这个函数看上去相当简单,但它只适用于值为整数的链表。如果你需要在一个字符串链表中查找,你不得不另外编写一个函数。这个函数和上面那个函数的绝大部分代码相同,只是第 2 个参数的类型以及节点值的比较方法不同。

        一种更为通用的方法是使查找函数与类型无关,这样它就能用于任何类型的值的链表。我们必须对函数的两个方面进行修改,使它与类型无关。

        首先,我们必须改变比较的执行方式,这样函数就可以对任何类型的值进行比较。这个目标听上去好像不可能,如果你编写语句用于比较整型值,它怎么还可能用于其它类型如字符串的比较呢? 解决方案就是使用函数指针。调用者编写一个比较函数,用于比较两个值,然后把一个指向此函数的指针作为参数传递给查找函数。而后查找函数来执行比较。使用这种方法,任何类型的值都可以进行比较。

        我们必须修改的第 2 个方面是向比较函数传递一个指向值的指针而不是值本身。比较函数有一个 void    * 形参,用于接收这个参数。然后指向这个值的指针便传递给比较函数。(这个修改使字符串和数组对象也可以被使用。字符串和数组无法作为参数传递给函数,但指向它们的指针却可以。)

        使用这种技巧的函数被称为回调函数(callback   function),因为用户把一个函数指针作为参数传递其它函数,后者将”回调“用户的函数。任何时候,如果你所编写的函数必须能够在不同的时刻执行不同类型的工作或者执行只能由函数调用者定义的工作,你都可以使用这个技巧。
     
        【提示】
      在使用比较函数的指针之前,它们必须被强制转换为正确的类型。因为强制类型转换能够躲开一般的类型检查,所以你在使用时必须格外小心,确保函数参数类型是正确的。

        在这个例子里,回调函数比较两个值。查找函数向比较函数传递两个指向需要进行比较的值的指针,并检查比较函数的返回值。例如:零表示相等的值,现在查找函数就与类型无关,因为它本身并不执行实际的比较。确实,调用者必须编写必需的比较函数,但这样做是很容易的,因为调用者知道链表中所包含的值的类型。如果使用几个分别包含不同类型值的链表,为每种类型编写一个比较函数就允许单个查找函数作用于所有类型的链表。

        程序段01 是类型无关的查找函数的一种实现方法。 注意函数的第 3 个参数是一个函数指针。这个参数用一个完整的原型进行声明。同时注意虽然函数绝不会修改参数 node 所指向的任何节点,但 node 并未被声明为 const 。如果 node 被声明为 const,函数将不得不返回一个const结果,这将限制调用程序,它便无法修改查找函数所找到的节点。

        /*
        **程序 01 ——类型无关的链表查找函数
        **在一个单链表中查找一个指定值的函数。它的参数是一个指向链表第 1 个节点的指针、一个指向我们需要    查找的值的指针和一个函数指针。
        **它所指向的函数用于比较存储于链表中的类型的值。
        */
        #include    <stdio.h>
        #include    "node.h"
     
        Node *
        search_list( Node *node,    void   const    *value,    int   (*compare)( void    const    *, void const *) )
        {
            while (node!=NULL){
                if(compare(&node->value,value)==0)
                    break;
            node=node->link;
            }
            return node;
        }

        指向值参数的指针和 &node->value 被传递给比较函数。后者是我们当前所检查的节点值。
     
        在一个特定的链表中进行查找时,用户需要编写一个适当的比较函数,并把指向该函数的指针和指向需要查找的值的指针传递给查找函数下面是一个比较函数,它用于在一个整数链表中进行查找。
        int
        compare_ints( void const *a, voidconst *b )
        {
            if( *(int *)a == *(int *)b )
                return 0;
            else
                return 1;
        }

        这个函数像下面这样使用:

        desired_node = search_list ( root,&desired_value, compare_ints );

        注意强制类型转换:比较函数的参数必须声明为 void * 以匹配查找函数的原型,然后它们再强制转换为 int * 类型,用于比较整型值。

        如果你希望在一个字符串链表中进行查找,下面的代码可以完成这项任务:

        #include    <string.h>
        ...
        desired_node = search_list( root,"desired_value", strcmp);

        碰巧,库函数 strcmp 所执行的比较和我们需要的完全一样,不过有些编译器会发出警告信息,因为它的参数被声明为 char * 而不是
void *。

        (二)转移表
        转换表最好用个例子来解释。下面的代码段取自一个程序,它用于实现一个袖珍式计算器。程序的其他部分已经读入两个数(op1和op2)和一个操作数(oper)。下面的代码对操作符进行测试,然后决定调用哪个函数。

        switch( oper ){
        case ADD:
                result = add( op1, op2);
                break;
        case SUB:
                result = sub( op1, op2);
                break;
        case MUL:
                result = mul( op1, op2);
                break;
        case DIV:
                result = div( op1, op2);
                break;
       
          ......

        对于一个新奇的具有上百个操作符的计算器,这条switch语句将非常长。

        为什么要调用函数来执行这些操作呢? 把具体操作和选择操作的代码分开是一种良好的设计方法,更为复杂的操作将肯定以独立的函数来实现,因为它们的长度可能很长。但即使是简单的操作也可能具有副作用,例如保存一个常量值用于以后的操作。

        为了使用 switch 语句,表示操作符的代码必须是整数。如果它们是从零开始连续的整数,我们可以使用转换表来实现相同的任务。转换表就是一个函数指针数组。

        创建一个转换表需要两个步骤。首先,声明并初始化一个函数指针数组。唯一需要留心之处就是确保这些函数的原型出现在这个数组的声明之前。

        double add (double,double);
        double sub (double,double);
        double mul (double,double);
        double div (double,double);
        ......
        double ( *oper_func[] )( double, double)={
            add,sub,mul,div,...
        };

        初始化列表中各个函数名的正确顺序取决于程序中用于表示每个操作符的整型代码。这个例子假定ADD是0 ,SUB是1,MUL是2,依次类推。

        第 2 个步骤是用下面这条语句替换前面整条 switch 语句!
        result = oper_func[ oper ](op1,op2 );
        oper从数组中选择正确的函数指针,而函数调用操作符执行这个函数。

 

 

 

 

 

回调函数

分类:编程成长经历2008-11-07 18:34109人阅读评论(0)收藏举报

回调函数

(一篇好文章与大家分享!看过之后有如醍醐灌顶般舒服!心中的诸多疑惑顿解!)


      
调用(calling)机制从汇编时代起已经大量使用:准备一段现成的代码,调用者可以随时跳转至此段代码的起始地址,执行完后再返回跳转时的后续地址。 CPU为此准备了现成的调用指令,调用时可以压栈保护现场,调用结束后从堆栈中弹出现场地址,以便自动返回。借堆栈保护现场真是一项绝妙的发明,它使调用者和被调者可以互不相识,于是才有了后来的函数和构件,使吾辈编程者如此轻松愉快。若评选对人类影响最大之发明,在火与车轮之后,笔者当推压栈调用。
      
话虽这样说,此调用机制并非完美。回调函数就是一例。函数之类本是为调用者准备的美餐,其烹制者应对食客了如指掌,但实情并非如此。例如,写一个快速排序函数供他人调用,其中必包含比较大小。麻烦来了:此时并不知要比较的是何类数据--整数、浮点数、字符串?于是只好为每类数据制作一个不同的排序函数。更通行的办法是在函数参数中列一个回调函数地址,并通知调用者:君需自己准备一个比较函数,其中包含两个指针类参数,函数要比较此二指针所指数据之大小,并由函数返回值说明比较结果。排序函数借此调用者提供的函数来比较大小,借指针传递参数,可以全然不管所比较的数据类型。被调用者回头调用调用者的函数(够咬嘴的),故称其为回调(callback)。
        
回调函数使程序结构乱了许多。Windows API函数集中有不少回调函数,尽管有详尽说明,仍使初学者一头雾水。恐怕这也是无奈之举。无论何种事物,能以树形结构单向描述毕竟让人舒服些。如果某家族中孙辈又是某祖辈的祖辈,恐怕无人能理清其中的头绪。但数据处理之复杂往往需要构成网状结构,非简单的客户/服务器关系能穷尽。
       Windows
系统还包含着另一种更为广泛的回调机制,即消息机制。消息本是 Windows的基本控制手段,乍看与函数调用无关,其实是一种变相的函数调用。发送消息的目的是通知收方运行一段预先准备好的代码,相当于调用一个函数。消息所附带的 WParam LParam相当于函数的参数,只不过比普通参数更通用一些。应用程序可以主动发送消息,更多情况下是坐等 Windows发送消息。一旦消息进入所属消息队列,便检感兴趣的那些,跳转去执行相应的消息处理代码。操作系统本是为应用程序服务,由应用程序来调用。而应用程序一旦启动,却要反过来等待操作系统的调用。这分明也是一种回调,或者说是一种广义回调。其实,应用程序之间也可以形成这种回调。假如进程 B收到进程 A发来的消息,启动了一段代码,其中又向进程 A发送消息,这就形成了回调。这种回调比较隐蔽,弄不好会搞成递归调用,若缺少终止条件,将会循环不已,直至把程序搞垮。若是故意编写成此递归调用,并设好终止条件,倒是很有意思。但这种程序结构太隐蔽,除非十分必要,还是不用为好。
      
利用消息也可以构成狭义回调。上面所举排序函数一例,可以把回调函数地址换成窗口handle。如此,当需要比较数据大小时,不是去调用回调函数,而是借 API函数 SendMessage向指定窗口发送消息。收到消息方负责比较数据大小,把比较结果通过消息本身的返回值传给消息发送方。所实现的功能与回调函数并无不同。当然,此例中改为消息纯属画蛇添脚,反倒把程序搞得很慢。但其他情况下并非总是如此,特别是需要异步调用时,发送消息是一种不错的选择。假如回调函数中包含文件处理之类的低速处理,调用方等不得,需要把同步调用改为异步调用,去启动一个单独的线程,然后马上执行后续代码,其余的事让线程慢慢去做。一个替代办法是借 API 函数PostMessage发送一个异步消息,然后立即执行后续代码。这要比自己搞个线程省事许多,而且更安全。
      
如今我们是活在一个 object时代。只要与编程有关,无论何事都离不开 object。但 object并未消除回调,反而把它发扬光大,弄得到处都是,只不过大都以事件(event)的身份出现,镶嵌在某个结构之中,显得更正统,更容易被人接受。应用程序要使用某个构件,总要先弄清构件的属性、方法和事件,然后给构件属性赋值,在适当的时候调用适当的构件方法,还要给事件编写处理例程,以备构件代码来调用。何谓事件?它不过是一个指向事件例程的地址,与回调函数地址没什么区别。
   
不过,此种回调方式比传统回调函数要高明许多。首先,它把让人不太舒服的回调函数变成一种自然而然的处理例程,使编程者顿觉气顺。再者,地址是一个危险的东西,用好了可使程序加速,用不好处处是陷阱,程序随时都会崩溃。现代编程方式总是想法把地址隐藏起来(隐藏比较彻底的如 VB Java),其代价是降低了程序效率。事件例程使编程者无需直接操作地址,但并不会使程序减速。更妙的是,此一改变,本是有损程序结构之奇技怪巧变成一种崭新设计理念,不仅免去被人抨击,而且逼得吾等凡人净手更衣,细细研读,仰慕至今。只是偶然静心思虑,发觉不过一瓶旧酒而已,故引得此番议论,让诸君见笑了。事件驱动程序设计是围绕着消息基础形成的,发生一个事件,伴随着一大堆的消息。
   
我理解回调机制window在执行某个API函数的过程中,调用指定的一个函数。我们可以模拟一下:
   
假设 ms提供一个函数叫做  EnumFont ,该函数是得到所有的字体,假设它的实现是
EnumFont()
{
  while ( (f =FindNextFont()) !=NULL)
  {
       printf("fontname: " +f.name);
  }
}
这样就循环显示出所有的字体名称。但是,开发者可能对字体信息另有用处,那么如何才能让开发者能使用这些信息呢,于是做改进:
EnumFont( void*  userFunc )
{
  while ( (f =FindNextFont()) !=NULL)
  {
       printf("fontname: " +f.name);
       if ( userFunc!=NULL)  userFunc( f) ;
  }
}
假设userFunc是一个函数 void f( FontObject font).这样使用者只需要定义一个函数:
  void myfunc( FontObject font)
{
                 listCtrl.Addstring (font.name);
  }
通过使用 EnumFont ( myfunc)就可以将所有额字体信息添加到一个列表框中。那么我们称 myfunc是一个回调函数,即让某个系统函数调用的函数。因此可以得出结论:
1
回调函数是由开发者按照一定的原型进行定义的函数
2
回调函数并不由开发者直接调用执行
3
回调函数通常作为参数传递给系统API,由该API来调用。
4
回调函数可能被系统API调用一次,也可能被循环调用多次。
比如函数int EnumFontFamilies(
                    HDC hdc,                    // handle to device control
                    LPCTSTRlpszFamily,  // pointer to family-namestring
                    FONTENUMPROClpEnumFontFamProc,  // pointer tocallback function
                    LPARAM lParam        // pointer to application-supplied data
                   );
其中的   FONTENUMPROC lpEnumFontFamProc就是一个回调函数,该函数遵照格式int CALLBACK EnumFontFamProc( ENUMLOGFONTFAR *lpelf,  NEWTEXTMETRIC FAR *lpntm,int FontType,  LPARAM lParam )进行定义。
   
回调函数主要用于一些比较费时的操作,或响应不知道何时将会发生的事件,回调函数提供了一种异步的机制,相对于同步执行,提高了效率.前者的例子如WriteFileEx,ReadFileEx,函数的最后一个参数是一个回调函数的指针,程序中调用WriteFileEx以后,就直接返回了,可以继续进行其他工作,系统在读写操作完成后通知程序作善后处理.后者的例子就是windows的事件机制回调函数的另一个用途,是用于一些枚举函数,EnumDisplayModes,每找到一种支持的显示模式,就通知回调函数,由回调函数具体处理,这是因为 EnumDisplayModes本身并不知道用户要如何处理.,用户提供回调函数,定制系统的功能,这样,不同的用户提供不同的回调函数,可以使系统具有不同的功能.这就是所谓的plugin.使用回调函数实际上就是在调用某个函数(通常是API函数)时,将自己的一个函数(这个函数为回调函数)的地址作为参数传递给那个函数。而那个函数在需要的时候,利用传递的地址调用回调函数,这时你可以利用这个机会在回调函数中处理消息或完成一定的操作。至于如何定义回调函数,跟具体使用的API函数有关,一般在帮助中有说明回调函数的参数和返回值等。

C/C++中函数指针的含义(闭关修炼内功!哈哈!)

分类:编程成长经历2008-11-08 16:01125人阅读评论(0)收藏举报

函数存放在内存的代码区域内,它们同样有地址,我们如何能获得函数的地址呢?

  如果我们有一个int test(int a)的函数,那么,它的地址就是函数的名字,这一点如同数组一样,数组的名字就是数组的起始地址。

  定义一个指向函数的指针用如下的形式,以上面的test()为例:

int (*fp)(int a);//这里就定义了一个指向函数的指针

  函数指针不能绝对不能指向不同类型,或者是带不同形参的函数,在定义函数指针的时候我们很容易犯如下的错误。

int *fp(int a);//这里是错误的,因为按照结合性和优先级来看就是先和()结合,然后变成了一个返回整形指针的函数了,而不是函数指针,这一点尤其需要注意!

  下面我们来看一个具体的例子:

#include <iostream>
#include <string>
using namespacestd;
 
int test(inta);
 
void main(intargc,char* argv[])  
{
    cout<<test<<endl;//
显示函数地址
    int(*fp)(int a);
    fp=test;//
将函数test的地址赋给函数学指针fp
    cout<<fp(5)<<"|"<<(*fp)(10)<<endl;
//
上面的输出fp(5),这是标准c++的写法,(*fp)(10)这是兼容c语言的标准写法,两种同意,但注意区分,避免写的程序产生移植性问题!
    cin.get();
}
 
int test(int a)
{
    returna;
}

typedef定义可以简化函数指针的定义,在定义一个的时候感觉不出来,但定义多了就知道方便了,上面的代码改写成如下的形式:

#include <iostream>
#include <string>
using namespacestd;
 
int test(inta);
 
void main(intargc,char* argv[])  
{
    cout<<test<<endl;
    typedefint (*fp)(inta);//
注意,这里不是生命函数指针,而是定义一个函数指针的类型,这个类型是自己定义的,类型名为fp
    fp fpi;//
这里利用自己定义的类型名fp定义了一个fpi的函数指针!
    fpi=test;
    cout<<fpi(5)<<"|"<<(*fpi)(10)<<endl;
    cin.get();
}
 
int test(int a)
{
    returna;
}

 

函数指针同样是可以作为参数传递给函数的,下面我们看个例子,仔细阅读你将会发现它的用处,稍加推理可以很方便我们进行一些复杂的编程工作。

//-------------------该例以上一个例子作为基础稍加了修改-----------------------------
#include <iostream>  
#include <string>  
using namespacestd;  
  
int test(int);  
 
int test2(int(*ra)(int),int);
 
void main(intargc,char* argv[])    
{  
    cout<<test<<endl;
    typedefint (*fp)(int);  
    fp fpi;
    fpi=test;//fpi
赋予test函数的内存地址
 
    cout<<test2(fpi,1)<<endl;//
这里调用test2函数的时候,这里把fpi所存储的函数地址(test的函数地址)传递了给test2的第一个形参
    cin.get();
}  
  
int test(int a)
{  
    returna-1;
}
 
int test2(int(*ra)(int),intb)//
这里定义了一个名字为ra的函数指针
{
    intc=ra(10)+b;//
在调用之后,ra已经指向fpi所指向的函数地址即test函数
    returnc;
}

  利用函数指针,我们可以构成指针数组,更明确点的说法是构成指向函数的指针数组,这么说可能就容易理解的多了。

#include <iostream>  
#include <string>  
using namespacestd;
 
void t1(){cout<<"test1";}
void t2(){cout<<"test2";}
void t3(){cout<<"test3";}
void main(intargc,char* argv[])    
{
    void*a[]={t1,t2,t3};
    cout<<"
比较t1()的内存地址和数组a[0]所存储的地址是否一致"<<t1<<"|"<<a[0]<<endl;
 
    cout<<a[0]();//
错误!指针数组是不能利用数组下标操作调用函数的
 
    typedefvoid (*fp)();//
自定义一个函数指针类型
    fp b[]={t1,t2,t3}; //
利用自定义类型fpb[]定义趁一个指向函数的指针数组
    b[0]();//
现在利用指向函数的指针数组进行下标操作就可以进行函数的间接调用了;
    cin.get();
}

  仔细看上面的例子可能不用我多说大家也会知道是怎么一会事情了,最后我们做一个重点小结,只要记住这一点,对于理解利用函数指针构成数组进行函数间接调用就很容易了!

void* a[]={t1,t2,t3};
cout<<"
比较t1()的内存地址和数组a[0]所存储的地址是否一致"<<t1<<"|"<<a[0]<<endl;

cout<<a[0]();//
错误!指针数组是不能利用数组下标操作调用函数的

  上面的这一小段中的错误行,为什么不能这么调用呢?

  前一篇教程我们已经说的很清楚了,不过在这里我们还是复习一下概念,指针数组元素所保存的只是一个内存地址,既然只是个内存地址就不可能进行a[0]()这样地址带括号的操作,而函数指针不同它是一个例外,函数指针只所以这么叫它就是因为它是指向函数指向内存的代码区的指针,它被系统授予允许与()括号操作的权利,进行间接的函数调用,既然函数指针允许这么操作,那么被定义成函数指针的数组就一定是可以一样的操作的。

 

原创粉丝点击