【问题标题】:Take the address of a one-past-the-end array element via subscript: legal by the C++ Standard or not?通过下标获取一个过去的数组元素的地址:C++ 标准是否合法?
【发布时间】:2010-11-02 13:20:14
【问题描述】:

我已经看到它多次断言 C++ 标准不允许以下代码:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];

&array[5] 在这种情况下是合法的 C++ 代码吗?

如果可能的话,我想要一个参考标准的答案。

知道它是否符合 C 标准也很有趣。如果它不是标准 C++,为什么决定将其区别于 array + 5&array[4] + 1

【问题讨论】:

  • @Brian:不,它只需要检查边界是否需要运行时来catch 错误。为避免这种情况,标准可以简单地说“不允许”。这是最好的未定义行为。不允许您这样做,并且运行时和编译器不需要告诉您是否这样做。
  • 好吧,澄清一下,因为标题误导了我:一个指针越过数组末尾不是越界的。一般来说,越界指针是不允许的,但标准对越界指针要宽松得多。如果您特别询问过去的指针,您可能需要编辑标题。如果您想了解越界指针一般,您应该编辑您的示例。 ;)
  • 他一般不会询问过去的指针。他在询问如何使用 & 运算符来获取指针。
  • @Matthew:但答案取决于指针指向的位置。您可以在过去的情况下获取地址,但不能在超出范围的情况下获取地址。
  • 第 5.3.1.1 节一元运算符'*':'结果是一个引用对象或函数的左值'。 5.2.1 节下标表达式 E1[E2] 与 *((E1)+(E2)) 相同(根据定义)。通过我在这里阅读标准。结果指针没有取消引用。 (请参阅下面的完整说明)

标签: c++ c standards language-lawyer


【解决方案1】:

是的,这是合法的。来自C99 draft standard

§6.5.2.1,第 2 段:

后缀表达式后跟方括号中的表达式 [] 是下标 指定数组对象的元素。下标运算符[]的定义 是E1[E2](*((E1)+(E2))) 相同。由于转换规则 适用于二元 + 运算符,如果 E1 是一个数组对象(等效地,指向 数组对象的初始元素)和E2 是一个整数,E1[E2] 指定E2-th E1 的元素(从零开始计数)。

§6.5.3.2,第 3 段(强调我的):

一元 & 运算符产生其操作数的地址。如果操作数的类型为 ‘‘type’’, 结果的类型为“指向 type 的指针”。如果操作数是一元 * 运算符的结果, 该运算符和 & 运算符均未计算,结果好像两者都是 省略,除了对运算符的约束仍然适用并且结果不是 左值。类似地,如果操作数是 [] 运算符的结果,则 & 运算符和 [] 所隐含的一元 * 都不会被计算,结果就像 & 运算符一样 已删除,[] 运算符更改为 + 运算符。否则,结果是 指向由其操作数指定的对象或函数的指针。

§6.5.6,第 8 段:

当一个整数类型的表达式被添加到指针或从指针中减去时, 结果具有指针操作数的类型。如果指针操作数指向一个元素 一个数组对象,并且数组足够大,结果指向一个元素的偏移量 原始元素使得结果和原始的下标的差异 数组元素等于整数表达式。换句话说,如果表达式P 指向 数组对象的i-th 元素,表达式(P)+N(等效于N+(P))和 (P)-N(其中N 的值为n)分别指向i+n-th 和i−n-th 元素 数组对象,前提是它们存在。此外,如果表达式P 指向最后一个 数组对象的元素,表达式(P)+1 指向数组对象的最后一个元素 数组对象,如果表达式Q 指向数组对象的最后一个元素, 表达式(Q)-1 指向数组对象的最后一个元素。如果两个指针 操作数和结果指向同一数组对象的元素,或最后一个元素 数组对象的元素,评估不应产生溢出;否则, 行为未定义。如果结果指向数组对象的最后一个元素,它 不得用作被评估的一元 * 运算符的操作数。

请注意,标准明确允许指针指向数组末尾之后的一个元素,前提是它们没有被取消引用。在 6.5.2.1 和 6.5.3.2 中,表达式&array[5] 等价于&*(array + 5),它等价于(array+5),它指向数组末尾的后一位置。这不会导致取消引用(通过 6.5.3.2),因此它是合法的。

