为什么 new int[n] 合法而 int array[n] 不合法?

19

以下是代码:

foo(int n){
    int array[n];
}

我知道这是无效的语法,并且它无效是因为C++标准要求数组大小在编译时设置(虽然有些编译器支持以下语法)。

但是,我也知道以下是有效的语法:

bar(int n){
    int *array = new int[n];
}

我不明白为什么允许这样做,难道这不就像创建一个在运行时确定大小的数组吗?如果需要这样做,这是一种好的实践方法还是应该使用vector呢?


3
所谓的可变长度数组。下面是一些解释为什么它们在C++中不被允许 -> https://groups.google.com/forum/#!topic/comp.std.c++/K_4lgA1JYeg 顺便提一句,在C中是允许使用的。 - mip
7
第二个案例是堆分配的,而第一个(在C99中有效)将被分配到栈上。 - Thilo
1
在支持可变长度数组(VLAs)的C版本中,int array[n]; 创建了一个数组对象,其类型实际上是 int[n]。而 new int[n] 并不会创建数组类型;它只创建一个指向分配的数组第一个元素的 int* 指针。 - Keith Thompson
@Zaibis 所以它不是堆栈/堆内存?讨论的答案表明,对于任何编译器来说,这都是一个很好的情况。 - MatthiasB
1
@Thilo 不是重复的问题。链接的问题问为什么 int array[n] 是无效的。而这个问题问的是为什么 new int[n]有效的。这意味着它与重复问题的距离尽可能远。 - milleniumbug
显示剩余2条评论
9个回答

27
那是因为前者在栈上分配,后者在堆上分配。 当你在栈上分配一些东西时,正确构建它需要知道对象的大小。 C99允许在运行时指定大小,在构建和拆卸上述栈时会出现一些复杂性,因为无法在编译时计算其大小。必须发出机器代码以执行程序的执行过程中进行计算。这可能是这个特性未包含在C ++标准中的主要原因。 相反,堆没有固定的结构,正如名称所示。可以分配任何大小的块,没有特定的顺序,只要它们不重叠且有足够的(虚拟)内存。在这种情况下,在编译时知道大小并不那么重要。 还要记住,栈具有有限的大小,主要用于检测无限递归是否在消耗所有可用内存之前。通常,极限约为1MB,并且很少达到。除非您分配大型对象,否则应将其放置在堆中。 至于您应该使用什么,可能是std :: vector 。但这真的取决于您要做什么。 还要注意,C ++ 11有一个std :: array类,其大小必须在编译时知道。 C ++ 14应该引入了std :: dynarray,但由于关于编译时未知大小的堆栈分配仍有很多工作要做,所以被推迟了。 块通常出于性能原因按顺序分配,但这不是必需的。 正如指出的那样,在编译时知道大小并不是一个硬性要求,但它会让事情变得简单些。

我很欢迎 std::dynarray,它可以像 std::vector 一样在堆上分配,因为 std::vector 并不总是你所需要的(它可以增长,需要可移动/可复制类型,以及默认构造函数等)。 - mip
听起来你想要 std::unique_ptr<T[]>。它可以在运行时创建具有大小,但是创建后大小不能更改,内容也不会被移动。但是对于默认构造函数没有帮助。 - Ben Voigt
@Stefano Sanfilippo:“这是因为前者在堆栈上分配,而后者在堆上分配。” 这是完全错误的。纯C标准甚至没有使用“堆栈”或“堆”这些词。C++只是为了他所谓的标准实现函数而放弃了它们。但是可能会有一个编译器会反过来做,它仍然会完全遵守标准。更有可能的是:C代码可以在不支持堆栈/堆的环境中运行。因此,它甚至不能被尊重。我在这里讨论了这个问题:http://stackoverflow.com/q/27298414/2003898 - dhein
@Zaibis,你的问题的答案已经说得很清楚了。虽然没有堆栈也可以工作,但这是扩展语言规范和VLA不在C++标准中的参考模型,我认为这是原因。此外,据我所知,没有任何现代C/C++编译器不遵循这个模型,因此在考虑这个特定的问题时,该说法可能是正确的。 - Stefano Sanfilippo

