将整数转换为指针 - 为什么要先转换为长整型?(如p =(void *)42;)

26
GLib文档中,有一章关于类型转换宏的讨论。在将int转换为void*指针的讨论中,它说道(重点在于我):

Naively, you might try this, but it's incorrect:

gpointer p;
int i;
p = (void*) 42;
i = (int) p;

Again, that example was not correct, don't copy it. The problem is that on some systems you need to do this:

gpointer p;
int i;
p = (void*) (long) 42;
i = (int) (long) p;
(来源:GLib 2.39.92参考手册,第类型转换宏章节。)
为什么需要将其强制转换为long
在将int转换为指针的过程中,是否应该自动进行所需的扩展?

3
我认为,因为int可以是16位而long至少是32位,如果您直接将int转换为long,则可能会出现16个未定义的位。但是,在64位机器上,long仍可能是32位,而指针的大小可能为64位,从而出现相同的问题(如果问题存在)。 - invalid_id
4
将整数类型转换为指针是依赖实现的,这意味着符合标准的编译器必须详细记录在此发生的确切情况。如果引用这段话的作者能够具体说明哪些系统需要进行long强制转换将会很好(如果他们完全避免使用此技术,因为有更可靠的替代方法,那就更好了)。 - M.M
3
如果你要将指针转回相同类型而不是更宽的类型,那么你并不关心它被“扩展”成什么样子,只要从指针到窄整数类型的转换保留最低有效位即可(这是通常的行为)。由于甚至没有提及哪种编译器会定义这些转换以引起麻烦,我不得不假设glib作者们在迷信胡言。 - Pascal Cuoq
2
我已经说服了自己。这是一个或多个人不理解他们试图为其提供兼容性层的工作。 "#define G_MINFLOAT FLT_MIN"表示“可以在gfloat中保存的最小正值”,这是明显错误的。更重要的是,如果您有C99编译器,则没有一个定义是有用的,只有少数定义可与C90兼容。请参考https://developer.gnome.org/glib/stable/glib-Basic-Types.html。 - Pascal Cuoq
1
@Ayxan:感谢您发布悬赏,我仍然很想知道答案:-)。是的,“*void”是一个打字错误。 - sleske
显示剩余4条评论
5个回答

13

那份口头流利的文档是错的,无论是他们(自由选择的)的示例还是一般情况下都是如此。

gpointer p;
int i;
p = (void*) 42;
i = (int) p;

gpointer p;
int i;
p = (void*) (long) 42;
i = (int) (long) p;

在符合C语言标准的所有实现中,这两种方式都会导致变量ip具有相同的值。
这个示例选择不当,因为根据 C11 实验标准 n157: 5.2.4.2.1 Sizes of integer types ,42 保证可以由 intlong 来表示。

一个更具有说明性(且可测试)的示例是:

int f(int x)
{
  void *p = (void*) x;
  int r = (int)p;
  return r;
}

如果 void* 能够表示所有 int 的值,那么这个过程将对 int 值进行往返转换,实际上意味着 sizeof(int) <= sizeof(void*)(理论上:填充位等等,实际上并不重要)。对于其他整数类型,同样的问题,同样的实际规则(sizeof(integer_type) <= sizeof(void*))。

相反,真正的问题,适当说明:

void *p(void *x)
{
  char c = (char)x;
  void *r = (void*)c;
  return r;
}

哇,那真的不可能行得通,对吧?(实际上,它可能行得通)。

为了往返一个指针(长期以来软件做了不必要的事情),您还必须确保您往返的整数类型可以明确地表示指针类型的每个可能的值。

从历史上看,许多软件都是由假设指针可以通过int进行往返的猴子编写的,可能是因为K&R C语言的隐式int特性,以及许多人忘记#include <stdlib.h>,然后将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 capable of holding object pointers)中提供了一个(自然符合标准的)解决方案,它们被指定为保证指针 -> 整数类型 -> 指针的往返(但不是反向往返)。


1
这个答案大部分关注指针通过 int 来回传递,但问题是关于一个 int 通过指针来回传递。我在其他地方(Windows 上)看到过代码使用 "int -> long -> pointer" 而不是 "int -> pointer"。那就是主要的问题。为什么有人会这样做呢? - Ayxan Haqverdili
如果目标是通过void*来回转换int,那么插入一个long是绝对完全无用的。也许这个答案的主要教训是:程序员是懒惰、愚蠢的猴子,每当他们认为可以逃避学习所使用的语言规则时,他们就会选择执行一次雨舞。编程的基本规则似乎是“如果它已经坏了,也不要修复它”。 - EOF
令人沮丧的是,显然那就是答案,尽管我已经看到它被做了几次... - Ayxan Haqverdili
1
这并不是真的;(void *)42 在某些实现上可能立即导致陷阱,并且不同的实现对于那些不陷入陷阱的结果没有给出相同的结果。 此外,对于相同的实现,(void *)42 == (void *)(long)42 也没有要求。 - M.M
@M.M 我并不是说代码在所有符合规范的实现上都能够正常工作,我是说带有 long 强制转换和不带 long 强制转换的代码之间没有区别。我唯一能想到的导致差异的原因是 UB(比如你提到的立即陷阱),在这种情况下,两者 都是 UB,任何差异纯属偶然,不能依赖。 - EOF
2
强制类型转换的结果是实现定义的,实现可以定义(int)42(long)42具有不同的结果。 - M.M