【讨论】:

  • 他明确询问了 C++。这是两者之间移植时不能依赖的那种细微差别。
  • 他问了这两个问题:“知道它是否符合 C 标准也很有趣。”
  • @Matthew Flaschen:C++ 标准通过引用合并了 C 标准。附录 C.2 包含一个更改列表(ISO C 和 ISO C++ 之间的不兼容性),并且没有任何更改与这些条款相关。因此,&array[5] 在 C 和 C++ 中是合法的。
  • C 标准是 C++ 标准中的规范性参考。这意味着 C++ 标准引用的 C 标准中的规定是 C++ 标准的一部分。这并不意味着 C 标准中的所有内容都适用。特别是附录 C 是信息性的,而不是规范性的,所以仅仅因为本节中没有突出显示差异并不意味着 C 的“版本”适用于 C++。
  • @PoweredByRice:它在 C99 中是合法的,请阅读上面引用的标准段落,其中明确指出 &* 运算符都没有被评估。 C++ 是不同的。据我所知,C++11(在最初编写此答案时仍在起草中)没有类似的子句。
【解决方案2】:

您的示例是合法的,但这只是因为您实际上并没有使用越界指针。

让我们先处理越界指针(因为这就是我最初解释您的问题的方式,在我注意到该示例改用了一个过去的指针之前):

一般来说,您甚至不允许创建越界指针。一个指针必须指向数组中的一个元素,或者末尾的一个元素。无处可去。

甚至不允许指针存在,这意味着您显然也不允许取消引用它。

以下是标准对该主题的规定:

5.7:5:

当一个表达式具有整数 类型被添加到或减去 指针,结果的类型为 指针操作数。如果指针 操作数指向一个元素 数组对象,并且数组很大 够了,结果指向一个 元素与原始元素的偏移 元素这样的差异 结果的下标和 原始数组元素等于 积分表达式。换一种说法, 如果表达式 P 指向第 i 个 数组对象的元素, 表达式 (P)+N(等价于, N+(P)) 和 (P)-N(其中 N 具有 值 n) 分别指向 的 i+n-th 和 i-n-th 元素 数组对象,前提是它们存在。 此外,如果表达式 P 指向 到数组的最后一个元素 对象,表达式 (P)+1 分 过去数组的最后一个元素 对象,如果表达式 Q 指向 一个数组的最后一个元素 对象,表达式 (Q)-1 指向 数组对象的最后一个元素。 如果指针操作数和 结果指向相同的元素 数组对象,或最后一个 数组对象的元素, 评估不应产生 溢出; 否则,行为是 未定义

(强调我的)

当然,这是给operator+的。因此,为了确定,以下是标准对数组下标的说明:

5.2.1:1:

表达式E1[E2]*((E1)+(E2)) 相同(根据定义)

当然,有一个明显的警告:您的示例实际上并未显示越界指针。它使用“一个过去的结束”指针,这是不同的。指针是允许存在的(如上所述),但据我所知,标准并没有说明取消引用它。我能找到的最接近的是 3.9.2:3:

[注意:例如,数组末尾的地址(5.7)将被视为 指向可能位于该地址的数组元素类型的不相关对象。 ——尾注]

在我看来,这意味着是的,您可以合法地取消引用它,但是读取或写入该位置的结果是未指定的。

