函数指针 - 为什么需要它,我什么时候可以不用?

4
免责声明:我已经阅读了无数关于这个主题的文章,但仍然不理解它们。例如:为什么要这样做:
void func(int a, void (*callback)(int))
{
    /* do something with a and callback */
    callback(3);
}

void pointme(int b)
{
    /* do something with b */
}

int main()
{
    void (*pf)(int);
    pf = &pointme;
    func(10, pf);
}

当我可以简单地这样做:
void func(int a)
{
    pointme(3);
    /* do something with a*/
}

void pointme(int b)
{
    /* do something with b */
}

int main()
{
    func(10);
}

我真的不理解。任何帮助都将不胜感激。谢谢!!!

1
没有意义,特别是因为你的第一个版本无法编译。 - Amit
4
使用函数指针的一个原因是当您不知道在编译时会调用哪个函数时。例如,想象一下通用排序函数-它知道执行排序的算法,但不了解数据,也不知道如何比较数据以确定正确的顺序。可以提供特定于数据的函数指针,以使通用排序函数作为不透明库使用。 - mah
2
选择C或C++,它们并不相同。例如,在C++中,您应该使用std::function - GManNickG
@Otringal func(10, pf(3)); 不会编译通过,因为func()的第二个参数是一个函数指针,而pf(3)不是。你可以调用func(10, pf);(但不能在当前的函数签名下传递3)。 - mah
1
@Otringal 就像你可以调用 pointme(3) 一样,你也可以调用 pf(3)(在将 pf 指向该函数后)。函数指针与任何函数名称一样使用——实际上,函数名称本质上就是函数指针。对于调用函数的语法,你熟悉的那种方式同样适用于使用函数指针。你甚至可以跳过本地函数指针变量,例如调用 func(10, pointme); - mah
显示剩余11条评论
3个回答

6
当您可以直接调用函数时,您应该直接调用它。但是,在某些情况下,您无法进行直接调用,因为您要调用的函数在您的代码中不存在。当然,在完成的程序中会有这个函数,但在许多情况下,您将开发一个与其他人的代码交互的库,并且需要单独编译。
此外,有时您可以直接调用函数,但不想这样做以避免代码重复。
这就是函数指针派上用场的地方:调用者可以告诉您的函数调用他的哪个函数。
考虑设计一个线程库,让用户并行运行其函数。该库不能直接引用用户代码,原因如下:
- 您的代码不知道用户将同时运行哪些函数 - 您不想为用户可能决定传递给您的库的每种函数编写单独的函数。

如果我的代码中不存在它,那么使用函数指针也会出现同样的问题吗?因为它指向的是不在我的代码中的函数,所以我仍然无法编译我的独立项目。此外,函数指针如何消除重复?因为我调用函数的常规方式只需要一个函数调用,访问函数指针也是一样的。我的意思是,我仍然需要“做它”。 - Otringal
@Otringal 函数指针仍然是一个指针。它没有比其他指针更神奇的功能。就像其他指针可以传递给库一样,函数指针也可以传递给库。 - PaulMcKenzie
阅读您的最新编辑后,我终于找到了一个满足我的思考方式的答案(所有其他答案对于我的知识水平来说仍然不是很相关)。总之:即使我可以在另一个函数的主体中硬编码一个函数调用,但如果我想要传递另一个函数,则无法使用虚拟/通配符函数占位符。就像函数指针允许我不用重载“母”函数的所有可能的“被调用”函数组合一样,从而创建一个通用函数指针,以后可以更改。 - Otringal
@Otringal 被调用的函数可能不存在于您的代码中,但它需要存在于调用您的函数的人的代码中。只要正确声明函数指针,您的代码就可以编译。 - Sergey Kalinichenko
2
@Otringal "所有其他答案对于我这个知识贫乏的人来说仍然不是很相关)" -- 因为你可能认为代码都是你自己的,而不是“库/用户”模型。库编写者不知道用户函数的名称,但库确实有能力调用此用户函数。库如何完成这一壮举,即在没有名称的情况下调用函数?用户传递指向该函数的指针,将此指针存储在某个地方,并在适当的时候调用该函数。 - PaulMcKenzie
显示剩余6条评论

