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

How do function pointers in C work?

提问人: 提问时间:5/8/2009 最后编辑:Yuval Adam 更新时间:10/18/2022 访问量:991607

问:

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

因此,继续回答自己问题的传统,我决定对最基本的东西做一个简短的总结,供那些需要快速深入了解该主题的人使用。

C 函数指针

评论

42赞 Abbafei 5/22/2013
另外:有关对 C 指针的深入分析,请参阅 blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge。此外,《从头开始编程》还展示了它们在机器层面上的工作方式。理解 C 的“内存模型”对于理解 C 指针的工作原理非常有用。
10赞 Bogdan Alexandru 8/28/2014
很棒的信息。不过,从标题来看,我本来希望真正看到对“函数指针如何工作”的解释,而不是它们是如何编码的:)
2赞 1/9/2021
以下答案更短,更容易理解:stackoverflow.com/a/142809/2188550

答:

1734赞 Yuval Adam 5/8/2009 #1

C 语言中的函数指针

让我们从一个基本函数开始,我们将指出它:

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

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

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

评论

26赞 Rich.Carpenter 5/8/2009
感谢您提供的重要信息。您能否补充一些关于函数指针的使用或恰好特别有用的见解?
374赞 hlovdal 5/9/2009
“functionPtr = &addInt;”也可以写成(而且经常是)“functionPtr = addInt;”,这也是有效的,因为标准说,在这种情况下,函数名称被转换为函数的地址。
26赞 Johannes Schaub - litb 5/11/2009
hlovdal,在这种情况下,有趣的是解释一下,这就是使人们能够编写 functionPtr = ******************addInt 的原因;
126赞 giant91 10/13/2013
@Rich.Carpenter 我知道这已经晚了 4 年,但我认为其他人可能会从中受益:函数指针可用于将函数作为参数传递给其他函数。出于某种奇怪的原因,我花了很多时间才找到答案。所以基本上,它给了 C 伪一流的功能。
30赞 Peter Cordes 8/30/2015
@Rich.Carpenter:函数指针非常适合运行时 CPU 检测。具有某些函数的多个版本,以利用 SSE、popcnt、AVX 等。启动时,将函数指针设置为当前 CPU 的每个函数的最佳版本。在其他代码中,只需通过函数指针进行调用,而不是在 CPU 功能上随处都有条件分支。然后你可以做复杂的逻辑来决定这一点,即使这个 CPU 支持 ,它也很慢,所以早期的实现仍然更快。x264/x265 广泛使用它,并且是开源的。pshufb
125赞 2 revs, 2 users 99%Nick #2

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

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

评论

10赞 Alexei Averchenko 9/16/2012
如果要以某种方式从迭代中提取任何输出(例如闭包),还应该传递指向用户指定数据的指针。
3赞 Jonathon Reinhart 4/30/2015
同意。我所有的迭代器都如下所示:.迭代器的返回值还允许我提前停止(如果非零)。int (*cb)(void *arg, ...)
340赞 3 revs, 2 users 99%coobird #3

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

例如,以下行是用 C 语言编写的:

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

是的,缺少运算符是一个死的放弃,但它似乎确实暗示我们正在将某个类的文本设置为 .->newString"hello"

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

这是如何实现的?

该类实际上是一个带有一堆函数指针的类,这些指针充当模拟方法的一种方式。以下是该类的部分声明:StringstructString

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();

可以看出,类的方法实际上是指向声明函数的函数指针。在准备 的实例时,调用该函数是为了设置指向其各自函数的函数指针:StringStringnewString

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

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

    self->set(self, "");

    return self;
}

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

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

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

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

有了这个次要的解释,必须把对你自己的引用排除在外,我们将进入下一部分,即 C 语言中的继承

假设我们想做一个子类,比如说一个 。为了使字符串不可变,该方法将不可访问,同时保持对 和 的访问,并强制“构造函数”接受:StringImmutableStringsetgetlengthchar*

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

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

至于实现,唯一相关的代码是“构造函数”函数,即:ImmutableStringnewImmutableString

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

在实例化 时,指向 和 方法的函数指针实际上引用了 and 方法,通过遍历作为内部存储对象的变量。ImmutableStringgetlengthString.getString.lengthbaseString

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

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

例如,如果出于某种原因,我们想更改方法的行为以始终返回类中,那么所要做的就是:length0ImmutableString

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

添加覆盖方法可以通过添加以下方法执行:lengthImmutableStringlengthOverrideMethod

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

