我刚刚开始围绕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
显然,多余的论点都被默默地抛弃了。
现在我想了解这里发生了什么。
我来自Java,那里的类型检查要严格得多,所以这种行为让我有点困惑。也许我正在经历文化冲击: - )。
答案 0 :(得分:15)
不会丢弃额外参数。它们被正确放置在堆栈上,就像调用需要三个参数的函数一样。但是,由于您的函数仅关注一个参数,因此它仅查看堆栈的顶部,并且不会触及其他参数。
根据以下两个事实,这个呼叫有效的事实是纯粹的运气:
编译器无法通过一个简单的原因警告您这样的潜在问题 - 在一般情况下,它在编译时不知道指针的值,因此它无法评估它指向的内容。想象一下,函数指针指向在运行时创建的类虚拟表中的方法?所以,你告诉编译器它是一个指向具有三个参数的函数的指针,编译器会相信你。
答案 1 :(得分:12)
如果你把汽车扔成锤子,编译器会告诉你汽车是锤子但你不会把汽车变成锤子。编译器可能成功地使用汽车来驱动钉子,但这是依赖于实现的好运。这仍然是一件不明智的事情。
答案 2 :(得分:3)
是的,它是未定义的行为 - 任何事情都可能发生,包括它似乎“工作”。
强制转换会阻止编译器发出警告。此外,编译器不要求诊断可能的原因未定义的行为。这样做的原因是要么不可能这样做,要么这样做会太困难和/或导致很多开销。
答案 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)
我肯定不确定,但如果运气或,如果它是特定于编译器的话,你肯定不想利用这种行为。
它不值得警告,因为演员表是明确的。通过强制转换,您可以通知编译器您更了解。特别是,你正在构建一个void*
,因此你说“取这个指针所代表的地址,并使其与其他指针相同” - 演员只是告诉编译器你确定目标地址的内容实际上是相同的。虽然在这里,我们知道这是不正确的。
答案 6 :(得分:1)
我应该在某些时候刷新我对C调用约定的二进制布局的记忆,但我很确定这就是发生的事情:
答案 7 :(得分:0)
回答你的问题:
纯粹的运气 - 您可以轻松践踏堆栈并覆盖指向下一个执行代码的返回指针。由于您使用3个参数指定了函数指针,并调用了函数指针,因此剩余的两个参数被“丢弃”,因此行为未定义。想象一下,如果第二个或第三个参数包含二进制指令,并弹出调用过程堆栈....
当您使用void *
指针并进行投射时,没有任何警告。即使您已明确指定-Wall
切换,这在编译器眼中也是非常合法的代码。 编译器假设您知道自己在做什么!这是秘密。
希望这有帮助, 最好的祝福, 汤姆。