【问题标题】:Definedness of pointer-integer casts指针整数类型转换的定义
【发布时间】:2020-07-24 16:23:08
【问题描述】:

我对从指针到整数和各种相关操作的强制转换的定义性(未定义性、实现定义性)感兴趣。主要是我对 C11 感兴趣,但欢迎其他标准版本(甚至 C++)的答案。

就本问题而言,假设 C 实现提供 intptr_t

考虑以下函数:

#include <assert.h>
#include <stdint.h>

int x;
int y;
int z[2];

void f1(void) {
    int *p = &x;
    intptr_t i = p;
}

void f2(void) {
    int *p = &x;
    intptr_t i1 = p;
    intptr_t i2 = p;
    assert(i1 == i2);
}

void f3(void) {
    int *p1 = &x;
    int *p2 = &y;
    intptr_t i1 = p1;
    intptr_t i2 = p2;
    assert(i1 != i2);
}

void f4(void) {
    int *p1 = &x;
    intptr_t i1 = p1;
    int *p2 = i1;
    intptr_t i2 = p2;
    assert(i1 == i2);
}

void f5(void) {
    int *p1 = &z[0];
    int *p2 = &z[1];
    intptr_t i1 = p1;
    intptr_t i2 = p2;
    assert(i1 < i2);
}

  • 哪些函数调用未定义(实现定义)的行为?
  • 如果使用void* 而不是int*,会有什么变化吗?作为指针目标的任何其他数据类型怎么样?
  • 如果我使用从int*intptr_t 并返回的显式转换,会有什么变化吗? (问,因为 GCC 警告演员。)
  • 保证不会触发哪个asserts?

【问题讨论】:

  • 需要注意的重要一点是,如果p 是指向T 的第一个字节的指针,则通常可能存在一个指针q,其比较等于p,但会指向内存中恰好位于*p 之前的任何对象的末尾,并且无法访问*p。因此,没有什么可以禁止反复无常的实现将(T*)(intptr_t)p 视为等于p 但不能取消引用以访问*p 的指针(例如q),因此大多数代码会尝试取消引用由通过intptr_t 的往返会调用未定义的行为。

标签: c++ c language-lawyer


【解决方案1】:

这就是 C11 标准对intptr_t 的看法:

7.20.1.4 能够保存对象指针的整数类型

以下类型指定有符号整数类型,其属性是任何指向void的有效指针都可以转换为此类型,然后转换回指向void的指针,结果将与原始指针比较:

intptr_t

uintptr_t 也一样(除了有符号 -> 无符号)。

同样来自“6.5.4p3 Cast operator”:

涉及指针的转换,除了 6.5.16.1 的约束允许的情况外,应通过显式强制转换来指定。

6.5.16.1 没有提到将指针分配给整数类型,反之亦然(0 常量除外)。这意味着您在分配时确实需要强制转换,gcc 只允许它作为编译器扩展(而且它根本不使用-pedantic-errors 编译)

至于这些转换中返回的确切值,标准是这样说的:

6.3.2.3 指针

p5 整数可以转换为任何指针类型。除非前面指定,结果是实现定义的,[...]

p6 任何指针类型都可以转换为整数类型。除非前面指定,结果是实现定义的。 [...]


您拥有的基本保证是:

int x;
(int*) (void*) (intptr_t) (void*) &x == &x;
/* But the void* casts can be implicit */
(int*) (intptr_t) &x == &x;

并且没有必要强制转换为相同的整数。例如,以下情况可能为真:

int x;
(intptr_t) &x != (intptr_t) &x;

在必要时添加强制转换,并将断言转换为返回(因为 assert(false) 是未定义的行为),您的函数都没有未定义的行为,但 f2f4f5 可以为 false。 f3 必须为真,因为两个整数必须不同才能转换为不同的指针。

