免责声明:我已经阅读了无数关于这个主题的其他文章,但我仍然没有得到它们。示例:为什么要这样做:
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
函数指针将依次指向func1
和func2
中的每一个。 运行时,它会生成以下输出:
[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
不需要知道其元素的类型,它只需要知道一个元素是否"小于"或"等于"另一个元素。 它调用cmpInt
和cmpFloat
函数来执行实际比较;其他逻辑都不需要类型信息。 我不必为每种不同类型的排序算法复制胆量(sort_int
、sort_float
、sort_foo
);我只需要提供正确的比较功能即可qsort
.
同样,display
函数所做的只是打印出一个逗号分隔的字符串列表,用 {
和 }
括起来。 它让fmtInt
和fmtFloat
担心整数和浮点数如何格式化的细节。 我不必复制不同类型的任何显示逻辑。
到目前为止,您可能已经注意到我一直在吓人的引号中加上"通用"。 问题是你必须将所有内容的地址作为void *
传递,这意味着你把类型安全扔出了窗外。 编译器无法保护我避免为给定数组传递错误的比较或格式化函数;我只会得到乱码输出(或运行时错误)。像C++、Java和C#这样的语言提供了模板化功能,允许你编写泛型代码,但仍然保持类型安全(即,如果你使用错误的类型,编译器仍然可以对你大喊大叫)。
函数指针还有其他用途,但我已经在这个答案上花费了太多的时间和精力,而不是我应该花的。
有关一个简单的示例,请查看函数 qsort。
void qsort (void* base, size_t num, size_t size, int (*compar)(const void*,const void*));
第 4 个参数是函数指针。
它是一个可以对任何类型的数据进行排序的函数,前提是程序员(您)提供了一个更简单的函数,该函数只需比较两个项目并指示哪个更大。
函数qsort
显然不知道您的数据类型。 而且,您无需了解快速高效地对数据进行排序的复杂性。 但只要你提供一个函数来进行比较,并且标准库提供了一个排序例程,两者就可以协同工作,完成一个强大的任务。