为什么C数组的长度不能为0?


13

C11标准说,大小和可变长度的数组“都应具有大于零的值”。不允许长度为0的理由是什么?

特别是对于可变长度的数组,每隔一段时间具有零的大小是很有意义的。当静态数组的大小来自宏或构建配置选项时,它也很有用。

有趣的是,GCC(和clang)提供了允许零长度数组的扩展。Java还允许长度为零的数组。


7
stackoverflow.com/q/8625572 ... “零长度的数组将很棘手,并且难以与每个对象具有唯一地址的要求相协调。”
罗伯特·哈维

3
@RobertHarvey:给定struct { int p[1],q[1]; } foo; int *pp = p+1;pp将是一个合法的指针,但*pp没有唯一的地址。为什么零长度数组不能包含相同的逻辑?假设int q[0]; 在结构中给出,q将指向其有效性类似于上述p+1示例的地址。
超级猫

@DocBrown从C11标准6.7.6.2.5谈论用于确定VLA大小的表达式“……每次对其进行评估时,其值均应大于零。” 我不了解C99(似乎很奇怪他们会更改它),但听起来您的长度不能为零。
凯文·考克斯

@KevinCox:是否有免费的C11标准在线版本(或相关部件)?
布朗

最终版本不是免费提供的(真可惜),但是您可以下载草稿。最新的草案是open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
凯文·考克斯

Answers:


11

我要打赌的问题是C数组只是指向已分配内存块的开头的指针。大小为0意味着您有一个指向...的指针?您一无所有,因此必须选择一些任意的东西。您不能使用null,因为0长度的数组看起来像空指针。到那时,每个不同的实现都会选择不同的任意行为,从而导致混乱。



8
@delnan:好吧,如果您想学究它,可以定义数组和指针算法,以便可以方便地使用指针访问数组或模拟数组。换句话说,它的指针算术和数组索引在C中是等效的。但是无论如何结果都是相同的……如果数组的长度为零,那么您仍然没有指向任何对象。
罗伯特·哈维

3
@RobertHarvey完全正确,但是您的结束语(以及回顾后的整个答案)似乎像是一种令人困惑和混乱的方式来解释这样的数组(我认为这就是这个答案所称的“分配的内存块”吗?)sizeof0,这将如何引起麻烦。使用适当的概念和术语可以解释所有这些内容,而又不失简洁或清晰。混合使用数组和指针只会冒着散布数组=指针误解(在其他情况下更重要)的风险,这毫无益处。

2
不能使用null,因为0长度的数组看起来像null指针 ”-实际上,这正是Delphi所做的。空的dynarray和空的longstrings从技术上讲是空指针。
JensG 2014年

3
-1,我对@delnan满意。这没有任何解释,特别是在OP关于支持零长度数组的概念的一些主要编译器的内容中。我很确定可以在C中以实现独立的方式提供零长度数组,而不是“导致混乱”。
布朗

6

让我们看一下数组通常在内存中的布局方式:

         +----+
arr[0] : |    |
         +----+
arr[1] : |    |
         +----+
arr[2] : |    |
         +----+
          ...
         +----+
arr[n] : |    |
         +----+

注意,没有一个单独的对象arr存储第一个元素的地址。当数组出现在表达式中时,C 根据需要计算第一个元素的地址。

所以,让我们想一想:0元素的数组本来没有存储预留了它,这意味着什么来计算阵列地址(换一种说法,还有的标识符的对象映射)。就像说,“我要创建一个int不占用内存的变量”。这是一个荒谬的操作。

编辑

Java数组与C和C ++数组完全不同。它们不是原始类型,而是从派生的引用类型Object

编辑2

下面的注释中提出了一个要点-“大于0”约束仅适用于通过常量表达式指定大小的数组;允许VLA的长度为0声明具有0值的非恒定表达式的VLA并非违反约束,但确实会引发未定义的行为。

显然,VLA与常规数组是不同的动物,它们的实现可以使大小为0。不能声明它们static或不在文件范围内,因为在程序启动之前必须知道此类对象的大小。

从C11开始,不需要任何实现来支持VLA也是毫无价值的。


3
抱歉,但是恕我直言,您和Telastyn一样缺少重点。零长度数组很有意义,现有的实现(例如OP告诉我们的实现)表明可以实现。
布朗

@DocBrown:首先,我要解决的是为什么语言标准最有可能禁止它们。其次,我想举一个长度为0的数组有意义的示例,因为老实说我想不出一个。最可能的实现是将其T a[0]视为T *a,但是为什么不仅仅使用它T *a呢?
约翰·博德2014年

抱歉,但是我不赞成标准为何禁止这样做的“理论推理”。阅读我的答案,如何轻松地实际计算地址。我建议您按照Robert Harveys在问题下的第一条评论中的链接阅读第二条答案,这里有一个有用的例子。
布朗

@DocBrown:啊。该struct黑客。我从来没有亲自使用过它。从来没有解决过需要可变大小struct类型的问题。
约翰·博德2014年