【讨论】:

  • 完美回答了我的问题,谢谢!出于好奇,哪个版本的 GCC 无法使用-pedantic 编译我的代码?我检查了 GCC 10.1.0,它只产生警告 (gcc -Wall -Wextra -pedantic -std=c11 foo.c)
  • 虽然将int * 直接转换为intptr_t 确实会产生实现定义的结果,并且原则上这并不禁止(intptr_t) &amp;x != (intptr_t) &amp;x 计算为1,但我认为您会很难找到以一种可能的方式定义转换的实现。请注意,“定义的实现”仍然是定义的——只是不确定是否在不同的实现中定义一致。另一方面,未定义的行为是完全不同的野兽。
  • f3 必须为真,因为两个整数必须不同才能转换为不同的指针 这是从哪里来的?为什么不能有一个假设的实现使用值本身以外的东西来跟踪整数值的出处?
  • @LanguageLawyer 我们可以肯定地假设memcmp(i1, i2, sizeof i1) == 0 可能是假的。甚至(void*) +(intptr_t) (void*) &amp;x 也可能是实现定义的。但是 intptr -> 指针转换似乎适用于整数值,无论任何非值填充如何,因为第 6.3.2.3 节(指针)中的脚注提到了“用于将 [...] 整数转换为指针的映射函数” ,而不是指向指针的整数类型的对象。
  • @JohnBollinger:在分段模式 80386 上,指针为 48 位;没有复制指针的指令,但有写入低 32 位和高 16 位的指令。如果p 是保存在寄存器中的指针,puipuintptr_t 的地址,那么设置*upuip = (uintptr_t)p 的最快方法是,如果相等的指针不需要产生相等的整数,则写入底部的 48 位upuip,而前 16 位保持不变。
【解决方案2】:

如果intptr_t 存在,那么它能够持有void* 而不会丢失数据:

以下类型指定有符号整数类型,其属性是任何指向 void 的有效指针都可以转换为该类型,然后转换回指向 void 的指针,结果将与原始指针进行比较:

   intptr_t

(§7.20.1.4p1)

但是,如果指针不是指向 void 的指针,则除非它是空指针,否则所有赌注都将关闭:

整数可以转换为任何指针类型。除非前面指定,结果是实现定义的,可能没有正确对齐,可能不指向引用类型的实体,并且可能是陷阱表示。 任何指针类型都可以转换为整数类型。除非前面指定,结果是实现定义的。如果结果不能以整数类型表示,则行为未定义。结果不必在任何整数类型的值范围内。 (§6.3.2.3p5-6)

“先前指定”是void*和整数类型之间的转换,以及空指针常量到整数类型的转换。

因此,一个严格正确的程序需要插入 void* 演员表:

intptr_t i = (intptr_t)(void*)p;
T* p = (void*)i;

没关系,因为任何对象类型和void*之间的往返转换都保证无损:

指向 void 的指针可以转换为指向任何对象类型的指针或从指向任何对象类型的指针转​​换。指向任何对象类型的指针都可以转换为指向 void 的指针并再次返回;结果应与原始指针比较。

(第一行中的显式intptr_t 是必要的,因为赋值不会从指针隐式转换为整数,尽管一些编译器允许将其作为扩展。但是,在C 中,赋值操作会在void* 之间进行隐式转换和其他指针类型(§6.5.16.1p1,第四个要点)。)

请注意,“对象类型”不包括函数指针。

此外,序列void*intptr_tvoid* 的值等于原始值这一事实并不意味着序列intptr_tvoid*intptr_t 具有相同的属性。虽然指针和整数之间的转换“旨在与执行环境的寻址结构保持一致”,但该声明位于脚注中,因此不规范。无论如何,“执行环境的寻址结构”可能允许同一地址的多个表示。 (您不必看得太远就能找到示例:-)。 )

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-07-10
    • 2014-08-15
    • 1970-01-01
    • 2015-01-06
    • 1970-01-01
    • 2020-06-15
    • 1970-01-01
    相关资源
    最近更新 更多