感谢 ilproxyil 在这里纠正最后一点,回答了您问题的最后一部分:

  • array + 5 实际上并没有 取消引用任何东西,它只是 创建一个指向末尾的指针 的array
  • &array[4] + 1 取消引用 array+4(非常安全), 获取该左值的地址,并且 向该地址添加一个,这 导致一个过去的指针 (但那个指针永远不会得到 已取消引用。
  • &array[5] 取消引用数组+5 (据我所知是合法的, 并导致“一个不相关的对象 数组的元素类型”,作为 上面说),然后采取 该元素的地址,它也 似乎足够合法。

所以他们做的事情并不完全相同,尽管在这种情况下,最终结果是一样的。

【讨论】:

  • &array[5] 指向过去。但是,这不是获取该地址的合法方式。
  • 最后一句不正确。 "array + 5" 和 "&array[4] + 1" 不会取消对末尾的引用,而 array[5] 可以。 (我还假设您的意思是 &array[5],但评论仍然有效)。前两个只是指向一个过去的结尾。
  • @jalf 那个注释 - 我认为它只是想说如果 b 直接分配在数组“a”之后,“a + sizeof a”对“&b”同样有效,并且结果地址同样“指向”同一个对象。不多。请记住,所有注释都是信息性的(非规范性的):如果它会陈述这样的基本重要事实,例如在数组对象之后存在位于过去的对象,那么需要使这样的规则成为规范性
  • 就 ANSI C (C89/C90) 而言,这是正确的答案。如果您严格遵守标准,则 &array[5] 在技术上是无效的,而 array+5 是有效的,尽管几乎每个编译器都会为这两个表达式生成相同的代码。 C99 更新标准以明确允许 &array[5]。有关详细信息,请参阅我的答案。
  • @jalf,注释所在的整个文本也以“如果类型 T 的对象位于地址 A...”开头地址 A 的对象。”所以你的报价没有(在这种情况下也不能)说地址 A 总是有一个对象。
【解决方案3】:

合法的。

According to the gcc documentation for C++&array[5] 是合法的。在 C++ and in C 中,您可以安全地处理数组末尾之后的元素 - 您将获得一个有效的指针。所以&array[5]作为表达式是合法的。

但是,尝试取消引用指向未分配内存的指针仍然是未定义的行为,即使指针指向有效地址。因此,即使指针本身是有效的,尝试取消引用由该表达式生成的指针仍然是未定义的行为(即非法)。

在实践中,我想它通常不会导致崩溃。

编辑:顺便说一句,这通常是 STL 容器的 end() 迭代器的实现方式(作为指向过去的指针),因此这是合法实践的一个很好的证明。

编辑:哦,现在我知道您并不是真的在问持有指向该地址的指针是否合法,而是询问获取指针的确切方式是否合法。我会听从其他回答者的意见。

【讨论】:

  • 我会说你是正确的,当且仅当 C++ 规范没有说 &* 必须被视为无操作。我想它可能不会这么说。
  • 您引用的页面(正确地)表示,指向末尾是合法的。 &array[5],技术上首先取消引用 (array + 5),然后再次引用它。所以它在技术上是这样的:(&*(array + 5))。幸运的是,编译器足够聪明,知道 &* 可以被分解为无。但是,他们没有必须这样做,因此,我会说它是 UB。
  • @Evan:还有更多。查看核心问题 232 的最后一行:std.dkuug.dk/JTC1/SC22/WG21/docs/cwg_active.html#232。最后一个例子看起来是错误的——但他们清楚地解释了区别在于“左值到右值”的转换,在这种情况下不会发生。
  • @Richard:有趣,似乎有一些关于这个主题的辩论。我什至同意它应该被允许:-P.
  • 这是一种未定义的行为,就像人们一直在讨论的“对 NULL 的引用”一样,而且似乎所有人都投票赞成“这是未定义的行为”的答案
【解决方案4】:

我相信这是合法的,它取决于发生的“左值到右值”转换。最后一行Core issue232有以下内容:

我们同意标准中的方法似乎没问题:p = 0; *p;本质上不是错误。左值到右值的转换会给它带来不确定的行为

虽然这是一个稍微不同的例子,但它确实表明 '*' 不会导致左值到右值的转换,因此,假设表达式是 '&' 的直接操作数,它需要一个左值,然后是行为已定义。

【讨论】:

  • +1 获取有趣的链接。我仍然不确定我是否同意 p=0;*p;定义明确,因为我不相信 '*' 对于值不是指向实际对象的指针的表达式是明确定义的。
  • 作为表达式的语句是合法的,并且意味着对该表达式求值。 *p 是一个调用未定义行为的表达式,因此实现所做的任何事情都符合标准(包括给你的老板发电子邮件,或下载棒球统计数据)。
  • 请注意,该问题的状态仍然是“起草”,它还没有进入标准,至少 C++11 和 C++14 的那些草案版本我可以找到。
  • “空左值”提案从未被任何已发布的标准采纳
  • 问题中的注释明确提到了&*a[n] 问题:“同样,只要不使用该值,就应该允许取消引用指向数组末尾的指针”。不幸的是,这个问题自 2003 年以来一直在消退,标准中还没有解决方案。
【解决方案5】:

我不认为这是非法的,但我确实认为 &array[5] 的行为是未定义的。

  • 5.2.1 [expr.sub] E1[E2] 与 *((E1)+(E2)) 相同(根据定义)

  • 5.3.1 [expr.unary.op] 一元 * 运算符 ...结果是一个左值,引用表达式指向的对象或函数。

此时您有未定义的行为,因为表达式 ((E1)+(E2)) 实际上并未指向对象,并且标准确实说明了结果应该是什么,除非它这样做。

  • 1.3.12 [defns.undefined] 如果本国际标准省略了对行为的任何明确定义的描述,也可能会出现未定义的行为。

如其他地方所述,array + 5&array[0] + 5 是获取超出数组末尾的指针的有效且定义明确的方法。

【讨论】:

  • 关键点是:“'*'的结果是一个左值”。据我所知,只有当您对该结果进行左值到右值转换时,它才会变为 UB。
  • 我会争辩说,由于 '*' 的结果仅根据应用运算符的表达式的对象来定义,因此它是未定义的 - 通过省略 - 结果是什么如果表达式没有实际引用对象的值。不过,这还不是很清楚。
【解决方案6】:

除了上面的答案,我会指出 operator& 可以被类覆盖。因此,即使它对 POD 有效,但对您知道无效的对象执行此操作可能不是一个好主意(很像一开始就覆盖 operator&())。

【讨论】:

  • +1 关于将 operator& 带入讨论,即使专家建议永远不要覆盖它,因为某些 STL 容器依赖于它返回指向元素的指针。这是在他们了解更多之前就进入标准的事情之一。
【解决方案7】:

这是合法的:

int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];

