C ++中越界访问和未定义行为

8

我知道在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++标准中找不到相关段落。
是否有人能够在标准中指出具体的段落,解释这种情况?


2
这里不适用于@RichardCritten,因为table+4是一个有效的指针。它的间接调用会引发UB。 - YSC
我卡住了。"通过无效指针值间接引用[...]具有未定义行为" (1),但是"_对象结束后的指针"_不是无效指针值(2). - YSC
我能提供的最好的建议是基于未明确定义的单词假设性 - YSC
1
很确定这里是正当理由。它说N-1元素是数组中的最后一个对象,因此指向“对象N”的指针实际上并没有指向数组中的对象。 - NathanOliver
1
@YSC 标准并不总是明确说明什么是未定义行为;许多未指定的事情都是通过其省略来暗示的(因此它是未定义的)。形成有效指针并不意味着如果底层源不可达,则可以对指针进行解引用。为此,像数组的定义这样简单的东西可能就足够了,例如 [decl.array]/6,其中仅声明从 0N-1 的元素存在于连续空间中。任何超出此范围的内容都将是不可访问的对象。 - Human-Compiler
显示剩余8条评论
2个回答

7
这是未定义行为。我们可以将几个段落并置以确信此事。首先,我不会明确证明,table[4]*(table + 4)。我们只需要问一下指针值 table + 4 的属性以及它如何与间接寻址运算符的要求相关联即可。
关于指针,我们有以下段落:

[basic.compound]

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

  • 指向对象或函数的指针(该指针称为指向该对象或函数),或
  • 超出对象末尾的指针([expr.add]),或
  • 该类型的空指针值,或
  • 无效指针值。
我们的指针属于第二个项目符号的类型,而不是第一个。至于间接寻址运算符:
“*” 运算符是一个一元运算符,用于解引用指针。应用该运算符的表达式必须是指向对象类型或函数类型的指针,并且结果是指向表达式所指向的对象或函数的左值。如果表达式的类型为“指向 T 的指针”,则结果的类型为“T”。
从这段话中可以明显看出,该运算符仅适用于前面一段描述的类别的指针。
因此,我们对一个指针值应用了一个未定义行为的操作。结果是未定义的行为。

"我不会明确证明它" -> "表达式 E1[E2]*((E1)+(E2)) 完全相同(根据定义)" https://timsong-cpp.github.io/cppwp/expr.sub - 463035818_is_not_a_number
由于这被标记为“语言律师”,您可能还想包括 [decl.array]/6,以明确证明数组占用从 0N-1 编号的 N 个连续存储元素(这使得 N 成为上面证明中的 past-the-end 指针)。 - Human-Compiler
如果 sizeof(int) 是 4,那么对于 int array[3][5];(int*)((char*)array + 20) 属于哪个弹药类别?你知道它会属于 [expr.add]/6。 - Language Lawyer
@supercat 我100%反对将对象当作字符类型的数组来处理。不要触碰它们。 - Language Lawyer
@supercat 他们是,但是……需要小心并使用正确的工具。我们可以使用char数组来访问array[1][0],在标准C++20中(以及一旦编译器实现了这些更改的先前版本,因为它们被采用为DR),通过依赖于隐式对象创建,但我们需要更多的代码。然而,所有这些都与此Q/A的相关性很小;为了说明你在这种情况下的观点,我认为只需问一下array[0][5]是否是指向结尾的指针还是指向array[1][0]的指针(因为我们知道它在那里)。答案是它是一个指向结尾的指针,不指向array[1][0] - bogdan
显示剩余10条评论

4
下标操作符通过加法操作符定义。在这个相同的表达式中,数组衰减为第一个元素的指针,因此指针算术规则适用。间接操作符用于加法的假设结果。
“[expr.sub]”:方括号内后跟表达式的后缀表达式是一种后缀表达式。其中一个表达式应为类型为“T”数组的glvalue或类型为“T”指针的prvalue,另一个应为未作用域枚举类型或整型的prvalue。结果的类型为“T”。类型“T”应为完全定义的对象类型。表达式E1[E2]与*((E1)+(E2))(根据定义)相同,...
如果数组索引超过最后一个元素即E2> std::size(E1),则假设的指针算术本身是未定义的。
“[expr.add]”:当将具有整数类型的表达式J添加到或从指针类型的表达式P时,结果具有P的类型。 • 如果P计算为空指针值...(不适用) • 否则,如果P指向具有n个元素的数组对象x的数组元素i,则表达式P+J和J+P(其中J具有值j)指向x的(可能虚构的)数组元素i+j,如果0≤i+j≤n,并且表达式P-J指向x的(可能虚构的)数组元素i−j,如果0≤i−j≤n。(当i-j> n时不适用) • 否则,行为未定义。
在E2 == std::size(E1)的情况下(这是示例的最后一次迭代),加法的假设结果是指向数组之外的一个元素的指针。虚拟的指针算术是定义良好的。
“[basic.compound]”:指向一个对象结束后的指针类型值表示对象占用的存储空间结束后的内存中的第一个字节。
访问是根据对象定义的。但是那里没有对象,甚至没有存储空间,因此不存在行为的定义。
好吧,在某些情况下,该指针可能与指向指定地址的其他无关对象相关联。下面的注释说明了超出末尾的指针不是指向共享地址的这种不相关对象的指针。我找不到哪个规范规则导致了这一点。

[注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

我找不到是哪个规范规则导致了这个问题 - 我认为它是指针类型值列表上方的注释:这样的值只能是“以下之一”,因此一个指向对象末尾之后的 指针 本质上不是一个 指向 对象的指针,因此它不能 指向 对象。 - bogdan

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