通过下标取超出数组末尾的元素地址:C++标准是否允许?

88

我已经多次看到有人声称以下代码不符合C++标准:

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

&array[5]在这个上下文中是否是合法的C++代码?

如果可能的话,我希望能够提供一个标准的参考。

如果它不符合C++标准,那么它是否符合C标准呢?如果它不是标准的C++,为什么决定将其与array + 5&array[4] + 1区别对待?


7
不需要边界检查,除非运行时需要捕获错误。为了避免这种情况,标准可以直接规定“不允许”。这是最好的未定义行为。你不能这样做,如果你这样做,运行时和编译器也没有必要告诉你。 - jalf
1
@Matthew:但是答案取决于指针指向哪里。在“超出边界”的情况下,您不允许获取地址,但在“结束时的下一个位置”情况下是可以的。 - jalf
附带说明:你可以简单地说 int* array_begin = array; - rlbond
“超出结尾”是指定范围和迭代器的方式,因此如果这不是合法的话,那将非常奇怪。 - Nikos C.
FYI:在GCC中,“one-past”指针的“==”比较可能会给出错误的结果(bug [61502] (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61502),[演示](https://godbolt.org/z/ssETn9MG7))。 - pmor
显示剩余3条评论
14个回答

47

是合法的。根据C99草案标准:

§6.5.2.1,第2段:

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

§6.5.3.2,第3段(强调属于本人):

一元运算符&返回其操作数的地址。如果操作数的类型为“type”,则结果的类型为“指向type的指针”。如果操作数是一元*运算符的结果, 则不会对该运算符或&运算符求值,结果就好像两个运算符都省略一样,但是运算符的限制仍然适用, 而且结果不是左值。类似地,如果操作数是[]运算符的结果,则既不会对&运算符也不会 对由[]隐含的一元*求值,结果就好像&运算符被删除并将[]运算符更改为+运算符一样。否则,结果是指向其操作数所指示的对象或函数的指针。

§6.5.6,第8段:

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

注意,标准明确允许指针指向数组的末尾之后的一个元素,前提是它们不被解引用。根据6.5.2.1和6.5.3.2章节,表达式 &array[5] 等同于 &*(array + 5),等同于 (array+5),这将指向数组的末尾之后的一个元素。根据6.5.3.2章节,这不会导致解引用操作,因此是合法的。

3
他明确询问了C++。这是在两者之间移植时不能依赖的微妙差别。 - Matthew Flaschen
3
他询问了两个问题:“同样有趣的是知道它是否符合C标准。” - CB Bailey
2
@Matthew Flaschen:C++标准通过引用包含了C标准。附录C.2列出了一些变更(ISO C和ISO C++之间的不兼容性),但是这些变更都与这些条款无关。因此,在C和C++中,&array[5]是合法的。 - Adam Rosenfield
14
C++标准中规范引用了C标准,这意味着C++标准中引用的C标准条款是C++标准的一部分。但这并不意味着C标准中的所有内容都适用于C++。特别地,附录C是信息性的,而非规范性的,因此在该部分未突出显示差异并不意味着C版本适用于C++。 - CB Bailey
2
@PoweredByRice:在C99中是合法的,请阅读上面引用标准的段落,其中明确指出&*运算符都不会被计算。但是C++则不同。根据我所了解到的,当时正在起草的C++11标准没有类似的条款。 - Adam Rosenfield
显示剩余8条评论

43
您的示例是合法的,但只是因为您实际上没有使用越界指针。
首先让我们处理越界指针(因为这是我在注意到示例使用一个超出末尾指针之前最初解释您的问题的方式):
通常,您甚至不允许创建越界指针。指针必须指向数组内的元素或“结束时的下一个”元素。不得指向其他位置。
指针甚至不允许存在,这意味着您显然也不允许对其进行引用。
以下是标准对此的说明:
5.7:5: > 当将具有整数类型的表达式添加到指针中或从指针中减去时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且该数组足够大,则结果指向与原始元素相距整数表达式的元素,以便结果和原始数组元素的下标之差等于整数表达式。换句话说,如果表达式 P 指向数组对象的第 i 个元素,则表达式 (P)+N(等价地,N+(P))和 (P)-N(其中 N 的值为 n)分别指向数组对象的第 i + n 和 i−n 个元素,前提是它们存在。而且,如果表达式 P 指向数组对象的最后一个元素,则表达式 (P)+1 指向数组对象的最后一个元素之后,如果表达式 Q 指向数组对象的最后一个元素之后,则表达式 (Q)-1 指向数组对象的最后一个元素。如果指针操作数和结果都指向同一数组对象的元素或该数组对象的末尾元素之一,则评估不会产生溢出;否则,行为是未定义的。未定义

当然,这是针对operator+的。所以只为了确保,这里是标准对数组下标的说明:

5.2.1:1:

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

当然,有一个明显的警告:您的示例实际上并没有显示出越界指针。它使用了“超过末尾”的指针,这是不同的。指针被允许存在(如上所述),但就我所看到的标准而言,没有关于对其进行解引用的内容。我能找到的最接近的是3.9.2:3:

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

这似乎意味着,是的,您可以合法地对其进行解引用,但是读取或写入该位置的结果是未指定的。

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

  • array + 5实际上没有解引用任何内容,它只是创建了一个指向array末尾之后的指针。
  • &array[4] + 1解引用array+4(这是完全安全的),获取该lvalue的地址,并将其加一,这导致了一个超过末尾的指针(但该指针从未被解引用)。
  • &array[5]解引用array+5(就我所看到的而言是合法的,并导致“数组元素类型的不相关对象”,如上所述),然后获取该元素的地址,这也似乎足够合法。

因此它们并不是完全相同的东西,尽管在这种情况下,最终结果是相同的。


5
&array[5] 指向的是数组之后的一个地址,但这种方法并不合法,不能用它来获取该地址。 - Matthew Flaschen
5
最后一句话不正确。"array + 5" 和 "&array[4] + 1" 并没有解除一个在结束之后的引用,而 array[5] 却有。(我也假设你是指 &array[5],但评论依然成立)。前两者只是简单地指向末尾的下一个位置。 - user83255
2
@jalf 那条注释 - 我认为它只是想说如果b直接分配在数组“a”之后,“a + sizeof a”与“&b”同样有效,并且结果地址同样“指向”同一对象。不多不少。请记住,所有注释都是信息性的(非规范性的):如果它要陈述像是数组对象之后有位于“past-the-end”的对象这样基本重要的事实,那么这样的规则就需要成为规范性的。 - Johannes Schaub - litb
2
就 ANSI C(C89/C90)而言,这是正确的答案。如果您严格遵循标准,&array[5] 在技术上是无效的,而 array+5 是有效的,尽管几乎每个编译器都会为这两个表达式生成相同的代码。C99 更新了标准,明确允许 &array[5]。有关完整详细信息,请参见我的答案。 - Adam Rosenfield
2
@jalf,还有整个注释的文本都是以“如果类型为T的对象位于地址A…”开头的。这意味着“以下文本假定在地址A处存在一个对象。”因此,根据这个条件,你引用的内容并不会(也不能)说在地址A处总是有一个对象。 - Johannes Schaub - litb
显示剩余20条评论

17

它是合法的。

根据C++的gcc文档&array [5]是合法的。在C ++和C中,您可以安全地寻址数组结束后的一个元素-您将获得有效指针。因此,作为表达式的&array [5]是合法的。

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

实际上,我想这通常不会导致崩溃。

编辑:顺便说一下,这通常是STL容器的end()迭代器的实现方式(作为one-past-the-end的指针),所以这是一种合法的做法。

编辑:哦,现在我明白了,您实际上并不是在问是否持有指向该地址的指针是合法的,而是询问获取指针的确切方法是否合法。对于此问题,我将推迟给其他回答者。


4
如果C++规范没有规定&*必须被视为无操作,则我认为你是正确的。我想它可能没有这样规定。 - Tyler McHenry
8
您所引用的页面(正确地)说明了指向数组结尾后一个元素是合法的。&array[5] 技术上首先对 (array + 5) 取间接寻址,然后再次取地址。因此,技术上它就像这样:(&(array + 5))。幸运的是,编译器足够聪明,知道 & 可以被简化为空。但是,它们不一定要这样做,因此,我认为这是未定义行为。 - Evan Teran
4
@Evan:这还有更多内容。请查看核心问题232的最后一行:http://std.dkuug.dk/JTC1/SC22/WG21/docs/cwg_active.html#232。那里的最后一个示例看起来似乎有误,但他们明确解释了区别在于“lvalue-to-rvalue”转换,在本案例中不会发生。 - Richard Corden
2
这是一种与“指向NULL引用”的事情相同的未定义行为,人们一直在讨论并似乎都投票支持回答说“这是未定义行为”。 - Johannes Schaub - litb
@RichardCorden:如果存在未定义行为,可能会出现其他问题。是否有编译器可以查看 int array[5]; &array[5];,对子表达式 array[5] 进行编译时边界检查并拒绝编译它?如果确实存在未定义行为,他们有权这样做,尽管在 C 中是合法的,并且人们依赖它,但这可能不是编译器实现的最受欢迎的错误;-) - Steve Jessop
显示剩余7条评论

10
我不认为这是非法的,但我确实相信 &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 是获取指向数组末尾之外的指针的有效且定义良好的方式。

关键点是:“ '*' 的结果是一个左值”。据我所知,只有在对该结果进行左值转右值的转换时才会变为未定义行为。 - Richard Corden
1
我认为由于'*'的结果仅根据应用运算符的表达式所引用的对象定义,因此如果表达式没有实际引用对象的值,则未定义该结果-由于省略。 然而,这远非清楚明了。 - CB Bailey

10

我认为这是合法的,并且取决于“左值到右值”的转换是否发生。最后一行核心问题232如下:

我们认为标准中的方法似乎没问题:p = 0; *p; 不是本质上的错误。左值到右值的转换会导致未定义的行为。

虽然这个例子略有不同,但它确实表明 '*' 不会导致左值到右值的转换,因此,鉴于该表达式是'&'的直接操作数,而 '&' 需要一个左值,那么行为是被定义的。


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

7

除了以上答案,我要指出operator&可以被类覆盖。因此,即使对于POD来说是有效的,但对于你知道无效的对象来说,这可能不是一个好主意(就像一开始覆盖操作符&()一样)。


4
即使一些专家建议不要重载operator&,因为一些STL容器依赖于它返回指向元素的指针,但我赞成将其列入讨论。这是在他们更好地了解之前就已经被纳入标准的那些东西之一。请+1表示支持。 - David Rodríguez - dribeas

3

这是合法的:

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。- end note]
上述内容的重点:
“结果是一个引用该对象或函数的左值”。
一元运算符“*”返回一个引用int的左值(没有解引用)。然后一元运算符“&”获取左值的地址。
只要没有对越界指针进行解引用,那么操作就完全符合标准,并且所有行为都已定义。因此,根据我的阅读,上述内容是完全合法的。
STL算法依赖于行为被明确定义的事实,是一种暗示,标准委员会已经考虑到了这一点,我相信有一些明确涵盖这一点的东西。
下面的评论部分提出了两个观点:
(请阅读:但它很长,我们俩最终变得像是巨魔)
观点1:
由于第5.7段第5款,这是非法的。
当将具有整数类型的表达式添加到指针中或从指针中减去时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向原始元素的偏移量为整数表达式的i + n-th和i-n-th元素,分别。换句话说,如果表达式P指向数组对象的第i个元素,则表达式(P)+N(等效于N+(P))和(P)-N(其中N的值为n)分别指向数组对象的第i + n个和i-n个元素,前提是它们存在。此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向数组对象的最后一个元素之后的一个元素,如果表达式Q指向数组对象的最后一个元素之后的一个元素,则表达式(Q)-1指向数组对象的最后一个元素。如果指针操作数和结果都指向同一数组对象的元素或超过数组对象的最后一个元素,则评估不会产生溢出;否则,行为未定义。
虽然这部分是相关的,但它并未显示未定义行为。我们所讨论的数组中的所有元素都在数组内或在其末尾之一(由上面的段落明确定义)。
第二个参数如下所示:*是解引用运算符。虽然这是描述“*”运算符的常用术语,但标准故意避免使用该术语,因为“解引用”一词在语言和底层硬件方面的含义并不明确。尽管访问数组末尾之外的内存肯定是未定义的行为。但我并不认为一元*操作符在这种情况下(标准未定义的方式)访问内存(读取/写入内存)。在此上下文中(由标准定义(参见5.3.1.1)),一元*操作符返回一个引用对象的lvalue。在我对语言的理解中,这不是访问底层内存。然后,此表达式的结果立即由一元&操作符使用,该操作符返回引用对象的地址。还提供了许多对维基百科和非规范来源的引用。我发现所有这些都与本问题无关。C++由标准定义。
结论:我愿意承认标准中可能有很多部分我没有考虑到,这些可能证明我的上述论点是错误的。以下没有提供任何证据。如果您向我展示了一个标准引用,并表明这是未定义行为,那么我将:
1.接受答案。 2.在所有大写字母中写下这很愚蠢,我错了。
这不是争论:并不是整个世界的一切都由C++标准定义。开放你的思想。

