我最近在C语言中有一些函数指针的经验。

因此,按照回答自己问题的传统,我决定对最基本的问题做一个简短的总结,以供那些需要快速切入主题的人使用。


当前回答

函数指针的另一个好用法:在版本之间轻松切换

当您需要在不同的时间或不同的开发阶段使用不同的功能时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但该软件的最终版本将放在Avnet ZedBoard(它有显示器和控制台的端口,但最终版本不需要这些端口)上。所以在开发过程中,我将使用printf查看状态和错误消息,但当我完成后,我不希望打印任何内容。以下是我所做的:

版本.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

在版本c中,我将定义版本h中的2个函数原型

版本.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

注意函数指针在版本.h中如何原型化为void(*zprintf)(constchar*,…);当它在应用程序中被引用时,它将在其指向的任何位置开始执行,这一点尚未定义。在版本.c中,注意在board_init()函数中,zprintf被分配了一个唯一的函数(其函数签名匹配),这取决于版本中定义的版本

运行代码将如下所示:

主程序c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

如果处于调试模式,上述代码将使用printf,如果处于发布模式,则不执行任何操作。这比浏览整个项目并注释或删除代码要容易得多。我所需要做的就是在版本.h中更改版本,其余的都由代码完成!

其他回答

我最喜欢的函数指针用途之一是迭代器-

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

一旦有了基本的声明器,函数指针就很容易声明:

id:id:id是指针:*D:D指针指向函数:D(<parameters>):D函数获取<parameter>返回

而D是使用相同规则构建的另一个声明器。最后,在某个地方,它以ID结尾(参见下面的示例),这是声明实体的名称。让我们尝试构建一个函数,该函数获取指向不获取任何内容并返回int的函数的指针,并返回指向获取char并返回int函数的指针

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

如您所见,使用typedef构建它非常容易。如果没有typedef,上面的声明器规则也不难,而且应用一致。如您所见,我漏掉了指针指向的部分,以及函数返回的内容。这是出现在声明最左边的内容,并不重要:如果已经建立了声明人,则在末尾添加。让我们这样做吧。始终如一地构建它,首先使用[和]显示结构:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

如您所见,可以通过一个接一个地附加声明符来完全描述类型。施工有两种方式。一种是自下而上的,从非常正确的事情(树叶)开始,一直到标识符。另一种方式是自上而下的,从标识符开始,一直到叶子。我会双向展示。

自下而上

构造从右边的东西开始:返回的东西,它是取char的函数。为了保持声明人的不同,我将对他们进行编号:

D1(char);

直接插入了char参数,因为它很简单。通过用*D2替换D1来添加指向声明器的指针。注意,我们必须用括号括住*D2。通过查找*-运算符和函数调用运算符()的优先级可以知道这一点。如果没有括号,编译器会将其读为*(D2(char p))。当然,这不会再简单地用*D2取代D1。声明符周围始终允许使用括号。实际上,如果你添加了太多,你不会出错。

(*D2)(char);

返回类型已完成!现在,让我们用返回<parameters>的函数声明器函数来替换D2,这就是我们现在的D3(<parameter>)。

(*D3(<parameters>))(char)

注意,不需要括号,因为我们希望D3这次是函数声明器,而不是指针声明器。很好,唯一剩下的就是它的参数。该参数的处理方式与我们处理返回类型的处理方式完全相同,只是将char替换为void。所以我会复制它:

(*D3(   (*ID1)(void)))(char)

我已经用ID1替换了D2,因为我们已经完成了该参数(它已经是一个指向函数的指针,不需要另一个声明器)。ID1将是参数的名称。现在,我在上面的结尾告诉我们,一个添加了所有声明者修改的类型,即出现在每个声明的最左边的类型。对于函数,这将成为返回类型。对于指向类型等的指针……写下类型时很有趣,它将以相反的顺序出现在最右边:)无论如何,替换它会产生完整的声明。当然,这两次都是int。

int (*ID0(int (*ID1)(void)))(char)

在该示例中,我调用了函数ID0的标识符。

自上而下

这从类型描述中最左边的标识符开始,当我们从右边走过时,将该声明符包装起来。从函数获取<parameters>开始返回

ID0(<parameters>)

描述中的下一件事(在“返回”之后)是指针指向。让我们合并它:

*ID0(<parameters>)

然后,下一件事是functon获取<parameters>返回。该参数是一个简单的字符,因此我们再次将其放入,因为它非常简单。

(*ID0(<parameters>))(char)

注意我们添加的括号,因为我们再次希望先绑定*,然后绑定(char)。否则它将读取获取<parameters>返回函数的函数。。。。不,甚至不允许返回函数的函数。

现在我们只需要放置<parameters>。我将展示一个简短的嘲笑版本,因为我认为你现在已经知道该怎么做了。

pointer to: *ID1
... function taking void returning: (*ID1)(void)

就像我们使用自底向上方法一样,只需将int放在声明符之前,我们就完成了

int (*ID0(int (*ID1)(void)))(char)

好东西

自下而上还是自上而下更好?我习惯于自下而上,但有些人可能更喜欢自上而下。我认为这是品味问题。顺便说一句,如果您在该声明中应用所有运算符,您将得到一个int:

int v = (*ID0(some_function_pointer))(some_char);

这是C中声明的一个很好的属性:声明断言,如果在使用标识符的表达式中使用这些运算符,那么它将在最左边生成类型。阵列也是如此。

希望你喜欢这个小教程!现在,当人们对函数奇怪的声明语法感到疑惑时,我们可以联系到这一点。我尽量少放C内部构件。随意编辑/修复其中的内容。

“从头开始”功能具有“开始执行的内存地址”。在汇编语言中,它们被称为(调用“函数的内存地址”)。现在回到C。如果函数有内存地址,那么它们可以由C中的指针操作

