为什么函数指针和数据指针在C / C ++中不兼容?

我已经读过将函数指针转换为数据指针,反之亦然,在大多数平台上都能正常工作,但不保证能够正常工作。 为什么会这样? 两者都不应该只是简单地存入主存储器,因此兼容吗?

架构不必将代码和数据存储在同一存储器中。 使用哈佛架构,代码和数据存储在完全不同的内存中。 大多数体系结构都是Von Neumann体系结构,代码和数据存储在同一个内存中,但C并不仅限于某些类型的体系结构(如果可能的话)。

一些计算机具有(具有)用于代码和数据的单独地址空间。 在这样的硬件上它只是不起作用。

该语言不仅适用于当前的桌面应用程序,还允许在大量硬件上实现。


看起来C语言委员会从来没有打算将void*作为指向函数的指针,他们只是想要一个指向对象的generics指针。

C99理由说:

6.3.2.3指针
C现已在各种架构上实现。 虽然这些体系结构中的一些具有统一指针,这些指针是某种整数类型的大小,但是最大可移植代码不能假设不同指针类型和整数类型之间的任何必要的对应关系。 在某些实现中,指针甚至可以比任何整数类型更宽。

使用void* (“指向void指针”)作为通用对象指针类型是C89委员会的一项发明。 指定函数原型参数的愿望刺激了这种类型的采用,这些参数要么悄悄地转换任意指针(如在fread ),要么在参数类型不完全匹配时抱怨(如在strcmp )。 关于函数的指针没有任何说法,这可能与对象指针和/或整数不相称。

注意在最后一段中没有关于指向函数的指针 。 它们可能与其他指针不同,委员会也知道这一点。

对于那些记得MS-DOS,Windows 3.1及更早版本的人来说,答案非常简单。 所有这些都用于支持几种不同的内存模型,具有不同的代码和数据指针特征组合。

因此,例如对于Compact模型(小代码,大数据):

 sizeof(void *) > sizeof(void(*)()) 

相反,在Medium模型中(大代码,小数据):

 sizeof(void *) < sizeof(void(*)()) 

在这种情况下,您没有单独的代码和日期存储空间,但仍然无法在两个指针之间进行转换(缺少使用非标准__near和__far修饰符)。

另外,不能保证即使指针大小相同,它们也指向相同的东西 - 在DOS小内存模型中,指针附近使用的代码和数据,但它们指向不同的段。 因此,将函数指针转换为数据指针不会给你一个与函数有任何关系的指针,因此没有用于这样的转换。

指向void的指针应该能够容纳指向任何类型数据的指针 – 但不一定是指向函数的指针。 有些系统对指向函数的指针有不同的要求,而不是指向数据的指针(例如,有数据与代码有不同寻址的DSP,MS-DOS上的介质模型使用32位指针代码,但只有16位数据指针) 。

除了这里已经说过的,看看POSIX dlsym()很有意思:

ISO C标准不要求指向函数的指针可以来回转换为指向数据的指针。 实际上,ISO C标准不要求void *类型的对象可以保存指向函数的指针。 但是,支持XSI扩展的实现确实需要void *类型的对象可以保存指向函数的指针。 但是,将指向函数的指针转换为指向另一种数据类型(void *除外)的指针的结果仍未定义。 请注意,如果尝试从void *指针到函数指针的转换,则需要符合ISO C标准的编译器生成警告,如下所示:

  fptr = (int (*)(int))dlsym(handle, "my_function"); 

由于此处提到的问题,未来版本可以添加新函数以返回函数指针,或者可以弃用当前接口以支持两个新函数:一个返回数据指针而另一个返回函数指针。

对于dlsym() C ++ 11解决了C / C ++和POSIX之间长期不匹配的问题。 只要实现支持此function,就可以使用reinterpret_cast将函数指针转换为数据指针。

