C、 指向函数强制转换的指针,代码不清楚

C、 指向函数强制转换的指针,代码不清楚,c,pointers,casting,C,Pointers,Casting,Mike Ash的评论如下: 代码如下: void Tester(int ign, float x, char y) { printf("float: %f char: %d\n", x, y); } int main(int argc, char **argv) { float x = 42; float y = 42; Tester(0, x, y); void (*TesterAlt)(int, ...) = (void *)

Mike Ash的评论如下:

代码如下:

void Tester(int ign, float x, char y) 
{ 
    printf("float: %f char: %d\n", x, y); 
} 

int main(int argc, char **argv) 
{ 
    float x = 42; 
    float y = 42; 
    Tester(0, x, y); 

    void (*TesterAlt)(int, ...) = (void *)Tester; 
    TesterAlt(0, x, y); 

    return 0; 
}
我不清楚他在主要角色中扮演的角色

TesterAlt是指向函数返回void的指针,该函数返回void与函数测试仪的返回类型相同。他分配给这个函数指针functiontester,但他将后一个返回类型转换为void类型的指针(我不确定这一点)

如果我编译代码时更改了该行:

void (*TesterAlt)(int, ...) = (void)Tester;
我得到一个编译器错误:

initializing 'void (*)(int, ...)' with an expression of incompatible type 'void'
void (*TesterAlt)(int, ...) = (void) Tester;
他为什么要选这个角色?他的语法是什么意思

编辑: 我对我最初的问题不是很清楚,我不理解这个语法以及我必须如何阅读它

(void *)Tester;

据我所知,测试员被铸造成“指向虚空的指针”,但看起来我的解释是错误的。如果它不是指向void的指针,那么如何读取该代码以及原因?

您会收到此错误消息,因为您无法对已转换为
(void)
的表达式执行任何有用的操作。原始代码中的
(void*)
强制转换引用指针本身,而不是返回类型

实际上,
(void*)Tester
是从函数指针
Tester
到void指针的转换。这是一个只指向给定地址的指针,但没有关于它的有用信息

转换为
(void)Tester
是对“void类型”的转换,这会导致一个表达式,而您无法将其分配给任何对象

让我们回到
(void*)Tester
-您可以通过将指针转换回正确的类型来使用它。但在这个意义上什么是“适当的”?“正确”意味着原始函数的函数签名和以后使用的指针类型必须相同。违反此要求不会导致编译时错误,但会导致执行时的未定义行为

有人可能会认为一个签名有一个int,然后省略号会覆盖一个固定参数计数的情况,但事实并非如此。确实存在一些系统,例如,调用
void()(int-ign,float x,char-y)
纯粹是使用寄存器,而调用
void()(int,…)
的方法是将参数推到堆栈中

请查看以下代码:

int va(int, ...);
int a(int, int, char);

int test() {
    int (*b)(int, int, char) = va;
    int (*vb)(int, ...) = a;
    a(1, 2, 3);
    va(1, 2, 3);
    b(1, 2, 3);
    vb(1, 2, 3);
}
(注意,我将
float
更改为
int
。)

在分配
b
vb
时,我交换了各自的函数原型。这样做的结果是,通过引用
b
,我确实调用了
va
,但是编译器假定了一个错误的函数原型。这同样适用于
vb
a

请注意,在x86上,这可能会起作用(我没有检查它),但我从这段代码中获得的AVR程序集如下

    # a(1, 2, 3):
    ldi r24,lo8(gs(va))
    ldi r25,hi8(gs(va))
    std Y+2,r25
    std Y+1,r24
    ldi r24,lo8(gs(a))
    ldi r25,hi8(gs(a))
    std Y+4,r25
    std Y+3,r24
    ldi r20,lo8(3)
    ldi r22,lo8(2)
    ldi r23,0
    ldi r24,lo8(1)
    ldi r25,0
    rcall a

    # va(1, 2, 3):
    push __zero_reg__
    ldi r24,lo8(3)
    push r24
    push __zero_reg__
    ldi r24,lo8(2)
    push r24
    push __zero_reg__
    ldi r24,lo8(1)
    push r24
    rcall va
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__

    # b(1, 2, 3):
    ldd r18,Y+1
    ldd r19,Y+2
    ldi r20,lo8(3)
    ldi r22,lo8(2)
    ldi r23,0
    ldi r24,lo8(1)
    ldi r25,0
    mov r30,r18
    mov r31,r19
    icall

    # vb(1, 2, 3)
    push __zero_reg__
    ldi r24,lo8(3)
    push r24
    push __zero_reg__
    ldi r24,lo8(2)
    push r24
    push __zero_reg__
    ldi r24,lo8(1)
    push r24
    ldd r24,Y+3
    ldd r25,Y+4
    mov r30,r24
    mov r31,r25
    icall
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
    pop __tmp_reg__
这里我们看到,作为非vararg的
a()
通过
r20..r25
馈送,而作为vararg的
va()
通过
推送
馈送到堆栈


关于
b()
vb()
,我故意混淆了定义,忽略了我得到的警告。因此,调用如上所述,但由于混淆,它们使用了错误的调用约定。这就是为什么会这样。在x86上运行时,OP中的代码可能会工作,也可能不会工作(可能会工作),但在切换到x64之后,它可能会开始失败,乍一看没有人知道为什么会这样。因此,我们再次看到:避免未定义的行为是一项严格的要求。它可能会像预期的那样工作,但你根本没有保证。更改编译器标志可能足以更改行为。或者将代码移植到不同的体系结构。

这确实不是标准C,但在J.5.7中被列为C标准的“通用扩展”:
“指向函数的指针可以转换为指向对象的指针或无效,允许检查或修改函数”
@Lubdin这是正确的,它也可以用于检查和其他用途,但为了调用它,我们必须使用具有适当类型的函数指针。如果不这样做,我们就会有代码将参数放在错误的位置(不是堆栈,而是寄存器,反之亦然)->UB@Gopi,是的,它会的。请参阅我更新的问题,其中我展示了给定平台在这两种情况下可能产生的差异。@glglglgl非常感谢您的深入解释,但我仍然不理解该语法,请参阅我的编辑。另外,UB的含义是什么?@AR89 UB=未定义的行为,这是C语言编程时的一个非常常见的表达式。我的观点是,内部发生的情况可能不同,因此使用正确的函数签名很重要。只是永远不要使用变量参数函数或宏:它们是100%多余的功能,这也是既慢又危险的。@lundinibtd。这是一个小技巧特性,有一些陷阱,但我发现它非常有用。然而,当你使用它们时,你必须知道你在做什么。