【问题标题】:How do function pointers in C work?C 中的函数指针是如何工作的?
【发布时间】:2021-07-13 01:35:48
【问题描述】:

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

因此,按照自己回答问题的传统,我决定对基础知识做一个小总结,供那些需要快速深入了解该主题的人使用。

【问题讨论】:

标签: c function-pointers


【解决方案1】:

C 中的函数指针

让我们从一个我们将指向的基本函数开始:

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

首先,让我们定义一个指向函数的指针,该函数接收 2 个ints 并返回一个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;
}

【讨论】:

  • 感谢您提供的重要信息。您能否补充一些关于函数指针在哪里使用或恰好特别有用的见解?
  • "functionPtr = &addInt;"也可以写成(通常是)“functionPtr = addInt;”这也是有效的,因为标准规定此上下文中的函数名将转换为函数的地址。
  • hlovdal,在这种情况下,有趣的是解释这是使人们能够编写 functionPtr = ******************addInt;
  • @Rich.Carpenter 我知道这已经晚了 4 年,但我认为其他人可能会从中受益:函数指针对于将函数作为参数传递给其他函数很有用 .出于某种奇怪的原因,我花了很多时间寻找答案。所以基本上,它提供了 C 伪一流的功能。
  • @Rich.Carpenter:函数指针非常适合运行时 CPU 检测。拥有一些函数的多个版本以利用 SSE、popcnt、AVX 等。在启动时,将函数指针设置为当前 CPU 的每个函数的最佳版本。在您的其他代码中,只需通过函数指针调用,而不是在 CPU 功能上到处都有条件分支。然后你可以做复杂的逻辑来决定那个好,即使这个CPU支持pshufb,它很慢,所以早期的实现仍然更快。 x264/x265 广泛使用它,并且是开源的。
【解决方案2】:

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方法将不可访问,同时保持对get和length的访问,并强制“构造函数”接受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 时,指向get 和length 方法的函数指针实际上是指String.get 和String.length 方法,方法是通过base 变量,这是一个内部存储的@ 987654361@对象。

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

我们可以进一步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 语言编写面向对象的编程风格,所以可能有些地方我没有很好地解释,或者可能只是在最好的方面偏离了标准在 C 中实现 OOP。但我的目的是试图说明函数指针的众多用途之一。

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

【讨论】:

  • 这个答案太可怕了!它不仅暗示 OO 在某种程度上依赖于点符号,它还鼓励将垃圾放入您的对象中!
  • 这是 OO 没问题,但不是 C 风格的 OO。您错误地实现的是 Javascript 风格的基于原型的 OO。要获得 C++/Pascal 风格的 OO,您需要: 1. 为每个具有虚拟成员的 class 的虚拟表拥有一个 const 结构。 2. 在多态对象中有指向该结构的指针。 3. 通过虚拟表调用虚拟方法,并直接调用所有其他方法——通常遵循一些ClassName_methodName 函数命名约定。只有这样,您才能获得与使用 C++ 和 Pascal 相同的运行时和存储成本。
  • 使用不适合 OO 的语言使用 OO 总是一个坏主意。如果您想要 OO 并且仍然使用 C,只需使用 C++。
  • @rbaleksandar 告诉 Linux 内核开发人员。 “总是一个坏主意”完全是你的意见,我坚决不同意。
  • 我喜欢这个答案,但不要投射 malloc
【解决方案3】:

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

这些字符串文字是 32 位 x86 机器代码的字节。 0xC3 是 an x86 ret instruction。

您通常不会手动编写这些,而是​​使用汇编语言编写,然后使用像 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反汇编,我们可以得到汇编,发现这段代码违反了ABI,破坏了EBX(一个保留调用的寄存器),通常效率很低。

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 位代码运行:所有这些操作系统上的默认调用约定在堆栈上传递参数,而不是在寄存器中更有效地传递参数。但是 EBX 在所有正常的调用约定中都是调用保留的,因此将其用作临时寄存器而不保存/恢复它很容易使调用者崩溃。

【讨论】:

  • 注意:如果启用了数据执行保护(例如在 Windows XP SP2+ 上),这将不起作用,因为 C 字符串通常不会被标记为可执行文件。
  • 嗨,马特!根据优化级别,GCC 通常会将字符串常量内联到 TEXT 段中,因此即使在较新版本的 windows 上也可以使用,前提是您不允许这种类型的优化。 (IIRC,两年前我发帖时的 MINGW 版本,在默认优化级别内联字符串文字)
  • 有人能解释一下这里发生了什么吗?那些看起来很奇怪的字符串文字是什么?
  • @ajay 看起来他正在将原始十六进制值(例如 '\x00' 与 '/0' 相同,它们都等于 0)写入字符串,然后将字符串转换进入一个 C 函数指针,然后执行 C 函数指针,因为他是魔鬼。
  • 嗨 FUZxxl,我认为它可能因编译器和操作系统版本而异。上面的代码似乎在 codepad.org 上运行良好; codepad.org/FMSDQ3ME
