在C语言中,函数指针可以实现以下功能:
还有其他一些功能,这里不再赘述。
插件
如果你使用过任何图像编辑器、音频编辑器、浏览器等应用程序,你可能会使用某种插件。也就是说,一些第三方库提供了一小段代码,它并不是原始应用程序的一部分,但允许你在不升级或重建应用程序的情况下添加新功能。这是通过将代码打包成共享或动态链接库(Windows上的.dll文件,Linux上的.so文件)来完成的。程序可以在运行时加载该文件的内容,然后执行该库中包含的函数。
一个真实世界的例子需要更多的空间和时间来解释,但这里有一个玩具程序和库,用于说明这个概念:
#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 )
{
void *lib1handle = dlopen( "lib1.so", RTLD_LAZY | RTLD_LOCAL );
char **(*libGetNames)( void ) = dlsym( lib1handle, "getNames" );
if ( libGetNames )
{
char **names = libGetNames();
while ( names && *names)
{
printf( "calling %s\n", *names );
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
函数指针将依次指向
func1
和
func2
。运行时,它会产生以下输出:
[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#这样的语言提供了模板功能,使您可以编写通用代码,同时仍然保持类型安全(即,如果使用错误的类型,编译器仍然能够警告您)。
函数指针还有其他用途,但我已经在这个答案上花费了太多的时间和精力。
std::function
。 - GManNickGfunc(10, pf(3));
不会编译通过,因为func()
的第二个参数是一个函数指针,而pf(3)
不是。你可以调用func(10, pf);
(但不能在当前的函数签名下传递3
)。 - mahpointme(3)
一样,你也可以调用pf(3)
(在将pf
指向该函数后)。函数指针与任何函数名称一样使用——实际上,函数名称本质上就是函数指针。对于调用函数的语法,你熟悉的那种方式同样适用于使用函数指针。你甚至可以跳过本地函数指针变量,例如调用func(10, pointme);
。 - mah