我已经多次看到有人断言以下代码是 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?

有帮助吗?

解决方案

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

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

一般来说,你甚至不被允许 创造 越界指针。指针必须指向数组中的元素,或者 一过终点. 。无处。

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

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

5.7:5:

当将具有积分类型的表达式添加到指针中时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素的元素偏移,以使结果和原始数组元素的下标的差异等于积分表达式。换句话说,如果表达式p指向数组对象的第i-the元素,则表达式(p)+n(等效地,n+(p))和(p)-n(其中n具有值n)点如果它们存在,则分别是数组对象的i+n-th和i-n-元素。此外,如果表达式p指向数组对象的最后一个元素,则表达式(p)+1指向一个数组对象的最后一个元素,并且express 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 (这是完全安全的),获取该lvalue的地址,并在该地址中添加一个,这导致了一个末端的指针(但该指针从未被剥离。
  • &array[5] dereences数组+5(据我所知,这是合法的,并且如上所述,导致“阵列元素类型的无关对象”),然后拿出该元素的地址,这似乎也足够合法。

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

其他提示

是的,这是合法的。来自 C99标准草案:

§6.5.2.1,第 2 段:

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

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

一元 & 运算符产生其操作数的地址。如果操作数的类型为‘‘类型'',结果具有类型' 类型’’。如果操作数是一元的结果 * 操作员,既不 & 评估了运算符,结果好像两者都被省略了,只是对操作员的约束仍然适用,结果不是LVALUE。相似地, 如果操作数是 a 的结果 [] 运算符,既不是 & 运算符,也不是一元运算符 * 这是由 [] 被评估,结果就像 & 卸下操作员,并 [] 运算符更改为 + 操作员. 。否则,结果是指向其操作数指定的对象或函数的指针。

§6.5.6,第 8 段:

当将具有整数类型的表达式添加到指针中或从指针中添加时,结果具有指针操作数的类型。如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素的元素偏移,以使结果和原始数组元素的下标的差异等于整数表达式。换句话说,如果表达式 P 指向 i- 数组对象的第一个元素,表达式 (P)+N (相当于, N+(P)) 和 (P)-N (在哪里 N 有价值 n)分别指向 i+n-th 和 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),因此它是合法的。

它的合法的。

根据用于C ++ gcc的文档,&array[5]是合法的。在这两种C ++ 和用C 可以安全地解决了元件一个过去的阵列的端部 - 你会得到一个有效的指针。所以&array[5]作为表达是合法的。

但是,它仍然是不确定的行为来尝试取消引用指针未分配存储器,即使指针指向一个有效地址。因此试图解除引用由该表达式生成的指针仍然是不确定的行为(即非法),即使指针本身是有效的。

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

编辑:顺便说一下,这通常是如何结束()迭代器STL容器实现(作为指针,以一个过去的最末端),所以这是一个很好的证明了实践是法律<。 / p>

编辑:哦,现在我看到你没有真正询问是否持有一个指针的地址是合法的,但如果获得指针,准确的方式是合法的。我会推迟到了其他应答者。

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

  

我们认为,在标准的方法似乎好:P = 0; * P;并非天生就是一个错误。左值到右值转换会给它未定义行为

虽然这是稍有不同的例子,它做什么节目是,“*”不会导致左值到右值的转换等等,因为该表达是“&”立即操作数,其预计左值则行为定义

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

在除了上述的答案,我将指出操作员可以重写为类。因此,即使它是有效的吊舱,它可能不是一个好主意,你知道一个对象做的不是有效的(就像摆在首位覆盖运营商的())。

这是合法的:

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除外)。因此获得的lvalue可以以有限的方式使用(例如,以初始化参考初始化);该左值不得转换为右值,请参阅 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.

还提供了对维基百科和非规范来源的许多其他参考。所有这些我都觉得无关紧要。 C++ 由标准定义.

结论:

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

  1. 留下答案。
  2. 全部大写,这是愚蠢的,我对所有人来说都是错误的。

这不是一个论点:

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

即使是合法的,为什么从约定离开?阵列+ 5较短无论如何,在我的意见,更具有可读性。

编辑:如果你想让它由对称的,你可以写

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

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

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

  2. 不可能达到这个收益 end (array) if array 不是数组而是任意集合类型。

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

这是地址常量表达式是一个指针,指向一个左值....指针应明确使用一元&运算...或使用阵列(4.2)...类型的表达式来创建。的下标操作符[] ...可以在创建一个地址常量表达式的使用,但对象的值不应通过使用这些操作符的访问。如果下标操作者使用时,它的操作数中的一个必须是一个积分常数表达式。

我看来像&阵列[5]是合法的C ++,作为一个地址常量表达式。

有完全合法的。

从STL的载体<>模板类不正是此当调用myVec.end():它可以让你的指针(在此作为一个迭代),它指向一个元件过去的数组的末尾

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top