[英]Cast int to pointer - why cast to long first? (as in p = (void*) 42; )
在GLib文档中,有一章是关于类型转换宏的。 在将int
转换为void*
指针的讨论中,它说(强调我的):
天真地,您可能会尝试这样做,但这是不正确的:
gpointer p; int i; p = (void*) 42; i = (int) p;
同样,那个例子是不正确的,不要复制它。 问题是在某些系统上你需要这样做:
gpointer p; int i; p = (void*) (long) 42; i = (int) (long) p;
(来源:GLib 2.39.92 的 GLib 参考手册,章节类型转换宏)。
为什么需要转换为long
?
任何需要的int
扩展是否应该不会作为指针转换的一部分自动发生?
根据C99: 6.3.2.3
引用:
5 整数可以转换为任何指针类型。 除了前面指定的之外,结果是实现定义的,可能没有正确对齐,可能没有指向引用类型的实体,并且可能是陷阱表示。56)
6 任何指针类型都可以转换为整数类型。 除了前面指定的,结果是实现定义的。 如果结果不能以整数类型表示,则行为未定义。 结果不必在任何整数类型的值范围内。
根据您提到的链接中的文档:
指针的大小始终至少为 32 位(在 GLib 打算支持的所有平台上)。 因此,您可以在指针值中存储至少 32 位整数值。
所以,代码
gpointer p;
int i;
p = (void*) (long) 42;
i = (int) (long) p;
正如 GLib 所宣传的那样,它更安全、更便携,并且仅适用于最多 32 位整数。
glib 文档是错误的,无论是对于他们(自由选择的)示例还是一般而言。
gpointer p;
int i;
p = (void*) 42;
i = (int) p;
和
gpointer p;
int i;
p = (void*) (long) 42;
i = (int) (long) p;
在所有符合 c 的实现上都会导致i
和p
相同值。
该示例选择不当,因为42
保证可以用int
和long
表示(C11 草案标准 n157: 5.2.4.2.1 Sizes of integer types )。
一个更具说明性(和可测试)的例子是
int f(int x)
{
void *p = (void*) x;
int r = (int)p;
return r;
}
这将往返int
值如果void*
可以表示int
可以表示的每个值,这实际上意味着sizeof(int) <= sizeof(void*)
(理论上:填充位,yadda,yadda,实际上并不重要) . 对于其他整数类型,同样的问题,同样的实际规则( sizeof(integer_type) <= sizeof(void*)
)。
相反,真正的问题,正确说明:
void *p(void *x)
{
char c = (char)x;
void *r = (void*)c;
return r;
}
哇,这是不能工作的可能,对吧? (实际上,它可能)。 为了往返指针(软件已经做了很长时间没有必要),您还必须确保您往返的整数类型可以明确表示指针类型的每个可能值。
从历史上看,很多软件是由猴子编写的,它们假设指针可以通过int
来回传输,这可能是因为 K&R c 的隐式int
-"feature" 以及很多人忘记#include <stdlib.h>
然后转换malloc()
的结果malloc()
到指针类型,从而意外地通过int
往返。 在机器上,代码是为sizeof(int) == sizeof(void*)
,所以这是有效的。 当切换到具有 64 位地址(指针)的 64 位机器时,很多软件都期望两个互斥的东西:
1) int
是一个 32 位 2 的补码整数(通常也期望有符号溢出环绕)
2) sizeof(int) == sizeof(void*)
一些系统(咳Windows咳)也假定sizeof(long) == sizeof(int)
,大多数其他系统有 64 位long
。
因此,在大多数系统上,将往返中间整数类型更改为long
固定(不必要地损坏)代码:
void *p(void *x)
{
long l = (long)x;
void *r = (void*)l;
return r;
}
当然,在 Windows 上除外。 从好的方面来说,对于大多数非 Windows(和非 16 位)系统sizeof(long) == sizeof(void*)
是正确的,因此往返是双向的。
所以:
当然,c 标准在intptr_t
/ uintptr_t
(C11 草案标准 n1570: 7.20.1.4 Integer types能够保持对象指针)中有一个(自然符合标准的)解决方案,它被指定为保证
指针 -> 整数类型 -> 指针
往返(虽然不是相反)。
我认为这是因为这种转换是依赖于实现的。 为此最好使用uintptr_t
,因为它在特定实现中是指针类型的大小。
正如在Askmish的回答中所解释的,从整数类型到指针的转换是由实现定义的(参见例如N1570 6.3.2.3 指针§5 §6和脚注67 )。
从指针到整数的转换也是实现定义的,如果结果不能用整数类型表示,则行为是undefined 。
现在,在大多数通用架构上, sizeof(int)
小于sizeof(void *)
,因此即使是那些行
int n = 42;
void *p = (void *)n;
当用 clang 或 gcc 编译时会产生警告(参见例如这里)
warning: cast to pointer from integer of different size [-Wint-to-pointer-cast]
从 C99 开始,头文件<stdint.h>
引入了一些可选的固定大小类型。 一对夫妇,特别是,应该在这里使用n1570 7.20.1.4 能够保存对象指针的整数类型:
以下类型指定了一个有符号整数类型,其属性是任何指向 void 的有效指针都可以转换为该类型,然后转换回指向 void 的指针,并且结果将与原始指针相等:
intptr_t
以下类型指定了一个无符号整数类型,其属性是任何指向 void 的有效指针都可以转换为该类型,然后转换回指向 void 的指针,并且结果将与原始指针相等:
uintptr_t
这些类型是可选的。
因此,虽然long
可能比int
更好,但为了避免未定义的行为,最可移植(但仍由实现定义)的方法是使用其中一种类型(1) 。
Gcc 的文档指定了转换是如何发生的。
4.7 数组和指针
将指针转换为整数或反之的结果 (C90 6.3.4、C99 和 C11 6.3.2.3) 。
如果指针表示大于整数类型,则从指针到整数的转换会丢弃最高有效位,如果指针表示小于整数类型,则符号扩展(2) ,否则位不变。
如果指针表示小于整数类型,则从整数到指针的转换丢弃最高有效位,如果指针表示大于整数类型,则根据整数类型的符号进行扩展,否则位不变。
当从指针转换为整数并再次返回时,结果指针必须引用与原始指针相同的对象,否则行为未定义。 也就是说,不能使用整数算术来避免 C99 和 C11 6.5.6/8 中禁止的指针算术的未定义行为。
[...]
(2) GCC 的未来版本可能会零扩展,或使用目标定义的 ptr_extend 模式。 不要依赖符号扩展。
其他的, 嗯...
n1570 6.3.1.3 Signed and unsigned integers中提到了不同整数类型(本例中为int
和intptr_t
)之间的转换
当一个整数类型的值被转换为
_Bool
以外的另一个整数类型时,如果该值可以用新类型表示,则不变。否则,如果新类型是无符号的,则通过重复加或减一个新类型可以表示的最大值来转换该值,直到该值在新类型的范围内。
否则,新类型是有符号的,值不能在其中表示; 要么结果是实现定义的,要么引发实现定义的信号。
因此,如果我们从一个int
值开始,并且实现提供了一个intptr_t
类型和sizeof(int) <= sizeof(intptr_t)
或INTPTR_MIN <= n && n <= INTPTR_MAX
,我们可以安全地将其转换为intptr_t
然后再转换背部。
intptr_t
可以转换为void *
,然后转换回相同的(1) (2) intptr_t
值。
对于int
和void *
之间的直接转换,即使在提供的示例中,值 (42) 足够小,不会导致未定义的行为,通常情况下也不相同。
我个人认为链接的 GLib 文档中为这些类型转换宏给出的原因很有争议(重点是我的)
很多时候,GLib、GTK+ 和其他库允许您以空指针的形式将“用户数据”传递给回调。 有时你想传递一个整数而不是一个指针。 您可以分配一个整数 [...] 但这很不方便,而且稍后必须释放内存很烦人。
指针的大小始终至少为 32 位(在 GLib 打算支持的所有平台上)。 因此,您可以在指针值中存储至少 32 位整数值。
我会让读者决定他们的方法是否比简单的方法更有意义
#include <stdio.h>
void f(void *ptr)
{
int n = *(int *)ptr;
// ^ Yes, here you may "pay" the indirection
printf("%d\n", n);
}
int main(void)
{
int n = 42;
f((void *)&n);
}
(1) 我想引用Steve Jessop对这些类型的回答中的一段话
把这当作它所说的意思。 它没有说明大小。
uintptr_t
可能与void*
大小相同。 它可能更大。 可以想象它会更小,尽管这样的 C++ 实现方法有悖常理。 例如,在void*
为 32 位,但仅使用 24 位虚拟地址空间的某些假设平台上,您可以拥有满足要求的 24 位uintptr_t
。 我不知道为什么实现会这样做,但标准允许这样做。
(2) 实际上,标准明确提到了void* -> intptr_t/uintptr_t -> void*
转换,要求这些指针比较相等。 它没有明确规定在intptr_t -> void* -> intptr_t
的情况下,两个整数值比较相等。 它只是在脚注67中提到“将指针转换为整数或将整数转换为指针的映射函数旨在与执行环境的寻址结构保持一致。”。
据我了解,代码(void*)(long)42
比(void*)42
“更好”,因为它消除了gcc
警告:
cast to pointer from integer of different size [-Wint-to-pointer-cast]
在void*
和long
具有相同大小但与int
不同的环境中。 根据 C99,§6.4.4.1 ¶5:
整数常量的类型是其值可以在其中表示的相应列表中的第一个。
因此, 42
被解释为int
,如果将这个常量直接分配给void*
(当sizeof(void*)!=sizeof(int)
),上面的警告会弹出,但每个人都想要干净的编译。 这就是Glib 文档指出的问题(问题?):它发生在某些系统上。
所以,两个问题:
令我感到奇怪的是,即使这两种情况在 C 标准和 gcc 实现说明(请参阅gcc 实现说明)中具有相同的状态, gcc
只显示 2 的警告。
另一方面,很明显,转换为long
并不总是解决方案(仍然,在大多数情况下,在现代 ABI sizeof(void*)==sizeof(long)
),有许多可能的组合取决于大小int
, long
, long long
和void*
,适用于64 位架构和一般情况。 这就是为什么 glib 开发人员试图为指针找到匹配的整数类型并为mason
构建系统相应地分配glib_gpi_cast
和glib_gpui_cast
。 稍后,这些mason
变量在此处用于以正确的方式生成这些转换宏(对于基本 glib 类型,另请参阅此内容)。 最终,这些宏首先将一个整数转换为另一个与void*
大小相同的整数类型(这种转换符合标准,没有警告)用于目标架构。
这个摆脱警告的解决方案可以说是一个糟糕的设计,现在已经被intptr_t
和uintptr_t
解决了,但它有可能是由于历史原因而存在的: intptr_t
和uintptr_t
是可用的,因为 C99和 Glib早在 1998 年就开始了它的开发,所以他们为同一问题找到了自己的解决方案。 似乎有一些尝试改变它:
GLib 依赖于有效 C99 工具链的各个部分,因此是时候尽可能使用 C99 整数类型,而不是像 1997 年那样进行配置时发现。
然而没有成功,它似乎从未进入主分支。
简而言之,正如我所看到的,最初的问题已经从为什么这段代码更好变成了为什么这个警告不好(并且让它静音是个好主意吗? )。 后来已经回答了其他地方,而这也可以帮助:
从指针转换为整数(反之亦然)会导致代码不可移植,并且可能会创建指向无效内存位置的意外指针。
但是,正如我上面所说,这条规则似乎不符合上述问题 1 的警告条件。 也许其他人可以对这个话题有所了解。
我对这种行为背后的基本原理的猜测是 gcc 决定在原始值以某种方式改变时发出警告,即使是微妙的。 正如gcc doc所说(强调我的):
如果指针表示小于整数类型,则从整数到指针的转换丢弃最高有效位,如果指针表示大于整数类型,则根据整数类型的符号进行扩展,否则位不变。
因此,如果大小匹配,则位没有变化(没有扩展、没有截断、没有填充零)并且不会抛出警告。
此外, [u]intptr_t
只是适当限定整数的typedef
:在将[u]intptr_t
分配给void*
时抛出警告是没有[u]intptr_t
,因为这确实是它的目的。 如果规则适用于[u]intptr_t
,则它必须适用于typedef
ed 整数类型。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.