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

137

我读到过将函数指针转换为数据指针,反之亦然,这在大多数平台上都有效,但不保证适用于所有情况。为什么会出现这种情况?两者不都只是主内存中的地址,所以应该兼容吗?


16
在标准C中未定义,在POSIX中定义。注意区别。 - ephemient
10
请注意POSIX在数据类型部分中所述的内容:_§2.12.3 指针类型。 所有函数指针类型都应具有与指向void类型的指针相同的表示形式。将函数指针转换为void *不应改变该表示形式。由此转换产生的void *值可以通过显式强制转换回原始函数指针类型,而不会丢失信息。 注意:ISO C标准不要求这样做,但它是符合POSIX标准所必需的。 - Jonathan Leffler
2
这是该网站“关于”部分的问题。 :) :) 在此处查看您的问题 - ZooZ
@JonathanLeffler:链接的网页没有提到指针类型的转换,也没有第2.12.3节。也许它已经改变了。 我(有点模糊)记得函数指针/void*转换的保证现在仅适用于由dlsym()返回的值。 - Keith Thompson
1
@KeithThompson:世界在变化,POSIX也在变化。我在2012年写的内容在2018年已经不适用了。POSIX标准改变了措辞。现在它与dlsym()相关联——请注意“应用程序使用”部分的末尾,其中说:“请注意,将void *指针转换为函数指针,如:fptr = (int (*)(int))dlsym(handle, "my_function");在ISO C标准中未定义。该标准要求此转换在符合规范的实现上正常工作。” - Jonathan Leffler
显示剩余2条评论
14个回答

179

架构并不一定要将代码和数据存储在同一块内存中。使用哈佛架构,代码和数据分别存储在完全不同的内存中。大多数架构都是冯·诺依曼架构,其中代码和数据存储在同一块内存中,但 C 语言不局限于特定类型的架构。


16
即使代码和数据在物理硬件中存储在同一位置,软件和内存访问通常会防止在没有操作系统“批准”的情况下运行数据作为代码。这与 DEP 等技术有关。 - Michael Graczyk
16
函数指针可能具有不同于数据指针的表示形式,这至少与拥有不同地址空间一样重要(甚至更加重要)。 - Michael Burr
15
即使没有哈佛架构,也可以使用不同的地址空间来存储代码和数据指针——旧版DOS的“Small”内存模型就是这样实现的(使用CS!=DS的近指针)。 - caf
1
即使是现代处理器也会因为指令和数据缓存通常被分别处理而难以处理这样的混合情况,即使操作系统允许您在某个地方编写代码。 - PypeBros
3
在调用VirtualProtect之前,你无法将数据区域标记为可执行。 - Dietrich Epp
显示剩余3条评论

38

有些计算机的代码和数据拥有不同的地址空间。在这种硬件上,指针会失效。

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


似乎C语言委员会从未打算将void*作为函数指针,他们只想要一个通用的对象指针。

C99 Rationale 指出:

6.3.2.3 指针
C现在已经被实现在各种体系结构上。虽然其中一些体系结构具有大小等于某些整数类型的统一指针,但最大程度地可移植的代码不能假定任何不同指针类型和整数类型之间的对应关系。在某些实现中,指针甚至可能比任何整数类型都宽。

void*(“指向void”的指针)作为一种通用的对象指针类型是由C89委员会发明的。采用这种类型的目的是为了指定函数原型参数,这些参数可以静默地转换任意指针(如fread),或者如果参数类型与所需类型不完全匹配,则抛出错误(如strcmp)。关于函数指针没有任何说明,它们可能与对象指针和/或整数不相容。

请注意,最后一段中“没有任何说明函数指针”的话。他们可能与其他指针不同,委员会已经意识到这一点。


