Christope's answer 是正确的。这是补充。
函数指针不能本身提供异步行为。标准居然bar这个。我对 C 标准比对 C++ 标准更熟悉,所以我会使用它。我的理解是在这一点上两者应该大致相同。
C11 标准对函数和函数指针的描述
让我们从 C 中函数调用的定义开始,在 6.5.2.2 第 3 段中给出:
后缀表达式后跟括号 () 包含可能为空的逗号分隔的表达式列表是函数调用。后缀表达式表示被调用的函数。表达式列表指定函数的参数。
并由第 1 段中的约束修改:
表示被调用函数 (92) 的表达式应具有指向返回 void 或返回数组类型以外的完整对象类型的函数的类型指针。
重要的是,随附的脚注 92 说:
大多数情况下,这是转换作为功能指示符的标识符的结果。
所以,C11 标准基本上将函数调用定义为调用函数指针的东西。并且,为此目的,命名函数标识符会自动转换为指向标识符中代码的函数指针。因此,C 认为函数和函数指针之间没有区别。
一个实验
虽然参考标准总是好的,但看看可靠的实现如何做事也非常有用。让我们做一个测试,我们编写相当简单的代码,然后查看底层程序集
代码:
#include <stdio.h>
#include <stdlib.h>
typedef void (*my_func_ptr)(int,int);
void my_function(int x, int y)
{
printf("x = %d, y = %d, x + y = %d\n",x,y,x+y);
}
int main()
{
/* declared volatile so the compiler has to call the function through
* the pointer and cannot optimize it to call the function directly */
volatile my_func_ptr fp = my_function;
my_function(3,5);
fp(3,6);
return 0;
}
我在 Mac OS X 上使用 gcc 编译代码,并使用默认优化 (gcc -o fptr fptr.c),这实际上是 LLVM 库的 gcc 前端。为了查看汇编,我在lldb 下运行程序,在main 处设置断点,并发出disassemble -f 命令,它反汇编当前函数。我使用settings set target.x86-disassembly-flavor intel 进行英特尔风格的组装。 lldb中默认的是AT&T风格,看起来有点不一样。
汇编中的main 例程是这样的:
push rbp
mov rbp, rsp
sub rsp, 0x20 ; sets up the stack frame
mov edi, 0x3 ; my_function(3,5). 1st arg: edi
mov esi, 0x5 ; 2nd arg: esi
lea rax, qword ptr [rip - 0x59] ; loads address of my_function into rax
mov dword ptr [rbp - 0x4], 0x0
mov qword ptr [rbp - 0x10], rax ; saves address of my_function on stack
call 0x100000ed0 ; explicit call to my_function
mov eax, 0x0
mov edi, 0x3 ; fp(3,6). 1st arg: edi
mov esi, 0x6 ; 2nd arg: esi
mov rcx, qword ptr [rbp - 0x10] ; rcx <- address of my_function from stack
mov dword ptr [rbp - 0x14], eax
call rcx ; call address at rcx
mov eax, dword ptr [rbp - 0x14]
add rsp, 0x20
pop rbp
ret
请注意,这两个函数调用本质上是相同的。他们使用相同的程序集。两次实际调用都使用call op 调用。唯一的区别是地址第一次是硬编码的,而第二次地址存储在rcx寄存器中。另请注意,代码没有任何异步。
C11 关于序列点的说法
当您开始对序列点进行推理时,您实际上会发现,在单个线程中,标准不允许您期望的那种异步行为。在大多数情况下,C11 约束编译器执行由序列点按顺序分隔的代码。在第 5.1.2.3 节(程序执行)中,程序的执行顺序被定义为一系列序列点。相关定义基本上在第 3 段中:
Sequenced before 是由单个线程执行的评估之间的不对称、传递、成对关系,这会在这些评估之间产生偏序。给定任意两个评估 A 和 B,如果 A 在 B 之前排序,则 A 的执行将在 B 的执行之前。
在那段的后面:
在表达式 A 和 B 的求值之间存在一个序列点意味着与 A 相关的每个值计算和副作用都在与 B 相关的每个值计算和副作用之前排序。
基本上,这确立了由序列点分隔的代码必须同步(按顺序)执行。但是,如果编译器可以推断出两段代码不能相互影响,该标准提供了一个输出,在第 4 段中:
在抽象机中,所有表达式都按照语义的规定进行评估。如果一个实际的实现可以推断出它的值没有被使用并且没有产生所需的副作用(包括调用函数或访问易失性对象引起的任何副作用),则它不需要评估表达式的一部分。
那么,函数指针是如何进入这个的呢?附录 C 阐明了序列点位于表达式语句之间的含义,表达式语句本质上是以分号结尾的语句(参见 6.8.3)。这包括函数调用。
如何阻止函数指针的异步执行
考虑两个顺序函数调用:
f();
g();
都不接受参数,所以推理有点简单。调用必须按顺序执行,除非编译器可以推断f() 的任何副作用在g() 中未使用,反之亦然。编译器可以在函数中对此进行推理的唯一方法是编译器是否可以使用函数的代码。一般来说,这对于函数指针是不可能的,因为指针可以指向任何满足函数指针类型约束的函数。
请注意,在某些情况下,编译器可以推断出正确的函数(如果函数指针只分配一次并且存在于本地范围内),但通常情况并非如此。因此,编译器必须按照给出的顺序执行函数,并且第一个函数必须在第二个函数之前返回。
线程和协程库呢
C11 标准对线程有不同的规则。请注意,第 5.1.2.3 节将其自身限制为在单个线程中执行。使用堆栈的协程库本质上打破了 C11 机器模型,并且绑定到一组特定的实现(即:不需要移植到任何 C 环境)。协程库本质上必须提供自己的一组顺序保证。