如果我在C/C++中定义了一个0大小的数组会发生什么?

153

好奇问一下,如果我在代码中定义一个长度为零的数组int array[0];,会发生什么?GCC没有任何抱怨。

示例程序

#include <stdio.h>

int main() {
    int arr[0];
    return 0;
}

澄清

我实际上正试图弄清楚以这种方式初始化的零长度数组是否被优化掉,而不是像Darhazer评论中的可变长度那样指向它们。

这是因为我必须将一些代码发布到公众之中,所以我正在努力确定是否必须处理SIZE 定义为0的情况,在某些具有静态定义的int array[SIZE];的代码中会发生这种情况。

我实际上很惊讶GCC没有抱怨,这引发了我的问题。从我得到的答案来看,缺少警告在很大程度上是由于支持未使用新[]语法更新的旧代码。

因为我主要是在想错误问题,所以我将Lundin的回答标记为正确答案(Nawaz的回答最先,但不是很完整)--其他人指出它在尾部填充结构的实际用途,虽然相关,但不完全是我寻找的内容。


55
很不幸,在C++中,由于未定义行为、非标准扩展和其他异常情况,尝试自己实践通常并不能带来知识之路。 - Benjamin Lindley
7
@JustinKirk 我也曾经因为测试并且看到它起作用而被困住了。但是由于我在帖子中收到的批评,我知道了测试和看到它起作用并不意味着它是有效和合法的。因此有时自测是无效的。 - StormByte
3
@JustinKirk,参考Matthieu的答案,里面有一个使用它的示例。在数组大小是模板参数的情况下,它也可能会很有用。问题中的示例显然是缺乏上下文的。 - Mark Ransom
3
@JustinKirk:在Python中[]或者在C中""的作用是什么?有时候,你需要一个函数或者宏来要求一个数组,但是你没有任何数据可以放进去。 - dan04
19
什么是“C/C++”?这是两种不同的编程语言。 - Lightness Races in Orbit
显示剩余9条评论
8个回答

111

一个数组不能有零大小。

ISO 9899:2011 6.7.6.2:

如果表达式是常量表达式,它的值必须大于零。

上述文字对于普通数组(第1段)和可变长度数组(VLA)都是正确的。如果表达式的值小于或等于零,则 VLA 的行为是未定义的(第5段)。这是 C 标准中的规范文本。编译器不允许以不同方式实现此规范。

gcc -std=c99 -pedantic 对于非 VLA 情况会发出警告。


37
“它必须实际上给出一个错误”-“警告”和“错误”的区别在标准中并未被认可(只提到“诊断”),唯一必须停止编译的情况是遇到“#error”指令,这是警告和错误之间的真实世界差异。 - Random832
12
一般来说, (C 或 C++) 标准只规定编译器必须 "允许" 什么,而不是必须 "禁止" 什么。在某些情况下,它们会规定编译器应该发出一个 "诊断",但这就是它们的具体规定了。其余的则由编译器供应商自行处理。附注:也请参考 Random832 所说的内容。 - mcmcc
10
“编译器不允许生成包含零长度数组的二进制文件。”标准绝对没有这样的说法。它只说当源代码包含一个大小为零的常量表达式的数组时,必须至少生成一条诊断信息。只有在遇到“#error”预处理器指令时,标准才禁止编译器生成二进制文件。请注意,翻译后的内容没有解释或添加其他信息。 - Random832
6
为所有正确的情况生成一个二进制文件可以满足第一点要求,对于错误的情况生成或不生成一个都不会影响它。对于第三点来说,打印一个警告就足够了。这种行为与第二点无关,因为标准没有定义此源代码的行为。 - Random832
15
你的陈述是错误的;符合标准的编译器确实可以生成包含零长度数组的二进制文件,只要发出警告即可。 - Keith Thompson
显示剩余16条评论

105

根据标准,这是不允许的。

