函数指针 - 为什么,什么时候我可以没有



免责声明:我已经阅读了无数关于这个主题的其他文章,但我仍然没有得到它们。示例:为什么要这样做:

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);
}

???我真的不明白。任何帮助将不胜感激。谢谢!!!

当我可以简单地做到这一点时 [...]

没错,如果可以的话,你应该直接调用该函数。但是,在某些情况下,您无法进行直接调用,因为您尝试调用的函数在代码中不存在。当然,该函数将存在于完成的程序中,但在许多情况下,您将开发一个与其他人的代码交互的库,并且需要自行编译。

此外,在某些情况下,您可以直接调用该函数,但您不想这样做以避免代码重复。

这时函数指针就派上用场了:调用方可以告诉你函数要调用他的哪些函数。

考虑设计一个允许用户并行运行其函数的线程库。此库无法直接引用用户代码,原因有两个:

  • 您的代码不知道用户将同时运行哪些函数,并且
  • 您不希望为用户可能决定传递给库的每种函数编写单独的函数。

在 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 func1n" ); }
void func2( void )      { printf( "called func2n" ); }

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 %sn", *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文件不是 build 命令的一部分;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添加新函数,而无需接触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不需要知道其元素的类型,它只需要知道一个元素是否"小于"或"等于"另一个元素。 它调用cmpIntcmpFloat函数来执行实际比较;其他逻辑都不需要类型信息。 我不必为每种不同类型的排序算法复制胆量(sort_intsort_floatsort_foo);我只需要提供正确的比较功能即可qsort.

同样,display函数所做的只是打印出一个逗号分隔的字符串列表,用 {} 括起来。 它让fmtIntfmtFloat担心整数和浮点数如何格式化的细节。 我不必复制不同类型的任何显示逻辑。

到目前为止,您可能已经注意到我一直在吓人的引号中加上"通用"。 问题是你必须将所有内容的地址作为void *传递,这意味着你把类型安全扔出了窗外。 编译器无法保护我避免为给定数组传递错误的比较或格式化函数;我只会得到乱码输出(或运行时错误)。像C++、Java和C#这样的语言提供了模板化功能,允许你编写泛型代码,但仍然保持类型安全(即,如果你使用错误的类型,编译器仍然可以对你大喊大叫)。

函数指针还有其他用途,但我已经在这个答案上花费了太多的时间和精力,而不是我应该花的。

有关一个简单的示例,请查看函数 qsort。

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

第 4 个参数是函数指针。

它是一个可以对任何类型的数据进行排序的函数,前提是程序员()提供了一个更简单的函数,该函数只需比较两个项目并指示哪个更大。

函数qsort显然不知道您的数据类型。 而且,您无需了解快速高效地对数据进行排序的复杂性。 但只要你提供一个函数来进行比较,并且标准库提供了一个排序例程,两者就可以协同工作,完成一个强大的任务。

相关内容

最新更新