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


130

我已经读过,在大多数平台上都可以将函数指针转换为数据指针,反之亦然,但不能保证能正常工作。为什么会这样呢?难道两个都不都是简单地寻址到主存储器并因此兼容吗?


16
在POSIX中定义的标准C中未定义。注意差异。
ephemient'2

我对此有些新意,但是您不应该在“ =”的右侧进行强制转换吗?在我看来,问题在于您正在分配一个空指针。但是我看到手册页可以做到这一点,所以希望有人可以教育我。我在网上人们看到了从dlsym转换返回值的示例,例如:daniweb.com/forums/thread62561.html
JasonWoof 2011年

9
请注意POSIX在“ 数据类型§2.12.3指针类型voidvoid *void *
乔纳森·莱夫勒

2
这是此网站的“关于”部分中的问题。.::) :) 在这里看到您的问题
ZooZ 2014年

1
@KeithThompson:世界在变化-POSIX也是如此。我在2012年写的内容在2018年不再适用。POSIX标准更改了措辞。现在,它与dlsym()— 相关联-请注意“应用程序使用情况”部分的末尾,其中指出:请注意, ISO C标准没有定义从void *指针到函数指针的转换(如 fptr = (int (*)(int))dlsym(handle, "my_function");:)。该标准要求此转换才能在符合标准的实现中正常工作。
乔纳森·莱夫勒

Answers:


171

架构不必将代码和数据存储在同一内存中。使用哈佛架构,代码和数据存储在完全不同的内存中。大多数体系结构都是冯·诺依曼体系结构,在同一内存中具有代码和数据,但是C绝不将自身限制为仅某些类型的体系结构。


15
同样,即使代码和数据存储在物理硬件中的同一位置,软件和内存访问也经常会在没有操作系统“批准”的情况下阻止将数据作为代码运行。DEP之类的。
Michael Graczyk 2012年

15
至少与具有不同的地址空间一样重要(可能更重要)是函数指针与数据指针的表示可能不同。
Michael Burr

14
您甚至不必具有哈佛体系结构即可使用不同的地址空间来存储代码和数据指针-旧的DOS“小”内存模型可以做到这一点(在指针附近CS != DS)。
CAF

1
即使是现代处理器也难以解决这种混合问题,因为指令和数据缓存通常是分开处理的,即使操作系统允许您在某处编写代码。
PypeBros's

3
@EricJ。直到您调用VirtualProtect,这才允许您将数据区域标记为可执行文件。
Dietrich Epp 2012年

37

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

该语言不仅为当前的桌面应用程序设计,而且允许在大量硬件上实现。


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

C99基本原理说:

6.3.2.3指针
C现在已在多种体系结构上实现。尽管其中一些体系结构具有统一的指针,这些指针的大小是某些整数类型的大小,但最大的可移植代码无法假定不同指针类型和整数类型之间的任何必要的对应关系。在某些实现中,指针甚至可以比任何整数类型都宽。

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

注意在上一段中没有提到任何有关函数的指针。它们可能与其他指标有所不同,委员会对此有所了解。


该标准可以通过简单地使数据类型具有相同的大小,并保证先分配给一个然后再返回将导致相同的值,而使它们兼容而不会引起混乱。他们使用void *来做到这一点,后者是唯一兼容所有内容的指针类型。
爱德华·斯特朗奇

15
@CrazyEddie您不能将函数指针分配给void *
哇,2012年

4
我在void *接受函数指针方面可能是错的,但重点仍然存在。位就是位。该标准可能要求不同类型的大小能够容纳彼此的数据,并且即使在不同的内存段中使用它们,也可以保证分配工作。存在这种不兼容性的原因是该标准无法保证,因此数据可能会在分配中丢失。
爱德华·斯特朗奇

5
但是sizeof(void*) == sizeof( void(*)() )在函数指针和数据指针大小不同的情况下,要求将浪费空间。这是在80年代写第一个C标准时的一种常见情况。
罗伯(Robᵩ)2012年

8
@RichardChambers:不同的地址空间也可能具有不同的地址宽度,例如Atmel AVR,它使用16位指令和8位数据;在这种情况下,将很难从数据(8位)指针转换为函数(16位)指针并再次返回。C应该易于实现;这种缓解的部分原因是由于数据和指令指针互不兼容。
约翰·博德2012年

30

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

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

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

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

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

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

