零长度数组

57

我正在重构一些旧代码,发现其中有一些包含零长度数组的结构体(如下所示)。当然,可以通过#pragma指令抑制警告,但我尝试创建包含这种结构的“新”结构却失败了(错误2233)。数组'byData'被用作指针,为什么不使用指针呢?或者长度为1的数组?当然,在过程中没有添加任何注释让我感到困扰...

有什么原因要使用这样的东西吗?在重构时有什么建议吗?

struct someData
{
   int nData;
   BYTE byData[0];
}

注意:这是 C++ 语言编写的,运行在 Windows XP 操作系统上,使用的是 VS 2003 开发环境。


4
这是“结构体技巧”,在comp.lang.c FAQ的问题2.6中有描述。丹尼斯·里奇(Dennis Ritchie)称其为“与C实现没有必要的亲密关系”。 C99引入了一种新的语言特性,“灵活数组成员”,以替代结构体技巧。即使是以缺乏C99支持而著称的微软编译器也支持灵活数组成员。 - Keith Thompson
5个回答

38

这是一个 C 语言的技巧。
要创建任意长度的数组:

struct someData* mallocSomeData(int size)
{
    struct someData*  result = (struct someData*)malloc(sizeof(struct someData) + size * sizeof(BYTE));
    if (result)
    {    result->nData = size;
    }
    return result;
}

现在你有一个名为someData的对象,其包含了一个指定长度的数组。


1
这个关于C++的问题,难道不应该至少使用new[]吗? - unwind
2
@unwind:无法使用new进行此操作。整个重点是这是一种C-Hack,而在C++中不需要(因为我们有更好的方法来完成它)。此外,我相当确定在C++中零长度数组是非法的(至少在C++03中是如此,不确定是否在C++11中更新)。 - Martin York
这个的流行术语是“结构体黑客”。 - Krishna Oza
2
除非在一般情况下,你的计算是错误的。根据数组中对象的类型,编译器需要强制执行某些对齐规则,而仅仅将成员的大小相加可能无法产生正确的大小。相反,使用offsetof宏来让编译器计算出正确的结果。(注意:对于BYTE,假设这些被定义为某种char变体,则不是问题。) - IInspectable

26

很不幸,有几个原因会导致你在结构的末尾声明零长度数组。这基本上使您能够从API返回可变长度结构。

Raymond Chen在该主题上撰写了一篇出色的博客文章。我建议您查看此帖子,因为它很可能包含您想要的答案。

请注意,在他的帖子中,处理大小为1的数组而不是0。 他的帖子仍然适用于您的问题。

http://blogs.msdn.com/oldnewthing/archive/2004/08/26/220873.aspx

编辑

注意:即使雷蒙德的帖子说0长度数组在C99中是合法的,但它们实际上仍然不合法。 在这里,您应该使用长度为1的数组。


2
这是因为零长度数组是标准的较新条目。 是指哪些标准呢?C++11仍然不允许使用0长度数组(§8.3.4/1),同样,C99也不允许(§6.7.5.2/1)。 - ildjarn
@ildjarn,我本质上是在复述 Raymond 在他的博客文章末尾所说的话。直到最近在另一个问题的评论讨论中与您交流之前,我并不知道在 C99 中仍然禁止使用长度为 0 的数组。我会更新答案。 - JaredPar
抱歉挑剔这么老的答案。:-P 我只是问一下,因为另一个问题链接到这里作为“证明”,0长度数组是合法的C++。:-] - ildjarn
@ildjarn 在挑剔旧答案时非常注重细节。当然不希望胡乱说出错误的数据 :) - JaredPar
你能找到任何关于使用0长度或[]数组对内存对齐的影响的参考资料吗?在一个项目中,一位同事发现最安全的用法是int arr[](因为int可以保护免受任何对齐问题的影响),但由于我们需要返回该数组,所以在我们的情况下最好的选择是void *arr[],这相当加密。 - Spidey
这是 wayback 存档的博客链接,以防有需要的人(原始页面已经无法访问)。 - iamkroot

24

这是一种旧的 C 语言技巧,用于实现灵活大小的数组。

在 C99 标准中不再需要使用此技巧,因为它支持 arr[] 语法。


3
遗憾的是,当涉及到C99支持时,Visual Studio非常欠缺。 :( - Alex B
7
不考虑你评论的普遍真实性,...MS VC v9编译器支持arr[]语法。 - Cheeso

12

你对于“为什么不使用大小为1的数组”的直觉是正确的。

这段代码实现了“C结构体黑科技”,但是声明长度为0的数组是一种约束违规。这意味着编译器可以在编译时拒绝你的黑科技,并输出一个停止翻译的诊断消息。

如果我们想要进行黑科技,就必须从编译器中逃脱。

实现“C结构体黑科技”的正确方式(与向后兼容至1989年的ANSI C方言以及可能更早的C方言)是使用一个大小正好为1的完全有效的数组:

struct someData
{
   int nData;
   unsigned char byData[1];
}

此外,不再使用 sizeof struct someData 来计算 byData 前面部分的大小,而是使用以下代码来计算:
offsetof(struct someData, byData);

为了在byData中为42字节的struct someData分配空间,我们需要使用以下代码:
struct someData *psd = (struct someData *) malloc(offsetof(struct someData, byData) + 42);

请注意,即使数组大小为零,offsetof的计算仍然是正确的。你看,整个结构体的sizeof可能包含填充字节。例如,如果我们有这样一个结构体:
struct hack {
  unsigned long ul;
  char c;
  char foo[0]; /* assuming our compiler accepts this nonsense */
};
struct hack 的大小可能会因为 ul 成员需要对齐而进行填充。如果 unsigned long 是4字节长的,那么 sizeof(struct hack) 可能是 8,而 offsetof(struct hack, foo) 几乎肯定是5。使用 offsetof 方法是在数组之前获取该结构体前面一部分准确大小的方法。
所以将代码重构为符合经典的、高度可移植的结构体 hack 是最好的做法。
为什么不使用指针呢?因为指针占用额外的空间并且必须被初始化。
还有其他很好的理由不使用指针,即指针需要一个地址空间才能有意义。 结构体 hack 是可外部化的:也就是说,当这样的布局符合外部存储(例如文件、数据包或共享内存)时,不想使用指针,因为它们没有意义。
几年前,我在内核和用户空间之间使用结构体 hack 进行共享内存消息传递接口。我不想在那里使用指针,因为它们只对生成消息的进程的原始地址空间有意义。软件的内核部分使用自己的映射查看该内存,位于不同地址上,因此一切都基于偏移量计算。

即使在C89中,访问数组的第一个元素之外的位置也会导致未定义行为。结构体技巧依赖于编译器自行“定义”此行为。该技巧与向后兼容到1989年的C方言兼容。 - M.M

2

值得指出的是,在我看来,最好的计算大小的方法是在上面链接的 Raymond Chen 文章中使用的方法。

struct foo
{
    size_t count;
    int data[1];
}

size_t foo_size_from_count(size_t count)
{
    return offsetof(foo, data[count]);
}

所需分配的第一个条目的偏移量也是所需分配的大小。我认为这是一种非常优雅的计算大小的方式。无论变量大小数组的元素类型是什么,offsetof(或Windows中的FIELD_OFFSET或UFIELD_OFFSET)总是以相同的方式编写。没有sizeof()表达式会意外出错。


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