使用悬空指针进行解引用操作是否属于未定义行为?

10

我找不到标准中说明这个程序未定义的地方:

#include <iostream>

int main() 
{
    int *p;
    {
        int n = 45;
        p = &n;
    }
    std::cout << *p;
}

§3.8中关于对象生命周期的所有情况似乎都不适用于此。


2
我相信 p 仍将指向 n 所在的内存位置,但您无法知道那里会有什么。 - mstbaum
@remyabel(我之前删除的评论问“无法预测结果”是否意味着未定义行为。)也许我对未定义行为的理解不够清楚。我的推理可能与mstbaum的类似,即我们不知道内存中的那个位置有什么内容,所以无法预测结果。这样不就足够了吗?我需要查阅标准来确认吗? - eigenchris
basic.stc.dynamic.deallocation/4 表明如果 p 指向的动态存储已被释放,那么会产生未定义行为(UB),但对于自动存储没有提及任何内容。 - M.M
1
@eigenchris,这些答案似乎都没有引用是否是UB的证据(它们只是宣布它是或不是,没有证据)。 - M.M
1
一个具有不确定值的对象具有不可预测的值。你会说这种行为是未定义的吗?不,因为标准规定了它。 - user3920237
显示剩余5条评论
5个回答

6
我不是100%确定,因为措辞有些含糊,但看起来这是由3.8/6覆盖的(我认为这种解释是正确的,因为在3.8/5中有一个非规范性示例:// undefined behavior, lifetime of *pb has ended):
...当对象的生命周期已经结束并且在重新使用或释放对象所占用的存储之前,任何引用原始对象的glvalue只能以有限的方式使用。如果程序满足以下情况,则行为未定义:
然后,第一个子弹是罪魁祸首:an lvalue-to-rvalue conversion (4.1) is applied to such a glvalue,:该转换必须在调用operator<<的时候或最终在ostream代码内部格式化整数值的读取点发生。

1
不知道这在标准中的位置,但由于n在这里存储在堆栈上,人们会认为当它超出作用域时内存会被“释放”。 - Alexander Rautenberg
另外,我的理解是3.7.3(“自动存储期”)中指出对象所占用的存储空间在块结束后不需要存在。 - Josh Kelley
1
你能否识别存储何时被重用或释放,因为这取决于该短语。特别是因为这需要在之前发生。 - Shafik Yaghmour
我认为“存储被释放”发生在变量超出范围时,因此这不适用(请参见3.7.3/1)。该部分试图讨论诸如放置删除和新内容的情况。 - M.M
关于更新版本,“such a glvalue”仍然指的是“在对象占用的存储被重新使用或释放之前”。 - M.M
仅仅解引用它就是未定义行为吗?例如,结果保证是45吗? - user3920237

2
* p是一个glvalue。代码cout << *p需要进行左值到右值转换。这是由C++14 [conv.lval]定义的。
第2点列举了各种情况,并描述了每种情况下的行为。没有哪一种适用于* p。特别是,最后一点是:
否则,由glvalue指示的对象中包含的值是prvalue结果。
但是,* p不表示对象。
在[基本.life]节中有几种情况,它们定义了lvalue-to-rvalue转换的作用,超出了[conv.lval]中所说的内容。这些情况涉及当已获取对象的存储空间,但是我们在对象的生命周期之外。但是它们不适用于* p,因为在上一个块结束时释放了存储器。
因此,这段代码的行为是未定义的:标准中没有定义当lvalue不表示对象并且不表示对象的有效存储时执行rvalue转换的含义。
这可能让人感到不满意,因为某些事情是“因遗漏而未定义的”,我们总是希望看到一个具体的声明“这是未定义的行为”,以确保我们没有忽略任何内容。但有时就是这样。

2
是的,这是未定义行为。
变量n具有自动存储期,参见[basic.stc.auto]p1
“属于块或参数作用域且没有显式声明为静态、线程本地或外部的变量具有自动存储期。这些实体的存储持续到它们被创建的块退出。”
存储在块退出时结束,在这种情况下也是如此。
我们可以从[basic.stc]p4中看到,p是一个无效指针,通过无效指针间接引用是未定义行为:
“当存储区域的持续时间结束时,表示该存储区域任何部分地址的所有指针的值都变成无效指针值。通过无效指针值进行间接引用和将无效指针值传递给释放函数具有未定义行为。任何其他使用无效指针值的行为都有实现定义的行为。26
为了完整起见,如果我们看一下[basic.compund]p3,我们可以看到指针类型有四个值:
每个指针类型的值都是以下之一: - 指向对象或函数的指针(指针被称为指向对象或函数), - 对象结束后的指针([expr.add]), - 该类型的空指针值, - 无效的指针值。 \expr.unary]p1告诉我们,表达式unary *应用于对象类型函数类型的指针。
一元*运算符执行间接引用:应用于它的表达式必须是一个指向对象类型或函数类型的指针,其结果是一个左值,引用表达式指向的对象或函数。如果表达式的类型是“指向T的指针”,则结果的类型是“T”。
由于无效指针既不是这两种类型,因此我们具有未定义行为。

