9

C 标准中未定义行为的示例之一是 (J.2):

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

如果声明从int a[4][5]to更改unsigned char a[4][5],访问是否a[1][7]仍会导致未定义的行为?我的观点是它没有,但我从其他人那里听到了不同意的意见,我想看看其他一些可能成为 SO 专家的想法。

我的推理:

  • 按照对 6.2.6.1 第 4 段和 6.5 第 7 段的通常解释,对象的表示asizeof (unsigned char [4][5])*CHAR_BIT位,并且可以作为unsigned char [20]与对象重叠的类型数组来访问。

  • a[1]具有unsigned char [5]作为左值的类型,但在表达式中使用(作为运算符的操作数[],或等效地作为运算+符 in的操作数*(a[1]+7)),它衰减为类型的指针unsigned char *

  • 的值a[1]也是一个指向“表示”a形式的字节的指针unsigned char [20]。这样解释,加7a[1]有效。

4

5 回答 5

4

想要编写符合标准的编译器的编译器供应商必须遵守标准的规定,而不是您的推理。该标准说,超出范围的数组下标是未定义的行为,没有任何异常,因此允许编译器崩溃。

引用我们上次讨论中的评论(C99 是否保证数组是连续的?

“您最初的问题是针对a[0][6],声明char a[5][5]。无论如何,这是 UB。使用char *p = &a[3][4];和访问p[0].是有效的。获取p[5]地址&p[6]仍然有效,但访问p[6]是在对象之外,因此是 UB。访问a[0][6]是在对象之外对象a[0],它的类型为数组 [5] 的字符。结果的类型无关紧要,重要的是你如何达到它。”

编辑:

有足够多的未定义行为案例,您必须扫描整个标准,收集事实并将它们结合起来,最终得出未定义行为的结论。这是明确的,您甚至在问题中引用了标准中的句子。它是明确的,没有任何解决方法的空间。

我只是想知道您希望我们在推理方面更加明确,以确信它确实是 UB?

编辑2:

在挖掘标准并收集信息之后,这里是另一个相关的引用:

6.3.2.1 - 3:除非它是 sizeof 运算符或一元 & 运算符的操作数,或者是用于初始化数组的字符串字面量,否则类型为 ''array of type'' 的表达式将转换为表达式类型 ''pointer to type'' 指向数组对象的初始元素并且不是左值。如果数组对象具有寄存器存储类,则行为未定义。

所以我认为这是有效的:

unsigned char *p = a[1]; 
unsigned char c = p[7]; // Strict aliasing not applied for char types

这是UB:

unsigned char c = a[1][7];

因为a[1]此时不是左值,而是进一步评估,违反了 J.2,数组下标超出范围。真正发生的事情应该取决于编译器如何在多维数组中实际实现数组索引。所以你可能是对的,它对每个已知的实现都没有任何影响。但这也是一个有效的未定义行为。;)

于 2010-09-22T11:26:52.957 回答
4

我会阅读 J2 中的这个“信息性示例”作为标准主体想要的提示:不要依赖这样一个事实,即数组索引计算意外地在“表示数组”范围内给出了一些东西。目的是确保所有单独的数组边界应始终在定义的范围内。

特别是,这允许实现进行积极的边界检查,如果您使用a[1][7].

这种推理与底层类型无关。

于 2010-09-22T06:41:48.850 回答
1

从 6.5.6/8

如果指针操作数和结果都指向 同一个数组对象的元素,或者 超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为未定义。

在您的示例中,a[1][7] 既不指向同一个数组对象 a[1],也不指向 a[1] 的最后一个元素,因此它是未定义的行为。

于 2010-09-22T03:47:27.503 回答
0

a[1][7]实际上,在实际的机器语言中,和a[2][2]for 的定义没有区别int a[4][5]。正如 R.. 所说,这是因为数组访问被转换为1 * sizeof(a[0]) + 7 = 122 * sizeof(a[0]) + 2 = 12* sizeof(int)当然)。机器语言对数组、矩阵或索引一无所知。它只知道地址。上面的 C 编译器可以做任何它喜欢的事情,包括基于索引器的简单边界检查——a[1][7]然后会超出范围,因为数组a[1]没有 8 个单元格。int在这方面, an和char或之间没有区别unsigned char

我的猜测是,区别在于int和之间的严格别名规则char——即使程序员实际上没有做错任何事情,编译器也被迫为数组做一个它不应该做的“逻辑”类型转换。正如 Jens Gustedt 所说,它看起来更像是一种启用严格边界检查的方法,而不是intor的真正问题char

我已经对 VC++ 编译器做了一些摆弄,它的行为似乎与您预期的一样。任何人都可以测试这个gcc吗?根据我的经验gcc,这类事情要严格得多。

于 2010-09-22T08:16:50.797 回答
-1

我相信引用的 (J.2) 示例是未定义行为的原因是链接器不需要将子数组 a[1]、a[2] 等在内存中彼此相邻放置。它们可能分散在内存中,也可能相邻但不是按预期顺序排列。将基本类型从 int 切换为 unsigned char 不会改变这一切。

于 2010-09-22T03:23:04.760 回答