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

5

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

#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 暗示这是未定义行为:

即使一个对象在给定下标的情况下似乎是可访问的(例如,在声明 int a[4][5] 的情况下给出左值表达式a[1][7]),数组下标越界也是不合法的 (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_1ptr4是合法的:

如果两个指针都是空指针、都是指向同一对象(包括指向对象及其开头的子对象的指针)或函数的指针、都是指向同一数组对象的最后一个元素的指针,或者一个是指向一个数组对象结束之后的位置,另一个是指向紧随第一个数组对象后面的另一个不同数组对象的开始位置,则两个指针相等。

因此,如果ptr3_plus_1ptr4都是有效的指针,比较相等且必须指向同一地址(无论如何,ptr4指向的对象必须与ptr3指向的对象在内存中相邻,因为数组存储必须是连续的),那么看起来*ptr3_plus_1*ptr4一样有效。

这是否是未定义的行为,如§6.5.6 ¶8和附录J.2所述,还是一个特殊情况?

澄清

尝试访问二维数组的最后一行之外的元素是未定义行为,这似乎是明确的。我感兴趣的问题是,是否可以通过使用前一行的元素的指针和指针算术来形成新指针来访问中间行的第一个元素。对我来说,附录J.2中的另一个示例可能更清楚地说明了这一点。

在第6.5.6节第8段中明确指出,尝试对指向数组结尾后一个位置的指针进行解引用会导致未定义行为,那么是否可能将类型为T[][]的二维数组第一行结尾后的指针作为T *类型的指针,并将其指向T[]类型的数组的第一个元素,也就是T类型的对象呢?


访问二维数组的一行末尾元素是否属于未定义行为(UB)?对于最后一行来说,确实是。但是在之前的行中,我没有看到任何未定义行为 - 它们都是连续的空间。 - chux - Reinstate Monica
7
您的6.5.6引用清楚地表明这是未定义行为。指针的来源很重要,不仅仅是它所代表的地址。 - T.C.
我不知道你在问什么。你的问题已经明确地给出了答案。它似乎没有列出任何异常情况。 - too honest for this site
1
据我所知,这是一个开放性问题,即尚未得到令人满意的解决,并且可能会在未来版本的标准中得到澄清。 - Dietrich Epp
1
@Olaf-- ptr3_plus_1 是一个有效的指针,它指向数组的末尾,因此不能被解引用;但是 ptr3_plus_1 也是一个有效的指针,它指向数组的第一个元素,因此应该可以被解引用。我正在努力协调我认为存在的明显矛盾。也许答案是 ptr3_plus_1 不能被说成是指向第二个数组的第一个元素的指针。 - ad absurdum
显示剩余9条评论
2个回答

5

因此,如果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 ,它们具有相同的表示形式。这并不保证,在它们具有不同表示形式的实现上,你的代码可能会失败,因此应明确这一点。
即使在典型实现中,虽然有很好的理由认为相同的表示形式意味着等效的值,但据我所知,官方解释是标准不保证这一点,因此程序不能依赖于它,因此实现可以假定程序不这样做并进行相应的优化。

我曾经也想过这个问题,但是§6.2.5 28不适用于这里吗?“同样,指向合格或不合格版本的兼容类型的指针应具有相同的表示和对齐要求。” - ad absurdum
@PeterJ 整个重点是传递指向指针的指针,以便复制指针值本身。不,我不是在复制一个字符,sizeof csizeof(const char *) 而不是 sizeof(char) - user743382
好的,我明白你关于§6.2.5 28的意思。通常情况下,我不需要说服标准认为UB是真的UB; 但是在这里,似乎很难承认同一种指针ptr3_plus_1不能像ptr4一样被解引用,除非可以说第一个指针根本没有指向任何对象。也许这就是你对不同表示的建议的理解....总之,暂时没有更多问题了。我会等一段时间再接受,看看是否有其他人可以提出解决方案。 - ad absurdum
@DavidBowling 标准确实没有说它不指向对象。但它也没有说它指向对象,你是从ptr3_plus_1ptr4之间的等价性推断出来的,但我的回答的重点是这种等价性不存在。 :) - user743382
不仅因为它们相等,而且因为ptr4指向的对象比ptr3指向的对象多一个,所以我的期望是ptr3_plus_1应该指向ptr4指向的对象。比较的合法性似乎只是支持证据。 - ad absurdum
显示剩余7条评论

4
一种调试实现可能使用“fat”指针。例如,指针可以表示为元组(地址,基础,大小),以检测越界访问。这种表示方式绝对没有任何问题或违反标准。因此,任何使指针超出[基础,基础+大小]范围的指针算术都将失败,并且在[基础,基础+大小)之外进行任何解引用也会失败。
请注意,基础和大小不是二维数组的地址和大小,而是指针所指向的数组(在本例中为行)的地址和大小。
在判断某个指针构造是否存在UB时,通过这种假想实现在脑海中运行您的示例非常有用。

1
intptr_t 可能具有与指针相同的表示形式。不明白为什么不允许这样做。顺便说一下,如果我没记错的话,没有要求对 intptr_t 进行算术运算在任何方式上同构于指针算术运算。 - n. m.
好的评论,尽管那将是一个非常大的整数! - curiousguy
1
@curiousguy 是的,但对于调试实现来说,这并不是很重要。 - n. m.
1
这样的实现可能不会定义intptr_t等内容。 - Stargateur
@curiousguy 是的。如果指针是这样表示的,它将读取指针和边界。 - n. m.
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接