是UB访问一个二维数组行结束的元素吗?

以下程序的行为是否未定义?

#include  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指向一个地址,该地址超出派生它的数组对象的末尾,并且取消引用此地址会导致未定义的行为。

此外, 附件J.2表明这是未定义的行为:

数组下标超出范围,即使一个对象显然可以使用给定的下标访问(如左边的表达式[1] [7],给出声明int a [4] [5] )(6.5.6)。

Stack Overflow问题中有一些关于这个问题的讨论, 一维访问多维数组:定义明确的C? 。 这里的共识似乎是通过一维下标对二维数组的任意元素的这种访问确实是未定义的行为。

我认为问题在于,形成指针ptr3_plus_2的地址甚至不合法,因此以这种方式访问​​任意二维数组元素是不合法的。 但是,使用此指针算法形成指针ptr3_plus_1的地址合法的。 此外,根据§6.5.9¶6比较两个指针ptr3_plus_1ptr4是合法的:

两个指针比较相等,当且仅当两个都是空指针时,两者都是指向同一对象的指针(包括指向对象的指针和在其开头的子对象)或函数,两者都是指向同一数组的最后一个元素之后的指针对象, 或者一个是指向一个数组对象末尾的指针,另一个是指向不同数组对象的开头的指针,该数组对象恰好跟随地址空间中的第一个数组对象。

所以,如果ptr3_plus_1ptr4都是有效的指针,它们必须比较相同且必须指向同一个地址( ptr4指向的对象必须在内存中与ptr3指向的对象相邻,因为数组存储必须是连续的) ,似乎*ptr3_plus_1*ptr4一样有效。

这是不确定的行为,如§6.5.6¶8和附件J.2中所述,还是这是一个例外情况?

澄清

似乎毫无疑问的是,尝试在二维数组的最后一行的末尾之后访问元素是未定义的行为。 我感兴趣的是通过使用指向前一行中的元素的指针和指针算法形成一个新指针来访问中间行的第一个元素是否合法。 在我看来,附件J.2中的另一个例子可以使这一点更清楚。

是否有可能协调§6.5.6¶8中的clear语句,试图取消引用一个指向一个数组末尾的位置的指针会导致未定义的行为,并指出指针经过第一行的末尾类型为T [] []的二维数组也是类型为T *的指针,指向类型为T的对象,即类型为T []的数组的第一个元素?

因此,如果ptr3_plus_1ptr4都是有效的指针,那么它们必须指向相同的地址并且必须指向相同的地址

他们是。

似乎*ptr3_plus_1*ptr4一样有效。

它不是。

指针是相等的,但不相等。 平等和等价之间区别的一个众所周知的例子是负零:

 double a = 0.0, b = -0.0; assert (a == b); assert (1/a != 1/b); 

现在,公平地说,两者之间存在差异,因为正零和负零具有不同的表示,典型实现上的ptr3_plus_1ptr4具有相同的表示。 这不保证,并且在它们具有不同表示的实现上,应该清楚您的代码可能会失败。

即使在典型的实现中,虽然有很好的论据可以说相同的表示意味着等价的值,但据我所知,官方解释是标准不能保证这一点,因此程序不能依赖它,因此实现可以假设程序不这样做并相应地进行优化。

调试实现可能使用“胖”指针。 例如,指针可以表示为元组(地址,基数,大小)以检测越界访问。 关于这种表述的标准绝对没有错误或相反。 因此,任何使指针超出[base,base + size]范围的指针算法都会失败,并且[base,base + size]之外的任何解除引用也会失败。

请注意,base和size 不是 2D数组的地址和大小,而是指针指向的数组(在本例中为行)。

在这种情况下听起来可能听起来微不足道,但在决定某个指针构造是否为UB时,通过这个假设的实现精神上运行您的示例是有用的。