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


我最近在C中使用了函数指针。

继续回答你自己的问题的传统,我决定对那些需要快速深入研究这个主题的人进行一些基本的总结。


1011
2018-05-08 15:49


起源


另外:有关C指针的深入分析,请参阅 blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge。也, 从头开始编程 显示它们如何在机器级别上工作。理解 C的“记忆模型” 对于理解C指针如何工作非常有用。 - Abbafei
好消息。虽然通过标题,我本来期望真正看到“函数指针如何工作”的解释,而不是如何编码:) - Bogdan Alexandru
“C中的函数指针如何工作?”好的,谢谢。 - Keith Thompson


答案:


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

1247
2018-05-08 15:49



感谢您提供的精彩信息。您能否在函数指针的使用位置或特别有用的位置添加一些见解? - Rich.Carpenter
“functionPtr =&addInt;”也可以写成(通常是)“functionPtr = addInt;”这也是有效的,因为标准表示此上下文中的函数名称被转换为函数的地址。 - hlovdal
hlovdal,在这个上下文中有趣的是解释这是什么使得一个人能够编写函数Ptr = ****************** addInt; - Johannes Schaub - litb
@ Rich.Carpenter我知道这已经晚了4年,但我认为其他人可能从中受益: 函数指针对于将函数作为参数传递给其他函数很有用。由于一些奇怪的原因,我花了很多时间寻找答案。所以基本上,它提供了C伪的一流功能。 - giant91
@ Rich.Carpenter:函数指针很适合运行时CPU检测。有一些函数的多个版本可以利用SSE,popcnt,AVX等。在启动时,将函数指针设置为当前CPU的每个函数的最佳版本。在您的其他代码中,只需通过函数指针调用,而不是在各处的CPU功能上都有条件分支。然后,即使这个CPU支持,你也可以做很好的判断 pshufb,它很慢,所以早期的实施仍然更快。 x264 / x265广泛使用它,并且是开源的。 - Peter Cordes


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 class实际上是声明函数的函数指针。在准备实例时 StringnewString 调用函数以设置指向各自函数的函数指针:

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

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

    self->set(self, "");

    return self;
}

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

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 变量是内部存储的 String 目的。

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

我们可以继续 C中的多态性

例如,如果我们想改变的行为 length 返回的方法 0 一直都在 ImmutableString 因某种原因,所有必须做的就是:

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

添加覆盖 length 方法 ImmutableString 可以通过添加一个来执行 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;
}

现在,而不是具有相同的行为 length 方法 ImmutableString 作为 String 上课,现在 length 方法将参考中定义的行为 lengthOverrideMethod 功能。

我必须添加一个免责声明,我仍然在学习如何使用C语言中的面向对象编程风格进行编写,因此可能有一点我没有解释得很好,或者可能只是在如何最好地实现OOP在C.但我的目的是试图说明函数指针的许多用法之一。

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


267



这个答案太可怕了!它不仅意味着OO以某种方式依赖于点符号,它还鼓励将垃圾放入您的对象中! - Alexei Averchenko
这是OO可以,但不是C风格OO附近的任何地方。你破坏性地实现了Javascript风格的基于原型的OO。要获得C ++ / Pascal样式的OO,您需要:1。为每个虚拟表创建一个const结构 类 与虚拟成员。 2.在多态对象中指向该结构。 3.通过虚拟表和所有其他方法直接调用虚方法 - 通常是坚持一些 ClassName_methodName 函数命名约定。只有这样,您才能获得与C ++和Pascal相同的运行时和存储成本。 - Kuba Ober
使用一种不打算成为OO的语言工作OO总是一个坏主意。如果你想要OO并且仍然使用C只能使用C ++。 - rbaleksandar
@rbaleksandar告诉Linux内核开发人员。 “总是一个坏主意” 严格意见,我坚决反对。 - Jonathon Reinhart
我喜欢这个答案,但不要施放malloc - cat


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

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

188



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


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

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

95



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


拥有基本声明符后,函数指针变得易于声明:

  • ID: IDID是一个
  • 指针: *DD指针
  • 功能: D(<parameters>)D功能服用 <参数> 回国

而D是使用相同规则构建的另一个声明符。最后,在某个地方,它结束了 ID (参见下面的示例),这是声明的实体的名称。让我们尝试构建一个函数,它接受一个函数的指针,不带任何东西并返回int,并返回一个指向函数的指针,该函数接受一个char并返回int。使用type-defs就像这样

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))。但这不是D1的简单替代 *D2 当然,还有。声明符周围始终允许使用括号。实际上,如果你添加了太多的东西,你就不会犯错。

(*D2)(char);

退货类型齐全!现在,让我们替换 D2 由函数声明器 功能服用 <parameters> 回国,是的 D3(<parameters>) 我们现在。

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

请注意,我们不需要括号   D3 这次是函数声明符而不是指针声明符。太棒了,唯一剩下的就是它的参数。该参数与我们完成的返回类型完全相同,只需使用 char 取而代之 void。所以我会复制它:

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

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

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

我已经调用了函数的标识符 ID0 在那个例子中。

自顶向下

这从类型描述最左边的标识符开始,在我们向右走过时包装该声明符。从...开始 功能服用 <参数> 回国

ID0(<parameters>)

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

*ID0(<parameters>)

然后接下来就是 功能 <参数> 回国。参数是一个简单的char,所以我们马上把它放进去,因为它真的很简单。

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

注意我们添加的括号,因为我们再次想要那个 * 首先绑定,然后 然后 该 (char)。否则它会读 功能服用 <参数> 返回功能......。 Noes,甚至不允许函数返回函数。

现在我们只需要放 <参数>。我将展示一个简短的衍生版本,因为我认为你现在已经知道如何做到这一点。

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


23





函数指针的另一个好用途:
无痛地在版本之间切换

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

version.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 我将定义2中存在的函数原型 version.h

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

上面的代码将使用 printf 如果处于调试模式,或者在释放模式下不执行任何操作。这比完成整个项目以及注释或删除代码要容易得多。我需要做的就是更改版本 version.h 并且代码将完成其余的工作!


21



你会失去很多表演时间。相反,您可以使用一个宏来启用和禁用基于Debug / Release的代码段。 - Akshay Immanuel D


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

13