C语言 将函数指针转换为另一类型

4ktjp1zp  于 11个月前  发布在  其他
关注(0)|答案(9)|浏览(145)

假设我有一个函数,它接受一个void (*)(void*)函数指针作为回调函数:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

字符串
现在,如果我有一个这样的函数:

void my_callback_function(struct my_struct* arg);


我能安全地做这件事吗?

do_stuff((void (*)(void*)) &my_callback_function, NULL);


我看过this question,也看过一些C标准,它们说你可以转换为“兼容函数指针”,但我找不到“兼容函数指针”的定义。

xxslljrj

xxslljrj1#

就C标准而言,如果你将一个函数指针转换为另一个不同类型的函数指针,然后调用它,这是 * 未定义行为 *。参见附录J.2(资料性):
在以下情况下,行为是未定义的:

  • 指针用于调用类型与所指向的类型不兼容的函数(6.3.2.3)。

第6.3.2.3节第8段内容如下:
一个指向一种类型的函数的指针可以被转换成指向另一种类型的函数的指针,然后再转换回来;结果应该与原始指针进行比较。如果转换后的指针被用来调用一个类型与所指向的类型不兼容的函数,则行为是未定义的。
所以换句话说,你可以将一个函数指针转换为一个不同的函数指针类型,再将它转换回来,然后调用它,一切都会正常工作。

  • compatible* 的定义有些复杂。可以在6.7.5.3部分第15段中找到:

对于两个兼容的函数类型,两者都应指定兼容的返回类型127。
此外,参数类型列表(如果两者都存在)应在参数的数量和省略号终止符的使用方面达成一致;如果一个类型有一个参数类型列表,而另一个类型由一个函数声明符指定,该函数声明符不是函数定义的一部分,并且包含一个空的标识符列表,参数列表不应有省略号终止符,并且每个参数的类型应与应用默认参数提升后的类型兼容。如果一个类型有参数类型列表,而另一个类型由包含(可能为空)标识符列表,两者应在参数数量上一致,并且每个原型参数的类型应与将默认参数提升应用于对应标识符的类型所产生的类型兼容。(在确定类型兼容性和复合类型时,用函数或数组类型声明的每个参数被视为具有调整的类型,而用限定类型声明的每个参数被视为具有其声明类型的非限定版本。
127)如果两个函数类型都是“旧样式”,则不比较参数类型。
判断两个类型是否兼容的规则在6.2.7节中描述,我不会在这里引用它们,因为它们相当长,但您可以在draft of the C99 standard (PDF)上阅读它们。
相关规则见6.7.5.1第2段:
对于两个兼容的指针类型,两者都应该是相同限定的,并且都应该是指向兼容类型的指针。
因此,由于void*is not compatible带有struct my_struct*void (*)(void*)类型的函数指针与void (*)(struct my_struct*)类型的函数指针不兼容,因此这种函数指针的转换在技术上是未定义的行为。
但在实践中,在某些情况下,你可以安全地摆脱强制转换函数指针。(x86中为4字节,x86_64中为8字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,在机器码级别上显然没有类型的概念。
你绝对不能做的事:

  • 在不同调用约定的函数指针之间进行强制转换。您将弄乱堆栈,最好的情况下,崩溃,最坏的情况下,成功时会出现巨大的安全漏洞。在Windows编程中,您经常传递函数指针。Win32要求所有回调函数都使用stdcall调用约定(宏CALLBACKPASCALWINAPI都扩展到了它)。如果你传递一个使用标准C调用约定(cdecl)的函数指针,会导致错误。
  • 在C中,在类成员函数指针和常规函数指针之间进行强制转换。这经常会让C新手感到困惑。类成员函数有一个隐藏的this参数,如果你将一个成员函数强制转换为一个常规函数,就没有this对象可供使用,同样会导致很多不好的结果。

另一个坏主意,有时可能会工作,但也是未定义的行为:

  • 函数指针和常规指针之间的转换(例如,将void (*)(void)转换为void*)。函数指针不一定与常规指针大小相同,因为在某些架构上,它们可能包含额外的上下文信息。这可能在x86上工作正常,但请记住,这是未定义的行为。
drnojrws

drnojrws2#

我最近问过关于GLib中一些代码的这个完全相同的问题。(GLib是GNOME项目的核心库,用C编写。)我被告知整个slots 'n'signals框架都依赖于它。
在整个代码中,有许多从类型(1)到类型(2)的转换示例:
1.第一个月

  1. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
    通常会使用以下调用进行链接:
int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

字符串
g_array_sort()中亲自查看:http://git.gnome.org/browse/glib/tree/glib/garray.c
上面的答案很详细,很可能是正确的--如果你是标准委员会的成员。Adam和Johannes的回答经过了充分的研究,值得称赞。然而,在野外,你会发现这些代码工作得很好。有争议吗?是的。考虑一下:GLib在大量平台上编译/工作/测试(Linux/Solaris/Windows/OS X)有各种各样的编译器/链接器/内核加载器(GCC/CLang/MSVC)。我想标准是该死的。
我花了一些时间思考这些答案,以下是我的结论:
1.如果你正在写一个回调库,这可能是可以的。
1.否则,不要这样做。
在写了这篇回复之后,我想了想,如果C编译器的代码使用了同样的技巧,我不会感到惊讶。而且由于(大多数/所有?)现代C编译器都是 Bootstrap 的,这意味着这个技巧是安全的。
还有一个更重要的问题需要研究:有人能找到一个平台/编译器/链接器/加载器,在那里这个技巧工作吗?这是一个主要的布朗尼点。我打赌有一些嵌入式处理器/系统不喜欢它。然而,对于桌面计算(可能还有移动的/平板电脑),这个技巧可能仍然有效。

yfjy0ee7

yfjy0ee73#

问题的关键不在于你能不能做到。简单的解决办法是

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

字符串
一个好的编译器只会在真正需要的时候为my_callback_helper生成代码,在这种情况下,你会很高兴它这样做了。

8yoxcaq7

8yoxcaq74#

如果返回类型和参数类型是兼容的,那么你就有一个兼容的函数类型-基本上(实际上更复杂:))。兼容性与“相同类型”相同,只是更宽松,允许有不同的类型,但仍然有某种形式的说法“这些类型几乎相同”。例如,在C89中,如果两个结构体在其他方面相同,但只是它们的名称不同,那么它们是兼容的。C99似乎改变了这一点。引用c rationale document(强烈推荐阅读,顺便说一句!):
在两个不同的翻译单元中的结构、联合或枚举类型声明并不正式声明相同的类型,即使这些声明的文本来自同一个包含文件,因为翻译单元本身是不相交的。因此,标准为这些类型指定了额外的兼容性规则,以便如果两个这样的声明足够相似,它们是兼容的。
也就是说-是的,严格来说这是未定义的行为,因为你的do_stuff函数或其他人会用一个以void*为参数的函数指针来调用你的函数,但是你的函数有一个不兼容的参数。但是,我希望所有的编译器都能编译并运行它而不会抱怨。(并将其注册为回调函数),然后它将调用您的实际函数。

