c和c++中的函数指针

函数指针是一个存着某个函数地址的变量。这个函数之后可以通用这个变量来调用。为什么需要函数指针呢?这边举个例子说明下。在编写程序时,我们经常把一些需要经常使用的特定功能的代码封装成函数。这样在每次使用的时候只需要调用函数即可,无需每次都写大段重复的代码。但是,如果我们需要在本质上一样的代码段使得每次执行的行为不一样,这个时候,我们就需要函数指针的帮助了。我们来看具体的例子。

使用函数指针的例子

函数作为其他函数的参数

如果你在写一个排序程序,你可能希望函数调用时可以选择排序顺序。一些程序员喜欢用升序排序,有一些倾向于选择降序,而其他程序员可能希望选择一种类似的,但不是这两种其一的排序方式。让你的用户在调用函数时可以选择要做什么的一种方法就是提供一个标志位作为函数的参数,但是这种方法缺乏灵活度。如果是在排序这个例子中,这种方法只能提供一个固定的排序种类集合,用户只能在该集合中选取某种比较方式。

另外一种更加灵活的方法就是允许用户通过给排序函数传入一个函数作为参数,自己决定比较数据的方法。这个函数用于比较两个输入的数据。我们稍后看一下语法。

回调函数

函数指针另外一个使用方式是用于设置在某个特定事件下被调用的”监听“函数或者”回调“函数。

为什么要写回调函数?在使用别人库的时候,我们经常见到回调函数。当我们编写图形用户界面(GUI)的时候就是一个例子。大部分时间,用户会与一个允许鼠标指针移动并重新绘制界面的循环进行交互。然而,有些时候用户会点击一个按钮或者在某个区域输入文字。这些操作称为事件,是需要你的程序去响应的。那我们如何知道发生了什么?答案是使用回调函数。当用户点击按钮时会调用我们编写的用于处理该事件的函数。

为了理解何时我们需要使用回调函数,让我们来看一下我们用一个拥有创建按钮函数(create_button)的GUI库创建一个按钮时发生了什么。我们需要指定按键需要在屏幕上展示的位置,按钮上显示的文本内容,还有一个在该按钮被点击时调用的函数。假设c(c++)中有一个原生的函数指针类型是 function 的话,那创建按钮的函数应该像下面一样:

void create_button(int x, int y, const char *text, function callback_func);

只要按钮被点击,callback_func 就会被调用。具体调用的 callback_func 是什么取决于 button。这就是 create_button 函数将函数指针作为参数的好处。

函数指针语法

在上面我们已经介绍过了函数指针的使用场景。接下来我们来介绍使用函数指针的相关语法。

申明一个函数指针的语法乍一看有点混乱,不过一旦我们理解了,事情就很简单了。让我们看一个简单的例子:

void (*foo)(int);

在这个例子中,foo是一个指向函数的指针。这个函数接受一个整型参数,没有返回值(void)。就好像我们声明了一个名为 “*foo” 的函数,它接受一个 int 并返回 void。现在,如果 *foo 是一个函数,那么 foo 必须是一个指向函数的指针。(类似地,像 int* x这样的声明可以读作 *x 是int,因此 x 必须是指向 int 的指针。)所以这个和之前接触到的指向变量的指针逻辑是一致的。申明一个函数指针的关键点就是我们只需要把原本放置函数名的地方换成 (*func_pointer_name)。

分析函数指针申明

有些人在函数申明中的 * 变多时感到困惑:

void *(*foo)(int *);

分析申明时的关键是从内向外读取;注意表达式的最里面的元素是*foo。foo 引用一个返回 void 并接受 int* 的函数。因此,foo就是指向这样一个函数的指针。

初始化函数指针

要初始化函数指针,必须给它程序中的函数地址。语法与任何其他变量类似:

#include 
void my_int_func(int x)
{
    printf( "%d\n", x );
}
 
int main()
{
    void (*foo)(int);
    /* the ampersand is actually optional */
    foo = &my_int_func;
 
    return 0;
}

使用函数指针

要调用由函数指针指向的函数,可以将函数指针视为要调用的函数的名称。调用它的行为执行解引用。解引用不需要自己执行:

#include 
void my_int_func(int x)
{
    printf( "%d\n", x );
}
 
 
int main()
{
    void (*foo)(int);
    foo = &my_int_func;
 
    /* 可以直接用指针名调用指向的函数,无需要解引用(即用(*foo))。call my_int_func (note that you do not need to write (*foo)(2) ) */
    foo( 2 );
    /* 当然,手动解引用仍然可行。but if you want to, you may */
    (*foo)( 2 );
 
    return 0;
}

