一:起因
(1)接着上一篇博客 大话 函数指针 和 指针函数
(2)对指针的应用是C语言编程的精髓所在,而回调函数就是C语言里面对函数指针的高级应用
(3)回调函数可以实现代码具体实现 和 功能描述的分开,可以实现代码的重用性,特别是代码的可扩展性
(4)要想实现函数与类型无关,就可以借助回调函数就可以达到这个目的,当然也可以通过泛型来实现相应的方法。
(5)使得函数可以作为参数来调用,非常灵活多变,能够代替复杂的swich- case 语句,特别是和enum(枚举进行结合时),更是如虎添翼
二:回调函数详解
(1)简而言之,回调函数是一个通过函数指针调用的函数。如果你把函数指针(函数的入口地址)传递给另一个函数,当这个函数指针被用来调用它所指向的函数时,我们就说这个函数是回调函数。
(2)为什么要使用回调函数呢?我们先看一个小例子:
Node * Search_List (Node * node, const int value)
{
while (node != NULL)
{
if (node -> value == value)
{
break;
}
node = node -> next;
}
return node;
}
解释:这个函数用于在一个单向链表中查找一个指定的值,返回保存这个值的节点。它的参数是指向这个链表第一个节点的指针以及要查找的值。但是考虑一个问题:它只能适用于值为整数的链表,如果查找一个字符串链表,我们不得不再写一个函数,其实大部分代码和现在这个函数相同,只是第二个参数的类型和比较的方法不同。
解决方法: 专门编写一个函数(回调函数),用于比较两个同类型的值(专门写一个比较函数即可解决问题),然后把一个指向这个函数的指针作为参数传递给查找函数,查找函数调用这个比较函数来执行比较,采用这个方法,任何类型的值得都可以进行比较。 但是还必须给查找函数传递一个指向待比较的值的指针而不是值本身,也就是一个void *类型的形参,这个指针会传递给回调函数,进行最终的比较。这样的修改可以让我们传递指向任何类型的指针到查找函数,从而完成对任何类型的比较,这就是指针的好处, 我们无法将字符串、数组或者结构体作为参数传递给函数,但是指向它们的指针却可以。(3)更改后的代码
NODE *Search_List(NODE *node, int (*compare)(void const *, void const *) ,
void const *desired_value);
{
while (node != NULL)
{
if (compare((node->value_address), desired_value) == 0)
{
break;
}
node = node->next;
}
return node;
}
//注意这里我们的链表节点是这样定义的:
typedef struct list
{
void *value_address;
struct list *next;
}NODE;
解释:这样定义可以让NODE *类型的指针指向存储任何类型数据的链表节点。而value_address就是指向具体数据的指针,我们把它定义为void *,表示一个指向未知类型的指针,这样链表就可以存储任何类型的数据了,而我们传递给查找函数Search_List的第一个参数就可以统一表示为:NODE *,否则,还是要分别写查找函数以适应存储不同数据类型的链表。
现在,查找函数与类型无关,因为它不进行实际的比较,因此,我们必须编写针对不同类型的比较函数(仅仅编写不同的比较函数即可),这是很容易实现的,因为调用者知道链表中所包含的值的类型,如果创建几个分别包含不同类型值的链表,为每种类型编写一个比较函数就允许单个查找函数作用于所有类型的链表。
(4)比较函数的编写
int int_compare(void const *a, void const *b)
{
if (*(int *)a == *(int *)b)
{
return 0;
}
else
{
return -1;
}
}
这个函数可以这样被使用:
desired_node = Search_List(root, int_compare, &desired_int_value);
如果你希望在一个字符串链表中进行查找,下面的代码就可以完成任务:(当然你也可以自己编写一个str_copare函数来代替string.h中的strcmp)
desired_node = Search_List(root, strcmp, “abcdefg”);
正好库函数strcmp所执行的比较和我们需要的一样,不过gcc会发出警告信息:因为strcmp的参数被声明为const char *而不是void const *。<strong><span style="font-size:14px;">
</span></strong>
三:回调函数实例
(1)例如 实现一个袖珍式计算器。程序的其他部分已经读入两个数(op1和op2)和一个操作数(oper)。
(2)下面的代码是我们通常(之前)对操作符进行测试,然后决定调用哪个函数:
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语句将非常长。
为什么要调用函数来执行这些操作呢?
把具体操作和选择操作的代码分开是一种良好的设计方法,
更为复杂的操作将肯定以独立的函数来实现,因为它们的长度可能很长
(3)现在的方法:
为了使用 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从数组中选择正确的函数指针,而函数调用操作符执行这个函数。
(4)具体代码如下:
#include <iostream>
using namespace std;
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};
double calculate(double, double, double (*func)(double,double));
int main(){
double op1,op2;
cin >> op1 >> op2;
int index = 0;// index 可以根据需要进行更改
//double result = oper_func[index](op1,op2);//
double result = calculate(op1,op2,oper_func[index]);
cout << "result= " << result << endl;
return (0);
}
double calculate(double op1, double op2, double (*func)(double,double))
{
return func(op1,op2);
}
double add(double op1, double op2)
{
return op1 + op2;
}
double sub(double op1, double op2)
{
return op1 - op2;
}
double mul(double op1, double op2)
{
return op1 * op2;
}
double div(double op1, double op2)
{
if(op2 != 0)
return op1 / op2;
return -1;// 失败了
}<strong>
</strong>
当然还可以通过枚举实现更加优化的方法,
详见blog