但是在C编译器中,把这些声明视为 灵活数组成员(FAM 声明已经成为了当前的实践:

C99 6.7.2.1, §16: 作为一种特殊情况,具有多个命名成员的结构的最后一个元素可以具有不完整的数组类型;这被称为灵活数组成员。

FAM 的标准语法是:

struct Array {
  size_t size;
  int content[];
};

这个想法是你会这样分配它:

void foo(size_t x) {
  Array* array = malloc(sizeof(size_t) + x * sizeof(int));

  array->size = x;
  for (size_t i = 0; i != x; ++i) {
    array->content[i] = 0;
  }
}

你还可以静态地使用它(gcc扩展):

Array a = { 3, { 1, 2, 3 } };

这也被称为尾填充结构(此术语早于C99标准的发表)或者结构体hack(感谢Joe Wreschnig指出)。

然而,在C99中,这种语法被规范化了(并且效果得到了保证)。在此之前,需要一个常量大小。

  • 1是可移植的方法,但相当奇怪。
  • 0更好地表示意图,但在标准上不合法,并且由一些编译器(包括gcc)支持为扩展。

然而,尾部填充的做法依赖于存储空间的可用性(小心使用malloc),因此不适合通常栈的使用。


@MatthieuM。也许您能解释一下为什么所有这些都是这样工作的?请参见http://stackoverflow.com/q/28306424/1286628(特别是粗体字中的问题)(是的,这是我自己的问题帖子的无耻宣传)。 - wkschwartz
1
使用大小为1的数组来进行结构体hack可以避免编译器发出警告,但这只是“可移植”的,因为编译器编写者足够好心承认了这种用法作为事实上的标准。如果不是禁止零大小数组的规定,程序员随后使用单元素数组作为糟糕的替代品,以及编译器编写者的历史态度,即它们应该在不需要符合标准的情况下满足程序员的需求,编译器编写者本可以轻松而有用地将foo[x]优化为foo[0],只要foo是单元素数组即可。 - supercat
@supercat:好评论,我从未想过从标准的严格阅读中可以得出哪些潜在的优化。更有趣的是,编译器可以假定 x 必须为 0,或者如果已知 x 不是 0,则不会执行此路径(因为这将是未定义行为)。 - Matthieu M.
@MatthieuM.:UB的超现代哲学直到结构体hack被弃用后才开始流行,因此我认为这些优化不会成为问题。另一方面,在许多实际情况下(例如,一个将数据项n存储在foo[n>>8][n & 255]中并具有宏定义大小限制的程序,该限制在许多构建中可能小于255),用foo[x]替换为foo[0]将是容易且有用的。某些处理器上的实现也可能针对数组具有上限为两个元素、128字节或256字节的情况进行优化。 - supercat
1
@RobertSsupportsMonicaCellio:正如答案中明确显示的那样,它在“结尾”处。我也提前加载了解释,以便从一开始就更清晰明了。 - Matthieu M.
显示剩余12条评论

59

在标准的C和C++中,不允许使用零大小数组。

如果您正在使用GCC,请使用-pedantic选项进行编译。它将会发出警告,并显示:

zero.c:3:6: warning: ISO C forbids zero-size array 'a' [-pedantic]

对于C ++,它也会发出类似的警告。


11
在 Visual C++ 2010 中:error C2466: 无法分配大小为 0 的常量大小数组 - Mark Ransom
4
-Werror 将所有警告转化为错误,但这并不能修复 GCC 编译器的不正确行为。 - Lundin
C++ Builder 2009同样正确地给出了一个错误:[BCC32 Error] test.c(3): E2021 数组必须至少有一个元素 - Lundin
1
你可以使用-pedantic-errors代替-pedantic -Werror - Stephan Dollberg
3
一个大小为零的数组并不完全等同于一个大小为零的std::array。(另外:我记得但找不到来源,可变长数组曾被考虑过并明确被拒绝作为C++的一部分。) - user79758
显示剩余2条评论

29

这完全是不合法的,一直以来都是如此,但很多编译器忽略了错误信号。我不确定你为什么要这样做。我知道的一个用途是从布尔值中触发编译时错误:

char someCondition[ condition ];

如果condition是假的,则会在编译时出现错误。尽管编译器允许这种情况,但我已经开始使用:

char someCondition[ 2 * condition - 1 ];

这会给出1或-1的大小,我从未找到过接受大小为-1的编译器。


这是一个有趣的黑客技巧。 - Alex Koay
11
这是元编程中常用的技巧,我认为如果使用STATIC_ASSERT的实现也采用了这种方法,我不会感到惊讶。 - James Kanze
为什么不直接使用以下代码:#if 某个条件 #error 错误信息 #endif - Jerfov2
1
@Jerfov2 因为条件可能不会在预处理时被知晓,只有在编译时才能确定。 - rmeador

11

零长度数组的另一种用途是用于制作可变长度对象(C99之前)。零长度数组带有[]但没有0的可变长数组不同。

引自gcc文档

零长度数组在GNU C中是被允许的。它们非常有用,可以作为一个结构的最后一个元素,这个结构实际上是可变长度对象的头部:

 struct line {
   int length;
   char contents[0];
 };
 
 struct line *thisline = (struct line *)
   malloc (sizeof (struct line) + this_length);
 thisline->length = this_length;

在 ISO C99 中,您可以使用灵活数组成员,语法和语义略有不同:

  • 灵活数组成员写为 contents[] 而不是 0。
  • 灵活数组成员具有不完整类型,因此不能应用 sizeof 运算符。

一个实际的例子是 kdbus.h(Linux 内核模块)中的“struct kdbus_item”的零长度数组。


2
在我看来,标准没有禁止零长度数组的好理由;它可以将零大小的对象作为结构体成员,并将它们视为 void* 以进行算术运算(因此,禁止添加或减去指向零大小对象的指针)。虽然柔性数组成员大多比零大小数组更好,但它们也可以充当一种“联合”,以别名方式而不会在后续内容中添加额外的“语法”间接层次(例如,给定 struct foo {unsigned char as_bytes[0]; int x,y; float z;},可以访问成员 x..z... - supercat
可以直接使用 myStruct.asFoo.x 等方式来访问,而不必多说。此外,如果我没记错的话,在 C 语言中,任何试图在结构体中包含一个灵活数组成员的尝试都会被拒绝,因此无法创建一个包含多个其他已知长度的灵活数组成员的结构体。 - supercat
@supercat 一个很好的理由是为了维护访问数组边界外的规则完整性。作为结构体的最后一个成员,C99的柔性数组成员实现了与GCC零大小数组完全相同的效果,但不需要向其他规则添加特殊情况。在我看来,这是一种改进,因为在ISO C中,sizeof x->contents是一个错误,而不是在gcc中返回0。不是结构体成员的零大小数组会引入一堆其他问题。 - M.M
@M.M:如果将两个指向大小为零的对象的相等指针相减定义为产生零(与任何大小的对象的相等指针相减一样),并且将不相等的指向大小为零的对象的指针相减定义为产生未指定值,那么它们会引起什么问题?如果标准规定实现可以允许在另一个结构体中嵌入包含FAM的结构体,前提是后者结构体中的下一个元素要么是具有与FAM相同元素类型的数组,要么是以这样一个数组开头的结构体,并且... - supercat
如果它能识别FAM作为数组的别名(如果对齐规则导致数组着陆在不同偏移量上,则需要进行诊断),那将非常有用。但事实上,没有很好的方法可以接受指向一般格式的结构体 struct {int n; THING dat[];} 的静态或自动变量持续时间的指针并能处理这些数据。 - supercat

9
我想补充一下,关于这个参数,在gcc的在线文档中有一个完整的页面。以下是一些引用:

在GNU C中允许使用零长度数组。

在ISO C90标准中,您需要将内容的长度设置为1。

以及

在3.0版本之前的GCC中,允许像可变数组一样静态初始化零长度数组。除了那些有用的情况外,它还允许在可能破坏后续数据的情况下进行初始化。

所以你可以:

int arr[0] = { 1 };

然后轰隆一声 :-)


3
@SurajJain 如果您想覆盖您的堆栈 :-) C语言不会检查您要写入的数组的索引与大小之间的关系,因此您可以执行a [100000] = 5。但如果你很幸运,你只是会导致你的应用程序崩溃,如果你够幸运的话 :-) - xanatos
Int a[0] ; 表示一个变量数组(零大小的数组),我现在如何给它赋值? - Suraj Jain
@SurajJain,“C不检查您正在编写的数组的索引与大小之间的关系”这句话哪部分不清楚?在C中没有索引检查,您可以在数组末尾后面进行编写并使计算机崩溃或覆盖宝贵的内存位。因此,如果您有一个包含0个元素的数组,则可以在0个元素的末尾后面进行编写。 - xanatos
请看此链接 https://www.quora.com/What-is-the-advantage-of-using-zero-length-arrays-in-C - Suraj Jain
哦,好的,非常感谢。你能再帮我一点吗?我几个月前提出了一个问题,结果被严重踩了,然后我进行了适当的修改和更正,现在开始得到了赞同,但是没有浏览量。如果您可以回答那个问题,并告诉我是否有遗漏,那将是很大的帮助。http://stackoverflow.com/questions/34826036/confused-about-pointer-dereferencing - Suraj Jain
显示剩余4条评论

