在C语言中,应该传递函数还是函数指针?

4
(注:链接另一个显示函数指针示例的答案没有帮助。我的问题正是关于不同答案中展示的多种方式,试图理解它们之间的差异)

背景:

我正在尝试理解在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函数的声明和调用。我在引号中包含了我最初调用每个样本的方式,只是为了区分它们(因此命名可能不正确)。

它们实际上只是execute函数的以下4种排列组合:

  • 将函数execute声明为接收void f()void (*f)()
  • 使用execute(print)execute(&print)调用函数execute

请注意,在所有情况下,函数都是使用f()调用的,而不是(*f)()。实际上有8种排列组合(仅出于简洁起见,此处仅显示4种)

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

我读到了什么

我读了几个例子(维基百科,StackOverflow,其他在StackOverflow答案中链接的资源),它们展示了不同的例子。 我的问题是为了理解这些差异

维基百科关于函数指针的文章展示了一个类似于代码片段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)不需要&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')F 带有 &
  • 参数声明为 Fn *fn带有 *
  • 参数调用为 fn(c)不带 (*fn)

这个答案

  • 参数传递为 func(print)不带 &
  • 参数声明为 void (*f)(int)带有 *
  • 参数调用为 (*f)(ctr)带有 (*fn)

这个答案 显示了两个示例:

  • 第一个示例类似于我的代码片段1:
    • 参数传递为execute(print)没有&
    • 参数声明为void f()没有*
    • 参数调用为f()没有*
  • 第二个示例类似于我的代码片段2:
    • 参数传递为execute(&print)&
    • 参数声明为void (*f)()*
    • 参数调用为f()没有*

