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

Yuv*_*dam 1170 c function-pointers

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

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

Yuv*_*dam 1413

C中的函数指针

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

int addInt(int n, int m) {
    return n+m;
}
Run Code Online (Sandbox Code Playgroud)

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

int (*functionPtr)(int,int);
Run Code Online (Sandbox Code Playgroud)

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

functionPtr = &addInt;
Run Code Online (Sandbox Code Playgroud)

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

int sum = (*functionPtr)(2, 3); // sum == 5
Run Code Online (Sandbox Code Playgroud)

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

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}
Run Code Online (Sandbox Code Playgroud)

我们也可以在返回值中使用函数指针(尝试跟上,它变得混乱):

// 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;
}
Run Code Online (Sandbox Code Playgroud)

但是使用它更好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;
}
Run Code Online (Sandbox Code Playgroud)

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

coo*_*ird 295

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

例如,以下行用C编写:

String s1 = newString();
s1->set(s1, "hello");
Run Code Online (Sandbox Code Playgroud)

是的,->缺乏一个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();
Run Code Online (Sandbox Code Playgroud)

可以看出,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;
}
Run Code Online (Sandbox Code Playgroud)

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

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}
Run Code Online (Sandbox Code Playgroud)

可以注意到的一件事是,没有对象实例的概念,并且具有实际上是对象的一部分的方法,因此必须在每次调用时传入"自身对象".(这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);
Run Code Online (Sandbox Code Playgroud)

基本上,对于所有子类,可用的方法再次是函数指针.这次,set方法的声明不存在,因此,不能在a中调用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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

例如,如果我们想要改变length方法的行为以便出于某种原因0ImmutableString类中返回所有时间,那么所有必须做的就是:

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

添加覆盖length方法ImmutableString可以通过添加lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

然后,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;
}
Run Code Online (Sandbox Code Playgroud)

现在,不是将类中的length方法ImmutableString作为String类具有相同的行为,现在该length方法将引用lengthOverrideMethod函数中定义的行为.

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

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

  • 这是OO可以,但不是C风格OO附近的任何地方.你破坏性地实现了Javascript风格的基于原型的OO.要获得C++/Pascal样式的OO,您需要:1.为具有虚拟成员的每个*class*的虚拟表创建一个const结构.2.在多态对象中指向该结构.3.通过虚拟表和所有其他方法直接调用虚方法 - 通常坚持使用一些`ClassName_methodName`函数命名约定.只有这样,您才能获得与C++和Pascal相同的运行时和存储成本. (27认同)
  • 这个答案太可怕了!它不仅意味着OO以某种方式依赖于点符号,它还鼓励将垃圾放入您的对象中! (20认同)
  • 使用不打算成为OO的语言来工作OO总是一个坏主意.如果你想要OO并且仍然只有C才能使用C++. (17认同)
  • @rbaleksandar告诉Linux内核开发人员.*"总是一个坏主意"*严格意见,我坚决反对. (17认同)
  • 我知道这个答案已经很老了,但我觉得 `s1->set(s1, "hello");` 只是为了看起来像 OOP 而使用函数指针,因为你必须将 `s1` 作为参数传递反正。我会坚持使用 `String_set(s1, "hello");` (4认同)
  • 我喜欢这个答案,但不要强制malloc (2认同)
  • 我有一个问题,我们不是在这个方法中为所有“String”对象创建函数指针吗?这不是浪费资源吗? (2认同)

Lee*_*Lee 219

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

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

你通常不会手工编写这些,你用汇编语言编写,然后使用汇编nasm程序将它组装成一个平面二进制文件,然后将其转换为C字符串文字.

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

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
    Run Code Online (Sandbox Code Playgroud)
  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);
    
    Run Code Online (Sandbox Code Playgroud)
  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
    
    Run Code Online (Sandbox Code Playgroud)
  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);
    
    Run Code Online (Sandbox Code Playgroud)

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

文本段已经阅读+ 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";
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

