【问题标题】:Out of the bounds in C++ and undefined behaviour超出 C++ 范围和未定义行为
【发布时间】:2021-07-09 12:34:23
【问题描述】:

我知道在 c++ 中,超出缓冲区范围的访问是未定义的行为。
这是来自 cppreference 的示例:

int table[4] = {};
bool exists_in_table(int v)
{
    // return true in one of the first 4 iterations or UB due to out-of-bounds access
    for (int i = 0; i <= 4; i++) {
        if (table[i] == v) return true;
    }
    return false;
}

但是,我在 c++ 标准中找不到相应的段落。
谁能指出我在标准中解释这种情况的具体段落?

【问题讨论】:

  • 不适用于@RichardCritten,因为table+4 是一个有效的指针。调用 UB 的是它的间接性。
  • 我卡住了。 “通过无效指针值的间接 [...] 具有未定义的行为” (1) 但 “指针越过对象末尾”不是无效的指针值 (2)。
  • 很确定this 是理由。它说元素 N-1 是数组中的最后一个对象,因此指向“对象 N”的指针实际上并不指向数组中的对象。
  • @YSC 标准并不总是明确地说明什么是 UB;许多未指定的事情都暗示了它的省略(因此它是未定义的)。形成一个有效的指针并不意味着如果基础源不可访问,则可以取消引用该指针。为此,像定义数组这样简单的事情可能就足够了,比如[decl.array]/6,它声明只有从0N-1 的元素存在于连续空间中。除此之外的任何东西都是不可访问的对象。

标签: c++ language-lawyer undefined-behavior


【解决方案1】:

这是未定义的行为。我们可以并列几段来说服它。首先,我不会明确证明,table[4]*(table + 4)。我们只需要问自己指针值table + 4 的属性以及它与间接运算符的要求有何关系。

在指针上,我们有这样一段话:

[basic.compound]

3每个指针类型的值都是以下之一:

  • 指向对象或函数的指针(该指针被称为指向对象或函数),或
  • 超过对象末尾的指针 ([expr.add]),或
  • 该类型的空指针值,或
  • 一个无效的指针值。

我们的指针属于第二个子弹的类型,而不是第一个。至于间接运算符:

[expr.unary.op]

1 一元 * 运算符执行间接:应用它的表达式应该是一个指向对象类型的指针,或者是一个指向函数类型的指针,并且结果是一个左值,指向要指向的对象或函数表达点。如果表达式的类型是“指向T的指针”,则结果的类型是“T”。

我希望通过阅读这一段很明显,该操作是为上一段中第一个项目符号所描述的类别的指针定义的。

因此,我们将操作应用于未定义其行为的指针值。结果是未定义的行为。

【讨论】:

  • “我不会明确证明”->“表达式E1[E2]*((E1)+(E2)) 相同(根据定义)”timsong-cpp.github.io/cppwp/expr.sub
  • 由于它被标记为 language-lawyer,您可能还想包含 [decl.array]/6 以明确证明数组占用 N 编号从 0N-1 的连续存储元素(这使得N 过去的指针,在你上面的证明中)
  • @Human-Compiler - 我不赞成 LL 的答案应该冗长而不是粗鲁的哲学。这些段落都是与imo相关的。不需要证明该指针属于第二个子弹的迂腐证明。重要的是我证明它不是第一个子弹。如果您对此立场强烈反对,请相应投票。
  • 如果 sizeof(int) 为 4,给定int array[3][5];(int*)((char*)array + 20) 属于哪个项目符号类别?它会是刚刚超过array[0][4] 的指针,还是指向array[1][0] 的指针?如果没有指定 table 后面的内容,则刚刚过去的指针不会是指向任何可指定对象的指针,但这并不能说明程序员知道数组对象末尾后面是什么的情况。
  • @supercat 如果sizeof(int) 是4,给定int array[3][5];(int*)((char*)array + 20) 会落入哪个子弹类别? 你知道它会落入 [expr.add] /6
【解决方案2】:

下标运算符是通过加法运算符定义的。数组衰减为指向此相同表达式中第一个元素的指针,因此适用指针算术规则。间接运算符用于加法的假设结果。

[expr.sub]

后缀表达式后跟方括号中的表达式是后缀表达式。 其中一个表达式应为“T 数组”类型的左值或“指向 T 的指针”类型的纯右值,另一个应为无范围枚举或整数类型的纯右值。 结果是类型“T”。 类型“T”应该是一个完全定义的对象类型。 表达式E1[E2]*((E1)+(E2)) 相同(根据定义),...


如果数组索引超过最后一个元素,即E2 &gt; std::size(E1)(在示例程序中不是这种情况),则假设的指针算法本身是未定义的。

[expr.add]

当一个整数类型的表达式 J 被添加到一个指针类型的表达式 P 中或从一个表达式 P 中减去时,结果具有 P 的类型。

  • 如果 P 的计算结果为空指针值 ... (不适用)
  • 否则,如果 P 指向具有 n 个元素 ([dcl.array]) 的数组对象 x 的数组元素 i,则表达式 P + J 和 J + P(其中 J 的值为 j)指向 (如果 0≤i+j≤n,则 x 的可能-假设的)数组元素 i+j,如果 0≤i-j≤n,则表达式 P - J 指向 x 的(可能-假设的)数组元素 i-j。 (当 i-j > n 时不适用)
  • 否则,行为未定义。

E2 == std::size(E1) 的情况下(在示例的最后一次迭代中就是这种情况),加法的假设结果是一个指向数组后面的指针并指向数组存储之外的指针。假设的指针算术定义良好。

[basic.compound]

一个指针类型的值是一个指针...超过一个对象的末尾表示...内存中的第一个字节存储结束后对象占用 p>

访问是根据对象定义的。但是那里没有对象,甚至没有存储,因此没有对行为的定义。

好的,在某些情况下可能指向的内存地址中有一个不相关的对象。下面的注释说,结束后的指针不是指向共享地址的此类不相关对象的指针。我找不到导致这种情况的规范规则。

[注 2:超过对象末尾的指针 ([expr.add]) 不被视为指向该对象类型的不相关对象,即使不相关对象位于该地址。 ...

另外,我们可以看看间接运算符的定义:

[expr.unary.op]

一元 * 运算符执行间接:应用它的表达式应该是一个指向对象类型的指针……结果是一个左值,指向表达式所指向的对象……。 ...

存在矛盾,因为没有可以引用的对象。


所以,总结:

int table[N] = {};
table[N] == 0; // UB, accessing non-existing object
table[N + 1];  // UB, [expr.add]
table + N;     // OK, one past last element
table[N];      // ¯\_(ツ)_/¯ See CWG 232

【讨论】:

  • “我找不到哪个规范规则导致了这个” - 我相信这是注释上方的指针类型值的列表:这样的值只能是“以下之一”,所以一个指针超过一个对象的末尾本质上不是一个指向一个对象的指针,所以它不能指向一个对象。
猜你喜欢
  • 2016-10-14
  • 2018-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-20
  • 1970-01-01
  • 1970-01-01
  • 2014-07-10
相关资源
最近更新 更多