2
在C语言中,函数指针可以实现以下功能:
  • 创建插件架构;
  • 创建“通用”函数和数据结构。
还有其他一些功能,这里不再赘述。 插件 如果你使用过任何图像编辑器、音频编辑器、浏览器等应用程序,你可能会使用某种插件。也就是说,一些第三方库提供了一小段代码,它并不是原始应用程序的一部分,但允许你在不升级或重建应用程序的情况下添加新功能。这是通过将代码打包成共享或动态链接库(Windows上的.dll文件,Linux上的.so文件)来完成的。程序可以在运行时加载该文件的内容,然后执行该库中包含的函数。
一个真实世界的例子需要更多的空间和时间来解释,但这里有一个玩具程序和库,用于说明这个概念:
/**
 * lib1.c
 *
 * Provides functions that are called by another program
 */
#include <stdio.h>

static char *names[] = {"func1", "func2", NULL};

char **getNames( void ) { return names; }
void func1( void )      { printf( "called func1\n" ); }
void func2( void )      { printf( "called func2\n" ); }

getNames函数基本上提供了一个库中可供调用的函数清单(有许多更好的方法来实现这个功能,但你应该知道我的意思)。

要将其构建为共享库,我按照以下步骤进行:

gcc -o lib1.so -std=c99 -pedantic -Wall -Werror -fPIC -shared lib1.c

这将创建共享库文件 lib1.so

现在我添加一个简单的驱动程序:

#include <stdio.h>
#include <dlfcn.h>

int main( void )
{
  /**
   * Open the shared library file
   */
  void *lib1handle = dlopen( "lib1.so", RTLD_LAZY | RTLD_LOCAL );

  /**
   * Load the "getNames" function into the current process space
   */
  char **(*libGetNames)( void ) =  dlsym( lib1handle, "getNames" );
  if ( libGetNames )
  {
    /**
     * call the "getNames" function in the shared library
     */
    char **names = libGetNames();
    while ( names && *names)
    {
      printf( "calling %s\n", *names );
      /**
       * Load each named function into the current process space
       * and execute it
       */
      void (*func)(void) =  dlsym( lib1handle, *names++ );
      if ( func )
        func();
    }
  }
  dlclose( lib1handle );
  return 0;
}

我按照以下方式构建了这段代码:

gcc -o main -std=c99 -Wall -Werror main.c -ldl

请注意,lib1.so文件不是构建命令的一部分;直到运行时,main程序才会知道该库代码的存在。
您还需要将当前目录放入LD_LIBRARY_PATH变量中,否则dlopen将无法找到该库:
[fbgo448@n9dvap997]~/prototypes/dynlib: export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

这段代码的作用只是通过库中的getNames函数获取函数名称列表,然后依次加载并执行库中的每个函数。libGetNames函数指针将指向库中的getNames函数,而func函数指针将依次指向func1func2。运行时,它会产生以下输出:
[fbgo448@n9dvap997]~/prototypes/dynlib: ./main
calling func1
called func1
calling func2
called func2

很激动人心,对吧?但这就是像Photoshop和Audacity这样的应用程序如何让你扩展它们的功能而无需升级或重建。你只需要下载正确的库,放到正确的位置,应用程序将加载该库的内容并使该代码可用于你使用。
当然,你可以静态链接库与主函数(main)并直接调用函数。但共享库概念的美妙之处在于它允许你向主函数(main)添加新函数而无需触及主函数本身。
“通用”函数和数据结构
C语言中一个经典的“通用”函数是qsort函数。使用qsort,你可以对任何类型的数组进行排序;你只需提供实际比较数组元素的函数即可。再举个愚蠢的例子:
#include <stdio.h>
#include <stdlib.h>

int cmpInt( const void *lhs, const void *rhs )
{
  const int *l = lhs, *r = rhs;
  return *l - *r;
}

int cmpFloat( const void *lhs, const void *rhs )
{
  const float *l = lhs, *r = rhs;
  return *l - *r;
}

char *fmtInt( char *buffer, size_t bufsize, const void *value )
{
  const int *v = value;
  sprintf( buffer, "%*d", (int) bufsize, *v );
  return buffer;
}

