在C/C++中,我们经常会像下面的代码那样使用一个指向函数的指针,我们称之为函数指针:

// demo.c
#include <stdio.h>

int func(int a) {
    return a + 1;
}

int main(int argc, char* argv[]) {
    int (*f)(int) = func;
    printf("%p\n", f);
    return 0;
}

上面的例子中,我们定义了一个函数func,然后通过函数指针f指向func,接着使用print函数打印指针变量f指向 的地址。代码平淡无奇,接着我们编译代码,然后使用objdump -D demo来查看生成的二进制结构如下:

0000000100003f34 <_func>:
100003f34: ff 43 00 d1 	sub	sp, sp, #16
100003f38: e0 0f 00 b9 	str	w0, [sp, #12]
100003f3c: e8 0f 40 b9 	ldr	w8, [sp, #12]
100003f40: 00 05 00 11 	add	w0, w8, #1
100003f44: ff 43 00 91 	add	sp, sp, #16
100003f48: c0 03 5f d6 	ret

上述结果是我在arm64-apple-darwin21.6.0环境下,使用clang 14.0编译出来的结果,gcc编译的结果稍微有点区别,但是对于本文分析问题影响不大。

正如我们所看到的,编译后的func函数位于0x102e0bf34地址,让我们记住这个地址,接着往后看。

接下来我们来运行编译后的二进制文件,很显然它应该输出的是func函数在内存中的地址:

./demo
0x102e0bf34

你猜对了,只是尽管函数指针也是指针,它指向的是内存中的一段代码,而不是内存中的数据。

通常情况下,函数指针作为回调是没有问题的,但是如果考虑到闭包的情况,我们发现函数指针就无能为力了。 因为闭包不仅是一个函数,而且还要能捕获相关的数据,由于函数指针仅仅是指向内存中的一段代码,并没有指向内存中的数据, 所以函数指针无法实现闭包的功能。如果要想实现,就需要做一些改进:

typedef void (*func) (void *);

struct closure {
    func f;
    void *arg;
};

如上,我们定义了一个结构closure,其中包含了两部分,一个指针变量指向一个函数,一个变量保存参数。 也就是说,closure既包含了一段代码,也包含了运行该代码所需要的数据,或者称之为上下文环境,不管它叫什么,总之就是 运行函数所需要的数据。

这其实就是C++中的std::function所实现的功能。

在C++中,你无法单纯使用函数指针来指向对象的成员函数,原因就是函数指针无法捕获其上下文(指向对象的指针)。std::function所做的与我们上面定义的closure结构其实没有太多本质的区别。 使用std::function,我们不仅仅可以存储一段代码,还可以存放必要的执行上下文,然后在合适的时候基于该上下文进行函数调用。