这个答案

  • 没有关于如何传递函数参数的示例(带或不带&
  • 参数声明为int (*functionPtr)(int, int) *
  • 参数调用为(*functionPtr)(2, 3) *

我在其中一个答案中找到的链接材料(实际上是C ++,但它不使用任何涉及函数指针的C ++ 特定内容):

  • 参数作为&Minus传递( &
  • 参数声明为float (*pt2Func)(float, float) *
  • 参数调用为pt2Func(a, b)不带 *

我提供了7个例子,至少提供了5种使用或不使用&*的组合,当作为参数传递函数/接收函数作为参数/调用作为参数接收的函数。

从所有这些中我所理解的

我认为之前的部分显示出在StackOverflow上关于最相关问题/答案没有达成一致的解释,在其他我链接的材料上也是如此(其中大多数链接在StackOverflow的答案中)。

似乎编译器处理所有这4种方式时要么相同,要么存在非常微妙的差异,在这样一个简单的示例中不会出现。

我理解为什么第二个打印在代码段2和4中将sizeof(f)打印为8。那是我的64位系统中指针大小)。但我不明白为什么第二个打印在execute函数声明参数而没有*的情况下也会打印指针的大小(代码段1和3),并且不理解为什么在第一个打印中函数变量的sizeof等于1

我的问题

  1. 这4个代码片段之间是否存在实际差异?为什么它们的行为都完全相同?
    • 如果存在实际运行时差异,您能解释一下每个代码片段中发生了什么吗?
    • 如果没有运行时差异,您能解释编译器在每种情况下执行了什么操作以使它们的行为相同吗(我假设编译器会看到f(myFunc)并实际使用f(&myFunc)或类似的内容。我想知道哪种是“规范”的方式)。
  2. 这4个代码片段中应该使用哪一个,为什么?
  3. 我应该使用f()还是(*f)()来调用通过参数接收的传递函数?
  4. 为什么在每个代码片段的第一个打印输出中,函数变量的大小(sizeof(print))总是1?在这种情况下,我们实际上得到的是什么sizeof?(显然不是指针的大小,在我的64位机器上,指针的大小为8个字节。如果我使用sizeof(&print),我将得到指针的大小)。
  5. 为什么在代码片段1和3中,在第二个打印输出中,sizeof(f)给出了8(指针的大小),尽管参数声明为void (f)()(因此没有*,我可以假设它不是一个指针)。

3
函数会像数组一样衰变成函数指针。这就回答了你的问题:为什么函数指针定义可以使用任意数量的 & 或 *?(原文链接:https://dev59.com/dmw15IYBdhLWcg3wA24A) - Lundin
1
因为“不集中注意力”而关闭。在该网站上,“不集中注意力”的定义是“如果您的问题有许多有效答案(但无法确定哪个 - 如果有的话 - 是正确的)”。这似乎不适用于此处,我不是在征求意见,而是在寻求解释。将其分成5个较小的问题只是引导答案的一种方式。如果我必须创建5个不同的问题,并具有相同的长背景说明,那将比将其全部分组要糟糕得多。 - Rafael Eyng
虽然我在某种程度上同意你的观点,但“我应该使用哪个4个代码片段中的哪一个,为什么?”这一部分可能被认为过于宽泛/主观,问题3也是如此。 - YakovL
@YakovL 我现在明白你的观点了。但是当我提问的时候,我还不知道它们都是相同的。因此这里的答案是“它并不重要,它们都是一样的,只是风格上的区别”,而不是“使用第一种方式”。所以,再次强调,这不是主观的看法。 - Rafael Eyng
5个回答

4
所有四个代码片段都是相同的。
关于如何调用execute,在C11 standard的6.3.2.1p4节中有所涉及:
“函数指示符是具有函数类型的表达式。除了它是sizeof运算符、_Alignof运算符或一元&运算符的操作数时,带有类型“返回类型的函数”的函数指示符被转换为具有类型“指向返回类型的函数的指针”的表达式。”
因此,由于这个原因,调用execute(print)execute(&print)是相同的。
关于execute的参数,这在6.7.6.3p8节中有所涉及:

将参数声明为“返回类型的函数”应调整为“返回类型的指针函数”,如6.3.2.1中所述。

这意味着void execute(void (*f)())void execute(void f())是相同的。

我应该使用4个片段中的哪一个?为什么?

这往往是一个风格问题,但我个人会将变量和参数声明为指向函数的指针类型,而不是函数类型,并且我会传递函数名称而不使用地址运算符。

我应该使用f()还是(*f)()来调用通过参数接收的传递函数?

这也是一个风格问题。我会选择f(),因为它更容易阅读。

为什么在每个片段的第一个打印中,函数变量的大小(sizeof(print))始终为1?在这种情况下,我们实际上得到了什么样的sizeof?(显然不是指针的大小,在我的64位机器上将是8字节。如果我使用sizeof(&print),我会得到指针的大小)

根据第6.5.3.4p1节的规定,sizeof不能应用于具有函数类型或不完整类型的表达式,括号中带有此类类型的名称,或指定位域成员的表达式。

不能对具有函数类型或不完整类型的表达式应用sizeof运算符,也不能对带有此类类型名称的表达式或指定位域成员的表达式应用该运算符。不能对函数类型或不完整类型应用_Alignof运算符。

这样做会导致未定义行为

在片段1和3中,为什么在第二个打印中,sizeof(f)给出了8(指针大小),即使参数声明为void(f)()(所以没有*,我可以假设它不是指针)?

这回到了上面的6.7.6.3p8,即将函数类型的函数参数转换为指向函数的指针类型,因此您得到的是指针的大小。


很好的回答。你能提供你引用的标准的版本吗?如果能附上链接就更完美了。 - Rafael Eyng
1
@RafaelEyng 这是C11。我放了一个链接,但它没有被正确指定。现在你应该能看到链接了。 - dbush

3
  1. 没有任何区别。因为根据C标准,函数标识符和指向它的指针虽然类型不同,但被视为完全相同。也就是说,在这种情况下,f*f&f是可以互换的。没有运行时差异,程序将被编译为在每个代码段中使用函数地址,所以编译器对于您的每个片段实际上做了相同的事情。

    请参见C99标准的§6.3.2.1点4(此处第46页 链接):

    函数标识符是具有函数类型的表达式。除非它是sizeof操作符54)的操作数或一元&操作符的操作数,否则带有类型“返回类型为函数”的函数标识符将转换为具有类型“指向返回类型为函数的指针”的表达式。

    54)由于此转换未发生,sizeof操作符的操作数仍然是函数标识符,并违反了6.5.3.4中的约束。

  2. 喜欢哪个就用哪个即可,这是常见的方式。函数的定义通常会要求将指向函数的指针作为参数,并传递函数名称本身。

  3. 无论哪种情况,都可以调用它。

  4. 在函数上使用sizeof()是被禁止的。因此,这是未定义行为,所看到的值没有意义。

    请参见C99标准的§6.5.3.4(此处第80页 链接):

    sizeof运算符不得应用于具有函数类型的表达式或不完全类型,也不得应用于该类型的括号名称或指定位字段成员的表达式。

  5. 同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]

 ...

