如果访问已分配的数组的末尾之外的区域,那么这是否属于未定义行为?

5

可能是重复问题:
“结构体黑客”技术上是否未定义行为?

通常在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;
}

我已经在几个地方看到过这种模式用来创建变长的结构体,而且实际上似乎也可以工作。这是否属于技术上未定义的行为?


1
Raymond Chen在Windows中有一篇关于这个模式的文章,标题为_为什么有些结构以大小为1的数组结尾?_。 - Tor Klingberg
3个回答

6
你所描述的被亲切地称为"结构体黑科技"。目前尚不清楚它是否完全可行,但它曾经广泛使用,并且现在已经开始被"灵活数组成员"所取代,如果它是结构体中的最后一个字段,则可以放置一个int data[];字段。

你知道有哪些编译器、平台或运行库不支持甚至破坏这个功能吗? - user395760
@KerrekSB 我不确定我理解为什么会有问题? - cnicutar
@delnan,它至少在gcc和cl上运行。我已经看到它在内核方面被广泛使用。 - cnicutar
1
@KerrekSB,C99或C11中不允许使用大小为零的数组。允许的是cnicutar所描述的:灵活的数组成员。它们的大小不是0,而是未指定的大小。 - Jens Gustedt
1
@delnan: 关注点在于,尝试对数组访问进行严格的边界检查的C实现可能会破坏它。然而,我认为这种担忧是错误的,至少当数组具有字符类型时是如此,因为涉及到超出数组大小的访问的指针算术在对象表示数组上是有效的(一个由malloc获得的整个对象重叠在一起的unsigned char数组)。因此,我认为在符合C标准的情况下不可能破坏代码。 - R.. GitHub STOP HELPING ICE
显示剩余4条评论

4
在“6.5.6加法运算符”下:
语义
8- [...]如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素相偏移的元素,使得所得到的和原始数组元素的下标之差等于整数表达式。[...]如果结果指向数组对象的最后一个元素之一,则不得将其用作评估的一元*运算符的操作数。
如果内存是由malloc分配的,则:
7.22.3内存管理函数 1- [...]如果分配成功,则返回的指针适当地对齐,以便可以将其分配给具有基本对齐要求的任何类型的对象的指针,然后用于访问在分配的空间中分配的此类对象或此类对象的数组(直到显式释放空间)。分配的对象的生命周期从分配到解除分配为止。
但是,这并不容忍在没有适当转换的情况下使用此类内存,因此对于如上定义的MyStruct,只能使用对象的声明成员。这就是为什么添加了“灵活数组成员”(6.7.2.1:18)的原因。
还要注意,附录J.2未定义的行为调用数组访问:
1- 在以下情况下行为未定义:[...] —将指针加减到数组对象中或刚好超出数组对象,并且整数类型产生的结果指向不在同一数组对象中或刚好超出该数组对象。 —将指针加减到数组对象中或刚好超出数组对象,并且整数类型产生的结果指向刚好超出数组对象,并用作评估的一元*运算符的操作数。 —数组下标超出范围,即使使用给定下标似乎可以访问对象(如在lvalue表达式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->items 只包含一个元素,而不考虑空间是否存在。建议使用另外一种写法:使数组大小大于最大可能情况(例如使用 int items[INT_MAX]),但由于其他原因,这种方法也是未定义的。 尽管无法在 C89 中实现“struct hack”,但委员会认为它仍然是一种有用的工具。因此,引入了“柔性数组成员”的新特性。除了空括号和 malloc 调用中“-1”的删除,它与 struct hack 的使用方式相同,但现在明确为有效代码。
引用原文: "Struct hack" 是未定义行为,这不仅得到了 C 规范本身的支持(我相信在其他答案中有引用),而且委员会甚至记录了他们的观点。
根据标准文件,答案是是,它是未定义行为,但根据事实上的 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。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接