就 C 标准而言,如果将函数指针转换为不同类型的函数指针,然后调用它,那就是未定义的行为。参见附件 J.2(资料性):
在以下情况下,该行为是未定义的:
- 指针用于调用与所指向的类型不兼容的函数
类型(6.3.2.3)。
第 6.3.2.3 节第 8 段内容如下:
指向一种类型函数的指针可以转换为指向另一种类型函数的指针
键入并再次返回;结果应等于原始指针。如果转换成
指针用于调用类型与所指向类型不兼容的函数,
该行为是未定义的。
换句话说,您可以将函数指针转换为不同的函数指针类型,再次将其转换回来,然后调用它,事情就会起作用。
的定义兼容的有点复杂。可以在第 6.7.5.3 节第 15 段中找到:
For two function types to be compatible, both shall specify compatible return types127.
此外,如果参数类型列表都存在,则参数类型列表的数量应一致
参数以及省略号终止符的使用;相应的参数应有
兼容类型。如果一种类型具有参数类型列表,而另一种类型由
函数声明符不是函数定义的一部分并且包含空
标识符列表,参数列表不应有省略号终止符以及每个参数的类型
参数应与应用程序产生的类型兼容
默认参数促销。如果一种类型有参数类型列表,另一种类型是
由包含(可能为空)标识符列表的函数定义指定,两者都应
参数个数一致,每个原型参数的类型为
与应用默认参数所产生的类型兼容
晋升为相应标识符的类型。 (在确定类型时
兼容性和复合类型,每个参数都用函数或数组声明
type 被视为具有调整后的类型,并且每个参数都使用限定类型声明
被视为具有其声明类型的非限定版本。)
127) 如果两个函数类型都是“旧式”,则不比较参数类型。
确定两种类型是否兼容的规则在第 6.2.7 节中描述,由于它们相当冗长,我不会在这里引用它们,但是您可以在C99 标准草案 (PDF) http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf.
相关规则见第 6.7.5.1 节第 2 段:
为了使两个指针类型兼容,两者都应具有相同的限定,并且两者都应是指向兼容类型的指针。
因此,自从一个void*
不兼容 https://stackoverflow.com/questions/11647220/are-void-pointer-and-pointer-to-some-structure-layout-compatible with a struct my_struct*
, 类型的函数指针void (*)(void*)
与类型的函数指针不兼容void (*)(struct my_struct*)
,因此函数指针的这种转换在技术上是未定义的行为。
但实际上,在某些情况下,您可以安全地摆脱函数指针的转换。在 x86 调用约定中,参数被压入堆栈,并且所有指针的大小相同(x86 中为 4 字节,x86_64 中为 8 字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。
事情你绝对can't do:
- 在不同调用约定的函数指针之间进行转换。你会弄乱堆栈,最好的情况是崩溃,最坏的情况是,通过一个巨大的安全漏洞默默地成功。在 Windows 编程中,经常传递函数指针。 Win32 期望所有回调函数都使用
stdcall
调用约定(其中宏CALLBACK
, PASCAL
, and WINAPI
全部展开为)。如果传递使用标准 C 调用约定的函数指针 (cdecl
),就会产生不好的结果。
- 在 C++ 中,类成员函数指针和常规函数指针之间的转换。这常常会让 C++ 新手犯难。类成员函数有一个隐藏的
this
参数,如果将成员函数转换为常规函数,则没有this
反对使用,同样会导致很多不好的结果。
另一个坏主意有时可能有效,但也是未定义的行为:
- 在函数指针和常规指针之间进行转换(例如,转换
void (*)(void)
to a void*
)。函数指针的大小不一定与常规指针相同,因为在某些体系结构上它们可能包含额外的上下文信息。这在 x86 上可能可以正常工作,但请记住这是未定义的行为。