优秀的答案!我认为您对第5点的回答不正确,因为“与4相同”。答案4提到了未定义行为。第5个问题似乎更多地涉及将 f 处理为 (*f),也就是说,它无论如何都是指针,因此大小为 8。大小为1的情况是关于未定义行为的。 - Rafael Eyng
1
@RafaelEyng 是的,这是未定义行为,所以发生的情况取决于编译器的实现。即使如此,同一编译器在不同上下文中可能会给出不同的答案,因为由于它是未定义行为,某些编译器假设可能会被违反,并且这可能会导致某些情况下输出不同。executef 的类型是 void f(),因此是函数类型,因此完全符合 §6.5.3.4 的描述。 - Marco Bonelli
@RafaelEyng 嗯,"指针"部分显然不在定义中。如果那是一个指针,sizeof确实是有效的。另外,printf("%p", print)也是未定义行为。 - Marco Bonelli
你能填写你所使用的编译和获取objdump命令吗? - Rafael Eyng
1
@RafaelEyng 好的,完成了。 - Marco Bonelli
显示剩余4条评论

3

根据C标准(6.3.2.1左值、数组和函数设计者)

4 函数设计者是具有函数类型的表达式。除非它是sizeof操作符65)或一元&操作符的操作数,否则返回类型为“返回类型的函数”的函数设计者将转换为具有类型“指向返回类型的函数的指针”的表达式

以及(6.7.6.3函数声明符(包括原型))

8 将参数声明为“返回类型的函数”应调整为“指向返回类型的函数的指针”,如6.3.2.1所述。

因此,例如这两个函数声明

void execute(void f( void ) );

并且

void execute(void ( *f )( void ) );

同一个函数可能被声明多次且出现在一个翻译单元中,尽管其中一个是冗余的。

注意,您不能对函数指示符应用 sizeof 运算符。

来自 C 标准 (6.5.3.4 sizeof 和 alignof 运算符):

1. 不能对具有函数类型或不完整类型的表达式进行 sizeof 运算符,对于此类类型的括号名称或指定位字段成员的表达式也是如此。alignof 运算符不能应用于函数类型或不完整类型。

但是,您可以将其应用于函数指针。

由于函数指示符隐式转换为函数指针,因此您可以将多个解引用运算符 * 应用于在表达式中使用的函数指示符。

以下是一个演示程序:

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

3

严格地讲,这个规则适用于调用代码,参见C17 6.3.2.1/4:

函数名是一个具有函数类型的表达式。除非它是 sizeof 运算符或一元 & 运算符的操作数,带有 "返回类型为 type 的函数" 类型的函数名被转换为具有 "返回类型为 type 的指针的函数" 类型的表达式。

通俗来讲,这可以称为“函数衰变”或类似的术语,因为它与“数组衰变”基本相同。

