c - 在C中,传递函数或传递函数指针?

  显示原文与译文双语对照的内容
关闭这个问题需要更多专注,它目前不接受答案想改进这个问题?更新问题,使它只针对一个问题编辑此帖子

2小时前

注意:链接另一个显示函数指针示例的答案没有帮助。问题是关于不同答案中展示的多种方法,并试图理解它们之间的区别)

上下文:

我试图理解在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)

  • Fragment 1:没有指针的传递,没有指针的接收"

    
    #include <stdio.h>
    
    
    
    void execute(void f()) {
    
    
     printf("2 %p %lun", f, sizeof(f));
    
    
     f();
    
    
    }
    
    
    
    void print() {
    
    
     printf("Hello!n");
    
    
    }
    
    
    
    int main() {
    
    
     printf("1 %p %lun", print, sizeof(print));
    
    
     execute(print);
    
    
     return 0;
    
    
    }
    
    
    
    
  • 代码片段2:与指针一起传递,并使用指针接收"

    
    #include <stdio.h>
    
    
    
    void execute(void (*f)()) {
    
    
     printf("2 %p %lun", f, sizeof(f));
    
    
     f();
    
    
    }
    
    
    
    void print() {
    
    
     printf("Hello!n");
    
    
    }
    
    
    
    int main() {
    
    
     printf("1 %p %lun", print, sizeof(print));
    
    
     execute(&print);
    
    
     return 0;
    
    
    }
    
    
    
    
  • Fragment 3:传递一个指针,并且没有指针接收"

    
    #include <stdio.h>
    
    
    
    void execute(void (f)()) {
    
    
     printf("2 %p %lun", f, sizeof(f));
    
    
     f();
    
    
    }
    
    
    
    void print() {
    
    
     printf("Hello!n");
    
    
    }
    
    
    
    int main() {
    
    
     printf("1 %p %lun", print, sizeof(print));
    
    
     execute(&print);
    
    
     return 0;
    
    
    }
    
    
    
    
  • 代码片段4:没有指针的传递和指针的接收"

    
    #include <stdio.h>
    
    
    
    void execute(void (*f)()) {
    
    
     printf("2 %p %lun", f, sizeof(f));
    
    
     f();
    
    
    }
    
    
    
    void print() {
    
    
     printf("Hello!n");
    
    
    }
    
    
    
    int main() {
    
    
     printf("1 %p %lun", print, sizeof(print));
    
    
     execute(print);
    
    
     return 0;
    
    
    }
    
    
    
    

对于所有示例:

  • 程序编译,并运行,没有错误或警告
  • Hello打印正确
  • %p中打印的值在第一个和第二个打印中相同
  • sizeof在第一次打印时总是1
  • 在第二个打印中,sizeof总是8

我读了什么

我已经阅读了几个示例(Wikipedia,StackOverflow,StackOverflow答案中链接的其他资源),其中很多示例显示了不同的示例,问题就是为了理解这些差异。

wikipedia关于函数指针的文章显示了与snippet 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) )

Wikipedia文章后面的一个示例告诉我们,传递函数时的&是不需要的,没有进一步的说明:


// 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)传递(在print上没有& )
  • 参数被声明为void (*f)(int) (* )
  • 参数被调用为(*f)(ctr) (使用(*fn) )

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

  • 第一种方法类似于代码片段1:
    • 参数作为execute(print) (没有& )传递
    • 参数被声明为void f() (没有* )
    • 参数被调用为f() (没有* )
  • 第二个代码片段类似于代码片段2:
    • 参数作为execute(&print) (& )传递
    • 参数被声明为void (*f)() (* )
    • 参数被调用为f()(没有*)

这个答案

  • 没有关于如何传递function argument (with or without & )的例子
  • 参数被声明为int (*functionPtr)(int, int) (* )
  • 参数被调用为(*functionPtr)(2, 3) (使用* )

我一个答案里找到的这些链接材料(实际上是C++,但它没有使用特定于C++的函数指针):

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

我在这里提供了7个例子,它们提供了至少5种使用或不使用& =�m쌧和*的组合,

我从这些中了解到什么

我相信上一节已经表明,对于StackOverflow上最相关的问题/答案以及我链接的其他材料(其中大多数都是在StackOverflow的答案中链接的)没有达成一致的解释。

所有这些4种方法都由编译器处理,或者存在非常微妙的差异,这些差异不会出现在这样的简单示例中。

我明白为什么第二个打印输出sizeof(f)8片段中的2和4,这是64-bit系统中的指针大小。但我不明白为什么第二次打印会打印指针的大小,即使在o�n���G��m쌧函数声明参数时没有*圆括号_eauc43onfyghzluomqidbzscamzafeqa8888_yfr_括号的情况下,也不明白为什么在第一次打印中函数变量有1G��m쌧,

  • 所有4段之间是否有实际的差异?为什么它们的行为完全一样?
  • 如果存在实际的运行时差异,你能解释一下它们中发生了什么?
  • 如果没有运行时的区别,你能解释一下编译器在每种情况下都做了什么,使它们的行为都相同(我假设编译器会看到f(myF}�e!G��m쌧,并实际使用f(&anm�ձ�����2�,或类似的东西,,我想知道哪个是""方法)
  • 我应该使用哪一个4段,为什么?
  • 我应该用f()(*f)()调用通过参数接收的传递函数?
  • 为什么在每个片段的第一次打印中,function variable ( sizeof(print) )的大小总是1?我们怎么知道这个箱子的大小?显然不是指针的大小,在64-bit机器中,它应该是8字节,如果使用sizeof(&print),我会得到指针的大小
  • 为什么在片段1和3上,在第二个打印中,sizeof(f)提供8(指针的大小),即使参数是

回答时,请试着打破后面的数字问题)