另外,不能保证即使指针大小相同,它们也指向同一事物-在DOS小型内存模型中,代码和数据都在指针附近使用,但它们指向的是不同的段。因此,将函数指针转换为数据指针根本不会给您一个与该函数有任何关系的指针,因此这种转换毫无用处。


回复:“将函数指针转换为数据指针将不会给您一个与该函数有任何关系的指针,因此这种转换毫无用处”:这并不能完全遵循。将转换int*void*会给您一个指针,您实际上无法做任何事情,但是能够执行转换仍然很有用。(这是因为void*可以存储任何对象指针,因此可以用于不需要知道它们所持有的类型的通用算法。如果允许的话,同样的事情也可以用于函数指针。)
ruakh

4
@ruakh:在转换的情况下int *,以void *中,void *是保证至少指向同一个对象作为原始int *没有-所以这是通用算法有用的访问被指向的对象,等int n; memcpy(&n, src, sizeof n);。如果将函数指针转换为a void *不会产生指向该函数的指针,则对于此类算法没有用-您唯一可以做的就是void *再次将其转换回函数指针,因此您可能会只需使用union包含void *和函数指针。
caf 2012年

@caf:足够公平。感谢您指出了这一点。就此而言,即使void* 确实指向了该函数,我认为将其传递给也是一个坏主意memcpy。:-P
ruakh 2012年

从上面复制:请注意POSIX在“ 数据类型§2.12.3指针类型”中所说的内容。所有函数指针类型均应具有与的类型指针相同的表示形式void。将函数指针转换为void *不得改变表示形式。void *可以使用显式强制转换将这种转换产生的值转换回原始函数指针类型,而不会丢失信息。注意:ISO C标准不要求这样做,但是为了符合POSIX要求,它是必需的。
乔纳森·莱夫勒

@caf如果只应将其传递给知道正确类型的某些回调,则我仅对往返安全性感兴趣,而不对那些转换后的值可能具有的其他关系感兴趣。
Deduplicator

23

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


1
但是dlsym()函数不应该返回空*以外的东西。我的意思是,如果void *不足以容纳函数指针,那么我们是否已经对它进行了修饰?
Manav

1
@Knickerkicker:是的,可能。如果有内存的话,可能在9或10年前在OpenGroup的电子邮件列表中详细讨论了dlsym的返回类型。副手,我不记得有什么(如果有的话)。
杰里·科芬

1
你是对的。似乎是您观点的相当不错的(尽管过时的)总结。
Manav


2
@LegoStormtroopr:有趣的是21个人同意投票的想法,但实际上只有3个人这样做。:-)
杰里·科芬

13

除了这里已经说过的内容外,看看POSIX还很有趣dlsym()

ISO C标准不要求可以将指向函数的指针前后转换为指向数据的指针。确实,ISO C标准并不要求void *类型的对象可以持有指向函数的指针。但是,支持XSI扩展的实现确实要求void *类型的对象可以容纳指向函数的指针。但是,将指向函数的指针转换为指向另一种数据类型(void *除外)的指针的结果仍然不确定。请注意,如果尝试从void *指针转换为函数指针,则需要遵循ISO C标准的编译器生成警告,如下所示:

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

由于此处提到的问题,将来的版本可能添加新函数以返回函数指针,或者不建议使用当前接口,而推荐使用两个新函数:一个返回数据指针,另一个返回函数指针。


这是否意味着使用dlsym获取函数的地址当前不安全?目前有安全的方法吗?
gexicide

4
这意味着当前POSIX从平台ABI要求功能和数据指针都可以安全地void*前后转换。
Maxim Egorushkin 2012年

@gexicide这意味着符合POSIX的实现对语言进行了扩展,使实现定义的含义符合标准本身的未定义行为。它甚至被列为C99标准的常见扩展之一,第J.5.7节“函数指针强制转换”。
David Hammen 2012年

1
@DavidHammen这不是语言的扩展,而是新的额外要求。C不需要void*与函数指针兼容,而POSIX则需要。
Maxim Egorushkin 2012年

9

C ++ 11解决了C / C ++与POSIX之间长期存在的不匹配问题dlsym()reinterpret_cast只要实现支持此功能,就可以用于将函数指针与数据指针进行转换。

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


