【问题标题】:Is it well-defined to access a partially-assigned array, past the assigned part?在分配的部分之后访问部分分配的数组是否定义明确?
【发布时间】:2018-05-04 11:33:30
【问题描述】:

假设我有这个代码:

void foo() {
  char s[10];
  char v1 = s[0]; // UB
  char v2 = s[10]; // also UB
}

void bar() {
  char s[10];
  strcpy(s, "foo");
  char v3 = s[3]; // v3 is zero
  char v4 = s[0]; // v4 is 'f'
  char v5 = s[4]; // What?
}

由于s[0]s[3] 的地址在strcpy 中被访问,并且s[0] 到s[9] 在连续内存中,我想整个数组应该包含一些值(包括不确定)。

关于v5的操作是否定义明确?或者v5 只是一个不确定的值(不会触发任何 UB)?

如果数组是 int 类型并且仍然部分赋值怎么办?

【问题讨论】:

  • @Lanting:C 问题不会重复 C++ 问题。不同语言的规则不同,答案必须针对每种语言,除非是专门询问两种语言的共同点。
  • @Stargateur:我相信字符类型可能有陷阱表示,unsigned char 除外。但是,C 2011 段落说读取具有陷阱表示的对象是未定义的行为,6.2.6.1 5,特别排除了字符类型。因此,读取具有陷阱表示的字符并不是未定义的。
  • @iBug: (a) C 标准中对字符类型的处理方式不同。如果您使用int 的数组,这个问题的答案会有所不同。 (b) 你的前提是有缺陷的。 char v1 = s[0]; 没有未定义的行为。
  • 如果可以使用register 存储类声明访问未初始化的对象,则它是未定义的。问题是,这如何转化为数组?似乎数组可以用register 声明,但 J2 附录说将寄存器分类数组转换为其第一个成员是 UB,但这种转换是定义索引的方式,所以看起来寄存器声明的数组是 un-可索引,但由于您正在索引,因此无法将数组声明为寄存器,因此取消引用应产生未指定的值但不会导致 UB。不过,这很奇怪。

标签: c arrays language-lawyer undefined-behavior


【解决方案1】:

它不能是未定义的,因为那里的 char 可能有一个陷阱表示,因为6.2.6.1p5 说访问任何具有字符类型的东西都是明确定义的。

可能因为6.3.2.1p2而未定义

一个左值,指定一个自动存储持续时间的对象, 本来可以用寄存器声明的存储类用于 需要指定对象的值的上下文,但是 对象未初始化。

所以问题是,数组可以用寄存器存储类声明吗?

答案是,它不可能有,因为你正在索引它。索引是根据6.5.2.1p2定义的

(

后缀表达式后跟方括号 [] 中的表达式 是数组对象元素的下标名称。这 下标运算符 [] 的定义是 E1[E2] 等同于 (*((E1)+(E2)))。由于适用于 二进制 + 运算符,如果 E1 是一个数组对象(相当于一个指针 到数组对象的初始元素)并且 E2 是一个整数, E1[E2] 表示 E1 的第 E2 个元素(从零开始计数)。 )

就数组转换到其第一个元素的地址而言,但对于寄存器分类的数组,根据项目符号点,这种转换将是未定义的:

具有数组类型的左值转换为指向初始值的指针 数组元素,且数组对象有寄存器存储类 (6.3.2.1)。

在附录J.2 Undefined behavior,这意味着该数组不能被声明为register

Footnote 121 in 6.7.1 Storage class specifiers 进一步阐述了这一点:

用 storage-class 声明的对象的任何部分的地址 说明符寄存器不能显式计算(通过使用 6.5.3.2 中讨论的一元 & 运算符)或隐式(通过 将数组名称转换为指针,如 6.3.2.1 中所述)。因此, 唯一可以应用于声明的数组的运算符 存储类说明符寄存器是 sizeof 和 _Alignof

(换句话说,虽然该语言允许寄存器数组,但它们本质上是不可用的)。

因此,代码如下:

char unspecified(void){ char s[1]; return s[0]; }

将返回一个未指定的值,但不会使您的程序的行为未定义。

【讨论】:

  • 这根本没有解决strcpy 问题... OP 似乎认为在第一种情况下v[0] 和数组的字节之间可能存在差异在第二种情况下字符串的结尾,所以你应该涵盖这两种情况是否实际上相同
【解决方案2】:

标准的作者认为没有必要明确描述迄今为止每个编译器一直以相同方式处理的极端情况,并且他们认为如果设计者不这样做,任何实现的行为可能会有所不同。 t 故意迟钝。涉及部分写入聚合的场景属于此类。

数组下标的行为被定义为获取数组的地址,对结果指针执行算术运算,然后访问结果地址。就我个人而言,我认为它应该被定义为一种单独的操作,其极端情况与显式获取数组地址、执行指针运算和转换结果略有不同,但标准根据这些步骤定义操作。因此,不是故意钝化的编译器应该将使用下标运算符访问的数组视为获取地址的对象,因此无论是否已写入都可以访问该对象。但是,这仍然对此类代码的行为留下了悬而未决的问题。

假设“unsigned char”是 8 位,“unsigned”是 24 位或更多,下面会返回什么值:

unsigned test1(unsigned char *p)
{
  unsigned x=p[0];
  unsigned y=p[0];
  unsigned z=y;
  return x | (y << 8) | (z << 16);
}
unsigned test(void)
{
  unsigned char foo[1];
  return test1(foo); // Note that this takes the address of 'foo'.
}

就我个人而言,我怀疑要求为test1 生成的代码必须表现得好像xyz 都在0..255 范围内保持相同的值会有什么真正的缺点,或者——绝对最小值——表现得好像 yz 保持相同的值。我不认为标准的作者会期望任何非钝化的实现都不会那样做,但标准实际上并不要求它,而且有些人似乎认为要求这种行为会过度限制优化.

【讨论】:

    【解决方案3】:

    是的,这是未定义的行为。

    部分分配的数组是一个包含已初始化和未初始化内存区域的数组。读取未初始化的内存区域是未定义的行为,就像读取任何其他未初始化的内存区域一样。

    【讨论】:

    • 读取数组的未初始化元素会导致不确定的值,而不是未定义的行为。
    猜你喜欢
    • 1970-01-01
    • 2021-12-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-27
    • 2010-10-25
    • 2022-11-01
    相关资源
    最近更新 更多