9
根据引用的 C99: 6.3.2.3:

5 整数可以转换为任何指针类型。除非已经指定,否则结果是实现定义的,可能未正确对齐,可能不指向所引用类型的实体,并且可能是陷阱表示。56)

6 任何指针类型都可以转换为整数类型。除非先前指定,否则结果是实现定义的。如果结果无法表示为整数类型,则行为是未定义的。结果不必在任何整数类型的值范围内。

根据您提到的 链接 的文档:

指针大小始终至少为 32 位(在 GLib 所有支持的平台上)。因此,您可以在指针值中存储至少 32 位整数值。

而且,long 至少保证为 32 位

所以,代码如下:

gpointer p;
int i;
p = (void*) (long) 42;
i = (int) (long) p;

正如GLib所宣传的那样,它对于最多32位整数来说更安全、更易移植且定义更清晰。


1
抱歉,我不明白。您列出的引号如何说明将强制转换为“long”更安全、更便携? - sleske
1
@sleske:指针的大小至少为32位(在GLib运行的系统上),并且“long”保证至少为32位。因此,在GLib运行的系统上不存在大小不匹配的问题。而“int”不能保证为32位。 - askmish
1
我明白你为什么将42转换为long,然后再转换为void*。但是先转换为long,再转换为int的意义何在?如果p最初就比较长,那么你首先将其截断为long,然后再截断为int。如果p比int短,你也不会得到任何好处。 - mcsim
我仍然不明白首先转换为长整型的意义。如果指针是64位而长整型是32位,那怎么办? - Ayxan Haqverdili
2
这个答案似乎没有解释为什么代码更“安全,更可移植”,只是断言它是这样的。 - M.M
显示剩余2条评论