请注意,函数指针语法是灵活的。它可以看起来像指针的大多数其他用法,使用&和*,也可以省略语法的这一部分。这类似于数组的处理方式,即裸数组衰减为指针,但也可以在数组前面加上&以请求其地址。

函数指针的应用

让我们回到排序示例中,我建议使用函数指针编写一个通用排序例程。在该例程中,调用排序函数的程序员可以指定确切的顺序。事实证明,C函数qsort就是这样做的。
在Linux手册页中,我们有以下qsort声明(来自stdlib.h):

void qsort(void *base, size_t nmemb, size_t size,
           int(*compar)(const void *, const void *));

需要注意的是 void* 的使用使得 qsort 可以操作任意的数据类型(在 c++ 中,我们一般使用模板来实现这个功能,当然也允许使用 void* 指针),因为 void* 指针可以指向任何数据。需要给定的参数有需要排序的array(base),元素个数(nmemb),元素的占用内存大小(size),比较函数指针(compar)。

在这里我们主要关心的是 compar 这个函数指针参数。它所指向的函数接受两个 void* 参数并返回一个 int。通过编写这个函数,用户可以在没有写特定的排序算法情况下就可以自己定义如何排序给定的 array(base)。需要注意的是,compar 返回一个整数 int。该函数在第一个参数小于第二个参数时返回-1,在两个参数相等时返回0,在第一个参数大于第二个参数时返回1。

举个例子,我们想要将一组数据进行升序排列,那么我们可以这样写:

#include 
 
int int_sorter( const void *first_arg, const void *second_arg )
{
    int first = *(int*)first_arg;
    int second = *(int*)second_arg;
    if ( first < second )
    {
        return -1;
    }
    else if ( first == second )
    {
        return 0;
    }
    else
    {
        return 1;
    }
}
 
int main()
{
    int array[10];
    int i;
    /* fill array */
    for ( i = 0; i < 10; ++i )
    {
        array[ i ] = 10 - i;
    }
    qsort( array, 10 , sizeof( int ), int_sorter );
    for ( i = 0; i < 10; ++i )
    {
        printf ( "%d\n" ,array[ i ] );
    }
 
}

使用多态和虚函数替代函数指针(c++)

通过使用虚拟函数,通常可以避免对显式函数指针的需求。例如,可以编写一个排序函数。该函数名接受一个名为compar的指针指向Sorter类。这个类里头有个虚函数 名为compare:

class Sorter
{
    public:
    virtual int compare (const void *first, const void *second);
};
 
// cpp_qsort, a qsort using C++ features like virtual functions
void cpp_qsort(void *base, size_t nmemb, size_t size, Sorter *compar);

在 cpp_qsortlimian 里面,每次需要比较的时候都会调用 compar->compare。对于重写(覆盖)这个虚函数的类来说,排序程序就将用这个新的比较特性来判断数据的大小。例如:

class AscendSorter : public Sorter
{
 
    virtual int compare (const void*, const void*)
    {
        int first = *(int*)first_arg;
        int second = *(int*)second_arg;
        if ( first < second )
        {
            return -1;
        }
        else if ( first == second )
        {
            return 0;
        }
        else
        {
            return 1;
        }
    }
};

然后,我们可以传入一个指向 AscendSorted 实例对象的指针到 cpp_qsort 中来进行升序排序。

是否真的没有用到函数指针

虚函数背后的实现用的就是函数指针。因此其实我们还是用了函数指针,只是编译器简化了工作。应用多态是一个合适的策略(例如在Java中),但是相比于只是简单的传入一个函数指针的方法,它带来了创建实例的额外开销。

函数指针总结

语法

申明

声明一个函数指针和申明一个函数差不多。只需要把原本的函数名(foo)替换成星号+函数(*foo)指针名。

void (*foo)(int);

初始化

我们可以直接通过函数名来得到函数的地址:

void foo();
func_pointer = foo;

或者通过给函数名加与号(&)的方式来获得:

void foo();
func_pointer = &foo;

调用

调用函数指针指向的函数可以直接用指针名来调用:

func_pointer( arg1, arg2 );

或者可以通过解引用的方式来调用函数:

(*func_pointer)( arg1, arg2 );

函数指针的优点

  • 函数指针提供了一种传递如何做某事的指令的方法
  • 您可以编写灵活的函数和库,允许程序员通过将函数指针作为参数传递来选择特定的行为
  • 这种灵活性也可以通过使用带有虚拟函数的类来实现

参考

Function Pointers in C and C++

你可能感兴趣的:(c和c++中的函数指针)