有趣,我正在查看旧的 C++ 草案,但这个措辞在 C++11 和 C++14 草案中并不存在,而出现在 C++17 草案中。这就是为什么在 2015 年没有人发现它的原因。 - Shafik Yaghmour
看起来这是由p0137r1添加的,以解决DR 1776 - Shafik Yaghmour
你能证明间接引用是通过无效指针值而不是其他方式发生的吗? - Language Lawyer
@LanguageLawyer,我不确定你在寻找什么,n的存储空间在第一个引号之后就已经超出了作用域,p指向n的存储空间,在第二个引号之后它成为了一个无效指针。你是想让我对此再具体一些吗? - Shafik Yaghmour
一元间接运算符 * 需要一个值。p 所表示的对象包含一个[无效指针]值,但是这个表达式原本并没有指定该值。它属于错误的值类别。为了让 * 满意,应该采取什么措施?它如何与无效指针值交互? - Language Lawyer

1

这绝对是未定义的行为(按照常识和标准的措辞)。

就标准而言,3.8/5非常明确关于允许和不允许的内容:

[...] 在对象的生命周期结束后,在重新使用或释放该对象所占用的存储之前,可以使用任何指向对象所在位置的存储器的指针,但只能以有限的方式使用 [...]并且将指针用作void*类型是被定义的
间接引用[...]是允许的[...]如下所述。如果程序:有未定义的行为
- ...
- [...] 作为static_cast的操作数,除非转换为指向cv void的指针,或者转换为指向cv char或cv unsigned char的指针
- [...] 作为dynamic_cast的操作数

对象的存储在作用域结束时结束,根据3.7.3/1(实际上这很可能不是真的,在函数结束时堆栈帧可能会被重置,但是形式上是这样发生的)。因此,解引用不会在生命周期结束后发生,而是在存储释放之前发生。它发生在存储释放之后
因此,您仍然可以在特殊条件下解引用指针(对于任何具有相同前提条件的类似段落,例如3.8/6,情况也是如此)。
进一步地,假设之前的段落不是真的,那么只允许将指针解引用为cv void*或在解引用之前将其转换为cv char(有符号或无符号)。换句话说,您不被允许像处理int一样处理指向的int。正如3.8/5所述,int*在对象的生命周期结束后实际上只是一个简单的void*。这意味着将其解引用为int*等同于进行转换(虽然不是显式的,但仍然是)。希望这个尝试会产生错误,但我想这对编译器来说确实很棘手。指针本身是良好而活跃的,并且它已通过获取一个有效对象的地址进行了安全派生,这可能很难诊断。

0

首先根据3.7.3 自动存储期,对象的存储将被释放:

在块范围内显式声明为register或未显式声明静态或外部变量具有自动存储期。这些实体的存储持续到创建它们的块退出。

接着根据3.8 对象生命周期

在对象的生命周期开始之前,但是在分配对象所占用的存储空间之后,或者在对象的生命周期结束之后并在重新使用或释放对象所占用的存储空间之前,任何指向对象将要或已经位于的存储位置的指针都可以被使用,但是只能进行有限的使用。

因此,对已释放存储空间中的变量进行指针解引用会导致未定义行为。


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