【问题标题】:Is it UB to access an element one past the end of a row of a 2d array?是否是 UB 访问一个超过二维数组行尾的元素?
【发布时间】:2017-12-12 17:47:57
【问题描述】:

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

#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)。

堆栈溢出问题One-dimensional access to a multidimensional array: well-defined C? 对此问题进行了一些讨论。这里的共识似乎是,这种通过一维下标访问二维数组的任意元素确实是未定义的行为。

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

当且仅当两个指针都是空指针时,两个指针比较相等 是指向同一对象的指针(包括指向对象的指针和 开头的子对象)或函数,两者都是指向一个的指针 过去同一个数组对象的最后一个元素,或者一个是指针 一个超过一个数组对象的末尾,另一个是指向 立即发生的不同数组对象的开始 跟随地址空间中的第一个数组对象。

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

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

澄清

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

是否可以调和第 6.5.6 节 ¶8 中的明确声明,即尝试取消引用指向数组末尾之后位置的指针会导致未定义的行为,认为指针超出数组末尾T[][] 类型的二维数组的第一行也是 T* 类型的指针,它指向 T 类型的对象strong>,即 T[]?

类型数组的第一个元素

【问题讨论】:

  • "是否是 UB 访问二维数组行尾的一个元素?"当然是最后一排。我看不到前面行的 UB - 它都是连续的空间。
  • 您的 6.5.6 引用清楚地表明这是 UB。指针的出处很重要,而不仅仅是它所代表的地址。
  • 我不知道你在问什么。您的问题已经明确说明了分析器。它显然没有列出任何例外。
  • 据我了解,这是一个悬而未决的问题,尚未得到令人满意的解决,可能会在未来版本的标准中得到澄清。
  • @Olaf-- ptr3_plus_1 是一个有效的指针,它指向数组末尾的一个位置,因此不能被取消引用;但ptr3_plus_1 也是指向数组第一个元素的有效指针,因此应该可以取消引用。我试图调和在我看来明显矛盾的东西。或许答案是ptr3_plus_1根本不能说是指向第二个数组的第一个元素的指针。

标签: c arrays pointers multidimensional-array language-lawyer


【解决方案1】:

所以,如果 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 不适用吗? “同样,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。”
  • @DavidBowling 那是完全不同的东西。这就是暗示 char *s = "Hello"; const char *c; memcpy (&amp;c, &amp;s, sizeof c); 尽管复制到不同的类型是安全的。
  • @PeterJ 重点是将指针传递给指针,以便复制指针值本身。不,我没有复制 1 个字符,sizeof csizeof(const char *),而不是 sizeof(char)
  • 好的;我明白你对 §6.2.5 28 的意思。通常我不需要说服标准认为 UB 实际上是 UB;但是在这里似乎很难保证正确类型的指针ptr3_plus_1 不能像ptr4 一样被取消引用,除非可以说第一个指针不指向一个对象全部。也许这就是你关于不同表示的建议给你的...... IAC,UV 现在。我将在接受之前稍等片刻,看看是否有其他人可以提出解决方案。
  • 请注意,我绝不会寻找一种方法来证明在实际代码中使用这些恶作剧是合理的!只是在周六浪费时间,不必要地思考标准.... ;)
【解决方案2】:

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

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

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

【讨论】:

  • 这样的实现是否允许与整数相互转换?
  • intptr_t 可能具有与指针相同的表示。不明白为什么不允许这样做。顺便说一句,如果我没记错的话,intptr_t 上的算术并不要求与指针算术有任何同构。
  • 好话,虽然那将是一个非常大的整数!
  • @curiousguy 是的,但对于调试实现来说,这并不重要。
  • 这样的实现可能不会定义 intptr_t & co。
猜你喜欢
  • 2019-04-30
  • 1970-01-01
  • 2012-06-01
  • 2020-09-26
  • 1970-01-01
  • 2018-12-11
  • 2021-03-04
  • 1970-01-01
  • 2011-10-24
相关资源
最近更新 更多