是否UB访问一个二维数组的行末尾的元素?

ex *_*ilo 6 c arrays pointers multidimensional-array language-lawyer

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

根据§6.5.68:

此外,如果表达式P指向数组对象的最后一个元素,则表达式(P)+1指向一个超过数组对象的最后一个元素....如果指针操作数和结果都指向元素的相同的数组对象,或者超过数组对象的最后一个元素,评估不应产生溢出; 否则,行为未定义.如果结果指向数组对象的最后一个元素之后,则不应将其用作已计算的一元*运算符的操作数.

由此看来,上述程序的行为似乎未定义; ptr3_plus_1指向一个地址,该地址超出派生它的数组对象的末尾,并且取消引用此地址会导致未定义的行为.

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

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

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

正如我所看到的那样,问题在于形成指针的地址甚至ptr3_plus_2是不合法的,因此以这种方式访问​​任意二维数组元素是不合法的.但是,使用此指针算法形成指针的地址合法的ptr3_plus_1.此外,合法的两个指针比较ptr3_plus_1,并ptr4根据§6.5.96:

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

所以,如果两个ptr3_plus_1ptr4是比较相等,并且必须指向相同的地址有效指针(对象指向ptr4到对象指向必须在内存相邻ptr3无论如何,因为阵列存储必须是连续的),这似乎这*ptr3_plus_1是有效的*ptr4.

这是不确定的行为,如§6.5.68和附件J.2中所述,或者这是一个例外情况?

澄清

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

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

小智 5

所以,如果这两个ptr3_plus_1ptr4是比较平等的,必须指向同一个地址有效指针

他们是.

它看起来和*ptr3_plus_1它一样有效*ptr4.

它不是.

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

double a = 0.0, b = -0.0;
assert (a == b);
assert (1/a != 1/b);
Run Code Online (Sandbox Code Playgroud)

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

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


n. *_* m. 4

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

请注意,基址和大小不是二维数组的地址和大小,而是指针指向的数组(本例中为行)的地址和大小。

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