8
在第一种情况下,您静态地分配内存空间以容纳整数。这是在编译程序时完成的,因此存储量是不可变的。
而在后一种情况下,您动态地分配内存空间以容纳整数。这是在程序运行时完成的,因此所需的存储量可以灵活调整。
第二次调用实际上是一个与操作系统通信的函数,用于查找要使用的内存位置。在第一种情况下,这个过程并不会发生。

没有静态分配,只有在栈上自动确定要分配的数量。 - Deduplicator

5
你问题的唯一有效答案是,“因为标准规定如此”。
与C99不同,C++从未费心去指定变长数组(VLAs),所以获取可变尺寸的数组的唯一方法是使用动态分配,例如mallocnew或其他某种内存管理器。
公平地说,运行时大小的堆栈分配会稍微使堆栈展开复杂化,这也会让使用该特性的函数的异常处理更加麻烦。
无论你的编译器是否将该C99特性作为扩展提供,始终严格控制堆栈使用是个好主意:没有恢复爆栈限制的方式,而错误情况被留给了未定义行为的原因。
在C++中模拟VLAs的最简单方法是,没有避免动态分配的性能优势(和爆栈危险):
unique_ptr<T[]> array{new T[n]};

1
这不仅仅是“没有费心”,而是可变长度数组在某些方面会带来积极的危害。 - Ben Voigt
是的,对于那些具有可变长度数组的函数,它会使得堆栈展开(因此也包括异常处理)变得更加复杂。不过这并不太糟糕。 - Deduplicator
正如你已经提到的,没有办法指示分配失败。 - Ben Voigt
@Deduplicator 有趣的代码片段,使用了 unique_ptr。不过,在什么情况下使用它比仅使用 vector 更可取呢? - Pradhan
@Pradhan:这个稍微轻量一些,会有影响吗?在大多数情况下,不会。 - Deduplicator

5
在这个表达式中
new int[n]
int[n] 不是类型。C++对“使用数组的new”和“不使用数组的new”有所不同。N3337标准草案中对new进行了如下解释:
当分配的对象为数组时(即使用语法或或表示数组类型时),产生指向该数组的初始元素(如果有)的指针。 指的是这种特殊情况(计算n并创建该大小的数组),例如: : [expression] attribute-specifier-seq[opt] [constant-expression] attribute-specifier-seq[opt]
然而,您不能在“通常”的声明中使用它,比如:
int array[n];

或在typedef中。
typedef int variable_array[n];

这与C99 VLAs不同,那里两者都被允许。

我应该使用向量吗?

是的,你应该。除非你有很强的理由不这样做(在过去7年中,我只有一次使用了new - 当我为学校作业实现vector时), 你应该始终使用向量。


5

int array[n] 在编译时在调用堆栈上分配一个固定长度的数组,因此需要在编译时知道 n 的值(除非使用特定于编译器的扩展允许在运行时进行分配,但数组仍然在堆栈上)。

int *array = new int[n] 在运行时在堆上分配一个动态长度的数组,因此不需要在编译时知道 n 的值。


4
这是因为C++语言不支持C99引入的“可变长数组”(VLA)功能。
C++在采用这个C功能方面落后,因为其库中的std::vector类型已经满足了大部分需求。
此外,C的2011标准撤销了VLA的强制性规定,将其变成了可选功能。
总之,VLA允许您使用运行时值来确定在自动存储器中分配的本地数组的大小。请注意保留HTML标记。
int func(int variable)
{
   long array[variable]; // VLA feature

   // loop over array

   for (size_t i = 0; i < sizeof array / sizeof array[0]; i++) {
     // the above sizeof is also a VLA feature: it yields the dynamic
     // size of the array, and so is not a compile-time constant,
     // unlike other uses of sizeof!
   } 
}