从标准来看,5.2.10段。 8,“有条件地支持将函数指针转换为对象指针类型,反之亦然”。 1.3.5将“有条件支持”定义为“不需要支持实现的程序构造”。

根据目标体系结构,代码和数据可以存储在基本上不兼容的,物理上不同的存储区域中。

undefined并不一定意味着不允许,它可能意味着编译器实现者可以更自由地按照自己的意愿去做。

例如,在某些体系结构上可能无法实现 – undefined允许它们仍然具有符合标准的“C”库,即使您不能这样做。

它们可以是具有不同空间要求的不同类型。 分配给一个可以不可逆地切片指针的值,以便分配返回导致不同的东西。

我相信它们可以是不同的类型,因为标准不希望限制在不需要时节省空间的可能实现,或者当大小可能导致CPU必须使用额外的废话等时…

另一种方案:

假设POSIX保证函数和数据指针具有相同的大小和表示(我找不到相关的文本,但引用的示例OP表明它们至少打算提出此要求),以下应该有效:

 double (*cosine)(double); void *tmp; handle = dlopen("libm.so", RTLD_LAZY); tmp = dlsym(handle, "cos"); memcpy(&cosine, &tmp, sizeof cosine); 

这可以通过遍历char []表示来避免违反别名规则, char []表示允许对所有类型进行别名。

另一种方法:

 union { double (*fptr)(double); void *dptr; } u; u.dptr = dlsym(handle, "cos"); cosine = u.fptr; 

但如果你想要100%正确的话,我会推荐使用memcpy方法。

唯一真正可移植的解决方案是不使用dlsym作为函数,而是使用dlsym来获取指向包含函数指针的数据的指针。 例如,在您的库中:

 struct module foo_module = { .create = create_func, .destroy = destroy_func, .write = write_func, /* ... */ }; 

然后在你的申请中:

 struct module *foo = dlsym(handle, "foo_module"); foo->create(/*...*/); /* ... */ 

顺便说一句,无论如何,这都是很好的设计实践,并且通过dlopen和静态链接支持动态链接的系统上的所有模块,或者用户/系统集成商不想使用动态链接,可以轻松支持动态加载。

在大多数体系结构中,指向所有普通数据类型的指针具有相同的表示forms,因此数据指针类型之间的转换是无操作的。

但是,可以想象函数指针可能需要不同的表示,也许它们比其他指针更大。 如果void *可以保存函数指针,那么这意味着void *的表示必须是更大的大小。 并且所有来自void *的数据指针的转换都必须执行这个额外的副本。

正如有人提到的,如果你需要这个,你可以使用union来实现它。 但是,void *的大多数用法仅用于数据,因此在需要存储函数指针的情况下增加所有内存使用将是繁重的。

函数指针大小与数据指针大小不同的现代示例: C ++类成员函数指针

直接引用自https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

 class Base1 { int b1; void Base1Method(); }; class Base2 { int b2; void Base2Method(); }; class Derived : public Base1, Base2 { int d; void DerivedMethod(); }; 

现在有两个可能的指针。

指向Base1的成员函数的指针可以用作指向Derived的成员函数的指针,因为它们都使用相同的this指针。 但是指向Base2的成员函数的指针不能用作指向Derived成员函数的指针,因为需要调整this指针。

有很多方法可以解决这个问题。 以下是Visual Studio编译器决定如何处理它:

指向乘法inheritance类的成员函数的指针实际上是一个结构。

 [Address of function] [Adjustor] 

使用多重inheritance的类的指向成员函数的大小是指针的大小加上size_t的大小。

tl; dr:当使用多重inheritance时,指向成员函数的指针可能(取决于编译器,版本,体系结构等)实际存储为

 struct { void * func; size_t offset; } 

这明显大于void *

我知道自2012年以来没有对此进行评论,但我认为添加我确实知道一种架构具有非常不兼容的数据和函数指针会很有用,因为对该架构的调用会检查权限并携带额外的信息。 没有多少铸造会有所帮助。 这是The Mill 。