16
无法将函数指针赋值给void * - ouah
@Crazy Eddie:所以,void*可以与函数指针兼容吗?我认为所有非函数指针都与函数指针不兼容,包括void* - gexicide
4
关于void*是否可以接受函数指针的问题,我可能是错误的,但核心意思不变。数据就是数据。标准可以要求不同类型的大小能够相互容纳数据,并且即使它们在不同的内存段中使用,赋值也将得到保证。存在这种不兼容性的原因是标准没有保证这一点,所以数据在赋值过程中可能会丢失。 - Edward Strange
5
要求 sizeof(void*) == sizeof( void(*)() ) 会浪费空间,因为函数指针和数据指针在大小上可能不同。这在80年代是一种常见情况,当时第一个C标准被编写出来。 - Robᵩ
8
不同的地址空间可能具有不同的地址“宽度”,例如Atmel AVR使用16位指令和8位数据; 在这种情况下,从数据(8位)转换为函数(16位)指针并进行反向转换将是困难的。 C语言本应易于实现;其中一部分容易性来自于数据和指令指针不兼容。 - John Bode
显示剩余7条评论

33

对于那些还记得MS-DOS、Windows 3.1及更早版本的人来说,答案相当简单。所有这些操作系统都支持多种不同的内存模型,具有不同组合的代码和数据指针特性。

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

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

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

sizeof(void *) < sizeof(void(*)())
在这种情况下,你没有单独的存储代码和数据的空间,但仍然无法在两个指针之间进行转换(除非使用非标准的 __near 和 __far 修饰符)。此外,即使指针大小相同,也不能保证它们指向相同的内容——在DOS Small内存模型中,代码和数据都使用近似指针,但它们指向不同的段。因此,将函数指针转换为数据指针将不会给你一个与函数有任何关系的指针,因此这种转换是没有用处的。

回复:“将函数指针转换为数据指针不会给您提供任何与函数相关的指针,因此没有这样的转换用途”:这并不完全正确。将int*转换为void*会给您一个指针,您实际上无法对其进行任何操作,但仍然有用能够执行此转换。(这是因为void*可以存储任何对象指针,因此可用于不需要知道其所持有类型的通用算法。如果允许,函数指针也可能非常有用。) - ruakh
4
int*转换为void*时,void*指针至少保证指向与原始int*相同的对象 - 因此对于访问所指对象的通用算法(如int n; memcpy(&n, src, sizeof n);)很有用。但将函数指针转换为void*时,如果它不能产生指向函数的指针,那么它就不适用于这样的算法 - 您唯一能做的是将void*再次转换回函数指针,因此最好使用包含void*和函数指针的union - caf
@caf: 好的,说得对。谢谢你指出来。另外,即使void*确实指向函数,我想让人们把它传给memcpy也是个坏主意。:-P - ruakh
请注意 POSIX 在 数据类型 中的规定:_§2.12.3 指针类型。 所有函数指针类型都应具有与指向 void 的指针类型相同的表示形式。将函数指针转换为 void * 不应更改其表示形式。由此转换产生的 void * 值可以使用显式转换回原始函数指针类型,而不会丢失信息。注意:ISO C 标准不要求这样做,但 POSIX 符合性要求这样做。 - Jonathan Leffler
如果它只是应该传递给某个回调函数,该回调函数知道正确的类型,那么我只关心往返安全性,而不关心这些转换值可能具有的任何其他关系。 - Deduplicator

27

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


1
你说得对。这篇文章似乎是一个相当不错的(尽管有点过时)总结了你的观点。 - Manav
1
@KnickerKicker:是的,理想情况下会有一个单独的 dlsym_function() 用于返回函数符号,或者 dlsym() 将返回一个包含 void *void (*)()union {} - caf
33
在Stack Overflow介绍页面上,出现了一个关于时间悖论的问题,其中有人给了一个回答,并且在问题被提出之前就回答了该问题,这个回答得到了一票赞同。 - user764357
2
@LegoStormtroopr:有趣的是,21个人同意点赞的想法,但只有大约3个人实际上这样做了。 :-) - Jerry Coffin
1
@pmor:重点是许多人已经点赞了“在问题被提出之前回答问题”的评论,但只有少数人实际上点赞了答案本身。 - Jerry Coffin
显示剩余3条评论