p5cysglq

p5cysglq5#

由于C代码编译成的指令根本不关心指针类型,所以使用你提到的代码是很好的。当你用回调函数运行do_stuff并将指针指向其他东西而不是my_struct结构作为参数时,你会遇到问题。
我希望我能通过展示什么不起作用来更清楚地说明这一点:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

字符串
或者...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts


基本上,只要数据在运行时继续有意义,您可以将指针转换为任何您喜欢的对象。

kgqe7b3p

kgqe7b3p6#

好吧,除非我理解错了,你可以这样转换一个函数指针。

void print_data(void *data)
{
    // ...
}

((void (*)(char *)) &print_data)("hello");

字符串
一个更简洁的方法是创建一个函数typedef。

typedef void(*t_print_str)(char *);
((t_print_str) &print_data)("hello");

f5emj3cl

f5emj3cl7#

这是一种未定义的行为,正如其他一些回答/评论所解释的那样。
但如果将void my_callback_function(struct my_struct* arg)替换为

void my_callback_function(void *arg)
{
   struct my_struct* p = arg;
   ... // use p
}

字符串
假设你真的传递了一个指向my_struct对象的指针作为参数,当通过函数指针调用函数时。
确实,指向任何对象的指针都可以被强制转换为void,然后在不改变其值的情况下强制转换回原始类型,但这只能保证像我上面建议的函数有效,而不是void my_callback_function(struct my_struct* arg)人们倾向于相信它们实际上是相同的。

a6b3iqyw

a6b3iqyw8#

如果你考虑一下C/C++中函数调用的工作方式,它们会将某些项压入堆栈,跳转到新的代码位置,执行,然后在返回时弹出堆栈。如果你的函数指针描述的函数具有相同的返回类型和相同的参数数量/大小,你应该没问题。
因此,我认为你应该能够安全地这样做。

kxkpmulp

kxkpmulp9#

Void指针与其他类型的指针兼容。它是malloc和malloc函数(memcpymemcmp)工作的基础。通常,在C(而不是C++)中,NULL是定义为((void *)0)的宏。
查看C99中的6.3.2.3(第1项):
指向void的指针可以转换为指向任何不完整类型或对象类型的指针,也可以从指向任何不完整类型或对象类型的指针转换为指向任何不完整类型或对象类型的指针

相关问题