【解决方案4】:

我最喜欢的函数指针用途之一是作为廉价且简单的迭代器 -

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

【讨论】:

  • 如果你想以某种方式从迭代中提取任何输出(想想闭包),你还应该传递一个指向用户指定数据的指针。
  • 同意。我所有的迭代器都是这样的:int (*cb)(void *arg, ...)。迭代器的返回值也让我提前停止(如果非零)。
【解决方案5】:

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

  • id:ID:ID 是一个
  • 指针:*D:D 指针
  • 函数:D(&lt;parameters&gt;):D 函数以 &lt;parameters&gt; 返回

而 D 是使用相同规则构建的另一个声明符。最后,在某处,它以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 参数,因为它很简单。通过将D1 替换为*D2 添加指向声明符的指针。请注意,我们必须在 *D2 周围加上括号。这可以通过查找*-operator 和函数调用运算符() 的优先级来知道。如果没有我们的括号,编译器会将其读取为*(D2(char p))。但这当然不再是 *D2 对 D1 的简单替换。声明符周围总是允许使用括号。因此,实际上,如果添加太多,也不会出错。

(*D2)(char);

返回类型完成!现在,让我们将D2 替换为函数声明符函数返回&lt;parameters&gt;,也就是我们现在的D3(&lt;parameters&gt;)。

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

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

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

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

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

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

自上而下

这从类型描述中最左边的标识符开始,当我们穿过右边时包装该声明符。从函数返回&lt;parameters&gt;开始

ID0(<parameters>)

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

*ID0(<parameters>)

接下来就是函数将&lt;parameters&gt; 返回。参数是一个简单的字符,所以我们马上又把它放进去,因为它真的很琐碎。

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

注意我们添加的括号,因为我们再次希望首先绑定*,然后然后绑定(char)。否则它会读取函数采用&lt;parameters&gt; 返回函数...。不,甚至不允许函数返回函数。

现在我们只需要输入&lt;parameters&gt;。我将展示一个简短的派生版本,因为我认为您现在已经知道如何去做了。

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 内部组件。随意编辑/修复其中的内容。

【讨论】:

    【解决方案6】:

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

    当您在不同的时间或不同的开发阶段需要不同的功能时,它们非常方便。例如,我正在一台有控制台的主机上开发一个应用程序,但该软件的最终版本将放在 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
    

    在version.c 中,我将定义version.h 中的两个函数原型

    版本.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;
    }
    

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

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

    当它在应用程序中被引用时,它将开始执行它指向的任何地方,尚未定义。

    在version.c 中,请注意board_init() 函数中的zprintf 分配了一个唯一函数(其函数签名匹配),具体取决于version.h 中定义的版本

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

    或

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

    运行代码将如下所示:

    mainProg.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,如果处于发布模式,则不执行任何操作。这比浏览整个项目并注释或删除代码要容易得多。我需要做的就是更改version.h 中的版本,剩下的一切由代码完成!

    【讨论】:

    • 你会损失很多性能时间。相反,您可以使用一个宏来启用和禁用基于调试/发布的一段代码。
    【解决方案7】:

    函数指针通常由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;
    }
    

    【讨论】:

      【解决方案8】:

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

      一个非常基本的例子,如果有一个名为 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

      【讨论】:

        【解决方案9】:

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

        我能想到的唯一例外是将函数指针视为指向单个值以外的东西。通过递增或递减函数指针或添加/减去函数指针的偏移量来进行指针运算实际上并没有任何实用性,因为函数指针仅指向单个事物,即函数的入口点。

        函数指针变量的大小,变量占用的字节数,可能因底层架构而异,例如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 风格 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};
        

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

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

        函数指针的应用领域

        DLL 库组件可以执行类似于 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 消息。

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

        【讨论】:

          【解决方案10】:

          从头开始函数有一些内存地址从它们开始执行的地方开始。在汇编语言中它们被称为(调用“函数的内存地址”)。现在回到 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");
          }
          

          After 那让我们看看机器是如何理解它们的。一瞥上述程序在 32 位架构中的机器指令。

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

          【讨论】:

          【解决方案11】:

          由于函数指针通常是类型化的回调,您可能想看看type safe callbacks。这同样适用于非回调函数的入口点等。

          C 既善变又宽容 :)

          【讨论】:

            猜你喜欢
            • 2011-05-30
            • 2020-03-16
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多