2
根据谁的说法?根据哪个段落?*执行解引用操作,即为解引用运算符。这就是它的作用。可以说,使用&获取结果值的新指针是无关紧要的。不能仅呈现一系列计算过程,在呈现最终表达式语义时假装中间步骤没有发生(或者该语言的规则不适用于每一个步骤)。 - Lightness Races in Orbit
3
我从同一段文字中引用:“结果是一个左值引用表达式所指向的对象或函数。”很明显,如果这个操作符没有对应的对象,那么它就没有定义的行为。你后续的陈述“返回了一个左值引用指向 int(没有解引用)”让我感到不理解。为什么你认为这不是一个解引用呢? - Lightness Races in Orbit
2
它返回指向所指对象的引用。如果不是解引用,那是什么?该段落说 * 执行间接寻址,从指针到被指对象的间接寻址称为解引用。你的论点本质上断言指针和引用是相同的东西,或者至少是隐含关联的,这显然是不正确的。int x = 0; int* ptr = &x; int& y = *x; 在这里,我对 x 进行了解引用。我不需要使用 y 就可以成立。 - Lightness Races in Orbit
2
@LokiAstari:我有一个问题,如果不是“调用一元*运算符返回引用到表达式指向的对象的左值”,那么你认为“解引用”是什么意思?(请注意,C++11规范的后续句子确实将此过程称为“解引用”) - Mooing Duck
2
@LokiAstari:我几天前就向你展示了参考资料,但你似乎拒绝承认它们的存在。我无法理解你如何能够为自己的行为辩护,但你一定是在恶意挑衅。 - Lightness Races in Orbit
显示剩余85条评论

