将函数指针转换为另一种类型

时间:2009-02-18 02:03:43

标签: c function-pointers

假设我有一个接受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标准,说你可以转换为'兼容函数指针',但我找不到'兼容函数指针'的含义。< / p>

7 个答案:

答案 0 :(得分:113)

就C标准而言,如果将函数指针强制转换为不同类型的函数指针然后调用它,则它是未定义的行为。见附件J.2(资料性):

  

在以下情况下,行为未定义:

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

第6.3.2.3节第8段内容如下:

  

指向一种类型的函数的指针可以转换为指向另一种函数的指针   打字再回来;结果应该等于原始指针。如果转换了   指针用于调用类型与指向类型不兼容的函数,   行为未定义。

换句话说,您可以将函数指针强制转换为不同的函数指针类型,再将其强制转换,然后调用它,事情就可以了。

compatible 的定义有点复杂。它可以在第6.7.5.3节第15段中找到:

  

要使两种函数类型兼容,两者都应指定兼容的返回类型 127

     

此外,参数类型列表,如果两者都存在,则应在数量上达成一致   参数和省略号终止符的使用;相应的参数应具备   兼容类型。如果一个类型具有参数类型列表而另一个类型由a指定   函数声明符,它不是函数定义的一部分,并且包含空   标识符列表,参数列表不应具有省略号终止符和每个的类型   参数应与应用程序产生的类型兼容   默认参数促销。如果一个类型具有参数类型列表而另一个类型是   由包含(可能为空)标识符列表的函数定义指定,两者都应该   同意参数的数量,每个原型参数的类型应该是   与应用默认参数产生的类型兼容   促销到相应标识符的类型。 (在确定类型   兼容性和复合类型,每个参数都用函数或数组声明   type被视为具有调整类型,并且每个参数都使用限定类型声明   被视为具有其声明类型的非限定版本。)

     

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调用约定中,参数被推送到堆栈上,并且所有指针都是相同的大小(x86中为4个字节,x86_64中为8个字节)。调用函数指针可归结为推送堆栈上的参数并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。

你绝对不能做的事情:

  • 在不同调用约定的函数指针之间进行转换。你会搞砸堆栈,最糟糕的是,在最糟糕的情况下,崩溃会在一个巨大的安全漏洞中成功。在Windows编程中,您经常传递函数指针。 Win32希望所有回调函数都使用stdcall调用约定(宏CALLBACKPASCALWINAPI都扩展为)。如果传递使用标准C调用约定(cdecl)的函数指针,则会导致错误。
  • 在C ++中,在类成员函数指针和常规函数指针之间进行转换。这通常会让C ++新手兴奋不已。类成员函数具有隐藏的this参数,如果将成员函数强制转换为常规函数,则不会使用this个对象,并且会导致很多错误。

另一个可能有效的坏主意但也是未定义的行为:

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

答案 1 :(得分:26)

我最近询问了有关GLib中某些代码的相同问题。 (GLib是GNOME项目的核心库,用C语言编写。)有人告诉我,整个插槽的信号框架都依赖于它。

在整个代码中,有许多从类型(1)到(2)的转换实例:

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)
  3. 通过这样的调用来链接:

    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

    中查看自己

    上述答案详细而且可能是正确的 - 如果您是标准委员会的成员。亚当和约翰尼斯因其精心研究的回应值得赞扬。但是,在野外,你会发现这段代码工作得很好。争议?是。考虑一下:GLib使用各种编译器/链接器/内核加载器(GCC / CLang / MSVC)在大量平台(Linux / Solaris / Windows / OS X)上编译/工作/测试。我想,标准是该死的。

    我花了一些时间思考这些答案。以下是我的结论:

    1. 如果您正在编写回调库,这可能没问题。注意事项 - 使用风险自负。
    2. 否则,不要这样做。
    3. 在编写此响应后深入思考,如果C编译器的代码使用同样的技巧,我不会感到惊讶。既然(大多数/全部?)现代C编译器都是自举的,这意味着这个技巧是安全的。

      一个更重要的研究问题:有人能找到一个平台/编译器/链接器/加载器吗?这个技巧有效吗?主要的布朗尼指向那个。我敢打赌,有些嵌入式处理器/系统并不喜欢它。但是,对于桌面计算(可能还有手机/平板电脑),这个技巧可能仍然有效。

答案 2 :(得分:7)

关键在于你是否可以。琐碎的解决方案是

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生成代码,如果真的需要它,在这种情况下你会很高兴它。

答案 3 :(得分:5)

如果返回类型和参数类型兼容,则您具有兼容的函数类型 - 基本上(实际上它更复杂:))。兼容性与“相同类型”相同,只是更宽松以允许具有不同类型但仍然具有某种形式的说“这些类型几乎相同”。例如,在C89中,如果两个结构在其他方面相同但只是它们的名称不同,则它们是兼容的。 C99似乎已经改变了。引自c rationale document(强烈推荐阅读,顺便说一下!):

  

两个不同翻译单元中的结构,联合或枚举类型声明不会正式声明相同的类型,即使这些声明的文本来自同一个包含文件,因为翻译单元本身是不相交的。因此,标准为这些类型指定了额外的兼容性规则,因此如果两个这样的声明足够相似,则它们是兼容的。

那说 - 是的,严格来说这是未定义的行为,因为你的do_stuff函数或其他人会使用void*作为参数的函数指针调用你的函数,但你的函数有一个不兼容的参数。但是,尽管如此,我希望所有编译器能够编译并运行它而不会呻吟。但是你可以通过让另一个函数取void*(并将其注册为回调函数)来做更干净的工作,然后调用你的实际函数。

答案 4 :(得分:3)

由于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

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

答案 5 :(得分:0)

如果你考虑函数调用在C / C ++中的工作方式,他们会推送堆栈中的某些项,跳转到新的代码位置,执行,然后在返回时弹出堆栈。如果您的函数指针描述具有相同返回类型和相同数量/大小的参数的函数,那么您应该没问题。

因此,我认为你应该能够安全地这样做。

答案 6 :(得分:0)

Void指针与其他类型的指针兼容。它是malloc和mem函数(memcpymemcmp)如何工作的支柱。通常,在C(而不是C ++)中,NULL是定义为((void *)0)的宏。

请参阅C99中的6.3.2.3(第1项):

  

指向void的指针可以转换为指向任何不完整或对象类型的指针