C 中的函数指针如何工作?

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

因此,继续回答您自己的问题的传统,我决定为需要快速深入学习该主题的人做一些基本的总结。

答案

C 中的函数指针

让我们从一个基本功能开始,我们将指向

int addInt(int n, int m) {
    return n+m;
}

首先,让我们定义一个指向接收 2 个int并返回一个int的函数的指针:

int (*functionPtr)(int,int);

现在我们可以安全地指向我们的功能:

functionPtr = &addInt;

现在我们有了一个指向函数的指针,让我们使用它:

int sum = (*functionPtr)(2, 3); // sum == 5

将指针传递给另一个函数基本上是相同的:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

我们也可以在返回值中使用函数指针(尝试保持一致,它会变得凌乱):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

但是使用typedef会更好:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}

C 中的函数指针可用于在 C 中执行面向对象的编程。

例如,以下几行用 C 编写:

String s1 = newString();
s1->set(s1, "hello");

是的, ->并且缺少new运算符是一个死路,但是这肯定意味着我们正在将某些String类的文本设置为"hello"

通过使用函数指针, 可以在 C 中模拟方法

这是如何完成的?

String类实际上是带有一堆函数指针的struct ,这些函数指针用作模拟方法的一种方式。以下是String类的部分声明:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

可以看出, String类的方法实际上是指向已声明函数的函数指针。在准备String实例时,将调用newString函数以设置指向它们各自函数的函数指针:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

例如,通过调用get方法调用的getString函数定义如下:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

可以注意到的一件事是,没有对象实例的概念,并且没有方法实际上是对象的一部分,因此必须在每次调用时传递 “自身对象”。 (并且internal只是一个隐藏的struct ,它在前面的代码清单中被省略了,它是一种执行信息隐藏的方法,但是与函数指针无关)。

因此,而不是能够执行s1->set("hello"); ,必须传入该对象才能对s1->set(s1, "hello")执行操作。

有了这个次要的解释,您必须将自己的引用传递给其他人,我们将进入下一部分,即C 中的继承

假设我们要创建String的子类,例如ImmutableString 。为了使字符串不可变,将无法访问set方法,同时保持对getlength访问,并强制 “构造函数” 接受char*

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

基本上,对于所有子类,可用的方法再次是函数指针。这次,不存在set方法的声明,因此,不能在ImmutableString调用它。

至于ImmutableString的实现,唯一相关的代码是 “构造函数” 函数newImmutableString

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

在实例化ImmutableString ,指向getlength方法的函数指针实际上通过内部存储的String对象的base变量来引用String.getString.length方法。

使用函数指针可以实现从超类继承方法。

我们可以进一步在 C 中继续多态性

例如,如果由于某种原因我们想更改length方法的行为以使其始终在ImmutableString类中返回0 ,那么所有要做的就是:

  1. 添加一个将用作覆盖length方法的函数。
  2. 转到 “构造函数”,然后将函数指针设置为覆盖length方法。

ImmutableString添加重写length方法可以通过添加lengthOverrideMethod来执行:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

然后,将构造函数中length方法的功能指针连接到lengthOverrideMethod

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

现在, ImmutableString类中的length方法与String类具有相同的行为,现在, length方法将引用lengthOverrideMethod函数中定义的行为。

我必须添加一个免责声明,即我仍在学习如何使用 C 进行面向对象的编程风格进行编写,因此可能有些地方我并没有很好地解释,或者就如何最好地实现 OOP 而言可能已超出标准但是我的目的是试图说明函数指针的许多用法之一。

有关如何在 C 中执行面向对象编程的更多信息,请参考以下问题:

被解雇的指南:如何通过手动编译代码来滥用 x86 机器上 GCC 中的函数指针:

这些字符串文字是 32 位 x86 机器代码的字节。 0xC3x86 ret指令

通常,您不会手工编写这些代码,而是使用汇编语言编写代码,然后使用像nasm这样的汇编程序将其汇编为平面二进制文件,然后将其nasm进制转储为 C 字符串文字。

  1. 返回 EAX 寄存器上的当前值

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. 编写交换功能

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
  3. 写一个 for 循环计数器到 1000,每次调用一些函数

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. 您甚至可以编写一个递归函数,计数为 100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);

请注意,编译器将字符串文字放在.rodata节(或 Windows 中的.rdata )中,该节作为文本段的一部分链接(以及函数代码)。

文本段具有 Read + Exec 权限,因此可以将字符串文字转换为函数指针,而无需mprotect()VirtualProtect()系统调用,就像需要动态分配的内存一样。 (或者gcc -z execstack将程序与堆栈 + 数据段 + 堆可执行文件链接在一起,作为一种快速技巧。)


要反汇编这些文件,可以对其进行编译以在字节上放置标签,然后使用反汇编程序。

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

使用gcc -c -m32 foo.c编译,并使用objdump -D -rwC -Mintel反汇编,我们可以得到汇编,并发现该代码通过破坏 EBX(保留调用的寄存器)而违反了 ABI,通常效率低下。

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

该机器代码将(可能)在 Windows,Linux,OS X 等上以 32 位代码运行:所有这些 OS 上的默认调用约定都将 args 传递给堆栈,而不是更有效地在寄存器中传递。但是 EBX 在所有正常的呼叫约定中都被保留呼叫,因此将其用作暂存寄存器而不保存 / 恢复它很容易使呼叫者崩溃。

我最喜欢的函数指针用法之一是便宜又简单的迭代器 -

