我已经读过将函数指针转换为数据指针,反之亦然在大多数平台上都有效,但不能保证有效。为什么会这样?两者不应该都是简单的主存地址,因此是兼容的吗?
14 回答
架构不必将代码和数据存储在同一内存中。使用哈佛架构,代码和数据存储在完全不同的内存中。大多数架构都是冯诺依曼架构,代码和数据在同一个内存中,但如果可能的话,C 不会将自己限制在某些类型的架构上。
一些计算机具有(具有)用于代码和数据的单独地址空间。在这样的硬件上它是行不通的。
该语言不仅是为当前的桌面应用程序设计的,而且允许它在大量硬件上实现。
似乎 C 语言委员会从未打算void*
成为函数指针,他们只是想要一个指向对象的通用指针。
C99 基本原理说:
6.3.2.3 指针
C 现在已在广泛的体系结构上实现。虽然其中一些架构具有统一指针,其大小与某些整数类型相同,但最大可移植代码不能假定不同指针类型和整数类型之间有任何必要的对应关系。在某些实现中,指针甚至可以比任何整数类型更宽。使用
void*
(“pointer tovoid
”) 作为通用对象指针类型是 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");
由于这里提到的问题,未来版本可能会添加一个新函数来返回函数指针,或者可能会弃用当前接口以支持两个新函数:一个返回数据指针,另一个返回函数指针。
C++11 解决了 C/C++ 和 POSIX 在dlsym()
. reinterpret_cast
只要实现支持此功能,就可以使用将函数指针转换为数据指针/从数据指针转换。
根据标准,5.2.10 段。8,“有条件地支持将函数指针转换为对象指针类型或反之亦然。” 1.3.5 将“有条件支持”定义为“不需要实现支持的程序构造”。
根据目标架构,代码和数据可能存储在根本不兼容的、物理上不同的内存区域中。
undefined 并不一定意味着不允许,它可能意味着编译器实现者有更多的自由来做他们想做的事情。
例如,在某些架构上可能无法实现 - undefined 允许它们仍然具有符合要求的“C”库,即使您不能这样做。
另一种解决方案:
假设 POSIX 保证函数和数据指针具有相同的大小和表示形式(我找不到这方面的文本,但引用的示例 OP 表明他们至少打算提出这个要求),以下应该有效:
double (*cosine)(double);
void *tmp;
handle = dlopen("libm.so", RTLD_LAZY);
tmp = dlsym(handle, "cos");
memcpy(&cosine, &tmp, sizeof cosine);
这通过char []
允许对所有类型进行别名的表示来避免违反别名规则。
还有一种方法:
union {
double (*fptr)(double);
void *dptr;
} u;
u.dptr = dlsym(handle, "cos");
cosine = u.fptr;
memcpy
但如果你想要绝对 100% 正确的 C ,我会推荐这种方法。
它们可以是具有不同空间要求的不同类型。分配给一个可以不可逆地分割指针的值,以便分配回导致不同的结果。
我相信它们可以是不同的类型,因为标准不想限制可能的实现,这些实现在不需要时可以节省空间,或者当大小可能导致 CPU 不得不做额外的废话来使用它时,等等......
唯一真正可移植的解决方案是不使用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
不支持动态链接的系统上的所有模块的动态加载和静态链接,或者用户/系统集成商不想使用动态链接的地方。
函数指针与数据指针大小不同的现代示例: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(); };
现在有两个可能的
this
指针。指向 的成员函数的
Base1
指针可以用作指向 的成员函数的指针Derived
,因为它们都使用相同的this
指针。但是指向 的成员函数的指针Base2
不能按原样用作指向 的成员函数的指针Derived
,因为this
需要调整指针。有很多方法可以解决这个问题。下面是 Visual Studio 编译器决定处理它的方式:
指向多重继承类的成员函数的指针实际上是一个结构。
[Address of function] [Adjustor]
使用多重继承的类的成员函数指针的大小是指针的大小加上 a 的大小
size_t
。
tl;dr:当使用多重继承时,指向成员函数的指针可能(取决于编译器、版本、体系结构等)实际上存储为
struct {
void * func;
size_t offset;
}
这显然大于 a void *
。
在大多数架构上,指向所有普通数据类型的指针都具有相同的表示形式,因此在数据指针类型之间进行转换是无操作的。
但是,可以想象函数指针可能需要不同的表示形式,也许它们比其他指针大。如果 void* 可以保存函数指针,这意味着 void* 的表示形式必须更大。并且所有指向/来自 void* 的数据指针都必须执行这个额外的副本。
正如有人提到的,如果你需要这个,你可以使用联合来实现。但是 void* 的大多数用途只是用于数据,因此增加所有内存使用量以防万一需要存储函数指针是很麻烦的。
我知道自 2012 年以来一直没有对此发表评论,但我认为补充一点我确实知道一个架构具有非常不兼容的数据和函数指针,因为对该架构的调用会检查权限并携带额外的信息,这将是有用的。再多的铸造也无济于事。这是磨坊。