以下程序的行为是否未定义?
#include <stdio.h>
int main(void)
{
int arr[2][3] = { { 1, 2, 3 },
{ 4, 5, 6 }
};
int *ptr1 = &arr[0][0]; // pointer to first elem of { 1, 2, 3 }
int *ptr3 = ptr1 + 2; // pointer to last elem of { 1, 2, 3 }
int *ptr3_plus_1 = ptr3 + 1; // pointer to one past last elem of { 1, 2, 3 }
int *ptr4 = &arr[1][0]; // pointer to first elem of { 4, 5, 6 }
// int *ptr_3_plus_2 = ptr3 + 2; // this is not legal
/* It is legal to compare ptr3_plus_1 and ptr4 */
if (ptr3_plus_1 == ptr4) {
puts("ptr3_plus_1 == ptr4");
/* ptr3_plus_1 is a valid address, but is it legal to dereference it? */
printf("*ptr3_plus_1 = %d\n", *ptr3_plus_1);
} else {
puts("ptr3_plus_1 != ptr4");
}
return 0;
}
根据§6.5.6 ¶8:
此外,如果表达式 P 指向的最后一个元素 数组对象,表达式(P)+1 指向最后一个 数组对象的元素....如果指针操作数和 结果指向同一个数组对象的元素,或者一个经过的数组 数组对象的最后一个元素,评估不得产生 溢出;否则,行为未定义。 如果结果指出 一个超过数组对象的最后一个元素,它不应该被用作 被评估的一元*运算符的操作数。
由此看来,上述程序的行为未定义; ptr3_plus_1
指向一个地址,该地址超出了派生它的数组对象的末尾,并且取消引用此地址会导致未定义的行为。
此外,Annex J.2表明这是未定义的行为:
数组下标超出范围,即使对象显然也是如此 可以使用给定的下标访问(如左值表达式) a [1] [7] 给出声明 int a [4] [5] )(6.5.6)。
Stack Overflow问题One-dimensional access to a multidimensional array: well-defined C?中对此问题进行了一些讨论。这里的共识似乎是通过一维下标对二维数组的任意元素的这种访问确实是未定义的行为。
我认为问题在于,形成指针ptr3_plus_2
的地址甚至不合法,因此以这种方式访问任意二维数组元素是不合法的。但是,使用此指针算法, 是合法的,以形成指针ptr3_plus_1
的地址。此外,根据§6.5.9 ¶6比较两个指针ptr3_plus_1
和ptr4
是合法的:
当且仅当两者都是空指针时,两个指针才相等 是指向同一对象的指针(包括指向对象的指针) 它开头的一个子对象)或函数,都是一个指针 超过同一个数组对象的最后一个元素,或者一个是指针 到一个数组对象的结尾,另一个是指针 立即发生的另一个数组对象的开始 跟随地址空间中的第一个数组对象。
因此,如果ptr3_plus_1
和ptr4
都是有效的指针,它们必须指向同一个地址(ptr4
指向的对象必须在内存中相邻无论如何,ptr3
指向的对象,因为数组存储必须是连续的),*ptr3_plus_1
似乎与*ptr4
一样有效。
这是未定义的行为,如§6.5.6¶8和附件J.2中所述,还是这是一个例外情况?
澄清
似乎毫无疑问,尝试在二维数组的 final 行的末尾之后访问元素是未定义的行为。我感兴趣的是通过使用指向前一行中的元素的指针和指针算法形成一个新指针来访问中间行的第一个元素是否合法。在我看来,附件J.2中的另一个例子可以使这一点更清楚。
是否有可能协调§6.5.6¶8中的clear语句,试图取消引用一个指向一个数组末尾的位置的指针会导致未定义的行为,并指出指针超过了 T [] [] 类型的二维数组的第一行也是 T * 类型的指针,指向类型 T的对象,即类型 T [] 的数组的第一个元素?
答案 0 :(得分:5)
因此,如果
ptr3_plus_1
和ptr4
都是有效的指针,那么它们必须指向同一个地址
他们是。
似乎
*ptr3_plus_1
与*ptr4
一样有效。
不是。
指针相等,但不相等。平等和等价之间区别的一个众所周知的例子是负零:
double a = 0.0, b = -0.0;
assert (a == b);
assert (1/a != 1/b);
现在,公平地说,两者之间存在差异,因为正零和负零具有不同的表示,ptr3_plus_1
和ptr4
在典型的实现上具有相同的表示。这不能保证,并且在它们具有不同表示的实现上,应该清楚您的代码可能会失败。
即使在典型的实现中,虽然有很好的论据可以说相同的表示意味着等价的值,但据我所知,官方的解释是标准不保证这一点,因此程序不能依赖它因此,实现可以假设程序不这样做并相应地进行优化。
答案 1 :(得分:3)
调试实现可能使用“胖”指针。例如,指针可以表示为元组(地址,基数,大小),以检测越界访问。关于这种表示形式,绝对没有错误或与标准相反。因此,任何使指针超出[base,base + size]范围之外的指针算法都将失败,并且任何超出[base,base + size]范围的取消引用也将失败。
请注意,基数和大小不是2D数组的地址和大小,而是指针指向的数组(在本例中为行)的地址和大小。
在这种情况下,这听起来似乎微不足道,但是当确定某个指针构造是否为UB时,通过这种假设的实现从脑子上运行示例是很有用的。