第 5.2.1 节下标表达式 E1[E2] 与 *((E1)+(E2)) 相同(根据定义)

因此我们可以说 array_end 也是等价的:

int *array_end = &(*((array) + 5)); // or &(*(array + 5))

第 5.3.1.1 节一元运算符'*':一元 * 运算符执行间接:应用它的表达式应是指向对象类型的指针,或 一个指向函数类型的指针,结果是一个左值,指向表达式所指向的对象或函数。 如果表达式的类型是“指向 T 的指针”,则结果的类型是“T”。 [注意:指向不完整类型的指针(其他 比 cv void) 可以取消引用。这样获得的左值可以以有限的方式使用(初始化引用,例如 例子);此左值不得转换为右值,请参见 4.1。 ——尾注]

以上重要部分:

'结果是一个引用对象或函数的左值'。

一元运算符 '*' 返回一个引用 int 的左值(没有取消引用)。一元运算符 '&' 然后获取左值的地址。

只要没有解除对越界指针的引用,那么该操作就完全被标准覆盖并且所有的行为都被定义了。所以通过我的阅读,以上是完全合法的。

许多 STL 算法依赖于明确定义的行为这一事实暗示标准委员会已经考虑到这一点,我确信有一些东西明确地涵盖了这一点。

下面的评论部分提出了两个论点:

(请阅读:但它很长,我们俩最终都变成了巨魔)

论据 1

这是非法的,因为第 5.7 节第 5 段

当一个整数类型的表达式被添加到指针或从指针中减去时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果和原始数组元素的下标之差等于积分表达式。换句话说,如果表达式 P 指向数组对象的第 i 个元素,则表达式 (P)+N(等效于 N+(P))和 (P)-N(其中 N 的值为 n)指向分别指向数组对象的第 i + n 个和第 i - n 个元素,前提是它们存在。此外,如果表达式 P 指向数组对象的最后一个元素,则表达式 (P)+1 指向数组对象的最后一个元素,如果表达式 Q 指向数组对象的最后一个元素,则表达式 (Q)-1 指向数组对象的最后一个元素。如果指针操作数和结果都指向同一个数组对象的元素,或者过去 数组对象的最后一个元素,评估不应产生溢出;否则,行为未定义。