2
不要忘记AFAIK,因为C99,C允许使用可变长度数组。而且,当数组大小为参数时,不必将值0视为特殊情况,可以简化许多程序。
布朗

2

您通常希望零(实际上是变量)大小的数组在运行时知道其大小。然后,将其打包struct并使用灵活的数组成员,例如:

struct my_st {
   unsigned len;
   double flexarray[]; // of size len
};

显然,灵活数组成员必须是它的最后一个成员,struct并且您需要先拥有一些东西。通常,这可能与该灵活数组成员的实际运行时占用的长度有关。

当然您会分配:

 unsigned len = some_length_computation();
 struct my_st*p = malloc(sizeof(struct my_st)+len*sizeof(double));
 if (!p) { perror("malloc my_st"); exit(EXIT_FAILURE); };
 p->len = len;
 for (unsigned ix=0; ix<len; ix++)
    p->flexarray[ix] = log(3.0+(double)ix);

AFAIK,这在C99中已经可以实现,并且非常有用。

顺便说一句,灵活的数组成员在C ++中不存在(因为很难定义何时以及如何构造和销毁它们)。但是请参阅未来的std :: dynarray


您知道,它们可能只限于琐碎的类型,不会有任何困难。
Deduplicator

2

如果表达式type name[count]是用某些函数编写的,则您告诉C编译器在堆栈帧上分配sizeof(type)*count字节并计算数组中第一个元素的地址。

如果表达式type name[count]是在所有函数和结构定义之外编写的,则您告诉C编译器在数据段上分配sizeof(type)*count字节并计算数组中第一个元素的地址。

name实际上是一个常量对象,它存储数组中第一个元素的地址,而每个存储某个内存地址的对象都称为指针,因此这就是您将其name视为指针而不是数组的原因。请注意,只能通过指针访问C中的数组。

如果count是一个求值为零的常数表达式,则您告诉C编译器在堆栈帧或数据段上分配零字节,并返回数组中第一个元素的地址,但是这样做的问题是第一个元素零长度数组不存在,并且您无法计算不存在的对象的地址。

这是合理的。count+1count-length数组中不存在,因此这是C编译器禁止将零长度数组定义为函数内部和外部的变量的原因,因为namethen 的内容是什么?name确切存储什么地址?

如果p是指针,则表达式p[n]等于*(p + n)

右边表达式中的星号*是指针的取消引用操作,这意味着访问指针所指向的内存p + n或访问其地址存储在中的内存p + n,其中p + n指针表达式是它的地址,p并将该数字加到该地址n上。指针类型的大小p

是否可以添加地址和数字?

是的,这是可能的,因为地址是通常以十六进制表示的无符号整数。


许多编译器曾经在标准禁止之前允许零大小的数组声明,并且许多编译器继续将此类声明作为扩展。如果人们意识到大小对象N具有N+1关联的地址,则该声明将不会造成问题,该地址的第一个N标识唯一的字节,而最后一个N每个点恰好超过那些字节之一。这样的定义即使在N0 的简并情况下也可以正常工作
。– supercat

1

如果要指向内存地址的指针,请声明一个。数组实际上指向您已保留的内存块。数组在传递给函数时会衰减到指针,但是如果它们指向的内存在堆上,那就没问题了。没有理由声明大小为零的数组。


2
通常,您不会直接执行此操作,而是由于使用宏或声明带有动态数据的可变长度数组时。
凯文·考克斯

数组永远不会指向。它可以包含指针,并且在大多数情况下,您实际上使用的是指向第一个元素的指针,但这是另一回事。
Deduplicator

1
数组名称是指向数组中包含的内存的常量指针。
ncmathsadist

1
不,在大多数情况下,数组名称会衰减为指向第一个元素的指针。差异通常至关重要。
Deduplicator

1

从最初的C89时代起,当C标准规定某事物具有未定义的行为时,这意味着“做任何事情都会使在特定目标平台上的实现最适合其预期目的”。该标准的作者不想试图猜测哪种行为最适合任何特定目的。当给定大小为零时,现有的具有VLA扩展名的C89实现可能具有不同但逻辑的行为(例如,某些行为可能将数组视为产生NULL的地址表达式,而其他行为将其视为可能等于地址的地址表达式)。另一个任意变量,但可以安全地将其添加为零而不进行陷阱)。如果有任何代码可能依赖于这种不同的行为,则该标准的作者将不会

该标准的作者没有试图猜测实现可能会做什么,或者没有建议任何行为应被视为优于任何其他行为,而只是允许实现者在他们认为合适的情况下使用判断力来处理这种情况。在后台使用malloc()的实现可能会将数组的地址视为NULL(如果大小为零的malloc产生null),那些使用堆栈地址计算的实现可能会产生与某些其他变量的地址匹配的指针,而某些其他实现可能会其他事情。我不认为他们期望编译器编写者会全力以赴,以使零尺寸的转角案例故意以无用的方式表现出来。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.