#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: IDID 是一个
  • 指针: *DD 指向的指针
  • 函数: D(<parameters>)D 函数采用<参数>返回

D 是使用相同规则构建的另一个声明符。最后,在某处以ID结尾(请参见下面的示例),该ID是已声明实体的名称。让我们尝试构建一个函数,该函数使用指向不带任何内容的函数的指针并返回 int,然后返回指向带 char 并返回 int 的函数的指针。使用 type-def 就是这样

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周围加上括号。通过查找*-operator和 function-call operator ()的优先级可以知道这一点。如果没有括号,编译器会将其读取为*(D2(char p)) 。但是,那当然不再是用*D2替换 D1 了。声明符周围总是允许有括号。因此,实际上,如果添加太多,就不会出错。

(*D2)(char);

返回类型完成!现在,让我们使用<parameters> returning的函数声明函数替换D2 ,它是我们现在的D3(<parameters>)

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

请注意,不需要括号,因为我们希望 D3这次是一个函数声明器,而不是指针声明器。太好了,剩下的就是它的参数了。参数的设置与返回类型完全相同,只是用char替换为void 。所以我将其复制:

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

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

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

在该示例中,我将其称为功能ID0的标识符。

自顶向下

它从类型描述的最左边的标识符开始,在我们通过右边的方式包装该声明符。从函数采用<参数>返回开始

ID0(<parameters>)

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

*ID0(<parameters>)

接下来的事情是functon 接受< parameters >返回 。该参数是一个简单的 char,因此我们马上将其重新放置,因为它确实很简单。

(*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 内部构件。随时编辑 / 修复其中的内容。

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

当您在不同的时间或不同的开发阶段需要不同的功能时,它们非常方便使用。例如,我正在具有控制台的主机上开发应用程序,但是该软件的最终版本将放在 Avnet ZedBoard 上(该端口具有用于显示和控制台的端口,但是不需要 / 不需要它们用于)。最终版本)。因此,在开发过程中,我将使用printf来查看状态和错误消息,但是当我完成后,我什么都不想打印了。这是我所做的:

版本

// 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

version.c我将定义version.h存在的 2 个函数原型

版本号

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

注意函数指针如何在version.h中原型化为

void (* zprintf)(const char *, ...);

当在应用程序中引用它时,它将在其指向的任何位置(尚未定义)开始执行。

version.c ,请注意board_init()函数中的功能,其中根据version.h定义的版本,为zprintf分配了唯一函数(其功能签名匹配)

zprintf = &printf; zprintf 调用 printf 进行调试

要么

zprintf = &noprint; zprintf 只是返回而不会运行不必要的代码

运行代码如下所示:

主程序

#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 ,在释放模式下将不执行任何操作。这比遍历整个项目并注释掉或删除代码要容易得多。我要做的就是更改version.h中的version.h ,其余的代码将完成!

函数指针通常由typedef定义,并用作参数和返回值。

上面的答案已经解释了很多,我只举一个完整的例子:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

C 中函数指针的一大用途是调用在运行时选择的函数。例如,C 运行时库有两个例程qsortbsearch ,它们使用指向一个函数的指针,该函数被调用来比较要排序的两个项目。这使您可以根据希望使用的任何标准分别对任何内容进行排序或搜索。

一个非常基本的示例,如果有一个名为print(int x, int y)的函数,而该函数又可能需要调用一个函数(具有相同类型的add()sub() ,那么我们将做什么,我们将向print()函数添加一个函数指针参数,如下所示:

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

输出为:

值是:410
值是:390

函数指针是包含函数地址的变量。由于它是一个指针变量,但是具有某些受限制的属性,因此您可以像使用数据结构中的任何其他指针变量一样使用它。

我能想到的唯一例外是将函数指针视为指向单个值以外的其他东西。通过递增或递减函数指针或对函数指针增加 / 减去偏移量来进行指针算术并不是真正的实用程序,因为函数指针仅指向单个对象,即函数的入口点。

函数指针变量的大小,该变量占用的字节数可能会根据基础架构(例如 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是一个接受intchar *并返回int的函数
  • pFunc是一个函数指针,指向该函数的函数的地址采用intchar *并返回int

因此从上面我们可以得到一个源代码行,其中将func()函数的地址分配给函数指针变量pFuncpFunc = 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 样式namespace将全局struct与功能指针一起使用

您可以使用static关键字指定名称为文件范围的函数,然后将其分配给全局变量,以提供类似于 C ++ 的namespace功能的方式。

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

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

然后,通过指定全局 struct 变量的完整名称和成员名称来使用该名称,以访问该函数。 const修饰符用于全局变量,因此不会偶然更改它。

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

功能指针的应用领域

DLL 库组件可以执行类似于 C 样式namespace方法的操作,在 C namespace方法中,从库接口中的工厂方法请求特定的库接口,该接口支持创建包含函数指针的struct 。. 该库接口加载请求的 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()bsearch()一起使用的比较委托函数指针,以提供排序项目列表或对项目列表进行二进制搜索的排序顺序。比较函数委托指定排序或二进制搜索中使用的排序规则算法。

另一个用途类似于将算法应用于 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 消息。

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

从头开始功能具有从其开始执行的位置的一些内存地址。在汇编语言中,它们被称为(称为 “函数的内存地址”)。现在返回 C,如果函数具有内存地址,则它们可以由 C 中的 Pointers 操纵。因此,按 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 包含函数的所需地址。