C语言中可变长度数组的理解困难

3

我在阅读一本书时发现,在声明数组时必须给出大小,或者在运行时使用malloc从堆中分配。我用C语言编写了这个程序:

#include<stdio.h>

int main() {
  int n, i;
  scanf("%d", &n);
  int a[n];
  for (i=0; i<n; i++) {
    scanf("%d", &a[i]);
  }
  for (i=0; i<n; i++) {
    printf("%d ", a[i]);
  }
  return 0;
}

这段代码运行良好。

我的问题是,这段代码如何能够正确地工作。它不违反C语言的基本概念吗?即数组大小必须在运行时之前声明或使用malloc()进行分配。我没有做这两件事情中的任何一件,那么为什么它可以正常工作?

解决我的问题的方法是变长数组,在C99中支持。但如果我在scanf("%d,&n);之前放置语句int a[n];,那么它就停止工作了。为什么会这样呢?如果变长数组在C中得到支持,为什么会发生这种情况?


你的书可能讨论的是C89;你的编译器可能使用的是C99。它们是有些不同的语言(C99是C89的演变; C11是C99的演变)。其中一个区别是VLA(可变长度数组)。 - pmg
你怎么确定它确实正常工作?在这种情况下,有时它会对简单的情况起作用,但在扩展时会出现问题。很可能内部存储器已经损坏,即使在“成功”执行时-你只是幸运地避免了程序崩溃。或者正如其他人指出的那样,你可能正在使用C99。 - Platinum Azure
可变长度数组非常麻烦(特别是如果很大)。如果可以避免使用它们,尽量避免 :) - pmg
4个回答

4

1
如果这些数组的长度在运行时确定,那么它们的内存分配是如何进行的?它们是像我们在普通运行时数组中使用malloc()一样在堆上分配存储空间,还是在栈上分配? - dark_shadow
@dark_shadow,这取决于具体的实现。堆分配可以通过在运行时简单地移动堆栈指针来避免,这就是alloca函数(一些平台提供的扩展,如Microsoft和GNU)所做的。 - Charles Salvia

3

从C99开始,你可以在块级作用域下声明变长数组。

例如:

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

    // Initialize the array
    for (int i = 0; i < n; i++) {
        array[i] = 42;
    }
}

请问你能解释一下块级作用域的含义吗? - dark_shadow
1
@dark_shadow 你只能在函数体内声明可变长度数组。 - ouah
感谢ouah的帮助。请看一下我的修改后的问题。我添加了一部分内容。能否告诉我原因?谢谢。 - dark_shadow
如果你把 int a[n] 的声明放在 scanf 之前,那么在声明 a 数组时 n 将不会被初始化。因此你可能会得到任何 n 值。 - ouah
谢谢,我明白了。我还有一个问题。这些可变长度数组是在堆栈上声明内存还是在堆上声明内存?我认为它们应该分配在堆栈上,因为它们绑定到块作用域,并且由于块作用域中的所有内容都在堆栈上,所以我认为这应该遵循这些规则。我对吗? - dark_shadow

0
只要在使用数组之前声明并分配内存,C 就会很高兴。C 的一个“特性”是它不验证数组索引,因此程序员有责任确保所有内存访问都是有效的。

0
变长数组是在C99中添加的新特性。
“变长”意味着数组的大小是在运行时而不是编译时确定的。它并不意味着数组的大小可以在创建后更改。数组在声明时被逻辑地创建。因此,你的代码看起来像这样。
int n, i;

创建两个变量n和i。最初这些变量未初始化。
scanf("%d", &n);

将一个值读入 n 中。

int a[n];

创建一个大小为当前n值的数组"a"。
如果交换第二步和第三步,将会尝试创建一个大小由未初始化值确定的数组。这很可能不会有好结果。
C标准没有明确指定数组的存储方式,但在实践中,大多数编译器(我相信有一些例外)都会在堆栈上分配它。通常的做法是在函数前奏中将堆栈指针复制到“帧指针”中。这样就允许函数在保持自己的堆栈帧的同时动态修改堆栈指针。
可变长度数组是一个应该谨慎使用的特性。编译器通常不会在堆栈分配上插入任何形式的溢出检查。操作系统通常在堆栈后面插入一个“守卫页”来检测堆栈溢出,并且会引发错误或增加堆栈大小,但是足够大的数组很容易越过守卫页。

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