然后,构造函数中方法的函数指针挂接到:lengthlengthOverrideMethod

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

现在,该方法在类中的行为与类中的方法不再具有相同的行为,而是引用函数中定义的行为。lengthImmutableStringStringlengthlengthOverrideMethod

我必须补充一个免责声明,我仍在学习如何用 C 语言使用面向对象的编程风格进行编写,因此可能有些点我没有很好地解释,或者可能只是在如何最好地在 C 中实现 OOP 方面偏离了目标。但我的目的是尝试说明函数指针的众多用途之一。

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

评论

33赞 Alexei Averchenko 9/16/2012
这个答案太可怕了!这不仅意味着OO在某种程度上依赖于点符号,它还鼓励将垃圾放入您的对象中!
34赞 Kuba hasn't forgotten Monica 3/19/2013
这是 OO,但与 C 风格的 OO 相差甚远。你完全实现的是 Javascript 风格的基于 prototype 的 OO。要获得 C++/Pascal 风格的 OO,您需要: 1.为每个具有虚拟成员的的虚拟表创建一个常量结构。2. 在多态对象中指针指向该结构。3. 通过虚拟表调用虚拟方法,以及直接调用所有其他方法——通常通过坚持一些函数命名约定。只有这样,您才能获得与 C++ 和 Pascal 相同的运行时和存储成本。ClassName_methodName
21赞 rbaleksandar 7/4/2013
使用一种不打算成为 OO 的语言来工作 OO 总是一个坏主意。如果你想要OO并且仍然有C,只需使用C++。
29赞 Jonathon Reinhart 4/30/2015
@rbaleksandar 告诉 Linux 内核开发人员。“总是一个坏主意”严格来说是你的意见,我坚决不同意。
8赞 cat 9/29/2016
我喜欢这个答案,但不要投射 malloc
26赞 3 revsJohannes Schaub - litb #4

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

  • id: : ID 是ID
  • 指针:指向的 D 指针*D
  • 函数: : D 函数接受 <parameters>返回D(<parameters>)

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

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

如您所见,使用 typedef 构建它非常容易。如果没有 typedefs,使用上述声明器规则并一致地应用也不难。如您所见,我错过了指针指向的部分以及函数返回的内容。这就是出现在声明最左边的内容,并且不感兴趣:如果已经构建了声明器,则会在末尾添加它。让我们开始吧。始终如一地建立它,首先是冗长的 - 显示使用 和 的结构:[]

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

正如你所看到的,可以通过一个接一个地附加声明符来完全描述一个类型。施工可以通过两种方式进行。一种是自下而上,从非常正确的东西(叶子)开始,一直到标识符。另一种方式是自上而下,从标识符开始,一直到叶子。我将展示两种方式。

自下而上

构造从右边的事物开始:返回的事物,即取字符的函数。为了保持声明符的区别,我将对它们进行编号:

D1(char);

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

(*D2)(char);

返回类型完成!现在,让我们用函数声明器函数替换,该函数接受 <parameters> 返回,这就是我们现在所处的位置。D2D3(<parameters>)

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

请注意,不需要括号,因为这次我们想成为一个函数声明器,而不是一个指针声明器。太好了,唯一剩下的就是它的参数。该参数的执行方式与我们执行返回类型完全相同,只是替换为 。所以我会复制它:D3charvoid

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

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

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

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

自上而下

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

ID0(<parameters>)

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

*ID0(<parameters>)

然后接下来是 functon 取<参数>返回。该参数是一个简单的字符,所以我们立即再次放入它,因为它真的是微不足道的。

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

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

现在我们只需要输入参数。我将展示一个简短的去河化版本,因为我认为你现在已经知道如何去做了。<>

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

1赞 user50049 #5

由于函数指针通常是类型化的回调,因此您可能需要查看类型安全的回调。这同样适用于非回调函数的入口点等。

C是相当善变和宽容的,同时:)

评论

0赞 julaine 11/24/2023
链接已断开。
262赞 4 revs, 3 users 50%Lee #6

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

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

你通常不会手动编写这些,你会用汇编语言编写,然后使用汇编器将其组装成一个平面二进制文件,然后将其十六进制转储到 C 字符串文字中。nasm

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

请注意,编译器将字符串文本放在该部分(或在 Windows 上)中,该部分作为文本段的一部分(以及函数代码)链接。.rodata.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";

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

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 在所有正常的调用约定中都是调用保留的,因此将其用作暂存寄存器而不保存/恢复它很容易使调用方崩溃。

评论