6
在结构体内部声明零大小数组如果被允许并且语义是这样的:(1) 它们将强制对齐,但不会分配任何空间,(2) 在结果指针位于与结构体相同的内存块中的情况下,索引数组将被视为已定义的行为,则会很有用。这种行为从未被任何C标准允许,但在编译器允许使用带有空括号的不完整数组声明之前,一些老的编译器允许使用它。
常见的使用大小为1的数组实现的结构体技巧是不可靠的,我认为没有任何要求编译器不得违反它。例如,如果编译器看到 int a[1],它有权将 a[i] 视为 a[0]。如果有人试图通过类似以下方式绕过结构体技巧的对齐问题:
typedef struct {
  uint32_t size;
  uint8_t data[4];  // Use four, to avoid having padding throw off the size of the struct
}
编译器可能会变聪明并假设数组大小确实为四:
; As written
  foo = myStruct->data[i];
; As interpreted (assuming little-endian hardware)
  foo = ((*(uint32_t*)myStruct->data) >> (i << 3)) & 0xFF;
这样的优化可能是合理的,特别是如果 myStruct->data 可以在与 myStruct->size 相同的操作中加载到寄存器中。我不知道标准中是否有禁止这种优化的内容,尽管它会破坏可能希望访问第四个元素之外的东西的任何代码。