char *fmtFloat( char *buffer, size_t bufsize, const void *value )
{
  const float  *v = value;
  sprintf( buffer, "%*.*f", (int) bufsize, 2, *v );
  return buffer;
}

void display( const void *data, size_t count, size_t size, char *(*fmt)(char *, size_t, const void *))
{
  const char *d = data;
  char buffer[10];
  printf( "{%s", fmt( buffer, sizeof buffer, &d[0] ));
  for ( size_t i = size; i < count * size; i += size )
    printf( ", %s", fmt( buffer, sizeof buffer, &d[i] ));
  printf( "}\n" );
}

int main( void )
{
  int   iarr[] = {9, 100, 53, 99, 4, 29, 44};
  float farr[] = {9, 100, 54, 99, 4, 29, 44};

  printf( "iarr before sort: " );
  display( iarr, sizeof iarr / sizeof *iarr, sizeof *iarr, fmtInt );
  qsort( iarr, sizeof iarr / sizeof *iarr, sizeof *iarr, cmpInt );
  printf (" iarr after sort: " );
  display( iarr, sizeof iarr / sizeof *iarr, sizeof *iarr, fmtInt );

  printf( "farr before sort: " );
  display( farr, sizeof farr / sizeof *farr, sizeof *farr, fmtFloat );
  qsort( farr, sizeof farr / sizeof *farr, sizeof *farr, cmpFloat );
  printf (" farr after sort: " );
  display( farr, sizeof farr / sizeof *farr, sizeof *farr, fmtFloat );

  return 0;
}

这段代码并不是很精彩 - 它定义了两个数组,一个 int 类型的数组和一个 float 类型的数组,然后展示它们、对它们进行排序,最后再次展示它们:

[fbgo448@n9dvap997]~/prototypes/dynlib: ./sorter
iarr before sort: {         9,        100,         53,         99,          4,         29,         44}
 iarr after sort: {         4,          9,         29,         44,         53,         99,        100}
farr before sort: {      9.00,     100.00,      54.00,      99.00,       4.00,      29.00,      44.00}
 farr after sort: {      4.00,       9.00,      29.00,      44.00,      54.00,      99.00,     100.00}

然而,我将类型信息与基本的排序和显示逻辑进行了“解耦”。qsort不需要知道其元素的类型,它只需要知道一个元素是否比另一个元素“小于”或“等于”。它调用cmpInt和cmpFloat函数执行实际的比较;其他任何逻辑都不需要类型信息。我不必为每种不同类型(sort_int、sort_float、sort_foo)复制排序算法的核心部分;我只需要为qsort提供正确的比较函数即可。
类似地,所有display函数要做的就是打印由逗号分隔的字符串列表,用大括号包围起来。它让fmtInt和fmtFloat来处理int和float的格式细节。我不必为不同类型复制任何显示逻辑。
到现在为止,你可能已经注意到我一直在用引号括住“通用”。问题在于,你必须将所有内容的地址作为void *传递,这意味着你正在放弃类型安全。编译器无法保护我免受为给定数组传递错误的比较或格式化函数的影响;我只会得到乱码输出(或运行时错误)。像C++、Java和C#这样的语言提供了模板功能,使您可以编写通用代码,同时仍然保持类型安全(即,如果使用错误的类型,编译器仍然能够警告您)。
函数指针还有其他用途,但我已经在这个答案上花费了太多的时间和精力。

哇,John Bode,真的非常感谢你!这是任何人在解释上能够深入到的程度,你真的很棒,花费时间写下这篇巨大的回答帖子。在你的例子中,我仍然缺乏一些语法理解,但这更好了,因为现在我可以逐步学习你的两个代码,并研究我不熟悉的每一个东西,直到我完全能够阅读和理解它们。这将是相当多的功课,但哦,天哪,它会得到回报!:D 非常感谢你! - Otringal

2

可以查看函数qsort的简单示例。

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

第四个参数是函数指针。
它是一个函数,可以对任何类型的数据数组进行排序,只要程序员()提供一个简单的函数,该函数仅比较两个项目并指示哪个更大。 qsort 函数显然不知道您的数据类型。而且您不需要知道快速高效地排序数据的复杂性。但只要您提供一个比较函数,标准库提供一个排序例程,这两者就可以共同完成强大的任务。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接