背景
群里小伙伴 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) ,其语法和语义略有不同:
- 灵活数组成员写成
contents[]
没有 0 。 - 灵活数组成员的类型不完整,因此可能无法应用
sizeof
运算符。作为零长数组的原始实现的特例,sizeof
的计算结果为零。 - 灵活数组成员只能作为非空结构体的最后一个成员。
- 包含灵活数组成员的结构体或包含此类结构的 union (可能是递归的)不能是结构体成员或数组元素。(但是, GCC 允许这些用途作为扩展。)
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.