假设我有一个函数接受 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);

我看过 这个问题 我查看了一些 C 标准,它们说可以转换为“兼容函数指针”,但我找不到“兼容函数指针”含义的定义。

有帮助吗?

解决方案

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

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

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

第 6.3.2.3 节第 8 段内容如下:

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

换句话说,您可以将函数指针转换为不同的函数指针类型,再次将其转换回来,然后调用它,事情就会起作用。

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

为了使两个函数类型兼容,两者都应指定兼容的返回类型127.

此外,参数类型列表(如果两者都存在)在数量上应一致 参数和省略号终止符的使用;相应参数应具有 兼容类型。如果一种类型具有参数类型列表,而另一种类型由 不属于函数定义且包含空的函数声明符 标识符列表,参数列表不得有省略号终止符和每个终止符的类型 参数应与应用 默认参数升级。如果一种类型具有参数类型列表,而另一种类型是 由包含(可能是空的)标识符列表的函数定义指定,两者都应 在参数数量上达成一致,每个原型参数的类型应为 与应用默认参数所生成的类型兼容 升级为相应标识符的类型。(在类型测定中 兼容性和复合类型,每个参数都用函数或数组声明 type 被视为具有调整后的类型,并且每个参数都声明为限定类型 被视为具有其声明类型的非限定版本。

127) 如果两个函数类型都是“旧式”,则不比较参数类型。

确定两种类型是否兼容的规则在第 6.2.7 节中描述,由于它们相当冗长,我不会在这里引用它们,但是您可以在 C99 标准草案 (PDF).

相关规则见第 6.7.5.1 节第 2 段:

为了使两个指针类型兼容,两者都应具有相同的限定,并且两者都应是指向兼容类型的指针。

因此,自从一个 void* 不兼容 与一个 struct my_struct*, 类型的函数指针 void (*)(void*) 与类型的函数指针不兼容 void (*)(struct my_struct*), ,因此函数指针的这种转换在技术上是未定义的行为。

但实际上,在某些情况下,您可以安全地摆脱函数指针的转换。在 x86 调用约定中,参数被压入堆栈,并且所有指针的大小相同(x86 中为 4 字节,x86_64 中为 8 字节)。调用函数指针归结为将参数压入堆栈并间接跳转到函数指针目标,并且在机器代码级别显然没有类型的概念。

事情你绝对 不能 做:

  • 在不同调用约定的函数指针之间进行转换。你会弄乱堆栈,最好的情况是崩溃,最坏的情况是,通过一个巨大的安全漏洞默默地成功。在 Windows 编程中,经常传递函数指针。Win32 期望所有回调函数都使用 stdcall 调用约定(其中宏 CALLBACK, PASCAL, , 和 WINAPI 全部展开为)。如果传递使用标准 C 调用约定的函数指针 (cdecl),就会产生不好的结果。
  • 在 C++ 中,类成员函数指针和常规函数指针之间的转换。这常常会让 C++ 新手犯难。类成员函数有一个隐藏的 this 参数,如果将成员函数转换为常规函数,则没有 this 反对使用,同样会导致很多不好的结果。

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

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

其他提示

我最近询问了有关 GLib 中某些代码的完全相同的问题。(GLib 是 GNOME 项目的核心库,用 C 语言编写。)我被告知整个 slot'n'signals 框架都依赖于它。

在整个代码中,有许多从类型 (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)

链通式调用很常见,如下所示:

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. 否则,不要这样做。

写完这个回复后,深入思考一下,如果 C 编译器的代码使用同样的技巧,我不会感到惊讶。由于(大多数/全部?)现代 C 编译器是自举的,这意味着该技巧是安全的。

一个更重要的研究问题:有人能找到这个技巧可以实现的平台/编译器/链接器/加载器吗 不是 工作?对此的主要印象分。我敢打赌有一些嵌入式处理器/系统不喜欢它。然而,对于桌面计算(可能还有移动/平板电脑),这个技巧可能仍然有效。

在点真的不是你是否可以。平凡解是

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

您有一个兼容功能类型如果返回类型和参数类型兼容 - 基本上(这是在现实中更复杂的:))。兼容性是一样的“同类型”只是更宽松,允许有不同的类型,但仍然有某种形式的说:“这些类型几乎相同”的。在C89,例如,两个结构,如果他们在其它方面相同,但只是他们的名字不同的是兼容的。 C99似乎已经改变了这一切。从 C原理文件引用(强烈建议阅读,顺便说一句!):

  

在两种不同的翻译单元结构,联合或枚举类型声明不正式宣布同类型,即使这些声明的文本来自同一个包含文件,因为翻译单位本身不相交。标准因而规定了这种类型的其他的兼容性规则,使得如果两个这样的声明是充分相似它们是兼容的。

这是说 - 是严格的,这是不确定的行为,因为你的do_stuff功能否则别人会调用具有void*作为参数的函数指针的功能,但你的函数有一个不兼容的参数。但无论如何,我希望所有的编译器来编译,没有呻吟运行它。但是你可以通过具有其它功能采取void*(和注册,作为回调函数),这将只是调用您的实际功能,然后做清洁。

由于C代码编译为它根本就不关心指针类型的指令,这是相当优良使用你提到的代码。你会碰到问题时,你会用你的回调函数指针到别的东西,然后my_struct结构参数运行do_stuff。

我希望我能更清楚地通过展示什么是行不通的:

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

基本上,你可以投三分球,以任何你喜欢的,只要数据继续令在运行时的感觉。

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

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

void指针是与其它类型的指示器的兼容。这是怎样的malloc和MEM功能(memcpymemcmp)工作的骨干力量。通常,在C(而不是C ++)NULL是定义为((void *)0)的宏。

在C99看6.3.2.3(第1种):

  

中的孔隙的指针可被转换成或从一个指针到任何不完整的或对象类型

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top