【问题标题】:Is it undefined behaviour to access an array beyond its end, if that area is allocated? [duplicate]如果分配了该区域,则访问超出其末尾的数组是否是未定义的行为? [复制]
【发布时间】:2012-09-10 15:03:08
【问题描述】:

可能重复:
Is the “struct hack” technically undefined behavior?

通常在 C 中访问超出其末尾的数组是未定义的行为。例如:

int foo[1];
foo[5] = 1; //Undefined behavior

如果我知道数组末尾之后的内存区域已被分配,使用 malloc 还是在堆栈上,它仍然是未定义的行为吗?这是一个例子:

#include <stdio.h>
#include <stdlib.h>

typedef struct
{
  int len;
  int data[1];
} MyStruct;

int main(void)
{
  MyStruct *foo = malloc(sizeof(MyStruct) + sizeof(int) * 10);
  foo->data[5] = 1;
}

我已经在几个地方看到过这个模式用于制作可变长度的结构,并且它似乎在实践中有效。是技术上未定义的行为吗?

【问题讨论】:

标签: c undefined-behavior


【解决方案1】:

您所描述的内容被亲切地称为"the struct hack"。目前尚不清楚它是否完全没问题,但它曾经并且被广泛使用。

最近 (C99),它已开始被“灵活数组成员”取代,如果它是结构。

【讨论】:

  • 我知道如果你有一个像{ double; char[1]; } 这样的结构,非?
  • 你知道有哪些环境(编译器、平台、运行时库)不明确支持,甚至破坏它吗?
  • @KerrekSB 我不确定我是否理解为什么会出现问题?
  • @KerrekSB,在 C99 或 C11 中不允许使用零大小的数组。允许的是 cnicutar 所描述的:灵活的数组成员。它们的大小不是 0,而是不确定的大小。
  • @delnan:令人担忧的是,尝试对数组访问进行严格边界检查的 C 实现可能会破坏它。但是,我认为这种担心是错误的,至少当数组具有字符类型时,因为在访问超过数组大小时所涉及的指针算术作为 object 表示 数组上的指针算术是有效的(一个unsigned charmalloc 获得的整个对象叠加)。因此,我认为在符合 C 标准的同时破坏代码是不可能的。
【解决方案2】:

6.5.6 加法运算符下:

语义

8 - [...] 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,使得结果和原始数组元素等于整数表达式。 [...] 如果结果指向数组对象的最后一个元素,则不应将其用作计算的一元 * 运算符的操作数。

如果内存是malloc分配的那么:

7.22.3 内存管理函数

1 - [...] 如果分配成功,则返回的指针经过适当对齐,以便可以将其分配给指向具有基本对齐要求的任何类型对象的指针,然后用于访问此类对象或数组分配的空间中的此类对象的数量(直到显式释放空间)。已分配对象的生命周期从分配一直延续到解除分配。

然而,这并不支持在没有适当转换的情况下使用此类内存,因此对于上面定义的MyStruct,只能使用对象的声明成员。这就是添加灵活数组成员 (6.7.2.1:18) 的原因。

另请注意,附录 J.2 未定义行为调用了数组访问:

1 - 在以下情况下行为未定义:[...]
— 将指针加或减到或刚好超出数组对象和 整数类型产生的结果不指向或仅超出同一数组 对象。
— 将指针加或减到或刚好超出数组对象和 整数类型产生的结果刚好超出数组对象,并用作 被评估的一元 * 运算符的操作数。
— 数组下标超出范围,即使对象显然可以使用 给定下标(如左值表达式 a[1][7] 给定声明 int a[4][5])

所以,正如您所注意到的,这将是未定义的行为:

  MyStruct *foo = malloc(sizeof(MyStruct) + sizeof(int) * 10);
  foo->data[5] = 1;

但是,您被允许执行以下操作:

  MyStruct *foo = malloc(sizeof(MyStruct) + sizeof(int) * 10);
  ((int *) foo)[(offsetof(MyStruct, data) / sizeof(int)) + 5] = 1;

C++ 在这方面比较宽松; 3.9.2 复合类型[basic.compound]有:

3 - [...] 如果T 类型的对象位于地址A,则表示cv T* 类型的指针,其值为地址A 指向该对象,不管值是如何获得的。

考虑到 C 对指针的更积极的优化机会,这是有道理的,例如使用 restrict 限定符。

【讨论】:

    【解决方案3】:

    C99 基本原理文档在第 6.7.2.1 节中讨论了这一点。

    C99 的一个新特性: 有一个常见的习语称为“struct hack”,用于创建包含可变大小数组的结构:

    ...

    这种结构的有效性一直值得怀疑。在对一份缺陷报告的回复中,委员会决定这是未定义的行为,因为数组p-&gt;items 只包含一项,而不管空间是否存在。另一种结构是 建议:使数组大小大于可能的最大情况(例如,使用int items[INT_MAX];),但由于其他原因,这种方法也未定义。

    委员会认为,尽管没有办法在 C89 中实现“struct hack”,但它仍然是一个有用的工具。因此引入了“灵活数组成员”的新特性。除了空括号和删除malloc 调用中的“-1”, this 的使用方式与 struct hack 相同,但现在是明确有效的代码。

    struct hack 是未定义的行为,不仅支持 C 规范本身(我确信其他答案中有引用),而且委员会甚至记录了它的意见。

    所以答案是是的,根据标准文档,这是未定义的行为,但根据de facto C 标准已经很好地定义了。我想大多数编译器作者都非常熟悉 hack。来自 GCC 的tree-vrp.c

       /* Accesses after the end of arrays of size 0 (gcc
          extension) and 1 are likely intentional ("struct
          hack").  */
    

    我认为你很有可能在编译器测试套件中找到 struct hack。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-09-26
      • 2014-07-13
      • 2013-11-20
      • 1970-01-01
      • 1970-01-01
      • 2017-12-12
      • 2013-04-16
      • 1970-01-01
      相关资源
      最近更新 更多