时间:

正式地,此规则适用于调用代码C17 6.3.2.1 /4:

函数指示符是有函数类型的表达式,除非是sizeof运算符或一元&运算符的操作数,否则类型为"function returning type"的函数指示符将转换为有"pointer function returning type"类型的表达式。

非正式地,这可以称为"函数衰减"或类似的,因为它非常像"阵列衰减"

在你的情况下,print是调用代码中的函数指示符,如果只键入print,那么上面的规则将启动并将函数转换为函数指针,如果键入&print,这是上述规则的一个例外,但最终得到的是一个函数指针,而不管。

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

在函数内部,然后正式的C17 6.7.6.3 /8有相同的函数参数规则:

参数"函数返回类型"的声明应调整为"返回_type的函数指针",如6.3.2.1.

如果键入void(*f)()上述方法不适用,但最终会得到一个函数指针。

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

  • 所有4段之间是否有实际的差异?

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

如果存在实际的运行时差异

如果没有运行时差异,可以解释编译器在每种情况下如何使它们的行为与(我假设编译器会看到f(myFunc),并且实际使用f(& myFunc)相同,或者类似下面的内容,我想知道哪个是""方法)。

拆开它们,你会发现相同的机器码,"标准方式"不是他们。

首先,你遇到了空圆括号的问题,这意味着"接受参数"C中已过时的样式,不应使用,不要与C++混淆,其中(void)()相同,这需要修复,然后,执行如下操作:

  • 我应该使用哪一个4段,为什么?

规范版本为:


typedef void func_t (void);


...


void execute(func_t* f);



替代规范样式,也可以接受,可能更常见:


typedef void (*func_t) (void);


...


void execute(func_t f);



使用这两种形式中的一种,对于调用代码,使用print&print并不重要,它是主观编码风格。

  • 我应该用f()或(>f )(调用通过参数接收的传递函数?

f()更具可读性,所以你应该使用。

  • 为什么在每个片段的第一次打印中,function variable (sizeof(print)的大小总是1?我们怎么知道这个箱子的大小?

它不是有效的C,所以,人都猜想你的编译器如何让它通过,这是对C语言6.5.3.4"sizeof运算符不能应用于有函数类型的表达式"的重大约束违背。

你的编译器是糟糕的,或者被配置为保留在"sucky模式"中,我建议使用gcc并将它配置为使用gcc -std=c11 -pedantic-errors的C编译器。

请参见4)。

不,根本没有区别,所有代码段的行为都是一样的,因为按照C标准,一个函数标识符和指向它的指针,虽然类型不同,但它们完全一样,也就是说,f*f&f可以互换,没有运行时差异,程序被编译为在四个代码段中使用函数的地址,因此编译器对每个代码段都执行相同的操作。

请参见6.3.2.1 point 4C99标准(第46页在这里):

函数指示符是有函数类型的表达式,除非是sizeof运算符(54 )或一元&运算符的操作数,否则,类型为"函数返回类型"的函数指示符。

54 ),因为此转换不发生,sizeof运算符的操作数保持函数指示符,并违反6.5.3.4中的约束。

你最喜欢的那个,是,经常看到函数被定义来获取指向函数的指针,然后调用传递函数名本身,风格问题。

没有任何区别,你可以在任何情况下调用它。

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

请参见6.5.3.4的C99标准(第80页在这里):

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

4.相同。

就像每点1一样,你可以自己验证编译器在所有情况下总是使用objdump查看生成的二进制文件,从而完成完全相同的事情,在所有情况下,在计算机上编译gcc -O1 (查看我用<<<标记的评论)的结果如下:


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]



从C标准(6.3.2.1 Lvalues,数组和函数指示符)

4A函数指示符是有函数类型的表达式,除非是sizeof运算符65 )或一元&运算符的操作数,否则类型为"函数返回类型"的函数指示符将转换为有"指向函数返回类型的指针"类型的表达式

和(6.7.6.3函数declarators (包括原型)))

8参数"函数返回类型"的声明应调整为"函数返回类型指针",如6.3.2.1所示。

例如,这两个函数声明


void execute(void f( void ) );




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



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

请注意,不能将sizeof运算符应用于函数指示符。

从C标准(6.5.3.4 sizeof和alignof运算符)

1sizeof运算符不能应用于有函数类型的表达式或不完全类型的表达式,不能应用于以括号括起的指定eld类型的成员的名称,或者,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;


}



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

为什么在每个片段的第一次打印中,function variable(sizeof(print)的大小总是1?我们怎么知道这个箱子的大小?显然不是指针的大小,在64-bit机器中,它应该是8字节,如果使用sizeof (&打印))我会得到指针的大小。

sizeof(function)在C中未定义,收到的结果是你收到的yf u圆括号内_u EAUCA6GL5ZXI4S4S7Nbzhe5ZWPJSGMGM2dzggy3sgrtddk4txgzgztipepe4dqoyha4f66lgll5zi4l4rafeqa88888_yfr_括号是yf_u u括号__str扩展,该扩展允许对衰减函数进行指针算术yf fu圆圆括号_uu圆括号内有无统一的趋势,有利于对衰减函数进行指针计算yf u圆括号内有无统一的江泽zuzdkzdzdgdgmfgfgg25fg25fg25fg3g3fg3g3fg3fg3fg3fA8_yfr_圆括号它在可移植代码开发中没有实际用途,但有时在嵌入式世界中很方便。

printf格式字符串错误,printf调用UB。

...