虽然该部分是相关的;它不显示未定义的行为。我们所说的数组中的所有元素要么在数组内,要么在数组的末尾(这在上面的段落中有很好的定义)。

论据 2:

下面显示的第二个参数是:* 是取消引用运算符。
尽管这是用于描述“*”运算符的常用术语;标准中故意避免使用该术语,因为术语“取消引用”在语言及其对底层硬件的意义方面没有得到很好的定义。

尽管访问超出数组末尾的内存绝对是未定义的行为。我不相信unary * operator 访问内存(读取/写入内存)在这种情况下(不是以标准定义的方式)。在这种情况下(由标准定义(见 5.3.1.1))unary * operator 返回一个lvalue referring to the object。在我对语言的理解中,这不是对底层记忆的访问。然后,unary & operator 运算符立即使用此表达式的结果,该运算符返回 lvalue referring to the object 引用的对象的地址。

还提供了许多其他对 Wikipedia 和非规范来源的引用。所有这些我都觉得无关紧要。 C++ 是由标准定义的

结论:

我愿意承认该标准的许多部分我可能没有考虑过,并且可能证明我的上述论点是错误的。 NON 在下面提供。如果你给我看一个显示这是 UB 的标准参考。我会的

  1. 留下答案。
  2. 全部大写,这是愚蠢的,我错了所有人都可以阅读。

这不是论据:

并非世界上的所有事物都是由 C++ 标准定义的。敞开心扉。

【讨论】:

  • 根据谁?根据哪一段? * 执行取消引用。它是取消引用运算符。这就是它的作用。可以说,您随后获得一个指向结果值的新指针(使用&)这一事实是无关紧要的。你不能只展示一个评估序列,展示最终的表达式语义,然后假装中间步骤没有发生(或者语言的规则并不适用于每个步骤)。
  • 我引用同一段话:the result is an lvalue referring to the object or function to which the expression points. 很明显,如果不存在这样的对象,则没有为该运算符定义的行为。您随后的声明is returning a lvalue referring to the int (no de-refeference) 对我来说毫无意义。为什么你认为这不是取消引用?
  • It returns a reference to what is being pointed at. 这是什么,如果不是取消引用?文中说* 执行间接,从指针到指针的间接称为解引用。您的论点本质上断言指针和引用是同一件事,或者至少是隐式链接的,这根本不是真的。 int x = 0; int* ptr = &x; int& y = *x; 在这里我取消引用 x。我不需要使用y 来实现这一点。
  • @LokiAstari:我有一个问题,如果不是“调用返回引用表达式指向的对象的左值的一元 * 运算符”,您认为“解除引用”意味着什么? (请注意,该标准的后续句子确实将此过程称为 C++11 规范中的取消引用)
  • @LokiAstari: I showed you references days and days ago;出于某种原因,您只是拒绝承认它们的存在。你怎么能证明这种行为是我无法理解的,但你必须是一个拖钓者。
【解决方案8】:

工作草案(n2798):

"一元&运算符的结果是 指向其操作数的指针。操作数 应为左值或限定 ID。 在第一种情况下,如果类型 表达式是“T”,类型 结果是“指向 T 的指针””(第 103 页)

array[5] 据我所知,它不是一个合格的 ID(列表在第 87 页);最接近的似乎是标识符,但虽然数组是标识符,但数组 [5] 不是。它不是左值,因为“左值指的是对象或函数。”(第 76 页)。 array[5] 显然不是一个函数,并且不能保证引用一个有效的对象(因为 array + 5 在最后分配的数组元素之后)。

显然,它可能在某些情况下有效,但它不是有效的 C++ 或安全的。

注意:在数组中添加一个是合法的(第 113 页):

"如果表达式 P [一个指针] 指向数组的最后一个元素 对象,表达式 (P)+1 分 过去数组的最后一个元素 对象,如果表达式 Q 指向 一个数组的最后一个元素 对象,表达式 (Q)-1 指向 数组对象的最后一个元素。 如果指针操作数和 结果指向相同的元素 数组对象,或最后一个 数组对象的元素, 评估不应产生 溢出”

但是使用 & 这样做是不合法的。

