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

我读到过,将函数指针转换为数据指针,反之亦然,在大多数平台上都可以工作,但不能保证能够工作。为什么会这样?难道两者不应该简单地将地址存入主存,从而兼容吗?

14541 次浏览

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

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

未定义并不一定意味着不允许,它可以意味着编译器实现者有更多的自由按照他们想要的方式来做。

例如,它可能不可能在一些架构-未定义允许他们仍然有一个符合‘ C’库,即使你不能这样做。

唯一真正可移植的解决方案是不对函数使用 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支持动态加载和静态链接系统上不支持动态链接的所有模块,或者在用户/系统集成商不想使用动态链接的地方。

另一个解决办法:

假设 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% 正确的 C,我会推荐 memcpy方法。

它们可以是具有不同空间要求的不同类型。赋值给 one 可以不可逆地切割指针的值,这样赋回的结果就不一样了。

我相信他们可以是不同的类型,因为标准不想限制可能的实现,节省空间时,它不需要或当大小可能导致 CPU 不得不做额外的废话使用它,等等。.

有些计算机的代码和数据有独立的地址空间,在这样的硬件上就是不能工作。

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


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

C99的基本原理是:

6.3.2.3指标
C 现在已经在很多架构上实现 体系结构具有统一的指针,其大小最多为某种整数类型 便携式代码不能假定不同指针类型和整数类型之间存在任何必要的对应关系。在某些实现中,指针甚至可以比任何整数类型都宽。

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

注意最后一段中的 没有提到指向函数的指针。它们可能与其他指针不同,委员会已经意识到了这一点。

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

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

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

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

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

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

例如,对于 Compactmodel (小代码,大数据) :

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

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

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

在这种情况下,代码和日期没有单独的存储空间,但是仍然不能在两个指针之间进行转换(除非使用非标准的 _ _ near 和 _ _ far 修饰符)。

此外,即使指针的大小相同,也不能保证它们指向同一个东西——在 DOS Small 内存模型中,指针附近使用的代码和数据都指向不同的段。因此,将一个函数指针转换为一个数据指针将不会得到一个与该函数有任何关系的指针,因此这种转换没有任何用处。

C + + 11解决了 C/C + + 和 POSIX 在 dlsym()方面长期存在的不匹配问题。只要实现支持这个特性,就可以使用 reinterpret_cast将函数指针转换为数据指针。

根据标准5.2.10第8段,“有条件地支持将函数指针转换为对象指针类型,或者将对象指针类型转换为对象指针类型。”1.3.5将“有条件支持”定义为“不需要实现支持的程序构造”。

在大多数体系结构中,指向所有普通数据类型的指针具有相同的表示形式,因此在数据指针类型之间进行强制转换是不可行的。

然而,可以想象,函数指针可能需要不同的表示形式,也许它们比其他指针大。如果 void * 可以包含函数指针,这意味着 void * 的表示形式必须是较大的大小。所有到 void * 的数据指针强制转换都必须执行这个额外的副本。

正如有人提到的,如果你需要这个,你可以通过工会来实现。但 void * 的大多数用途只是为了存储数据,因此增加它们的所有内存使用,以防万一需要存储函数指针,这将是一项繁重的工作。

我知道自从2012年以来这个问题还没有得到评论,但是我认为补充一点是有用的,我知道一个体系结构,它有 非常不兼容的数据和函数指针,因为对该体系结构的调用检查特权并携带额外的信息。再多的施法也无济于事。是 磨坊

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

直接引自 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(); };

现在有两个可能的 this指针。

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

有很多方法可以解决这个问题 编译器决定处理它:

指向多重继承类的成员函数的指针实际上是 建筑物。

[Address of function]
[Adjustor]

使用多重继承的类的指针到成员函数的大小是指针的大小加上 size_t的大小。

当使用多重继承时,指向成员函数的指针(取决于编译器、版本、架构等)实际上可以存储为

struct {
void * func;
size_t offset;
}

明显大于 void *