通过下标获取一个最后一个数组元素的地址:是否符合C ++标准?


76

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

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

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

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

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


7
@Brian:不,如果需要运行时才能捕获错误,则只需要进行边界检查。为了避免这种情况,该标准可以简单地说“不允许”。最好的情况是未定义的行为。您无权这样做,也不需要运行时和编译器告诉您是否这样做。
jalf

好的,请澄清一下,因为标题误导了我:指向数组末尾的指针不会超出范围。通常不允许使用超出范围的指针,但是该标准对于最后一点指针要宽得多。如果您专门询问最后一指针,则可能需要编辑标题。如果您想了解出界球的一般,你应该修改你的榜样。;)
杰夫

通常,他并不是在问指针。他在询问使用&运算符获取指针。
马修·弗拉申(Martin Flaschen)2009年

1
@Matthew:但是答案取决于指针指向的位置。允许您采用过去最后一次的地址,但不允许超出范围。
jalf

5.3.1.1节一元运算符'*':'结果是引用对象或函数的左值'。5.2.1节下标表达式E1 [E2](通过定义)与*(((E1)+(E2)))相同。通过我在这里阅读标准。不会取消结果指针的引用。(请参见下面的完整说明)
马丁·约克

Answers:


39

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

让我们先处理越界指针(因为这是我最初解释您的问题的方式,在我注意到该示例使用单点结束指针之前):

通常,甚至不允许您创建边界指针。指针必须指向数组中的一个元素,或者指向end之后的一个元素。无处。

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

这是该标准在主题上必须说的:

5.7:5:

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

(强调我的)

当然,这适用于操作员+。因此,可以肯定的是,这是标准关于数组下标的内容:

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] 取消引用array + 5(据我所知,这是合法的,并导致“数组元素类型的不相关对象”,如上所述),然后获取该元素的地址,这似乎也足够合法。

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


4
&array [5]指向过去。但是,这不是获得该地址的合法方法。
马修·弗拉申

5
最后一句不正确。“ array + 5”和“&array [4] + 1”不会在末尾取消引用,而array [5]会取消引用。(我还假设您的意思是&array [5],但该注释仍然有效)。前两个只是指向终点。
user83255

2
@jalf注意-我认为如果b在数组“ a”之后直接分配,则“ a + sizeof a”与“&b”同等有效,并且结果地址同样“指向”相同目的。不多。请记住,所有注释都是信息性的(非规范性的):如果要说出一些基本的重要事实,例如数组对象之后的对象位于过去,那么该规则就必须成为规范性的
Johannes Schaub-litb

2
就ANSI C(C89 / C90)而言,这是正确的答案。如果您遵循字母的标准,则&array [5]在技术上是无效的,而array + 5是有效的,即使几乎每个编译器都会为两个表达式生成相同的代码。C99更新了标准以明确允许&array [5]。请参阅我的答案以获取全部详细信息。
亚当·罗森菲尔德2009年

2
@jalf,注释中的整个文本也以“如果类型T的对象位于地址A ...开头” <-表示“以下文本假定地址A处有一个对象。” 所以,你的报价不(也不能,在此条件下)说,总有一个物体在地址A.
约翰内斯·绍布- litb

43

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

§6.5.2.1第2段:

后缀表达式后跟方括号的表达式[]是数组对象元素的下标名称。下标操作符的定义[]E1[E2]相同(*((E1)+(E2)))。由于适用于二进制+运算符的转换规则,如果E1是数组对象(等效于指向数组对象初始元素的指针)并且E2是整数,E1[E2]则将其指定为E2-th元素E1(从零开始计数)。

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

一元运算&符产生其操作数的地址。如果操作数的类型为“ type ”,则结果的类型为“ pointer to 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),因此是合法的。


1
他明确询问了C ++。这是在两者之间移植时不能依靠的细微差别。
马修·弗拉申

3
他问了两个问题:“知道它是否符合C标准也很有趣。”
CB Bailey 2009年

2
@Matthew Flaschen:C ++标准通过引用合并了C标准。附件C.2包含更改列表(ISO C和ISO C ++不兼容),并且所有更改均与这些条款无关。因此,&array [5]在C和C ++中是合法的。
亚当·罗森菲尔德2009年

12
C标准是C ++标准中的规范性参考。这意味着C ++标准引用的C标准中的规定是C ++标准的一部分。这并不意味着C标准中的所有内容都适用。特别是附件C是提供信息的,不是规范性的,因此,仅因为本节中未突出显示差异并不意味着C“版本”适用于C ++。
CB Bailey 2009年