7
据我理解,代码(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文档指出的问题(问题?):它发生在某些系统上。

因此,有两个问题:

  1. 将整数分配给相同大小的指针
  2. 将整数分配给不同大小的指针

令我感到好奇的是,尽管这两种情况在C标准和gcc实现说明中具有相同的状态(请参见gcc implementation notes),但gcc仅对第二种情况显示警告。

另一方面,很明显将类型转换为long并不总是解决方案(尽管在现代ABI中sizeof(void*)==sizeof(long)大多数情况下成立),这取决于intlonglong longvoid*的大小,对于64位架构通用而言有许多可能的组合。这就是为什么glib开发人员试图找到与指针匹配的整数类型,并相应地分配glib_gpi_castglib_gpui_castmason构建系统。随后,这些mason变量在这里被用来正确生成那些转换宏(也可参见this以了解基本的glib类型)。最终,这些宏首先将一个整数强制转换为另一个与void*大小相同的整数类型(这种转换符合标准,没有警告)以适应目标架构。
这个解决警告的方法可能是一个不好的设计,现在可以使用 intptr_tuintptr_t 来解决,但有可能出于历史原因而存在: intptr_tuintptr_t 可以 C99 中使用,而 Glib 的开发始于 1998年,所以他们找到了自己的解决方案。看起来曾经有 一些尝试 来改变它:

GLib 依赖于有效的 C99 工具链的各个部分,因此现在应该尽可能使用 C99 整数类型,而不是像 1997 年那样进行配置时发现。

然而没有成功,似乎它从未进入主分支。
简而言之,我认为原始问题已经从“为什么这段代码更好”变成了“为什么这个警告不好”(以及“消除它是否是一个好主意?”)。后者已经在其他地方得到解答,但this也可能会有所帮助:

将指针转换为整数或反之会导致代码不具备可移植性,并可能创建指向无效内存位置的意外指针。

但是,正如我上面所说,对于上述第一问题,此规则似乎不符合警告的资格。也许其他人可以阐明这个问题。
我猜测这种行为背后的理由是,即使是微小的改变也会导致gcc决定抛出一个警告,当原始值以某种方式被改变时。正如gcc doc所说(重点在于我):
如果将整数强制转换为指针,且指针表示比整数类型小,则舍弃最高有效位;如果指针表示比整数类型大,则根据整数类型的符号扩展;否则,位保持不变。因此,如果大小匹配,则没有位的更改(无扩展、截断或填充零),也不会抛出警告。另外,[u]intptr_t仅是适当限定的整数的typedef,并且在将[u]intptr_t分配给void*时不需要发出警告,因为这确实是它的目的。如果规则适用于[u]intptr_t,则必须适用于typedef的整数类型。

为什么会出现警告呢?为什么我们先转换为 long 类型后,警告就消失了呢? - Ayxan Haqverdili
如果删除 const,则会出现警告:https://gcc.godbolt.org/z/LMQ83s - Fusho
1
由于此问题被标记为“C”,请使用C版本,即使使用const也会出现警告:https://gcc.godbolt.org/z/plcxib - Fusho
嗯,Clang 尽可能地遵循 GCC 的方式,我想这归根结底取决于为什么 GCC 会这样做。 - Fusho
为什么复制符号位被认为是值的改变?无论如何,当我们将其转换为长整型时就会发生这种情况。 - Ayxan Haqverdili
显示剩余9条评论

6

我认为这是由于转换的结果与实现相关。最好使用uintptr_t 来完成此操作,因为在特定的实现中它的大小与指针类型相同。


4
size_t是一种无符号整数类型,旨在保存对象的最大大小,而不是指针的大小。uintptr_t是一种无符号整数类型,旨在保存指针的表示形式,其在标准中的存在表明了size_t并不是用于此目的。 - Pascal Cuoq

5

Askmish答案所解释的,从整数类型到指针的转换是实现定义的(例如参见N1570 6.3.2.3 Pointers§5§6以及脚注67)。

从指针到整数的转换也是实现定义的,如果结果不能表示为整数类型,则行为是未定义的

在大多数通用架构中,现在的情况是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 可以容纳对象指针的整数类型:

The following type designates a signed integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:

intptr_t  

The following type designates an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:

uintptr_t  

These types are optional.

所以,虽然 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 模式。不要依赖符号扩展。

其他人,好的...


在这种情况下,不同整数类型之间的转换(intintptr_t)在n1570 6.3.1.3 Signed and unsigned integers中提到。

  1. 将整数类型的值转换为除_Bool以外的其他整数类型时,如果该值可以由新类型表示,则保持不变。

  2. 否则,如果新类型是无符号的,则通过反复添加或减去比新类型中可以表示的最大值多一个的值,直到该值在新类型的范围内。

  3. 否则,新类型为有符号且该值无法表示在其中;结果要么是实现定义的,要么会引发实现定义的信号。


因此,如果我们从一个int值开始,并且实现提供了一个intptr_t类型并且sizeof(int) <= sizeof(intptr_t)INTPTR_MIN <= n && n <= INTPTR_MAX,我们可以安全地将其转换为intptr_t,然后再转换回来。
那个intptr_t可以转换为void *,然后再转换回相同的(1)(2)intptr_t值。
一般情况下,直接在intvoid *之间进行转换是不可行的,即使在所提供的示例中,该值(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中提到,“将指针转换为整数或将整数转换为指针的映射函数旨在与执行环境的寻址结构保持一致”。。

1
你的回答完全没有提到在转换为指针类型之前是否需要将其强制转换为long或其他整数类型。这就是整个问题所在。如果我无论如何都要将其转换为指针类型,那么将其转换为整数类型有什么意义呢?(void*)(intptr_t)42(void*)42好在哪里? - Ayxan Haqverdili
@Ayxan 在我的代码片段中,n(一个 int)被转换为 intptr_t(这是一种 整数 类型),然后在传递给 g(你可以假设它是 GLib 函数之一)时转换为 void *。在 g 中,指针再次被转换为 intptr_t,然后存储在 int 中。你可以将 int 转换为另一种整数类型,因为这是一个定义良好的操作(如果需要添加位,则会添加正确的位数),当另一种类型的大小更大时。你应该使用 intptr_t/uintptr_t,因为它是唯一保证能够转换为 void * 并返回的整数类型。 - Bob__
所以你的意思是我们首先将其转换为intptr_t,这是明确定义的,然后将其转换为void*,由于您引用的标准的部分明确定义,因此整个过程是明确定义的,不像直接将int转换为指针? - Ayxan Haqverdili
如果您的意思是这样,那么将变量先强制转换为intptr_t类型是有技术含义的。在这种情况下,将其转换为long没有任何意义。 - Ayxan Haqverdili
实际上,我会说这是实现定义,因为标准没有指定intvoid *的大小(无论是它们的绝对值还是它们之间的关系)。 - Bob__
显示剩余3条评论

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