在你的情况下,调用代码中的 print 是函数名。如果只输入 print,则会触发上述规则并将该函数转换为函数指针。如果输入 &print,则是上述规则的一个例外,但最终结果仍然是函数指针。

因此,在实践中,print&print 只是风格问题。


在函数内部,形式上,C17 6.7.6.3/8 对函数参数也有相同的规则:

将以 "返回类型为 type 的函数" 声明的参数调整为 "返回类型为 type 的指针的函数",如第6.3.2.1条规定。

如果输入 void(*f)(),则不会应用上述规则,但最终结果仍然是函数指针。


总之,将实际函数用作表达式的一部分几乎是不可能的,因为它总是衰变为函数指针。而且您不能将函数传递给其他函数,只能传递函数指针。

  1. 我的4个片段之间是否有区别?

唯一的区别在于风格和语法,这些区别只有从“语言律师”的角度来看才有趣——从应用程序员的角度来看,它们是相同的。

如果存在实际运行时差异

不存在。

如果没有运行时差异,您能否解释编译器在每种情况下所做的工作,以使它们都表现相同(我认为编译器会看到 f(myFunc) 并实际使用 f(&myFunc),或类似的操作。我想知道哪种方法是“规范”方式)。

反汇编它们,你会发现机器代码完全相同。没有“规范的方式”。

首先,你有空括号的问题,这意味着“接受任何参数”。这在 C 中已经过时,不应该使用。不要与 C++ 混淆,在那里 (void)() 是相同的。这需要修复。然后,请按以下方式操作:

  1. 我应该使用这4个代码片段中的哪一个?为什么?

规范版本如下:

typedef void func_t (void);
...
void execute(func_t* f);

另一种规范风格,同样可接受,也许更为常见:

typedef void (*func_t) (void);
...
void execute(func_t f);

使用其中任何一种形式。至于调用代码,不管你使用 print 还是 &print,这是主观的编码风格。

  1. 我应该使用 f() 还是 (*f)() 来调用通过参数传递的函数?

f() 更易读,因此您应该使用它。

  1. 为什么每个片段的第一个打印中函数变量的大小(sizeof(print))始终为1?在这种情况下我们实际上得到了什么sizeof?

这不是有效的C代码,因此任何人都猜不出你的编译器如何通过它。这是对C语言6.5.3.4“ sizeof运算符不得应用于具有函数类型的表达式”的明显限制违规。

因此,您的编译器要么很烂,要么配置为保持“烂模式”。我建议使用gcc,并将其配置为使用 gcc -std=c11 -pedantic-errors 进行C编译。

5.

见问题4)。


很棒的答案!如果您能提供C17参考链接,那就更好了。 - Rafael Eyng
1
@RafaelEyng 这不是免费的。您可以在此处购买:https://www.iso.org/standard/74528.html - Lundin

2

由于大多数问题已经得到解答:

为什么在每个片段的第一次打印中,函数变量的大小(sizeof(print))始终为1?在这种情况下,我们实际上获得了什么?(显然不是指针的大小,在我的64位机器上将为8字节。如果我使用sizeof(&print),我会获得指针的大小。)

在C语言中,sizeof(function)未定义。您收到的结果(1)是gcc扩展,它允许在衰减的函数上进行指针算术运算(用于地址计算)。它在可移植代码开发中没有任何实际用途,但在嵌入式世界中有时很方便。

您的printf格式字符串错误,会导致printf调用UB。


sizeof 函数在嵌入式系统中有什么用处?本地函数的大小是什么意思,以字节为单位?对我来说,这似乎又是 GNU 过度设计的一个特性。如果您确实想要在运行时计算函数大小等信息,则最好先转换为 uintptr_t。而且只有在执行特殊操作(例如从闪存复制函数到 RAM)时才有用。 - Lundin
@Lundin sizeof(function) 总是为1。https://godbolt.org/z/afTnNM - 0___________
@Lundin 如果我想计算函数的大小,我会使用另一种方法。 - 0___________

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接