1.首先需要声明一个指向函数的指针2.传递所需函数的地址

****注意->函数应为相同类型****

这个简单的计划将说明一切。

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

之后,让我们来看看机器是如何理解它们的。32位体系结构的上述程序的机器指令一瞥。

红色标记区域显示地址如何在eax中交换和存储。然后是eax上的调用指令。eax包含函数所需的地址。

函数指针的另一个好用法:在版本之间轻松切换

当您需要在不同的时间或不同的开发阶段使用不同的功能时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但该软件的最终版本将放在Avnet ZedBoard(它有显示器和控制台的端口,但最终版本不需要这些端口)上。所以在开发过程中,我将使用printf查看状态和错误消息,但当我完成后,我不希望打印任何内容。以下是我所做的:

版本.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

在版本c中,我将定义版本h中的2个函数原型

版本.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

注意函数指针在版本.h中如何原型化为void(*zprintf)(constchar*,…);当它在应用程序中被引用时,它将在其指向的任何位置开始执行,这一点尚未定义。在版本.c中,注意在board_init()函数中,zprintf被分配了一个唯一的函数(其函数签名匹配),这取决于版本中定义的版本

运行代码将如下所示:

主程序c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

如果处于调试模式,上述代码将使用printf,如果处于发布模式,则不执行任何操作。这比浏览整个项目并注释或删除代码要容易得多。我所需要做的就是在版本.h中更改版本,其余的都由代码完成!

函数指针是包含函数地址的变量。由于它是一个指针变量,虽然有一些限制性的财产,您可以像使用数据结构中的任何其他指针变量一样使用它。

我能想到的唯一例外是将函数指针视为指向某个值以外的其他值。通过递增或递减函数指针或向函数指针添加/减去偏移量来执行指针运算实际上没有任何用处,因为函数指针只指向一个对象,即函数的入口点。

函数指针变量的大小、该变量所占用的字节数可能会根据基础架构(例如x32或x64或其他)而变化。

函数指针变量的声明需要指定与函数声明相同类型的信息,以便C编译器执行通常执行的检查。如果没有在函数指针的声明/定义中指定参数列表,C编译器将无法检查参数的使用。在某些情况下,这种缺乏检查可能是有用的,但请记住,安全网已被移除。

一些示例:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

前两个声明在以下方面有些相似:

func是一个接受int和char*并返回int的函数pFunc是一个函数指针,它被分配了一个函数的地址,该函数接受一个int和一个char*并返回一个int

因此,从上面我们可以得到一个源行,其中函数func()的地址被分配给函数指针变量pFunc,如pFunc=func;中所示;。

注意与函数指针声明/定义一起使用的语法,其中括号用于克服自然运算符优先规则。

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

几个不同的用法示例

函数指针用法的一些示例:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

可以在函数指针的定义中使用可变长度参数列表。

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

或者根本不能指定参数列表。这可能很有用,但它消除了C编译器对提供的参数列表执行检查的机会。

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

C型脚轮

可以将C样式的强制转换与函数指针一起使用。但是要注意,C编译器可能对检查不严格,或者提供警告而不是错误。

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

将函数指针与相等值进行比较

您可以使用if语句检查函数指针是否等于特定的函数地址,尽管我不确定这有多有用。其他比较运算符的实用性似乎更低。

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

函数指针数组

如果你想有一个函数指针数组,其中每个元素的参数列表都有不同,那么你可以定义一个未指定参数列表的函数指针(不是void,这意味着没有参数,只是未指定),就像下面这样,尽管你可能会看到来自C编译器的警告。这也适用于指向函数的函数指针参数:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

使用带有函数指针的全局结构的C样式命名空间

您可以使用static关键字指定一个名称为file scope的函数,然后将其分配给一个全局变量,以提供类似于C++的命名空间功能的功能。

在头文件中定义一个结构,该结构将是我们的命名空间以及使用它的全局变量。

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

然后在C源文件中:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

然后,通过指定全局结构变量的完整名称和访问函数的成员名称,可以使用该函数。常量修饰符用于全局,因此不会意外更改。

int abcd = FuncThingsGlobal.func1 (a, b);

功能指针的应用领域

DLL库组件可以做一些类似于C风格的命名空间方法的事情,在这种方法中,特定的库接口是从支持创建包含函数指针的结构的库接口中的工厂方法请求的。。此库接口加载请求的DLL版本,创建一个具有必要函数指针的结构,然后将该结构返回给请求调用方使用。

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

这可用于:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

相同的方法可以用于为使用底层硬件的特定模型的代码定义抽象硬件层。函数指针由工厂用硬件特定函数填充,以提供实现抽象硬件模型中指定的功能的硬件特定功能。这可以用于提供由软件使用的抽象硬件层,该软件调用工厂函数以获得特定的硬件功能接口。

用于创建委托、处理程序和回调的函数指针

您可以使用函数指针来委派某些任务或功能。C中的经典示例是与标准C库函数qsort()和bsarch()一起使用的比较委托函数指针,用于提供排序顺序,以便对项目列表进行排序或对已排序的项目列表执行二进制搜索。比较函数委托指定排序或二进制搜索中使用的排序算法。

另一种用法类似于将算法应用于C++标准模板库容器。

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

另一个例子是GUI源代码,其中通过提供函数指针来注册特定事件的处理程序,该函数指针在事件发生时实际调用。Microsoft MFC框架及其消息映射使用类似的方法来处理传递到窗口或线程的Windows消息。

需要回调的异步函数类似于事件处理程序。异步函数的用户调用异步函数以启动某些操作,并提供一个函数指针,一旦操作完成,异步函数将调用该指针。在这种情况下,事件是完成其任务的异步函数。