2
即使它是合法的,为什么要违背常规呢?array + 5 反正更短,而且在我看来更易读。
编辑:如果你想让它对称,可以这样写。
int* array_begin = array; 
int* array_end = array + 5;

我认为我在问题中使用的样式更对称:数组声明和begin/end指针,有时我直接将它们传递给STL函数。这就是为什么我使用它而不是较短版本的原因。 - Zan Lynx
为了对称起见,我认为需要这样做:array_begin = array + 0; array_end = array + 5; 对于这么长时间延迟的评论回复你觉得怎么样? - Zan Lynx
这可能是世界纪录 :) - rlbond

2
工作草案(n2798):
“一元运算符 & 的结果是其操作数的指针。操作数必须是左值或限定标识符。在第一种情况下,如果表达式类型为“T”,则结果类型为“指向 T 的指针”。 (第103页)”
据我所知,array[5] 不是一个限定标识符(列表在第87页);最接近的可能是标识符,但虽然 array 是一个标识符,但 array[5] 不是。它不是左值,因为“左值是指对象或函数。” (第76页)。array[5] 显然不是函数,并且不能保证引用有效的对象(因为array + 5位于最后一个分配的数组元素之后)。
显然,在某些情况下它可能会起作用,但它不是有效的C ++或安全的。
注意:可以合法地添加来获取数组之外的一个元素(第113页):
如果指针表达式P指向数组对象的最后一个元素,则表达式(P)+1指向数组对象的最后一个元素之后的一个元素;如果表达式Q指向数组对象的最后一个元素之后的一个元素,则表达式(Q)-1指向数组对象的最后一个元素。如果指针操作数和结果都指向同一数组对象的元素或该数组对象的最后一个元素之一,那么评估不会产生溢出。但是使用&这样做是不合法的。

