如果我转换函数指针,更改参数的数量会发生什么

时间:2010-01-22 16:52:28

标签: c pointers casting function-pointers type-safety

我刚刚开始围绕C语言中的函数指针。要了解函数指针的转换是如何工作的,我编写了以下程序。它基本上创建了一个函数指针,该函数指向一个带有一个参数的函数,将它转换为带有三个参数的函数指针,并调用该函数,提供三个参数。我很好奇会发生什么:

#include <stdio.h>

int square(int val){
  return val*val;
}

void printit(void* ptr){
  int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr);
  printf("Call function with parameters 2,4,8.\n");
  printf("Result: %d\n", fptr(2,4,8));
}


int main(void)
{
    printit(square);
    return 0;
}

编译并运行时没有错误或警告(在Linux / x86上使用gcc -Wall)。我系统的输出是:

Call function with parameters 2,4,8.
Result: 4

显然,多余的论点都被默默地抛弃了。

现在我想了解这里发生了什么。

  1. 至于合法性:如果我正确理解Casting a function pointer to another type的答案,这只是未定义的行为。因此,运行并产生合理结果的事实只是纯粹的运气,对吗? (或编译器编写者的好处)
  2. 为什么gcc不会警告我这个,即使是Wall?这是编译器无法检测到的东西吗?为什么?
  3. 我来自Java,那里的类型检查要严格得多,所以这种行为让我有点困惑。也许我正在经历文化冲击: - )。

8 个答案:

答案 0 :(得分:15)

不会丢弃额外参数。它们被正确放置在堆栈上,就像调用需要三个参数的函数一样。但是,由于您的函数仅关注一个参数,因此它仅查看堆栈的顶部,并且不会触及其他参数。

根据以下两个事实,这个呼叫有效的事实是纯粹的运气:

  • 第一个参数的类型对于函数和转换指针是相同的。如果更改函数以获取指向字符串的指针并尝试打印该字符串,则会发生很好的崩溃,因为代码将尝试取消引用指向内存2的指针。
  • 默认使用的调用约定是调用者清理堆栈。如果更改调用约定,以便被调用者清理堆栈,最终调用者会在堆栈上推送三个参数,然后被调用者清理(或者更确切地说)尝试一个参数。这可能会导致堆栈损坏。

编译器无法通过一个简单的原因警告您这样的潜在问题 - 在一般情况下,它在编译时不知道指针的值,因此它无法评估它指向的内容。想象一下,函数指针指向在运行时创建的类虚拟表中的方法?所以,你告诉编译器它是一个指向具有三个参数的函数的指针,编译器会相信你。

答案 1 :(得分:12)

如果你把汽车扔成锤子,编译器会告诉你汽车是锤子但你不会把汽车变成锤子。编译器可能成功地使用汽车来驱动钉子,但这是依赖于实现的好运。这仍然是一件不明智的事情。

答案 2 :(得分:3)

  1. 是的,它是未定义的行为 - 任何事情都可能发生,包括它似乎“工作”。

  2. 强制转换会阻止编译器发出警告。此外,编译器不要求诊断可能的原因未定义的行为。这样做的原因是要么不可能这样做,要么这样做会太困难和/或导致很多开销。

答案 3 :(得分:3)

你的演员阵容中最糟糕的进攻是将数据指针强制转换为函数指针。它比签名更改更糟糕,因为无法保证函数指针和数据指针的大小相等。与许多理论未定义的行为相反,即使在高级机器上(不仅仅是在嵌入式系统上),也可以在野外遇到这种行为。

您可能会在嵌入式平台上轻松遇到不同大小的指针。甚至有一些处理器,其中数据指针和函数指针可以处理不同的事物(一个是RAM,另一个是ROM),即所谓的哈佛架构。在实模式的x86上,您可以混合使用16位和32位。 Watcom-C有一个DOS扩展器的特殊模式,数据指针是48位宽。特别是对于C,应该知道并非所有东西都是POSIX,因为C可能是异国硬件上唯一可用的语言。

某些编译器允许混合内存模型,其中代码保证在32位大小内,数据可通过64位指针寻址,或者相反。

编辑:结论,永远不会将数据指针强制转换为函数指针。

答案 4 :(得分:2)

行为由调用约定定义。如果您使用调用者推送和弹出堆栈的调用约定,那么在这种情况下它将正常工作,因为它只是意味着在调用期间堆栈上有额外的几个字节。我目前没有gcc方便,但是使用microsoft编译器,这段代码:

int ( __cdecl * fptr)(int,int,int) = (int (__cdecl * ) (int,int,int)) (ptr);

为呼叫生成以下程序集:

push        8
push        4
push        2
call        dword ptr [ebp-4]
add         esp,0Ch

注意调用后添加到堆栈的12个字节(0Ch)。在此之后,堆栈很好(假设在这种情况下被调用者是__cdecl,所以它不会尝试也清理堆栈)。但是使用以下代码:

int ( __stdcall * fptr)(int,int,int) = (int (__stdcall * ) (int,int,int)) (ptr);

程序集中未生成add esp,0Ch。如果在这种情况下被调用者是__cdecl,则堆栈将被破坏。

答案 5 :(得分:1)

  1. 我肯定不确定,但如果运气,如果它是特定于编译器的话,你肯定不想利用这种行为。

  2. 它不值得警告,因为演员表是明确的。通过强制转换,您可以通知编译器您更了解。特别是,你正在构建一个void*,因此你说“取这个指针所代表的地址,并使其与其他指针相同” - 演员只是告诉编译器你确定目标地址的内容实际上是相同的。虽然在这里,我们知道这是不正确的。

答案 6 :(得分:1)

我应该在某些时候刷新我对C调用约定的二进制布局的记忆,但我很确定这就是发生的事情:

  • 1:这不是纯粹的运气。 C调用约定是明确定义的,堆栈上的额外数据不是调用站点的一个因素,尽管被调用者可能会覆盖它,因为被调用者不知道它。
  • 2:使用括号进行“硬”演员表示告诉编译器你知道自己在做什么。由于所有需要的数据都在一个编译单元中,编译器可能足够聪明,可以发现这显然是非法的,但C的设计人员并没有专注于捕获角落案例可验证的错误。简而言之,编译器相信你知道自己在做什么(在许多C / C ++程序员的情况下可能不明智!)

答案 7 :(得分:0)

回答你的问题:

  1. 纯粹的运气 - 您可以轻松践踏堆栈并覆盖指向下一个执行代码的返回指针。由于您使用3个参数指定了函数指针,并调用了函数指针,因此剩余的两个参数被“丢弃”,因此行为未定义。想象一下,如果第二个或第三个参数包含二进制指令,并弹出调用过程堆栈....

  2. 当您使用void *指针并进行投射时,没有任何警告。即使您已明确指定-Wall切换,这在编译器眼中也是非常合法的代码。 编译器假设您知道自己在做什么!这是秘密。

  3. 希望这有帮助, 最好的祝福, 汤姆。