函数指针
函数指针是指向函数的指针。 因此“函数指针”本身首先是指针变量,只不过该指针变量指向函数。
声明
返回值类型 ( *指针变量名) (形参列表);
int (*pfunc)(int x);
int (*pfunc)(int); //无需将形参名给出
初始化
和普通指针一样,函数指针在使用之前必须初始化为指向某个函数。
int func(int x); //被指向的函数
pfunc = func; //初始化函数指针pfunc,使其指向函数func
pfunc = &func; //也是正确的
int (*pfunc)(int) = func; //也可以直接在声明时进行初始化
注意:在函数指针初始化之前需要有被指向函数func的原型,否则编译器就无法检查func的类型是否和pfunc所指向的类型一致。
使用:
int ans;
ans = func(5);
ans = pfunc(5);
ans = (*pfunc)(5); //也是正确的,但间接访问操作是不必要的
注意:在初始化赋值语句中的操作符 & 和调用函数指针时的 * 操作符都是可选的:
1. 函数名被使用时总是由编译器把它转换为函数指针。& 只是显示地说明了编译器将隐式执行的任务。
2. *pfunc把函数指针pfunc转换为一个函数名,编译器在执行函数调用之前又会把它转换回去。因此这个转换是不必要的。
函数指针常见用途:回调函数和转移表(函数指针数组)
回调函数
把函数指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
回调函数的意义
百度百科:可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。简而言之,回调函数就是允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
例:在单链表中查找一个值:
//参数node是一个指向链表第一个节点的指针,参数value是需要查找的值
Node *search_list(Node *node, int const value)
{
while(node != NULL)
{
if(node->value == value) break;
node = node->link;
}
return node;
}
上述函数只适用于值为int型的链表。如果需要在一个字符串链表中查找,又需要另外编写一个函数。
我们想要实现的是使查找函数与类型无关,事实上在查找函数的内部,只有在进行比较操作时才与类型有关,我们可以将这一部分分离出来,这可以使用函数指针实现,通过如下两个步骤:
1. 另编写一个用于比较的函数,把一个指向该函数的指针作为参数传递给查找函数,查找函数通过调用这个函数来执行比较。
2. 向用于比较的函数传递一个指针以便可以灵活地选择比较的类型,由于void指针可以被强制转换为任何类型的指针,因此可以用来作为形参。
声明函数指针:
int(*compare)(void const *, void const *);
函数指针作为查找函数的形参,并在查找函数内部被调用:
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;
}
整型比较函数或字符串比较函数都可以被查找函数调用:
int compare_ints(void const *a, void const *b)
{
if( *(int *a) == *(int *)b ) //void指针强制转换为int指针,并对指针取值进行比较
return 0;
else
return 1;
}
int strcmp(char const *s1, char const *s2); //库函数strcmp正好符合我们的功能需求
//调用
desired_node = search_list(root, &desired_value, compare_ints);
desired_node = search_list(root, "desired_str", strcmp);
注意:库函数strcmp的参数被声明为char*而不是void*,有些编译器可能会发出警告信息。
转移表(函数指针数组)
函数指针数组是一个其元素是函数指针的数组。
//函数指针数组声明
void (*func[10])(); //数组在声明时未初始化,需要给定数组的长度
//声明+初始化
int a(), b(), c(), d();
int (*state[])() = {a, b, c, d}; //数组声明的同时进行初始化,数组大小可以省略
//通过数组下标索引
result = func[index]();
result = (*func[index])(); //也是正确的,但间接访问操作是不必要的
把具体的操作和选择操作的代码分开是一种良好的设计方案。
例:取自一个实现袖珍式计算器的一段代码,用switch语句实现将会使代码非常长:
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;
...
{
使用函数指针数组代替:
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 (*oper_func[10])(double, double);
oper_func[0] = add;
oper_func[1] = sub;
...
//通过数组下标index找到正确的函数指针,调用对应的函数
result = oper_func[index](op1, op2);
注意:1. 在函数指针数组初始化之前,确保有这些函数的原型。2. 转移表的越界下标引用同普通数组一样是不合法的,但C编译器通常不检查下标(下标检查会带来不小的开销),因此最好一开始就保证转移表所使用的下标位于合法的范围内。例如可以在读取oper(值为ADD,SUB等)时,确定其值是有效的,以便能够正确地向数组下标(从0开始的连续整数)映射。