比较悬挂指针是否合法?

72

比较悬空指针是否合法?

int *p, *q;
{
    int a;
    p = &a;
}
{
    int b;
    q = &b;
}
std::cout << (p == q) << '\n';

请注意,pq都指向已经消失的对象。这合法吗?


11
定义"legal":合法的。 - user1804599
6
至少不是未定义行为。 - Puppy
45
@rightfold 我是否面临着被语言律师发出禁止令的风险? - fredoverflow
5
作为一个数据点,gcc将 int*f(){int a;return &a;} 优化为 return 0; - Marc Glisse
5
我想知道这样做有什么用处。 - Ed Heal
显示剩余9条评论
3个回答

59
介绍:第一个问题是是否合法使用p的值。
a被销毁后,p获得了所谓的“无效指针值”。引用自N4430(有关N4430状态的讨论请参见下面的“注意”):

当存储区域的持续时间结束时,代表已释放存储的任何部分的地址的所有指针的值都变为无效指针值

使用无效指针值时的行为也在N4430的同一部分中进行了涵盖(C++14 [basic.stc.dynamic.deallocation]/4中几乎相同的文本出现)。
通过无效指针值间接引用和将无效指针值传递给释放函数具有未定义的行为。对无效指针值的任何其他使用方式均具有实现定义的行为。
[脚注:某些实现可能定义复制无效指针值会导致系统生成的运行时错误。- 结束脚注]
因此,您需要查阅您的实现文档以了解此处应该发生什么(自C++14以来)。
上述引用中的术语“使用”意味着需要进行lvalue-to-rvalue转换,例如在C++14 [conv.lval/2]中:
当将lvalue-to-rvalue转换应用于表达式e并且...所引用的对象包含无效指针值时,行为是实现定义的。
历史: 在C++11中,这个说法是未定义的,而不是实现定义的; 它通过DR1438进行了更改。请参阅此帖子的编辑历史以获取完整的引用。

应用于 p == q: 假设我们在 C++14+N4430 中接受了评估 pq 的结果是实现定义的,并且实现没有定义硬件陷阱发生; [expr.eq]/2说:

如果两个指针都为空,都指向同一个函数或者代表相同的地址(3.9.2),则它们相等,否则它们不相等。

由于评估 pq 时获取的值是实现定义的,因此我们无法确定会发生什么。但它必须是实现定义的或未指定的。

在这种情况下,g++似乎展现出未指定的行为; 根据 -O 开关,我能够让它说 10,分别对应于在 a 被销毁后是否重复使用了相同的内存地址来创建 b


N4430的注意事项:这是一个针对C++14提出的缺陷解决方案,目前尚未被接受。它清理了很多关于对象生命周期、无效指针、子对象、联合和数组边界访问的措辞。

在C++14文本中,[basic.stc.dynamic.deallocation]/4及其后续段落定义了当使用delete时会产生一个无效指针值。然而,并没有明确说明同样的原则是否适用于静态或自动存储。

[basic.compound] / 3中有一个“有效指针”的定义,但过于模糊,不能明智地使用。[basic.life]/5(脚注)引用了相同的文本来定义指向具有静态存储期的对象的指针的行为,这表明它应该适用于所有类型的存储。

在N4430中,将文本从该部分移动到上一级,以便明确适用于所有存储持续时间。附有一个注释:

起草说明:这应该适用于所有可以结束的存储持续时间,而不仅仅是动态存储持续时间。在支持线程或分段堆栈的实现中,线程和自动存储可能会像动态存储一样表现。


我的观点:除了说p获取无效指针值之外,我没有看到任何一致的解释标准(pre-N4430)的方法。这种行为似乎没有被其他部分涵盖,除了我们已经查看过的部分。因此,在这种情况下,我很高兴将N4430措辞视为代表标准意图的方式。



24
@LightnessRacesinOrbit 请给我买一份标准,这样我就可以做到这一点(如果你能邮寄给我一份印刷本就太好了,这样我就可以在我的答案中展示实际的标准而不仅仅是其“内容”,因为它似乎对你没有任何相关性(我是指内容))。顺便说一句,Filip 也对印刷本感兴趣。 - Griwes
19
我们其余的人并不购买标准规范,我们引用最新的可自由获取的草案,通常是FDIS或类似的版本,但这些内容的措辞往往不会有太大变化。 - Puppy
19
如果你知道Nxxxx文件、FDIS和官方标准之间的区别,那么你应该能够识别出与公开免费获取的官方标准最接近的N号码。期望人们花费数百美元只是为了在类似于酒吧赌注的争论中获得更多说服力是荒谬的。 - zwol
13
@zwol:实际上,在类似酒吧赌注的争论中,规定任何入门障碍来击败对手是相当合理的。重点是要获胜,而不是正确;-)如果达到正确答案是关键,那么Lightness当然可以说:“...并且发布的标准是相同/不同”,而不是试图贬低引语却没有替换它。我的意思是,我认为Lightness是正确的,但Filip的引语的问题在于它们不支持他的主张,而不是它们不准确。 - Steve Jessop
9
个人而言,除非有拥有 C++14 副本的人指出差异(据我所知没有),否则我对 N3936 引用感到非常满意。对于 C++11 和 N3337 也是如此。 - M.M
显示剩余33条评论

4
历史上,有些系统使用指针作为右值可能会导致该系统获取由该指针中的某些位标识的一些信息。例如,如果一个指针可以包含对象头的地址和对象内的偏移量,则获取指针可能会导致系统还从该头获取一些信息。如果该对象已经不存在,则尝试从其标头获取信息可能会导致任意后果。
话虽如此,在大多数 C 实现中,所有在某个特定时刻存活的指针将永远保持与关系运算符和减法运算符相同的关系。实际上,在大多数实现中,如果有 char *p,则可以通过检查 (size_t)(p-base) < size 来确定它是否标识由 char *base; size_t size; 标识的对象的一部分;即使在对象的生命周期中存在任何重叠,这样的比较也会起作用。
不幸的是,标准没有定义代码可以指示其需要后者任何保证的手段,也没有标准手段可以询问特定实现是否可以承诺任何后者行为并拒绝编译,如果不能。此外,一些超现代实现将把两个指针上的任何使用关系或减法运算符视为程序员的承诺,即这些指针将始终标识相同的活动对象,并省略任何只有在假定不成立时才相关的代码。因此,即使许多硬件平台能够提供对许多算法有用的保证,也没有安全的方式可以利用任何这样的保证,即使代码永远不需要在自然提供它们的硬件上运行。

-3

指针包含它们所引用变量的地址。即使曾经存储在那里的变量被释放/销毁/不可用,地址仍然有效。 只要您不尝试使用这些地址上的值,您就是安全的,也就是说*p和*q将是未定义的。

显然,结果是实现定义的,因此如果不想深入研究汇编代码,这个代码示例可以用来研究编译器的特性。

这是否是有意义的做法完全是另一种讨论。


这不仅仅是“合法”的,而是“实现定义的”。 - Mark Hurd
1
(p == q) 的结果是“实现定义”,我同意。 - user2038893

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