通过下标获取末尾一位数组元素的地址:C++ 标准是否合法?
-
13-09-2019 - |
题
我已经多次看到有人断言以下代码是 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。我会
- 留下答案。
- 全部大写,这是愚蠢的,我对所有人来说都是错误的。
这不是一个论点:
并非全世界的所有事物都是由 C++ 标准定义的。敞开心扉。
工作草案( n2798 一>):
“一元&运算的结果是 一个指向它的操作数。操作数 应是一个左值或一个合格音响ED-ID。 在第一个情况下,如果类型 表达是“T”的类型 结果是“指针T.””(第103页)
阵列[5]是不是一个合格-id作为最好能告诉我(该列表是在页码87);最接近似乎是标识符,但同时阵列是一个标识符阵列[5]是没有的。因为“左值是指一个对象或功能。”这不是一个左值(第76页)。阵列[5]显然不是一个功能,并且不能保证引用有效的对象(因为阵列+ 5是最后一个分配的数组元素之后)。
显然,在某些情况下工作,但它不是有效的C ++或安全。
注意:是合法的添加以获得一个过去的阵列(第113页):
“如果表达式P [指针] 指向数组的最后一个元素 对象,表达式(P)1分 一个过去的所述阵列的最后一个元素 对象,并且如果表达式Q点 一个过去的阵列的最后一个元素 对象,表达式(Q)-1分 数组对象的最后一个元素。 如果指针操作数和两 结果指向相同的元件 阵列对象,或一个过去的最后 数组对象的元件,所述 评估也不得产生 溢流“
但它不是合法这样做使用&
即使是合法的,为什么从约定离开?阵列+ 5较短无论如何,在我的意见,更具有可读性。
编辑:如果你想让它由对称的,你可以写
int* array_begin = array;
int* array_end = array + 5;
由于以下原因,它应该是未定义的行为:
尝试访问越界元素会导致未定义的行为。因此,该标准并不禁止在这种情况下抛出异常的实现(即在访问元素之前检查边界的实现)。如果
& (array[size])
被定义为begin (array) + size
, ,在越界访问的情况下抛出异常的实现将不再符合标准。不可能达到这个收益
end (array)
if array 不是数组而是任意集合类型。
C ++标准,5.19,第4段:
这是地址常量表达式是一个指针,指向一个左值....指针应明确使用一元&运算...或使用阵列(4.2)...类型的表达式来创建。的下标操作符[] ...可以在创建一个地址常量表达式的使用,但对象的值不应通过使用这些操作符的访问。如果下标操作者使用时,它的操作数中的一个必须是一个积分常数表达式。
我看来像&阵列[5]是合法的C ++,作为一个地址常量表达式。
如果您的例子是不是一个一般的情况下,但一个特定的一个,那么它是允许的。你可以的合法的,据我所知,移动一个过去分配的存储块。 它不为一个通用的情况下,虽然即,你正在试图通过1从阵列的端部更远访问元素工作。
最近查询C-FAQ:链接文本
有完全合法的。
从STL的载体<>模板类不正是此当调用myVec.end():它可以让你的指针(在此作为一个迭代),它指向一个元件过去的数组的末尾