在C中传递一个函数还是传递一个函数指针?
Pass a function or pass a function pointer in C?
(注意:链接另一个显示函数指针示例的答案没有帮助。我的问题正是关于不同答案中显示的多种方式,并试图理解它们之间的区别)
上下文:
我试图了解在 C(非 C++)中将一个函数作为参数传递给另一个函数的正确方法。我见过几种不同的方式,但我不清楚其中的区别。
我的环境
我是 运行 macOS
我的编译器是 GCC:
$ gcc --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include/c++/4.2.1
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
我在我的 Makefile 中使用 CFLAGS=-Wall -g -O0
。
我的代码示例
以下 4 个片段都产生相同的结果(至少是相同的可见输出)。请注意,示例之间的唯一区别在于声明和 execute
函数的调用。我在引号中包含了我最初调用这些样本中的每一个的方式,只是为了区分它们(所以命名可能是错误的)。
它们实际上只是以下所有 4 个排列:
- 正在声明函数
execute
以接收 void f()
或 void (*f)()
- 用
execute(print)
或 execute(&print)
调用函数 execute
请注意,在所有情况下,函数都是通过 f()
调用的,而不是通过 (*f)()
调用的。 但我也用 (*f)()
进行了测试,结果相同。所以 8 个排列,实际上(为了简洁这里只显示 4 个)
片段 1:"passing without a pointer, receiving without a pointer"
#include <stdio.h>
void execute(void f()) {
printf("2 %p %lu\n", f, sizeof(f));
f();
}
void print() {
printf("Hello!\n");
}
int main() {
printf("1 %p %lu\n", print, sizeof(print));
execute(print);
return 0;
}
片段 2:"passing with a pointer, and receiving with a pointer"
#include <stdio.h>
void execute(void (*f)()) {
printf("2 %p %lu\n", f, sizeof(f));
f();
}
void print() {
printf("Hello!\n");
}
int main() {
printf("1 %p %lu\n", print, sizeof(print));
execute(&print);
return 0;
}
片段 3:"passing with a pointer, and receiving without a pointer"
#include <stdio.h>
void execute(void (f)()) {
printf("2 %p %lu\n", f, sizeof(f));
f();
}
void print() {
printf("Hello!\n");
}
int main() {
printf("1 %p %lu\n", print, sizeof(print));
execute(&print);
return 0;
}
片段 4:"passing without a pointer, and receiving with a pointer"
#include <stdio.h>
void execute(void (*f)()) {
printf("2 %p %lu\n", f, sizeof(f));
f();
}
void print() {
printf("Hello!\n");
}
int main() {
printf("1 %p %lu\n", print, sizeof(print));
execute(print);
return 0;
}
对于所有示例:
- 程序编译并运行,没有任何错误或警告
Hello
打印正确
%p
中打印的值在第一次和第二次打印中都是相同的
sizeof
在第一次打印时总是 1
sizeof
在第二次打印时总是 8
我读过的内容
我已经阅读了几个示例(Wikipedia、Whosebug 以及 Whosebug 答案中链接的其他资源),其中很多都展示了不同的示例。 我的提问正是为了理解这些差异。
Wikipedia article about function pointers 显示了一个类似于片段 4 的示例(由我简化):
#include <math.h>
#include <stdio.h>
double compute_sum(double (*funcp)(double), double lo, double hi) {
// ... more code
double y = funcp(x);
// ... more code
}
int main(void) {
compute_sum(sin, 0.0, 1.0);
compute_sum(cos, 0.0, 1.0);
return 0;
}
注:
- 参数作为
compute_sum(sin, 0.0, 1.0)
传递(没有 &
on sin
)
- 参数声明
double (*funcp)(double)
(和*
)
- 参数被调用为
funcp(x)
(没有*
,所以没有(*funcp)(x)
)
同一篇维基百科文章后面的例子告诉我们传递函数时的 &
不是必需的,没有任何进一步的解释:
// This declares 'F', a function that accepts a 'char' and returns an 'int'. Definition is elsewhere.
int F(char c);
// This defines 'Fn', a type of function that accepts a 'char' and returns an 'int'.
typedef int Fn(char c);
// This defines 'fn', a variable of type pointer-to-'Fn', and assigns the address of 'F' to it.
Fn *fn = &F; // Note '&' not required - but it highlights what is being done.
// ... more code
// This defines 'Call', a function that accepts a pointer-to-'Fn', calls it, and returns the result
int Call(Fn *fn, char c) {
return fn(c);
} // Call(fn, c)
// This calls function 'Call', passing in 'F' and assigning the result to 'call'
int call = Call(&F, 'A'); // Again, '&' is not required
// ... more code
注:
- 参数作为
Call(&F, 'A')
传递(with &
on F
)
- 参数声明
Fn *fn
(和*
)
- 参数被调用为
fn(c)
(without (*fn)
)
This answer:
- 参数作为
func(print)
传递(没有 &
on print
)
- 参数声明
void (*f)(int)
(和*
)
- 参数被调用为
(*f)(ctr)
(with (*fn)
)
This answer 显示 2 个示例:
- 第一个就像我的片段 1:
- 参数作为
execute(print)
传递(没有 &
)
- 参数声明
void f()
(没有*
)
- 参数被调用为
f()
(without *
)
- 第二个就像我的片段 2:
- 参数被传递为
execute(&print)
(with &
)
- 参数声明
void (*f)()
(和*
)
- 参数被调用为
f()
(without *
)
This answer:
- 没有关于如何传递函数参数的示例(带或不带
&
)
- 参数声明
int (*functionPtr)(int, int)
(和*
)
- 参数被调用为
(*functionPtr)(2, 3)
(with *
)
This linked material I found in one of the answers(实际上是 C++,但它不使用任何 C++ 特定的函数指针):
- 参数被传递为
&Minus
(with &
)
- 参数声明
float (*pt2Func)(float, float)
(和*
)
- 参数被调用为
pt2Func(a, b)
(without *
)
我在这里提供了 7 个示例,这些示例提供了至少 5 种使用或不使用 &
和 *
的组合,当将函数作为 argument/receiving 传递时将函数作为 [=341] =] 作为参数接收的函数。
我从这一切中了解到的
我相信上一节表明,对于 Whosebug 上最相关的 questions/answers 以及我链接的其他材料(其中大部分链接来自 Whosebug 的答案),没有达成一致的解释.
似乎所有这 4 种方式都由编译器以相同的方式处理,或者在如此简单的示例中没有出现非常细微的差异。
我理解为什么第二个打印在片段 2 和片段 4 中将 sizeof(f)
打印为 8
。这是我的 64 位系统中的指针大小)。但是我不明白为什么即使在 execute
函数声明的参数没有 *
的情况下(片段 1 和 3),第二个打印也会打印指针的大小,并且不明白为什么在第一次打印中,函数变量 sizeof
等于 1
.
我的问题
- 我的 4 个片段之间是否存在实际差异?为什么他们的行为都完全一样?
- 如果存在实际的运行时差异,您能否解释一下它们各自发生了什么?
- 如果没有运行时差异,您能否解释一下编译器在每种情况下做了什么以使它们的行为相同(我假设编译器会看到
f(myFunc)
并实际使用 f(&myFunc)
, 或类似的东西。我想知道哪个是 "canonical" 方式)。
- 我应该使用 4 个片段中的哪一个,为什么?
- 我应该使用
f()
还是 (*f)()
调用通过参数接收的传递函数?
- 为什么在每个片段的第一次打印中,函数变量 (
sizeof(print)
) 的大小总是 1
?在这种情况下,我们实际得到的 sizeof 是什么? (它显然不是指针的大小,在我的 64 位机器中它是 8 个字节。如果我使用 sizeof(&print)
,我将得到指针的大小)。
- 为什么在片段 1 和 3 上,在第二次打印中,
sizeof(f)
给我 8(指针的大小),即使参数声明为 void (f)()
(所以没有 *
,我可以假设它不是一个指针)。
Is there an actual difference between all of my 4 snippets? Why all of them behave exactly the same?
所有四个代码片段都是相同的。
关于如何调用 execute
,这在 C11 standard:
的第 6.3.2.1p4 节中有所介绍
A function designator is an expression that has function type. Except when it is the
operand of the sizeof
operator, the _Alignof
operator, or the unary &
operator, a
function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’.
因此调用 execute(print)
或 execute(&print)
是相同的。
关于 execute
的参数,这在第 6.7.6.3p8 节中有介绍:
A declaration of a parameter as ‘‘function returning type’’ shall be adjusted to ‘‘pointer to
function returning type’’, as in 6.3.2.1
所以这意味着 void execute(void (*f)())
和 void execute(void f())
是一样的。
Which one of the 4 snippets should I use, and why?
这往往是一种风格问题,但我个人会将变量和参数声明为指向函数的指针类型而不是函数类型,并且我会传递不带寻址运算符的函数名称。
Should I invoke the passed function received via parameter with f() or with (*f)()
这也是风格问题。我会选择 f()
,因为它更容易阅读。
Why in the first print of each snippet, the size of the function variable (sizeof(print)) is always 1? What are we actually getting the sizeof in this case? (it obviously not the size of a pointer, which would be 8 bytes in my 64-bit machine. I would get the size of a pointer if I used sizeof(&print))
根据第 6.5.3.4p1 节,明确禁止在函数指示符上使用 sizeof
:
The sizeof
operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member. The _Alignof
operator shall not be applied to a function type or an incomplete type.
这样做会调用 undefined behavior。
Why on snippets 1 and 3, in the second print, sizeof(f) gives me 8 (the size of a pointer), even though the parameter is declared as void (f)() (so without a *, I could assume it is not a pointer)
这可以追溯到上面的 6.7.6.3p8,即函数类型的函数参数被转换为函数指针类型,所以这就是你得到的大小。
来自 C 标准(6.3.2.1 左值、数组和函数指示符)
4 A function designator is an expression that has function type.
Except when it is the operand of the sizeof operator65) or the unary &
operator, a function designator with type ‘‘function returning type’’
is converted to an expression that has type ‘‘pointer to function
returning type’’
And (6.7.6.3 函数声明符(包括原型))
8 A declaration of a parameter as ‘‘function returning type’’ shall be
adjusted to ‘‘pointer to function returning type’’, as in 6.3.2.1.
例如这两个函数声明
void execute(void f( void ) );
和
void execute(void ( *f )( void ) );
声明相同的一个函数并且可以出现在一个翻译单元中,尽管其中一个是多余的。
请注意,您不能将 sizeof
运算符应用于函数指示符。
来自 C 标准(6.5.3.4 sizeof 和 alignof 运算符)
1 The sizeof operator shall not be applied to an expression that has
function type or an incomplete type, to the parenthesized name of
such a type, or to an expression that designates a bit-field member.
The alignof operator shall not be applied to a function type or an
incomplete type.
但是你可以把它应用到一个函数指针上。
此外,由于函数指示符被隐式转换为函数指针,因此您可以将多个取消引用运算符 * 应用于表达式中使用的函数指示符。
这是一个演示程序
#include <stdio.h>
void execute( void ( *f )( void ) );
void execute(void f( void ) )
{
f();
}
void print( void )
{
puts( "Hello" );
}
int main( void )
{
execute( **********print );
return 0;
}
形式上,此规则适用于调用代码,C17 6.3.2.1/4:
A function designator is an expression that has function type. Except when it is the
operand of the sizeof operator, or the unary & operator, a function designator with
type "function returning type" is converted to an expression that has type "pointer to function returning type".
非正式地,这可能被称为 "function decay" 或类似的,因为它的工作原理与 "array decay".
非常相似
在您的例子中,print
是调用代码中的函数指示符。如果你只是输入 print
,那么上面的规则就会生效并将函数变成一个函数指针。如果您键入 &print
,这是上述规则的一个例外,但无论如何您最终都会得到一个函数指针。
所以在实践中 print
或 &print
只是风格问题。
函数内部,然后正式C17 6.7.6.3/8对函数参数有相同的规则:
A declaration of a parameter as "function returning type" shall be adjusted to "pointer to function returning _type", as in 6.3.2.1.
如果您键入 void(*f)()
,则上述内容不适用,但无论如何您最终都会得到一个函数指针。
总而言之,将实际函数用作表达式的一部分几乎是不可能的,因为它总是会衰减为函数指针。而且你不能将函数传递给其他函数,只能传递函数指针。
- Is there an actual difference between all of my 4 snippets?
唯一的区别是风格和语法。这些差异仅从 "language lawyer" 的角度来看才有趣 - 从应用程序程序员的角度来看,它们是相同的。
If there is an actual runtime difference
没有
If there is no runtime difference, could you explain what the compiler did in each case to make all of them behave the same (I assume that the compiler would see f(myFunc) and actually use f(&myFunc), or something like this. I would like to know which one is the "canonical" way).
反汇编它们,你会发现相同的机器代码。 "canonical way" 是其中的 none 个。
首先你遇到了空括号的问题,这意味着 "accept any parameter"。这是 C 中过时的样式,不应使用。不要与 (void)
和 ()
相同的 C++ 混淆。这需要修复。然后,进行如下操作:
- Which one of the 4 snippets should I use, and why?
规范版本是:
typedef void func_t (void);
...
void execute(func_t* f);
替代规范样式,也可以接受并且可能更常见:
typedef void (*func_t) (void);
...
void execute(func_t f);
使用这两种形式中的任何一种。至于调用代码,用print
还是&print
都无所谓,这是主观的编码风格。
- Should I invoke the passed function received via parameter with f() or with (*f)()?
f()
更具可读性,因此您应该使用它。
- Why in the first print of each snippet, the size of the function variable (sizeof(print)) is always 1? What are we actually getting the sizeof in this case?
它不是有效的 C,因此任何人都可以猜测您的编译器是如何让它通过的。这是对 C 语言 6.5.3.4 "The sizeof operator shall not be applied to an expression that has function type" 的公然约束违反。
因此您的编译器要么很糟糕,要么被配置为保留在 "sucky mode" 中。我建议使用 gcc 并将其配置为带有 gcc -std=c11 -pedantic-errors
.
的 C 编译器
5.
参见 4).
不,完全没有区别。所有代码片段的行为都相同,因为根据 C 标准,函数标识符和指向它的指针虽然属于不同类型,但处理方式完全相同。也就是说,f
、*f
和 &f
在您的情况下可以互换。没有 运行 时间差,程序将被编译为使用四个代码段中每个代码段中的函数地址,因此编译器实际上对每个代码段都做同样的事情。
参见 C99 标准的 §6.3.2.1 第 4 点(第 46 页 here):
A function designator is an expression that has function type. Except when it is the operand of the sizeof
operator 54) or the unary &
operator, a function designator with type "function returning type" isconverted to an expression that has type "pointer to function returning type".
54) Because this conversion does not occur, the operand of the sizeof
operator remains a function designator and violates the constraint in 6.5.3.4.
你最喜欢的,真的。经常看到函数定义为采用指向函数的指针,然后调用传递函数名称本身。这是风格问题。
没有区别,你可以在任何情况下调用它。
标准禁止在函数上使用 sizeof()
。因此,这是未定义的行为,您看到的值没有意义。
参见 C99 标准的 §6.5.3.4(第 80 页 here):
The sizeof operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member.
同4.
根据第 1 点,您可以通过使用 objdump
查看生成的二进制文件来自行验证编译器在所有情况下总是做完全相同的事情。在所有情况下,我的机器上的结果如下(请参阅我用 <<<
标记的评论):
$ gcc -O1 x.c
$ objdump -Mintel -d a.out
...
0000000000000705 <execute>:
705: 53 push rbx
706: 48 89 fb mov rbx,rdi
709: ba 08 00 00 00 mov edx,0x8 # <<< move func address to rbx
70e: 48 89 fe mov rsi,rdi
711: 48 8d 3d e3 00 00 00 lea rdi,[rip+0xe3] # 7fb <_IO_stdin_used+0xb>
718: b8 00 00 00 00 mov eax,0x0
71d: e8 7e fe ff ff call 5a0 <printf@plt>
722: b8 00 00 00 00 mov eax,0x0
727: ff d3 call rbx # <<< call the func by its address
729: 5b pop rbx
72a: c3 ret
000000000000072b <main>:
72b: 48 83 ec 08 sub rsp,0x8
72f: ba 01 00 00 00 mov edx,0x1
734: 48 8d 35 b5 ff ff ff lea rsi,[rip+0xffffffffffffffb5] # 6f0 <print>
73b: 48 8d 3d c3 00 00 00 lea rdi,[rip+0xc3] # 805 <_IO_stdin_used+0x15>
742: b8 00 00 00 00 mov eax,0x0
747: e8 54 fe ff ff call 5a0 <printf@plt>
74c: 48 8d 3d 9d ff ff ff lea rdi,[rip+0xffffffffffffff9d] # <<< pass the func address
# (rip+...) == rip - 0x4e == 0x753 - 0x4e == 0x705
753: e8 ad ff ff ff call 705 <execute>
758: b8 00 00 00 00 mov eax,0x0
75d: 48 83 c4 08 add rsp,0x8
761: c3 ret
762: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
769: 00 00 00
76c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
...
由于大部分问题已得到解答:
Why in the first print of each snippet, the size of the function
variable (sizeof(print)) is always 1? What are we actually getting the
sizeof in this case? (it obviously not the size of a pointer, which
would be 8 bytes in my 64-bit machine. I would get the size of a
pointer if I used sizeof(&print)).
sizeof(function) 在 C 中未定义。您收到的结果 (1
) 是 gcc
扩展,它允许对衰减函数进行指针运算(用于地址计算)它确实在可移植代码开发中没有任何实际用途,但在嵌入式世界中有时会派上用场。
您的 printf
格式字符串错误,printf 调用 UB。
(注意:链接另一个显示函数指针示例的答案没有帮助。我的问题正是关于不同答案中显示的多种方式,并试图理解它们之间的区别)
上下文:
我试图了解在 C(非 C++)中将一个函数作为参数传递给另一个函数的正确方法。我见过几种不同的方式,但我不清楚其中的区别。
我的环境
我是 运行 macOS
我的编译器是 GCC:
$ gcc --version
Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdk/usr/include/c++/4.2.1
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin
我在我的 Makefile 中使用 CFLAGS=-Wall -g -O0
。
我的代码示例
以下 4 个片段都产生相同的结果(至少是相同的可见输出)。请注意,示例之间的唯一区别在于声明和 execute
函数的调用。我在引号中包含了我最初调用这些样本中的每一个的方式,只是为了区分它们(所以命名可能是错误的)。
它们实际上只是以下所有 4 个排列:
- 正在声明函数
execute
以接收void f()
或void (*f)()
- 用
execute(print)
或execute(&print)
调用函数
execute
请注意,在所有情况下,函数都是通过 f()
调用的,而不是通过 (*f)()
调用的。 但我也用 (*f)()
进行了测试,结果相同。所以 8 个排列,实际上(为了简洁这里只显示 4 个)
片段 1:"passing without a pointer, receiving without a pointer"
#include <stdio.h> void execute(void f()) { printf("2 %p %lu\n", f, sizeof(f)); f(); } void print() { printf("Hello!\n"); } int main() { printf("1 %p %lu\n", print, sizeof(print)); execute(print); return 0; }
片段 2:"passing with a pointer, and receiving with a pointer"
#include <stdio.h> void execute(void (*f)()) { printf("2 %p %lu\n", f, sizeof(f)); f(); } void print() { printf("Hello!\n"); } int main() { printf("1 %p %lu\n", print, sizeof(print)); execute(&print); return 0; }
片段 3:"passing with a pointer, and receiving without a pointer"
#include <stdio.h> void execute(void (f)()) { printf("2 %p %lu\n", f, sizeof(f)); f(); } void print() { printf("Hello!\n"); } int main() { printf("1 %p %lu\n", print, sizeof(print)); execute(&print); return 0; }
片段 4:"passing without a pointer, and receiving with a pointer"
#include <stdio.h> void execute(void (*f)()) { printf("2 %p %lu\n", f, sizeof(f)); f(); } void print() { printf("Hello!\n"); } int main() { printf("1 %p %lu\n", print, sizeof(print)); execute(print); return 0; }
对于所有示例:
- 程序编译并运行,没有任何错误或警告
Hello
打印正确%p
中打印的值在第一次和第二次打印中都是相同的sizeof
在第一次打印时总是 1sizeof
在第二次打印时总是 8
我读过的内容
我已经阅读了几个示例(Wikipedia、Whosebug 以及 Whosebug 答案中链接的其他资源),其中很多都展示了不同的示例。 我的提问正是为了理解这些差异。
Wikipedia article about function pointers 显示了一个类似于片段 4 的示例(由我简化):
#include <math.h>
#include <stdio.h>
double compute_sum(double (*funcp)(double), double lo, double hi) {
// ... more code
double y = funcp(x);
// ... more code
}
int main(void) {
compute_sum(sin, 0.0, 1.0);
compute_sum(cos, 0.0, 1.0);
return 0;
}
注:
- 参数作为
compute_sum(sin, 0.0, 1.0)
传递(没有&
onsin
) - 参数声明
double (*funcp)(double)
(和*
) - 参数被调用为
funcp(x)
(没有*
,所以没有(*funcp)(x)
)
同一篇维基百科文章后面的例子告诉我们传递函数时的 &
不是必需的,没有任何进一步的解释:
// This declares 'F', a function that accepts a 'char' and returns an 'int'. Definition is elsewhere.
int F(char c);
// This defines 'Fn', a type of function that accepts a 'char' and returns an 'int'.
typedef int Fn(char c);
// This defines 'fn', a variable of type pointer-to-'Fn', and assigns the address of 'F' to it.
Fn *fn = &F; // Note '&' not required - but it highlights what is being done.
// ... more code
// This defines 'Call', a function that accepts a pointer-to-'Fn', calls it, and returns the result
int Call(Fn *fn, char c) {
return fn(c);
} // Call(fn, c)
// This calls function 'Call', passing in 'F' and assigning the result to 'call'
int call = Call(&F, 'A'); // Again, '&' is not required
// ... more code
注:
- 参数作为
Call(&F, 'A')
传递(with&
onF
) - 参数声明
Fn *fn
(和*
) - 参数被调用为
fn(c)
(without(*fn)
)
This answer:
- 参数作为
func(print)
传递(没有&
onprint
) - 参数声明
void (*f)(int)
(和*
) - 参数被调用为
(*f)(ctr)
(with(*fn)
)
This answer 显示 2 个示例:
- 第一个就像我的片段 1:
- 参数作为
execute(print)
传递(没有&
) - 参数声明
void f()
(没有*
) - 参数被调用为
f()
(without*
)
- 参数作为
- 第二个就像我的片段 2:
- 参数被传递为
execute(&print)
(with&
) - 参数声明
void (*f)()
(和*
) - 参数被调用为
f()
(without*
)
- 参数被传递为
This answer:
- 没有关于如何传递函数参数的示例(带或不带
&
) - 参数声明
int (*functionPtr)(int, int)
(和*
) - 参数被调用为
(*functionPtr)(2, 3)
(with*
)
This linked material I found in one of the answers(实际上是 C++,但它不使用任何 C++ 特定的函数指针):
- 参数被传递为
&Minus
(with&
) - 参数声明
float (*pt2Func)(float, float)
(和*
) - 参数被调用为
pt2Func(a, b)
(without*
)
我在这里提供了 7 个示例,这些示例提供了至少 5 种使用或不使用 &
和 *
的组合,当将函数作为 argument/receiving 传递时将函数作为 [=341] =] 作为参数接收的函数。
我从这一切中了解到的
我相信上一节表明,对于 Whosebug 上最相关的 questions/answers 以及我链接的其他材料(其中大部分链接来自 Whosebug 的答案),没有达成一致的解释.
似乎所有这 4 种方式都由编译器以相同的方式处理,或者在如此简单的示例中没有出现非常细微的差异。
我理解为什么第二个打印在片段 2 和片段 4 中将 sizeof(f)
打印为 8
。这是我的 64 位系统中的指针大小)。但是我不明白为什么即使在 execute
函数声明的参数没有 *
的情况下(片段 1 和 3),第二个打印也会打印指针的大小,并且不明白为什么在第一次打印中,函数变量 sizeof
等于 1
.
我的问题
- 我的 4 个片段之间是否存在实际差异?为什么他们的行为都完全一样?
- 如果存在实际的运行时差异,您能否解释一下它们各自发生了什么?
- 如果没有运行时差异,您能否解释一下编译器在每种情况下做了什么以使它们的行为相同(我假设编译器会看到
f(myFunc)
并实际使用f(&myFunc)
, 或类似的东西。我想知道哪个是 "canonical" 方式)。
- 我应该使用 4 个片段中的哪一个,为什么?
- 我应该使用
f()
还是(*f)()
调用通过参数接收的传递函数? - 为什么在每个片段的第一次打印中,函数变量 (
sizeof(print)
) 的大小总是1
?在这种情况下,我们实际得到的 sizeof 是什么? (它显然不是指针的大小,在我的 64 位机器中它是 8 个字节。如果我使用sizeof(&print)
,我将得到指针的大小)。 - 为什么在片段 1 和 3 上,在第二次打印中,
sizeof(f)
给我 8(指针的大小),即使参数声明为void (f)()
(所以没有*
,我可以假设它不是一个指针)。
Is there an actual difference between all of my 4 snippets? Why all of them behave exactly the same?
所有四个代码片段都是相同的。
关于如何调用 execute
,这在 C11 standard:
A function designator is an expression that has function type. Except when it is the operand of the
sizeof
operator, the_Alignof
operator, or the unary&
operator, a function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’.
因此调用 execute(print)
或 execute(&print)
是相同的。
关于 execute
的参数,这在第 6.7.6.3p8 节中有介绍:
A declaration of a parameter as ‘‘function returning type’’ shall be adjusted to ‘‘pointer to function returning type’’, as in 6.3.2.1
所以这意味着 void execute(void (*f)())
和 void execute(void f())
是一样的。
Which one of the 4 snippets should I use, and why?
这往往是一种风格问题,但我个人会将变量和参数声明为指向函数的指针类型而不是函数类型,并且我会传递不带寻址运算符的函数名称。
Should I invoke the passed function received via parameter with f() or with (*f)()
这也是风格问题。我会选择 f()
,因为它更容易阅读。
Why in the first print of each snippet, the size of the function variable (sizeof(print)) is always 1? What are we actually getting the sizeof in this case? (it obviously not the size of a pointer, which would be 8 bytes in my 64-bit machine. I would get the size of a pointer if I used sizeof(&print))
根据第 6.5.3.4p1 节,明确禁止在函数指示符上使用 sizeof
:
The
sizeof
operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member. The_Alignof
operator shall not be applied to a function type or an incomplete type.
这样做会调用 undefined behavior。
Why on snippets 1 and 3, in the second print, sizeof(f) gives me 8 (the size of a pointer), even though the parameter is declared as void (f)() (so without a *, I could assume it is not a pointer)
这可以追溯到上面的 6.7.6.3p8,即函数类型的函数参数被转换为函数指针类型,所以这就是你得到的大小。
来自 C 标准(6.3.2.1 左值、数组和函数指示符)
4 A function designator is an expression that has function type. Except when it is the operand of the sizeof operator65) or the unary & operator, a function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’
And (6.7.6.3 函数声明符(包括原型))
8 A declaration of a parameter as ‘‘function returning type’’ shall be adjusted to ‘‘pointer to function returning type’’, as in 6.3.2.1.
例如这两个函数声明
void execute(void f( void ) );
和
void execute(void ( *f )( void ) );
声明相同的一个函数并且可以出现在一个翻译单元中,尽管其中一个是多余的。
请注意,您不能将 sizeof
运算符应用于函数指示符。
来自 C 标准(6.5.3.4 sizeof 和 alignof 运算符)
1 The sizeof operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member. The alignof operator shall not be applied to a function type or an incomplete type.
但是你可以把它应用到一个函数指针上。
此外,由于函数指示符被隐式转换为函数指针,因此您可以将多个取消引用运算符 * 应用于表达式中使用的函数指示符。
这是一个演示程序
#include <stdio.h>
void execute( void ( *f )( void ) );
void execute(void f( void ) )
{
f();
}
void print( void )
{
puts( "Hello" );
}
int main( void )
{
execute( **********print );
return 0;
}
形式上,此规则适用于调用代码,C17 6.3.2.1/4:
A function designator is an expression that has function type. Except when it is the operand of the sizeof operator, or the unary & operator, a function designator with type "function returning type" is converted to an expression that has type "pointer to function returning type".
非正式地,这可能被称为 "function decay" 或类似的,因为它的工作原理与 "array decay".
非常相似在您的例子中,print
是调用代码中的函数指示符。如果你只是输入 print
,那么上面的规则就会生效并将函数变成一个函数指针。如果您键入 &print
,这是上述规则的一个例外,但无论如何您最终都会得到一个函数指针。
所以在实践中 print
或 &print
只是风格问题。
函数内部,然后正式C17 6.7.6.3/8对函数参数有相同的规则:
A declaration of a parameter as "function returning type" shall be adjusted to "pointer to function returning _type", as in 6.3.2.1.
如果您键入 void(*f)()
,则上述内容不适用,但无论如何您最终都会得到一个函数指针。
总而言之,将实际函数用作表达式的一部分几乎是不可能的,因为它总是会衰减为函数指针。而且你不能将函数传递给其他函数,只能传递函数指针。
- Is there an actual difference between all of my 4 snippets?
唯一的区别是风格和语法。这些差异仅从 "language lawyer" 的角度来看才有趣 - 从应用程序程序员的角度来看,它们是相同的。
If there is an actual runtime difference
没有
If there is no runtime difference, could you explain what the compiler did in each case to make all of them behave the same (I assume that the compiler would see f(myFunc) and actually use f(&myFunc), or something like this. I would like to know which one is the "canonical" way).
反汇编它们,你会发现相同的机器代码。 "canonical way" 是其中的 none 个。
首先你遇到了空括号的问题,这意味着 "accept any parameter"。这是 C 中过时的样式,不应使用。不要与 (void)
和 ()
相同的 C++ 混淆。这需要修复。然后,进行如下操作:
- Which one of the 4 snippets should I use, and why?
规范版本是:
typedef void func_t (void);
...
void execute(func_t* f);
替代规范样式,也可以接受并且可能更常见:
typedef void (*func_t) (void);
...
void execute(func_t f);
使用这两种形式中的任何一种。至于调用代码,用print
还是&print
都无所谓,这是主观的编码风格。
- Should I invoke the passed function received via parameter with f() or with (*f)()?
f()
更具可读性,因此您应该使用它。
- Why in the first print of each snippet, the size of the function variable (sizeof(print)) is always 1? What are we actually getting the sizeof in this case?
它不是有效的 C,因此任何人都可以猜测您的编译器是如何让它通过的。这是对 C 语言 6.5.3.4 "The sizeof operator shall not be applied to an expression that has function type" 的公然约束违反。
因此您的编译器要么很糟糕,要么被配置为保留在 "sucky mode" 中。我建议使用 gcc 并将其配置为带有 gcc -std=c11 -pedantic-errors
.
5.
参见 4).
不,完全没有区别。所有代码片段的行为都相同,因为根据 C 标准,函数标识符和指向它的指针虽然属于不同类型,但处理方式完全相同。也就是说,
f
、*f
和&f
在您的情况下可以互换。没有 运行 时间差,程序将被编译为使用四个代码段中每个代码段中的函数地址,因此编译器实际上对每个代码段都做同样的事情。参见 C99 标准的 §6.3.2.1 第 4 点(第 46 页 here):
A function designator is an expression that has function type. Except when it is the operand of the
sizeof
operator 54) or the unary&
operator, a function designator with type "function returning type" isconverted to an expression that has type "pointer to function returning type".54) Because this conversion does not occur, the operand of the
sizeof
operator remains a function designator and violates the constraint in 6.5.3.4.你最喜欢的,真的。经常看到函数定义为采用指向函数的指针,然后调用传递函数名称本身。这是风格问题。
没有区别,你可以在任何情况下调用它。
标准禁止在函数上使用
sizeof()
。因此,这是未定义的行为,您看到的值没有意义。参见 C99 标准的 §6.5.3.4(第 80 页 here):
The sizeof operator shall not be applied to an expression that has function type or an incomplete type, to the parenthesized name of such a type, or to an expression that designates a bit-field member.
同4.
根据第 1 点,您可以通过使用 objdump
查看生成的二进制文件来自行验证编译器在所有情况下总是做完全相同的事情。在所有情况下,我的机器上的结果如下(请参阅我用 <<<
标记的评论):
$ gcc -O1 x.c
$ objdump -Mintel -d a.out
...
0000000000000705 <execute>:
705: 53 push rbx
706: 48 89 fb mov rbx,rdi
709: ba 08 00 00 00 mov edx,0x8 # <<< move func address to rbx
70e: 48 89 fe mov rsi,rdi
711: 48 8d 3d e3 00 00 00 lea rdi,[rip+0xe3] # 7fb <_IO_stdin_used+0xb>
718: b8 00 00 00 00 mov eax,0x0
71d: e8 7e fe ff ff call 5a0 <printf@plt>
722: b8 00 00 00 00 mov eax,0x0
727: ff d3 call rbx # <<< call the func by its address
729: 5b pop rbx
72a: c3 ret
000000000000072b <main>:
72b: 48 83 ec 08 sub rsp,0x8
72f: ba 01 00 00 00 mov edx,0x1
734: 48 8d 35 b5 ff ff ff lea rsi,[rip+0xffffffffffffffb5] # 6f0 <print>
73b: 48 8d 3d c3 00 00 00 lea rdi,[rip+0xc3] # 805 <_IO_stdin_used+0x15>
742: b8 00 00 00 00 mov eax,0x0
747: e8 54 fe ff ff call 5a0 <printf@plt>
74c: 48 8d 3d 9d ff ff ff lea rdi,[rip+0xffffffffffffff9d] # <<< pass the func address
# (rip+...) == rip - 0x4e == 0x753 - 0x4e == 0x705
753: e8 ad ff ff ff call 705 <execute>
758: b8 00 00 00 00 mov eax,0x0
75d: 48 83 c4 08 add rsp,0x8
761: c3 ret
762: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
769: 00 00 00
76c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
...
由于大部分问题已得到解答:
Why in the first print of each snippet, the size of the function variable (sizeof(print)) is always 1? What are we actually getting the sizeof in this case? (it obviously not the size of a pointer, which would be 8 bytes in my 64-bit machine. I would get the size of a pointer if I used sizeof(&print)).
sizeof(function) 在 C 中未定义。您收到的结果 (1
) 是 gcc
扩展,它允许对衰减函数进行指针运算(用于地址计算)它确实在可移植代码开发中没有任何实际用途,但在嵌入式世界中有时会派上用场。
您的 printf
格式字符串错误,printf 调用 UB。