在C99之前的GNU C方言中,VLA已经存在了。在没有VLA的C方言中,在声明中的数组维度必须是常量表达式。
即使在有VLA的C方言中,也只有某些数组可以是VLA。例如,静态数组不能是VLA,动态数组也不能是(例如结构体内部的数组,即使动态分配该结构的实例)。
无论如何,由于您正在使用C ++编码,这是无关紧要的!
请注意,使用operator new分配的存储空间不是VLA特性。这是一种特殊的C ++语法,用于动态分配,返回指针类型,正如您所知道的:
int *p = new int[variable];

与VLA不同的是,这个对象将持续存在直到使用delete []显式销毁,并且可以从周围的范围返回。


根据我的经验,new T[n]基本上就是“动态数组”的定义。你说动态数组不能是VLA,但在我看来,所有的动态数组都是VLA。这句话还暗示结构体内的数组是动态数组,但这与我熟悉的定义不符。结构体内的数组有时可能具有动态作用域,但即使如此也是勉强算动态数组。 - Mooing Duck
@MooingDuck 很明显,我的评论是关于ISO C 1999可变长度数组功能,而不是任何长度以某种方式可变的任何语言中的数组。 - Kaz
1
离开一段时间后回来,我意识到你将“动态”与“动态长度”联系在了一起,而我将“动态”与“动态作用域”联系在了一起。从这个角度来看,你的帖子并没有错,所以我删除了我的第一条评论。 - Mooing Duck
@MooingDuck 我所知道的“动态作用域”存在于Lisp和相关语言中。在某些语言中,“动态”也指C语言所谓的自动存储器。Lisp是其中之一,而我似乎记得(具有讽刺意味的是!)C的前身BCPL也是如此。Ritchie决定将BCPL的动态存储器重命名为“自动”。 :) - Kaz
我检查了一下,我用错了词。我想的是§3.7.4中的“动态存储期”。为了完整起见,C++中其他使用“dynamic”的包括“动态初始化”(在启动时初始化的全局变量),“动态类型”(更派生的类型和dynamic_cast),“动态对象”(一个变量???),“动态绑定”(多态类),“动态异常规范”(可变参数模板魔法),异常被“动态”包围try块捕获...列表比我预期的要长。 - Mooing Duck
1
@MoongDuck:啊哈,“动态包围块捕获异常”证明了其他语言的术语正在悄然回归。这个“动态”的意义与Lisp的“动态范围”和BCPL的“动态存储”重合:与过程或块激活以及堆栈有关。 - Kaz

4

因为它具有不同的语义:

如果n是一个编译时常量(与您的示例不同):

int array[n]; //valid iff n is compile-time constant, space known at compile-time

但是考虑当 n 是运行时的值时:

int array[n]; //Cannot have a static array with a runtime value in C++
int * array = new int[n]; //This works because it happens at run-time, 
                          // not at compile-time! Different semantics, similar syntax.

在C99中,您可以为数组设置运行时n,并且空间将在运行时在堆栈中创建。目前有一些类似的扩展提案用于C++,但是还没有成为标准。


4
不,第二个并没有声明一个数组。它是使用了operator new的数组形式,这种方式明确允许第一维是可变的。

3
你可以在堆栈上静态分配内存,也可以在上动态分配内存。
在第一种情况下,你的函数包含一个可能具有可变长度的数组声明,但这是不可能的,因为原始数组必须在编译时具有固定大小,因为它们是在堆栈上分配的。因此,它们的大小必须被指定为一个常量,例如5。你可以像这样写:
foo(){
    int array[5]; // raw array with fixed size 5
}

使用指针可以指定内存的变量大小,因为这个内存将在堆上动态分配。在第二种情况下,您使用参数n来指定将要分配的内存空间。
总之,我们可以说指针不是数组:使用指针分配的内存是在堆上分配的,而为原始数组分配的内存是在栈上分配的。
有很好的替代方案来替换原始数组,例如标准容器vector,它基本上是一个具有可变长度大小的容器。
确保您充分理解动态和静态内存分配之间的差异,以及在堆上分配的内存和在栈上分配的内存之间的difference

理想情况下,“int arr[n]”是错误的,不应该被采用。但是,仍然有许多新的CPP学习者这样做,并且它仍然有效。为什么会发生这种情况呢? - ajaysinghnegi

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