Zero Length Array in C

2022/09/05

背景

群里小伙伴 JW 在学习 CMU 的数据库公开课,抛出个问题:这行代码为啥定义一个容量为 0 的数组?

class HashTableBlockPage {
  // ...
  private:
    MappingType array_[0];
}

咱 C++ 用得不多,也忘得差不多了,不过这写法确实奇怪,做 Win 桌面开发的小伙伴 ZH 直接说:“这样写会告警的,可能就是写错了”,不了了之。

不久后,ZH 同学看文档时,给编译警告找到了答案

这个 zero-sized array in struct/union 在微软看来是“非标准”扩展,我决定弄明白这么写有什么渊源。

这样做有什么优势?

翻到 Quora 的一个高赞回答

他首先给出适用场景:

Zero-length arrays, also known as flexible arrays, are used to implement variable-length arrays, primarily in structures.

零长度数组,也称为灵活数组,用于实现可变长度数组,主要用在结构体中。

答主,给我们举了一个 Linux 内核中的例子

struct inotify_event {
	__s32		wd;		    /* watch descriptor */
	__u32		mask;		/* watch mask */
	__u32		cookie;		/* cookie to synchronize two events */
	__u32		len;		/* length (including nulls) of name */
	char		name[0];	/* stub for possible name */
};

这个结构体表示一个 inotify event ,它是一个动作,例如 “被写入(was written to)” 其目标文件名如 /home/rlove/wolf.txt

那么你可能有个疑问:结构体中的 name 需要多大长度呢?

答案是,文件名可以是任意长度。如果我仅仅需要返回文件名,只要简单返回一个动态申请 (dynamicallly-allocated) 的 char * 类型值。但是我们不得不返回这整个结构体。 此外,如果是实现一个系统调用,就无法在结构体内部分配指针,因为它们不会指向用户地址空间。而使用零长数组就是完美的解决方案。

inotify 的用户,通过在 inotify 文件描述符上调用 read() 来获取这个结构体。这个结构体的长度实际上是可变的。用户要读取 len 参数以查找 name 的大小,从而找到刚刚读取的结构体的总大小。

小结

系统调用时返回的结构体内部分配的指针无法指向用户地址空间。

GNU C 文档

https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html

Declaring zero-length arrays is allowed in GNU C as an extension. A zero-length array can be useful as the last element of a structure that is really a header for a variable-length object

在 GNU C 中允许将零长数组声明为扩展。零长数字可用做结构体的最后一个元素,它实际上是可变长对象的标头。

struct line {
  int length;
  char contents[0];
};

struct line *thisline = (struct line *)
  malloc (sizeof (struct line) + this_length);
thisline->length = this_length;

Although the size of a zero-length array is zero, an array member of this kind may increase the size of the enclosing type as a result of tail padding. The offset of a zero-length array member from the beginning of the enclosing structure is the same as the offset of an array with one or more elements of the same type. The alignment of a zero-length array is the same as the alignment of its elements.

尽管零长数组的大小是 0 ,但是由于尾部填充,这种数组成员可能会增加封闭类型 (enclosing type) 的大小。 零长数组成员与封闭结构开头的偏移量,与具有一个或多个相同类型元素的数组的偏移量相同。 零长数组的对齐方式与其元素的对齐方式相同。

因为缺少零长数组的扩展,在 ISO C90 中,上面示例中的 contents 数组通常会被声明为一个单元素 (single element)。 与零长数组不同,单元素只对封闭结构的大小起对齐作用,单元素数组 (one-element array) 始终占用至少与该类型的单个对象一样多的空间。 尽管不鼓励以这种方式使用单元素数组,但是 GCC 处理尾部单元素数组成员的访问类似于零长数组。

声明可变长度类型(如上面的 line 结构体)的首选机制是 ISO C99 中的灵活数组成员 (flexible array member) ,其语法和语义略有不同:

struct foo { int x; int y[]; };
struct bar { struct foo z; };

struct foo a = { 1, { 2, 3, 4 } };         // Valid.
struct bar b = { { 1, { 2, 3, 4 } } };     // Invalid.
struct bar c = { { 1, { } } };             // Valid.
struct foo d[1] = { { 1, { 2, 3, 4 } } };  // Invalid.

参考