是无符号字符 ; 一个 ; 未定义的行为?
来自C标准的未定义行为的示例之一(J.2):
– 数组下标超出范围,即使一个对象显然可以使用给定的下标访问(如左边的表达式a [1] [7],给出声明int a [4] [5])(6.5.6)
如果声明从int a[4][5]
更改为unsigned char a[4][5]
,访问a[1][7]
仍会导致未定义的行为? 我的意见是,它没有,但我从其他人那里听到了不同意见,我想看看其他一些想成为SO专家的想法。
我的推理:
-
根据6.2.6.1第4段和第6.5段第7段的通常解释,对象
a
的表示是sizeof (unsigned char [4][5])*CHAR_BIT
位,可以作为unsigned char [20]
类型的数组访问unsigned char [20]
与物体重叠。 -
a[1]
将unsigned char [5]
作为左值,但在表达式中使用(作为[]
运算符的操作数,或等效地作为*(a[1]+7)
+
运算符的操作数) ,它衰减到unsigned char *
类型的指针。 -
a[1]
值也是指向unsigned char [20]
forms的a
的“表示”的字节的指针。 以这种方式解释,在a[1]
添加7是有效的。
我会在J2中阅读这个“信息性的例子”,作为标准正文所需的暗示:不要依赖于数据索引计算在“表示数组”范围内意外地提供的事实。 目的是确保所有单个数组边界应始终位于定义的范围内。
特别是,如果使用a[1][7]
,这允许实现进行积极的边界检查,并在编译时或运行时向您发吠。
这种推理与底层类型无关。
想要编写符合标准的编译器的编译器供应商必须遵守标准所说的内容,而不是您的推理。 标准表示超出范围的数组下标是未定义的行为, 没有任何exception ,因此允许编译器爆炸。
引用我上次讨论中的评论( C99是否保证数组是连续的? )
“你的原始问题是a[0][6]
,声明为char a[5][5]
。这是UB,无论如何。使用char *p = &a[3][4];
是有效的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运算符或一元&运算符的操作数,或者是用于初始化数组的字符串文字,否则将类型为”数组’的数组的表达式转换为表达式类型为”指向类型’的指针,指向数组对象的初始元素,而不是左值 。 如果数组对象具有寄存器存储类,则行为未定义。
所以我认为这是有效的:
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,数组下标超出范围。 真正发生的事情应该取决于编译器如何在多维数组中实际实现数组索引。 所以你可能是对的,它对每个已知的实现没有任何影响。 但这也是一种有效的未定义行为。 ;)
从6.5.6 / 8
如果指针操作数和结果都指向同一个数组对象的元素 ,或者指向数组对象 的最后一个元素 ,则评估不应产生溢出; 否则,行为未定义。
在您的示例中,[1] [7]既不指向相同的数组对象a [1],也指向一个超过[1]的最后一个元素的数组,因此它是未定义的行为。
在引擎盖下,在实际的机器语言中,对于int a[4][5]
的定义, a[1][7]
和a[2][2]
之间没有区别。 正如R ..所说,这是因为数组访问被转换为1 * sizeof(a[0]) + 7 = 12
和2 * sizeof(a[0]) + 2 = 12
( * sizeof(int)
当然)。 机器语言对数组,矩阵或索引一无所知。 它只知道地址。 上面的C编译器可以做任何你喜欢的事情,包括基于索引器的天真边界检查 – a[1][7]
然后会超出限制因为数组a[1]
没有8个单元格。 在这方面, int
和char
或unsigned char
之间没有区别。
我的猜测是区别在于int
和char
之间的严格别名规则 – 即使程序员实际上没有做错任何事情,编译器也被迫为数组执行“逻辑”类型转换,它不应该做。 正如Jens Gustedt所说,它看起来更像是一种启用严格边界检查的方法,而不是int
或char
的真正问题。
我已经做了一些摆弄VC ++编译器,它似乎表现得像你期望的那样。 任何人都可以用gcc
测试吗? 根据我的经验, gcc
对这类事情要严格得多。
我认为引用(J.2)样本未定义行为的原因是链接器不需要将子数组[1],a [2]等放在内存中彼此相邻。 它们可以分散在内存中,也可以相邻但不按预期顺序排列。 将基类型从int切换到unsigned char不会改变这一点。