1
我点赞你,因为你是正确的。没有保证有一个对象位于超出末尾位置的位置。那个踩你的人可能误解了你(你听起来像在说任何数组索引操作都不涉及任何对象)。我认为这里有一件有趣的事情:它lvalue,但它也引用任何对象。因此,这与标准所说的相矛盾。因此,这会产生未定义的行为 :) 这也与这个有关:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#232 - Johannes Schaub - litb
2
@jalf,这个注释说“可能位于那个地址”。并不保证一定有一个位于那里 :) - Johannes Schaub - litb
1
标准规定 op* 的结果必须是一个左值,但只有当操作数是实际指向对象的指针时才说明了该左值是什么。这似乎意味着(非常奇怪)如果指向结尾的指针没有指向合适的对象,那么实现就必须从其他地方找到一个合适的左值并使用它。这真的会搞砸 &array[sizeof array]! - CB Bailey
3
然而,我仍然认为它不是左值。因为不能保证在数组[5]处有对象,所以数组[5]不能合法地引用一个对象。这正是我认为它是未定义行为的原因:它依赖于标准没有明确规定的一些行为,因此属于1.3.12[defns.undefined]范畴。 - Johannes Schaub - litb
1
litb,好的。我们可以说它/不一定/是一个lvalue,因此/绝对不/100%安全。 - Matthew Flaschen
显示剩余2条评论

1
由于以下原因,应该将其定义为未定义行为:
1. 尝试访问越界元素会导致未定义行为。因此,标准不禁止实现在这种情况下抛出异常(即在访问元素之前检查边界的实现)。如果将&(array[size])定义为begin(array)+size,则在访问越界时抛出异常的实现将不再符合标准。
2. 如果array不是数组而是任意集合类型,则无法使其产生end(array)

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