【讨论】:

  • 我支持你,因为你是对的。不能保证对象位于过去的位置。否决你的人可能误解了你(你听起来好像你说任何数组索引操作根本不指任何对象)。我认为这是一件有趣的事情:它一个左值,但它也确实不是引用一个对象。因此,这与标准所说的相矛盾。因此,这会产生未定义的行为 :) 这也与此相关:open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232
  • @jalf,注释说“可能位于该地址”。不保证有一个 :)
  • 标准规定 op* 的结果必须是左值,但它仅说明如果操作数是实际指向对象的指针,则左值是什么。这意味着(奇怪地)如果结束后的对象没有恰好指向合适的对象,则实现将不得不从其他地方找到合适的左值并使用它。这真的会搞砸 &array[sizeof array]!
  • "但是,我仍然认为它不是左值。因为没有对象保证在array[5],所以array[5] 不能合法地/引用/ 到一个对象。"
  • litb,很公平。假设它/不绝对/是一个左值,因此/绝对不是/100% 安全。
【解决方案9】:

即使是合法的,为什么要背离惯例?无论如何,array + 5 更短,而且在我看来,更具可读性。

编辑:如果你想要它是对称的,你可以写

int* array_begin = array; 
int* array_end = array + 5;

【讨论】:

  • 我认为我在问题中使用的样式看起来更加对称:数组声明和开始/结束指针,或者有时我将它们直接传递给 STL 函数。这就是为什么我使用它而不是较短的版本。
  • 为了对称,我认为它需要是 array_begin = array + 0;数组结束 = 数组 + 5;长时间延迟的评论回复是怎么回事?
  • 这可能是世界纪录:)
【解决方案10】:

这应该是未定义的行为,原因如下:

  1. 尝试访问越界元素会导致未定义的行为。因此,该标准不禁止实现在这种情况下抛出异常(即实现在访问元素之前检查边界)。如果将& (array[size]) 定义为begin (array) + size,则在越界访问的情况下引发异常的实现将不再符合标准。

  2. 如果 array 不是数组而是任意集合类型,则不可能产生 end (array)

【讨论】:

    【解决方案11】:

    C++ 标准,5.19,第 4 段:

    地址常量表达式是一个指向左值的指针....该指针应显式创建,使用一元 & 运算符...或使用数组 (4.2)...类型的表达式。下标运算符 []... 可用于创建地址常量表达式,但不能通过使用这些运算符访问对象的值。如果使用下标运算符,则其操作数之一应为整数常量表达式。

    在我看来 &array[5] 是合法的 C++,是一个地址常量表达式。

    【讨论】:

    • 我不确定最初的问题是否一定是在谈论具有静态存储的数组。即使我想知道 &array[5] 是否不是地址常量表达式,正是因为它没有指向指定对象的左值?
    • 我认为数组是静态的还是堆栈分配的并不重要。
    • 如果您引用 5.19.您用 ... 省略的部分表示“...指定静态存储持续时间的对象、字符串文字或函数......”。这意味着如果您的表达式涉及堆栈分配的数组,您不能使用 5.19 来推断这些表达式的有效性。
    • 你的引用是说如果&array[5] 是合法的,并且引用了一个静态存储持续时间数组,那么这将是一个地址常量。 (例如,与&array[99] 比较,本段中没有文字区分这两种情况)。
    【解决方案12】:

    如果您的示例不是一般情况,而是特定情况,那么它是允许的。您可以合法,AFAIK,将一个移过分配的内存块。 它不适用于一般情况,即您尝试访问距离数组末尾更远的​​元素。

    刚刚搜索过 C-Faq : link text

    【讨论】:

    • 最佳答案说“它是合法的”,我也说了同样的话。那么为什么要投反对票:)。我的回答有问题吗?
    【解决方案13】:

    这是完全合法的。

    当您调用 myVec.end() 时,stl 中的 vector 模板类会执行此操作:它会为您获取一个指针(此处为迭代器),该指针指向数组末尾的一个元素。

    【讨论】:

    • 但它是通过指针算法实现的,而不是通过形成对过去的引用然后将地址运算符应用于该左值。
    猜你喜欢
    • 2021-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多