9赞 SecurityMatt 2/12/2013
注意:如果启用了数据执行保护(例如,在 Windows XP SP2+ 上),则这不起作用,因为 C 字符串通常不会标记为可执行文件。
5赞 Lee 1/2/2014
嗨,马特!根据优化级别,GCC 通常会将字符串常量内联到 TEXT 段中,因此,如果您不禁止此类优化,这即使在较新版本的 Windows 上也有效。(IIRC,两年多前我写这篇文章时的 MINGW 版本在默认优化级别内联字符串文字)
11赞 ajay 1/20/2014
有人可以解释一下这里发生了什么吗?那些看起来很奇怪的字符串文字是什么?
65赞 ejk314 2/22/2014
@ajay 看起来他正在将原始十六进制值(例如“\x00”与“/0”相同,它们都等于 0)写入字符串,然后将字符串转换为 C 函数指针,然后执行 C 函数指针,因为他是魔鬼。
4赞 Lee 3/13/2014
嗨 FUZxxl,我认为它可能会因编译器和操作系统版本而异。上面的代码似乎在 codepad.org 上运行良好;codepad.org/FMSDQ3ME
25赞 2 revsZack Sheffield #7

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

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

在 I 中,我将定义version.cversion.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.cboard_init()zprintfversion.h

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

zprintf = &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;
}

上面的代码将在调试模式下使用 if,如果在发布模式下不执行任何操作。这比遍历整个项目并注释掉或删除代码要容易得多。我需要做的就是更改版本,剩下的工作由代码完成!printfversion.h

评论

7赞 AlphaGoku 5/11/2018
你会失去很多表演时间。相反,您可以使用一个宏来启用和禁用基于 Debug/Release 的代码段。
20赞 2 revs, 2 users 97%Mohit Dabas #8

从头开始函数有一些内存地址,它们从哪里开始执行。在汇编语言中,它们被称为(调用“函数的内存地址”)。现在回到 C 如果函数有内存地址,那么它们可以被 C.So 中的指针操作 根据 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");
}

enter image description here之后,让我们看看机器如何理解它们.在32位架构中对上述程序的机器指令的一瞥。

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

评论

0赞 Aaron Franke 4/19/2021
如何使用从方法返回的函数指针? 似乎只是使程序崩溃。我这里有一些上下文和失败的代码:stackoverflow.com/questions/67152106something()
24赞 2 revs, 2 users 99%Eric Wang #9

函数指针通常由 定义,并用作参数和返回值。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;
}
18赞 3 revs, 3 users 76%Vamsi #10

函数指针在 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

18赞 Richard Chambers #11

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

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

函数指针变量的大小,变量占用的字节数,可能因底层架构而异,例如 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是一个接受 an 和 a 并返回intchar *int
  • pFunc是一个函数指针,为其分配了一个函数的地址,该函数接受 an 和 a 并返回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;
}

函数指针数组

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

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 样式命名空间 将全局结构与函数指针一起使用

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

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

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 样式方法的操作,在该方法中,从支持创建包含函数指针的库接口中的工厂方法请求特定的库接口。此库接口加载请求的 DLL 版本,使用必要的函数指针创建结构,然后将该结构返回给请求调用方以供使用。namespacestruct

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 消息。

需要回调的异步函数类似于事件处理程序。异步函数的用户调用异步函数来启动某些操作,并提供一个函数指针,异步函数将在操作完成后调用该指针。在本例中,事件是完成其任务的异步函数。

1赞 Suman Chapai #12

指向函数的指针很有用,因为正如《C 编程语言》一书所说,C 语言中的函数不是变量。 这意味着,

// Say you have add function
int add(int x, int y){
    return x + y;
}

// Say you have another add function
int another_add(int x, int y){
    return y + x;
}


int main(){
    // Although the types of another_add and add are same
    // You can't do
    another_add = add
    
    // You have a compute function that takes a function of int's signature
    int (*compute)(int, int);
   
    // You won't even be able to pass functions to other functions
    // (Although when you do, C is just passing the pointer to that function)
    // So, compute(add) is really compute(&add)
    // But you can create a pointer to functions that are variables
    // you can assign to and/or pass to other functions

    int (*operation)(int, int);
    // Now you can do
    operation = &add;
    // You could also do, the following to do the same thing
    // When a function is passed in right hand side of assignment,
    // C knows that you mean pointer, and you don't need explicit &
    operation = add;
}

同样,数组也不是 C 中的变量。您可以编造一个与上面类似的示例并进行测试。