一个可以,但是不应该。合格的编译器必须为此生成警告(依次应触发错误,请参阅-Werror)。更好的(非UB)解决方案是检索指向dlsym(即void**)返回的对象的指针,并将其转换为指向函数指针的指针仍由实现定义,但不再导致警告/错误
康拉德·鲁道夫2012年

3
@KonradRudolph:不同意。专门编写了“有条件支持的”措词,以允许dlsymGetProcAddress编译而不会发出警告。
MSalters 2012年

@MSalters是什么意思,“不同意”?我是对还是错。在对dlsym文件明确地说是“编译器符合ISO C标准,需要在从一个void *指针的函数指针转换试图生成一个警告”。这没有太多的猜测空间。GCC(带有-pedantic确实会发出警告。再次,没有猜测可能。
康拉德·鲁道夫2012年

1
后续:我想我现在明白了。不是UB。它是实现定义的。我仍然不确定是否必须生成警告-可能不是。那好吧。
康拉德·鲁道夫2012年

2
@KonradRudolph:我不同意你的“不应该”,这是一种观点。答案特别提到了C ++ 11,在解决该问题时,我是C ++ CWG的成员。C99确实有不同的措辞,有条件地支持是C ++的发明。
MSalters 2012年

7

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


我理解“在物理上是截然不同的”,但是您能否详细说明“在根本上不兼容”的区别。就像我在问题中说的那样,void指针不是应该像任何指针类型一样大吗?还是我的推定是错误的?
Manav

@KnickerKicker:void *足够大,可以容纳任何数据指针,但不一定包含任何函数指针。
ephemient 2011年

1
回到未来:P
S辐

5

undefined不一定意味着不允许,它可能意味着编译器实现者有更多的自由来按照自己的意愿进行操作。

例如,在某些架构上可能是不可能的-未定义允许它们仍然具有一致的“ C”库,即使您不能这样做也是如此。


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;

但是memcpy如果您绝对希望100%正确的C语言,我会推荐这种方法。


5

它们可以是具有不同空间要求的不同类型。分配给一个指针可以不可逆地对指针的值进行切片,因此向后分配将产生不同的结果。

我相信它们可以是不同的类型,因为该标准不想限制可能的实现,这些实现可以在不需要时或节省空间的情况下节省空间,而这种大小可能导致CPU必须做更多操作才能使用它,等等。


3

唯一真正可移植的解决方案是不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不支持动态链接或用户/系统集成商不希望使用动态链接的系统上的所有模块通过静态链接来支持动态加载。


2
真好!虽然我同意这似乎更易于维护,但(对我而言)我仍然不知道如何在此之上进行静态链接。你能详细说明吗?
Manav

2
如果每个模块都有自己的foo_module结构(具有唯一的名称),则可以简单地创建一个带有数组struct { const char *module_name; const struct module *module_funcs; }和一个简单函数的额外文件,以在该表中搜索您要“加载”的模块并返回正确的指针,然后使用此指针代替dlopendlsym
R .. GitHub停止帮助ICE,

@R ..是的,但是由于必须维护模块结构,因此增加了维护成本。
user877329 2014年

3

一个现代的示例,其中函数指针的大小可以与数据指针的大小不同: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不能原样用作指向的成员函数Derivedthis 指针,因为该指针需要调整。

有许多解决方法。这是Visual Studio编译器决定处理它的方式:

指向多重继承类的成员函数的指针实际上是一个结构。

[Address of function]
[Adjustor]

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

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

struct { 
    void * func;
    size_t offset;
}

显然比void *


2

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

但是,可以想到的是,函数指针可能需要不同的表示形式,也许它们比其他指针更大。如果void *可以容纳函数指针,则意味着void *的表示形式必须更大。并且所有向/从void *进行数据指针的强制转换都必须执行此额外复制。

如某人所述,如果您需要此功能,则可以使用联合来实现。但是void *的大多数用法只是用于数据,因此,以防万一需要存储函数指针,增加它们的所有内存使用量将是繁重的工作。


-1

我知道自2012年以来就没有对此进行评论,但是我想补充一下,我确实知道一个体系结构具有非常不兼容的数据和功能指针的指针,这很有用,因为对该体系结构的调用会检查特权并携带额外的信息。大量的投放都无济于事。是磨坊


这个答案是错误的。例如,您可以将函数指针转换为数据指针并从中读取(如果您像往常一样具有从该地址读取的权限)。结果和在x86上一样有意义。
曼努埃尔·雅各布
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.