1
@亚当:谢谢你指出这一点。我从未完全确定C的历史。关于C ++ 0x,指的是C99,我对C99的更改知之甚少,我很确定C ++会继续参考C89 / 90,并且会逐案挑选C99的“理想”更改。这个问题/答案就是一个很好的例子。我想说C ++将继续使用“从左值到右值”(novalue-to-rvalue),因此没有未定义的行为,而不是集成“&* == no-op”的措辞。
理查德·科登

17

合法的。

根据C ++的gcc文档&array[5]是合法的。在C ++C语言中您都可以安全地在数组末尾寻址该元素-您将获得一个有效的指针。因此&array[5],表达是合法的。

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

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

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

编辑:哦,现在我看到您不是真的在问持有指向该地址的指针是否合法,但是获取指针的确切方法是否合法。关于这一点,我会请其他回答者来回答。


4
我会说,如果且仅当C ++规范没有说&*必须被视为无操作时,您才是正确的。我想它可能不会这么说。
Tyler McHenry,2009年

8
您引用的页面(正确)说,指向终点是合法的。&array [5],从技术上讲,首先要取消引用(数组+ 5),然后再次引用它。因此,从技术上讲,它是这样的:(&*(array + 5))。幸运的是,编译器足够聪明,可以知道&*可以归零。然而,他们不具备这样做,因此,我会说这是UB。
Evan Teran 2009年

4
@Evan:还有更多。查看核心问题232的最后一行:std.dkuug.dk/JTC1/SC22/WG21/docs/cwg_active.html#232。那里的最后一个示例看起来是错误的-但它们清楚地说明了区别是在“左值到右值”转换上,在这种情况下不会发生。
理查德·科登,2009年

@Richard:有趣的是,似乎对此话题有些争论。我什至同意应该允许它:-P。
Evan Teran

2
这与人们一直在讨论的“对NULL的引用”是一种未定义的行为,似乎所有人都投票赞成“这是未定义的行为”的答案
Johannes Schaub-litb

10

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

我们认为标准中的方法似乎还可以:p = 0; * p; 本质上不是错误。从左值到右值的转换将使其具有未定义的行为

尽管这是一个稍有不同的示例,但它确实表明'*'不会导致左值到右值的转换,因此,如果表达式是'&'的直接操作数,且期望该值是左值,那么就定义了行为。


+1为有趣的链接。我仍然不确定我是否同意p = 0; * p; 因为我不相信'*'是为值不是指向实际对象的指针的表达式定义的,因此定义了很好的定义。
CB Bailey 2009年

作为表达的陈述是合法的,并且意味着评估该表达。* p是一个可调用未定义行为的表达式,因此该实现所执行的所有操作均符合标准(包括向老板发送电子邮件或下载棒球统计信息)。
David Thornley 2009年

1
请注意,该问题的状态仍在“起草”中,并且尚未成为标准(尚未),至少我可以找到那些C ++ 11和C ++ 14的草案版本。
musiphil

“空的左值”的建议被采纳从未到任何公布的标准
MM

问题中的注释明确指出了该&*a[n]问题:“类似地,只要不使用该值,就应允许取消引用指向数组末尾的指针”。不幸的是,自2003年以来,这个问题一直没有解决,标准中还没有解决方案。
Pablo Halpern

9

我不认为这是非法的,但我确实认为&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。
理查德·科登,2009年

1
我认为,“ *”的结果仅根据要应用运算符的表达式所定义的对象来定义,因此,如果省略了表达式,则没有定义-如果省略,结果是什么实际引用对象的值。但是,这还远远不够。
CB Bailey 2009年

7

除了上述答案外,我还指出了可以将operator&覆盖为类。因此,即使它对POD有效,对您知道无效的对象也不是一个好主意(就像首先覆盖了operator&()一样)。


4
即使操作员建议不要覆盖它,因为某些STL容器依赖它将指针返回到元素中,因此即使讨论该问题,+ 1也是讨论的重点。这是他们在不了解之前就已经成为标准的事情之一。
大卫·罗德里格斯(DavidRodríguez)-dribeas,2009年

4

这是合法的:

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返回a lvalue referring to the object。在我对语言的理解中,这不是访问基础内存。然后,该表达式的结果将被unary & operator运算符立即使用,该运算符返回所引用对象的地址lvalue referring to the object

提出了许多其他有关Wikipedia和非规范资源的参考。我发现所有这些都不相关。C ++由标准定义

结论:

我想承认该标准的许多部分我可能没有考虑过,并且可能证明我的上述观点是错误的。 以下提供了NON。如果您给我看一个显示这是UB的标准参考书。我会

  1. 留下答案。
  2. 戴上大写字母,这是愚蠢的,所有人都读错了。

这不是一个参数:

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


2
根据谁?根据哪个段落?在*执行解除引用。它是取消引用运算符。这就是它的作用。可以争辩的是,随后获得一个指向结果值的新指针(使用&)是无关紧要的。您不能仅仅给出一个评估序列,给出最终表达的语义,并假装没有发生中间步骤(或者该语言的规则不适用于每个规则)。
Lightness Races in Orbit

3
我引用同一段话: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)对我而言毫无意义。您为什么认为这不是取消引用?
2013年

2
It returns a reference to what is being pointed at.如果不取消引用,这是什么?这段话说*执行间接操作,从指针到指针的间接操作称为解引用。您的论点实质上断言指针和引用是同一件事,或者至少是隐式链接,这根本不是事实。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在轨道上进行的比赛

2

工作草案(n2798):

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

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

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

注意:合法的加法运算是通过数组(第113页):

“如果表达式P [指针]指向数组对象的最后一个元素,则表达式(P)+1指向数组对象的最后一个元素之后,如果表达式Q指向数组对象的最后一个元素之后。数组对象,表达式(Q)-1指向数组对象的最后一个元素,如果指针操作数和结果都指向同一数组对象的元素,或者指向数组对象的最后一个元素,则求值不会产生溢出”

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


1
我赞成你,因为你是对的。没有任何对象可以保证位于过去的位置。打败您的人可能会误解您(您听起来好像说任何array-index-op都根本没有指向任何对象)。我认为这是一件有趣的事情:这一个左值,但它也不引用一个对象。因此,这与标准所说的是矛盾的。因此,这会产生未定义的行为:)这也与此相关: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 2009年

3
“但是,我仍然认为它不是左值。因为没有对象可以保证在array [5]处,所以array [5]不能合法地引用对象。” <-这就是我认为它是未定义行为的原因:它依赖于标准未明确指定的某些行为,因此落入1.3.12 [defns.undefined]
Johannes Schaub-litb

1
小东西,还算公平。假设它不是/绝对是左值,因此/绝对不是/ 100%安全。
马修·弗拉申

2

即使合法,为什么还要违背约定?无论如何,array + 5会更短,我认为可读性更高。

编辑:如果您希望通过对称可以编写

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

我认为我在问题中使用的样式看起来更加对称:数组声明和begin / end指针,或者有时我将它们直接传递给STL函数。这就是为什么我使用它而不是较短的版本。
Zan Lynx

为了对称,我认为它必须是array_begin = array + 0; array_end =数组+ 5; 长时间延迟的评论回复如何?
Zan Lynx

可能是世界纪录:)
rlbond 2010年

1

由于以下原因,它应该是未定义的行为:

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

  2. end (array)如果array不是数组而是任意集合类型,则无法产生这种收益。


0

C ++标准5.19第4段:

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

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


1
我不确定原来的问题一定是在谈论带有静态存储的数组。即使我想知道&array [5]是否不是因为它不指向指定对象的左值的地址常量表达式吗?
CB Bailey 2009年

我认为数组是静态的还是堆栈分配的都不重要。
David Thornley,2009年

如果您引用的是5.19,则可以。您省略的部分说:“……指定静态存储持续时间的对象,字符串文字或函数。……”。这意味着,如果您的表达式包含分配给堆栈的数组,则不能使用5.19推理这些表达式的有效性。
CB Bailey 2009年

您的报价是说,如果&array[5]合法且引用了静态存储期限数组,则该地址常数为常数。(&array[99]例如,与之相比,本段中没有文字可区分这两种情况)。
MM

-1

如果您的示例不是一般情况,而是特定情况,则可以使用。您可以合法AFAIK将内存移动到分配的内存块之外。但是,它不适用于一般情况,即您尝试访问距离数组末尾1个元素远的元素。

刚搜索的C-Faq:链接文本


最重要的答案是“合法”,我也说同样的话。那么为什么要下投票:)。我的答案有问题吗?
Aditya Sehgal

-2

这是完全合法的。

当您调用myVec.end()时,来自stl的vector <>模板类正是这样做的:它为您提供了一个指针(此处为迭代器),该指针指向数组末尾的一个元素。


但这是通过指针算法实现的,而不是通过形成对过去的引用,然后将地址操作符应用于该左值。
Ben Voigt 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.