这个机器代码(可能)在Windows,Linux,OS X等上以32位代码工作:所有这些操作系统上的默认调用约定都在堆栈中传递args而不是在寄存器中更有效.但EBX在所有正常的调用约定中都被调用保留,因此将其用作临时寄存器而不保存/恢复它可以轻松地使调用者崩溃.

  • @ajay 看起来他正在将原始十六进制值(例如 '\x00' 与 '/0' 相同,它们都等于 0)写入一个字符串,然后将该字符串转换为 C 函数指针,然后执行C 函数指针,因为他是魔鬼。 (53认同)
  • 有人可以解释一下这里发生了什么吗?什么是那些奇怪的字符串文字? (9认同)
  • 注意:如果启用了数据执行保护(例如,在Windows XP SP2 +上),则此功能不起作用,因为C字符串通常不会标记为可执行文件. (7认同)
  • 嗨马特!根据优化级别,GCC通常会将字符串常量内联到TEXT段中,因此即使在不允许此类优化的情况下,这也适用于较新版本的Windows.(IIRC,两年前我发布时的MINGW版本在默认优化级别上内联字符串文字) (4认同)
  • 在FUZxxl中,我认为它可能会因编译器和操作系统版本而异.上面的代码似乎在codepad.org上正常运行; http://codepad.org/FMSDQ3ME (2认同)
  • 你会被解雇,不仅失去你的工作,而且他们实际上会用汽油掩盖你并烧掉你,然后射击你.:)无论如何,聪明. (2认同)
  • @Lee 这里是一个更短的,希望更简单的变量交换 `((void(*)(int*,int*))"\x8B\x44\x24\x04\x8B\x5C\x24\x08\x8B\x08\x8B\ x13\x89\x10\x89\x0B\xC3 &lt;- 这会交换 a 和 b 的值")(&amp;a,&amp;b);` [在线试试!](https://tio.run/##dY7LDoIwEEX3fsUEg2lJMRUw0fhY6C@4ZFNaY6C@4ZFNaHY0xVcZVcZ5VcZ5VcZVZVKVZV1VKVZV1VKVZV1VKVZV1VKVZV1VKVZVK2FZVK2FKVKF @hzBjCagAOsOIMMGfEdptcb5gXxfaW@So3HQLCMjhdC@lYrElCCHwEbF6VeOmxO6ZAk6RDh8GTy9fnrm8l/XMXILZJP5JidY9iHcKm0BfsQVwtdlUMvkwkwkwktcwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwkwqrbf6veOmxO6ZAk6 (2认同)
  • @ceilingcat:这样效率更高,但您仍然会破坏 EBX,这很可能会使 32 位 PIE 可执行文件中的调用方崩溃。(gcc 通常使用 EBX 来保存相对于 PC 的参考点。)对于 32 位代码来说,最好的办法可能是保存/恢复寄存器。或者以非常慢的原子交换为代价获得最小的代码大小,使用 `xchg [ecx], eax` 这样你只需要 2 个指针 + 寄存器中的 1 个值,这样你就可以只用调用破坏的 EAX,ECX ,和 EDX。或者只是重新加载指针参数之一。 (2认同)

Nic*_*unt 109

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

#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);
}
Run Code Online (Sandbox Code Playgroud)

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

Joh*_*itb 24

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

  • id ID:: ID是一个
  • 指针*D:: D指针指向
  • 功能:D(<parameters>):d功能拍摄<参数,>返回

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

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);
Run Code Online (Sandbox Code Playgroud)

如您所见,使用typedef构建它非常容易.如果没有typedef,使用上述声明符规则并不难,一致地应用.如你所见,我错过了指针指向的部分,以及函数返回的东西.这就是声明最左边出现的内容,并不感兴趣:如果已经建立了声明者,最后会添加它.我们这样做.建立起来,首先罗嗦 - 使用[和显示结构]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]
Run Code Online (Sandbox Code Playgroud)

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

自下而上

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

D1(char);
Run Code Online (Sandbox Code Playgroud)

直接插入char参数,因为它很简单.通过替换D1为添加指向声明符的指针*D2.请注意,我们必须括起括号*D2.通过查找*-operator和函数调用运算符的优先级可以知道这一点().没有我们的括号,编译器会将其读作*(D2(char p)).但当然,这不再是D1的简单替代品*D2了.声明者总是允许使用括号.实际上,如果你添加了太多的东西,你就不会犯错.

(*D2)(char);
Run Code Online (Sandbox Code Playgroud)

退货类型齐全!现在,让我们用D2函数声明器函数<parameters>替换return,这是D3(<parameters>)我们现在所处的.

(*D3(<parameters>))(char)
Run Code Online (Sandbox Code Playgroud)

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

(*D3(   (*ID1)(void)))(char)
Run Code Online (Sandbox Code Playgroud)

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

int (*ID0(int (*ID1)(void)))(char)
Run Code Online (Sandbox Code Playgroud)

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

自顶向下

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