1
灵活数组成员 在 C99 中被添加为 struct hack 的合法版本。 - M.M
标准确实表明对不同数组成员的访问不会冲突,这会使得该优化变得不可能。 - Ben Voigt
@BenVoigt:C语言标准没有规定同时写入一个字节和读取包含该字节的单词的效果,但99.9%的处理器确实规定写入将成功,并且该单词将包含该字节的新版本或旧版本以及其他字节的不变内容。如果编译器针对这样的处理器,会有什么冲突吗? - supercat
@supercat:C语言标准保证同时写入两个不同数组元素不会发生冲突。因此,你认为(读取同时写入)是可行的论点是不足的。 - Ben Voigt
@BenVoigt:如果一段代码例如按顺序写入数组元素0、1和2,那么它将不被允许将所有四个元素读入一个长整型中,修改三个元素,然后将所有四个元素写回去。但我认为可以将所有四个元素读入一个长整型中,修改三个元素,将低16位作为短整型写回去,将16-23位作为字节写回去。而只需要读取数组元素的代码则可以将它们读入一个长整型中并使用它们。 - supercat

2
“肯定不能按照标准创建零大小的数组,但实际上每个最流行的编译器都允许这样做。因此,我将尝试解释为什么这可能是不好的。”
#include <cstdio>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        int empty[0];
    };
    A vals[3];
}

我就像一个人类期望的那样输出。
A()
A()
A()
~A()
~A()
~A()

Clang 打印出如下内容:
A()
~A()

GCC 打印出这个:
A()
A()
A()

这段文字的翻译如下:

这完全是奇怪的,所以如果你可以的话,在 C++ 中不要使用空数组是一个很好的理由。

此外,在 GNU C 中有一个扩展,它允许您在 C 中创建零长度数组,但是我正确理解的话,结构体中应该先有至少一个成员,否则如果在 C++ 中使用,就会出现像上面那样非常奇怪的例子。


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