13

除了已经在这里提到的内容外,值得一提的是看一下POSIX dlsym()

ISO C标准不要求函数指针可以相互转换为数据指针。事实上,ISO C标准不要求void*类型的对象能够保存函数指针。然而,支持XSI扩展的实现确实要求void*类型的对象可保存函数指针。但是,将函数指针转换为另一种数据类型(除了void*)的结果仍然未定义。请注意,符合ISO C标准的编译器在尝试将void*指针转换为函数指针时必须生成警告,如下所示:

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

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


这是否意味着使用dlsym获取函数地址目前是不安全的?目前有安全的方法吗? - gexicide
4
这句话的意思是,目前 POSIX 要求平台 ABI,函数指针和数据指针都可以安全地转换为 void* 并且回转。 - Maxim Egorushkin
@gexicide 这意味着符合POSIX标准的实现已经对语言进行了扩展,为标准本身中未定义的行为赋予了实现定义的含义。它甚至被列为C99标准常见扩展之一,即第J.5.7节函数指针转换。 - David Hammen
1
@DavidHammen 这不是语言的扩展,而是一个新的额外要求。C语言不要求void*与函数指针兼容,而POSIX则要求。 - Maxim Egorushkin

10
C++11对于C/C++和POSIX之间长期存在的关于dlsym()的不匹配问题提出了一个解决方案。只要实现支持这个特性,我们可以使用reinterpret_cast将函数指针转换为数据指针,反之亦然。
根据标准的5.2.10段落8:
将函数指针转换为对象指针类型,或者反之,是有条件支持的。
1.3.5将"有条件支持"定义为:
一个实现不需要支持的程序构造。

可以这样做,但不应该这样做。符合规范的编译器必须为此生成警告(进而触发错误,参见-Werror)。更好(且非UB)的解决方案是检索由dlsym返回的对象的指针(即void **),并将其转换为函数指针的指针仍然是实现定义,但不再引起警告/错误 - Konrad Rudolph
5
不同意。 "有条件支持"这个措辞是专门为了让dlsymGetProcAddress能够在编译时没有警告而编写的。 - MSalters
@MSalters 你说的“不同意”是什么意思?我要么对,要么错。dlsym文档明确表示:“符合ISO C标准的编译器在尝试将void 指针转换为函数指针时必须生成警告”。这就没有多少推测的余地了。而且GCC(使用-pedantic确实*会发出警告。再次强调,没有任何推测的可能性。 - Konrad Rudolph
1
跟进:我现在明白了。这不是未定义行为,而是实现定义的。我仍然不确定是否必须生成警告 - 可能不需要。哦,好吧。 - Konrad Rudolph
2
@KonradRudolph:我不同意你的“不应该”,那是一个观点。答案明确提到了C++11,并且当问题被解决时,我是C++ CWG的成员。C99确实有不同的措辞,条件支持是C++的发明。 - MSalters

7

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


我理解“物理上不同”,但您能否更详细地解释“根本上不兼容”的区别。正如我在问题中所说的,一个空指针难道不应该与任何指针类型一样大吗?还是这是我错误的假设。 - Manav
@KnickerKicker:“void *”足够大以保存任何数据指针,但不一定能保存所有函数指针。 - ephemient
1
回到未来 :P - SSpoke

5

另一种解决方案:

假设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;

但是如果你想要绝对100%正确的C语言,我建议使用memcpy方法。


5

未定义并不一定意味着不允许,它可以意味着编译器实现者有更多的自由来决定如何处理。

例如,在某些架构上可能不可能 - 未定义使得它们仍然可以拥有一个符合标准的'C'库,即使你无法执行此操作。


5

它们可以是不同类型,有不同的空间要求。将其分配给一个可能会不可逆地切片指针的值,以至于重新赋值会得到不同的结果。

我认为它们可以是不同类型,因为标准不想限制可能的实现,在不需要节省空间或当大小可能导致CPU执行额外操作时,可以节省空间等等。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接