ID0(<parameters>)
Run Code Online (Sandbox Code Playgroud)

描述中的下一个内容(在"返回"之后)是指针.让我们加入它:

*ID0(<parameters>)
Run Code Online (Sandbox Code Playgroud)

那么接下来的事情是functon服用<参数>返回.参数是一个简单的char,所以我们再次把它放进去,因为它真的是微不足道的.

(*ID0(<parameters>))(char)
Run Code Online (Sandbox Code Playgroud)

请注意,我们添加了括号,因为我们想再次的*结合第一,而随后(char).否则,它会读取功能拍摄<参数,>返回函数....Noes,甚至不允许函数返回函数.

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

pointer to: *ID1
... function taking void returning: (*ID1)(void)
Run Code Online (Sandbox Code Playgroud)

int就像我们用自下而上的方式放在声明器之前,我们就完成了

int (*ID0(int (*ID1)(void)))(char)
Run Code Online (Sandbox Code Playgroud)

好事

自下而上还是自上而下更好?我习惯了自下而上,但有些人可能会更自在地自上而下.我认为这是一个品味问题.顺便说一下,如果你在该声明中应用所有运算符,最终会得到一个int:

int v = (*ID0(some_function_pointer))(some_char);
Run Code Online (Sandbox Code Playgroud)

这是C中声明的一个很好的属性:声明声明如果使用标识符在表达式中使用这些运算符,那么它会在最左边产生类型.就像阵列一样.

希望你喜欢这个小教程!现在,当人们想知道函数的奇怪声明语法时,我们可以链接到这个.我试着把尽可能少的C内部装置.随意编辑/修复其中的内容.


Zac*_*eld 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
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

注意函数指针是如何在原型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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 你会失去很多表演时间.相反,您可以使用一个宏来启用和禁用基于Debug/Release的代码段. (4认同)

Eri*_*ang 16

函数指针通常由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;
}
Run Code Online (Sandbox Code Playgroud)


小智 14

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;
}
Run Code Online (Sandbox Code Playgroud)


Moh*_*bas 12

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

在此处输入图片说明之后让我们看看机器如何理解它们。上述程序在 32 位架构中的机器指令一览。

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


Ric*_*ers 5

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

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

函数指针变量的大小,该变量占用的字节数可能会根据基础架构(例如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.
Run Code Online (Sandbox Code Playgroud)

前两个声明的相似之处在于:

  • func是一个接受a int和a char *并返回an 的函数int
  • pFunc是一个函数指针,分配给该函数的函数的地址是a int和a char *并返回aint

因此,从上面我们可以在源代码行中将函数的地址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
Run Code Online (Sandbox Code Playgroud)

几种不同的用法示例

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

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
Run Code Online (Sandbox Code Playgroud)

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

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);
Run Code Online (Sandbox Code Playgroud)

或根本无法指定参数列表。这可能很有用,但是它消除了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);
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

比较功能指针与相等性

您可以使用一条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;
}
Run Code Online (Sandbox Code Playgroud)

函数指针数组

而且,如果您要有一个函数指针数组,每个参数列表中的元素都不同,那么您可以定义一个函数指针,其中未指定参数列表(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.
}
Run Code Online (Sandbox Code Playgroud)

C样式与函数指针一起namespace使用Globalstruct

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

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

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

extern const FuncThings FuncThingsGlobal;
Run Code Online (Sandbox Code Playgroud)

然后在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};
Run Code Online (Sandbox Code Playgroud)

然后,通过指定全局struct变量的完整名称和成员名称来使用该名称,以访问该函数。该const改性剂的全球化,所以它不能被意外的改变。

int abcd = FuncThingsGlobal.func1 (a, b);
Run Code Online (Sandbox Code Playgroud)

功能指针的应用领域

DLL库组件可以执行与C样式namespace方法类似的操作,在C 方法中,从支持创建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;
}
Run Code Online (Sandbox Code Playgroud)

这可以用于:

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

可以使用相同的方法为使用底层硬件的特定模型的代码定义抽象的硬件层。工厂用功能特定的功能填充功能指针,以提供实现特定抽象硬件模型中指定功能的功能。这可以用来提供由软件使用的抽象硬件层,该软件调用工厂函数以获取特定的硬件功能接口,然后使用提供的功能指针对基础硬件执行操作,而无需了解有关特定目标的实现细节。

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

您可以使用函数指针来委派某些任务或功能。在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);
Run Code Online (Sandbox Code Playgroud)

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

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