我已经多次看到有人声称以下代码不符合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
区别对待?
我已经多次看到有人声称以下代码不符合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
区别对待?
是合法的。根据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段:
注意,标准明确允许指针指向数组的末尾之后的一个元素,前提是它们不被解引用。根据6.5.2.1和6.5.3.2章节,表达式 &array[5] 等同于 &*(array + 5),等同于 (array+5),这将指向数组的末尾之后的一个元素。根据6.5.3.2章节,这不会导致解引用操作,因此是合法的。当将整数类型的表达式加到指针上或从指针中减去时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向相对于原始元素偏移的元素,使得结果和原始数组元素的下标之差等于整数表达式。 换句话说,如果表达式
P
指向数组对象的第i
个元素,则表达式(P)+N
(等价于N+(P)
)和(P)-N
(其中N
的值为n
)分别指向数组对象的第i+n
个和第i−n
个元素(如果它们存在)。 此外,如果表达式P
指向数组对象的最后一个元素,则表达式(P)+1
指向数组对象的最后一个元素之后的一个元素,如果表达式Q
指向数组对象的最后一个元素之后的一个元素,则表达式(Q)-1
指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一数组对象的元素或数组对象的最后一个元素之后,则评估不会产生溢出; 否则,行为是未定义的。 如果结果指向数组对象的最后一个元素之后,则不能将其用作评估的一元*
运算符的操作数。
&
和*
运算符都不会被计算。但是C++则不同。根据我所了解到的,当时正在起草的C++11标准没有类似的条款。 - Adam Rosenfield当然,这是针对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(就我所看到的而言是合法的,并导致“数组元素类型的不相关对象”,如上所述),然后获取该元素的地址,这也似乎足够合法。因此它们并不是完全相同的东西,尽管在这种情况下,最终结果是相同的。
它是合法的。
根据C++的gcc文档,&array [5]
是合法的。在C ++和C中,您可以安全地寻址数组结束后的一个元素-您将获得有效指针。因此,作为表达式的&array [5]
是合法的。
但是,即使指针指向有效地址,尝试引用未分配内存的指针仍然是未定义的行为,即非法的。因此,尝试解引用由该表达式生成的指针仍然是未定义行为(即非法的)。
实际上,我想这通常不会导致崩溃。
编辑:顺便说一下,这通常是STL容器的end()迭代器的实现方式(作为one-past-the-end的指针),所以这是一种合法的做法。
编辑:哦,现在我明白了,您实际上并不是在问是否持有指向该地址的指针是合法的,而是询问获取指针的确切方法是否合法。对于此问题,我将推迟给其他回答者。
int array[5]; &array[5];
,对子表达式 array[5]
进行编译时边界检查并拒绝编译它?如果确实存在未定义行为,他们有权这样做,尽管在 C 中是合法的,并且人们依赖它,但这可能不是编译器实现的最受欢迎的错误;-) - Steve Jessop我认为这是合法的,并且取决于“左值到右值”的转换是否发生。最后一行核心问题232如下:
我们认为标准中的方法似乎没问题:p = 0; *p; 不是本质上的错误。左值到右值的转换会导致未定义的行为。
虽然这个例子略有不同,但它确实表明 '*' 不会导致左值到右值的转换,因此,鉴于该表达式是'&'的直接操作数,而 '&' 需要一个左值,那么行为是被定义的。
&*a[n]
的问题:“同样地,只要不使用该值,就应该允许对数组末尾的指针进行解引用”。不幸的是,这个问题自2003年以来一直未得到解决,并且还没有被纳入标准。 - Pablo Halpern除了以上答案,我要指出operator&可以被类覆盖。因此,即使对于POD来说是有效的,但对于你知道无效的对象来说,这可能不是一个好主意(就像一开始覆盖操作符&()一样)。
这是合法的:
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)),一元*操作符
返回一个引用对象的lvalue
。在我对语言的理解中,这不是访问底层内存。然后,此表达式的结果立即由一元&操作符
使用,该操作符返回引用对象的地址
。还提供了许多对维基百科和非规范来源的引用。我发现所有这些都与本问题无关。C++由标准定义。*
执行解引用操作,即为解引用运算符。这就是它的作用。可以说,使用&
获取结果值的新指针是无关紧要的。不能仅呈现一系列计算过程,在呈现最终表达式语义时假装中间步骤没有发生(或者该语言的规则不适用于每一个步骤)。 - Lightness Races in Orbit*
执行间接寻址,从指针到被指对象的间接寻址称为解引用。你的论点本质上断言指针和引用是相同的东西,或者至少是隐含关联的,这显然是不正确的。int x = 0; int* ptr = &x; int& y = *x;
在这里,我对 x
进行了解引用。我不需要使用 y
就可以成立。 - Lightness Races in Orbit*
运算符返回引用到表达式指向的对象的左值”,那么你认为“解引用”是什么意思?(请注意,C++11规范的后续句子确实将此过程称为“解引用”) - Mooing Duckint* array_begin = array;
int* array_end = array + 5;
&(array[size])
定义为begin(array)+size
,则在访问越界时抛出异常的实现将不再符合标准。end(array)
。