C 提供了一种办法,让你声明大小在运行时才确定的数组。这样你既能得到像 malloc() 那样动态确定大小的好处,又不用操心事后 free() 释放内存。
不过,很多人不喜欢变长数组。比如,Linux 内核就禁用它们。这些理由我们稍后再细说。
这是本语言的一项可选特性。如果实现不支持变长数组,宏 __STDC_NO_VLA__ 就会被设为 1。(在 C99 中它们是强制的,到 C11 变成了可选的。)
#if __STDC_NO_VLA__ == 1
#error Sorry, need VLAs for this program!
#endif但由于 GCC 和 Clang 都懒得定义这个宏,这个办法能派上的用场可能有限。
我们先从一个示例入手,然后再去细节里找魔鬼。
普通数组用常量大小声明,像这样:
int v[10];但使用变长数组时,我们可以用运行时确定的大小来设定数组,像这样:
int n = 10;
int v[n];乍看之下这似乎是一回事,在很多方面也确实是,但这样做让你能灵活地先算出所需的大小,然后得到一个恰好那么大的数组。
我们来让用户输入数组的大小,然后往每个数组元素里存入“下标乘以 10”:
#include <stdio.h>
int main(void)
{
int n;
char buf[32];
printf("Enter a number: "); fflush(stdout);
fgets(buf, sizeof buf, stdin);
n = strtoul(buf, NULL, 10);
int v[n];
for (int i = 0; i < n; i++)
v[i] = i * 10;
for (int i = 0; i < n; i++)
printf("v[%d] = %d\n", i, v[i]);
}(第 8 行我调用了一个 fflush(),它应该能强制这一行输出,即便我在末尾没有换行符。)
第 12 行就是我们声明变长数组的地方——执行一旦越过这一行,数组的大小就被定成那一刻 n 的值。之后数组长度就不能再改了。
方括号里也可以放一个表达式:
int v[x * 100];有一些限制:
static 的174。另外,给数组大小传一个负值会引发未定义行为——至少在这个宇宙里是这样。
sizeof 与变长数组我们已经习惯了让 sizeof 告诉我们某个特定对象(包括数组)有多少字节。变长数组也不例外。
主要区别在于:对变长数组使用 sizeof 是在运行时执行的,而对非变长的变量,它是在编译时计算出来的。
不过用法是一样的。
你甚至可以用常用的数组技巧算出变长数组的元素个数:
size_t num_elems = sizeof v / sizeof v[0];上一行代码里有一个微妙而正确的推论:指针运算的行为和普通数组完全一样,正如你所预期。所以尽管放手去用,随心所欲吧:
#include <stdio.h>
int main(void)
{
int n = 5;
int v[n];
int *p = v;
*(p+2) = 12;
printf("%d\n", v[2]); // 12
p[3] = 34;
printf("%d\n", v[3]); // 34
}和普通数组一样,你也可以配合圆括号使用 sizeof(),在并不实际声明变长数组的情况下得到一个假想变长数组的大小:
int x = 12;
printf("%zu\n", sizeof(int [x])); // 在我的系统上打印 48你可以放手创建各种变长数组,让其中一维或多维由变量决定
int w = 10;
int h = 20;
int x[h][w];
int y[5][w];
int z[10][w][20];同样,你可以像遍历普通数组那样遍历它们。
把一维变长数组传进函数,可以和传入普通数组毫无区别。直接上手就行。
#include <stdio.h>
int sum(int count, int *v)
{
int total = 0;
for (int i = 0; i < count; i++)
total += v[i];
return total;
}
int main(void)
{
int x[5]; // 标准数组
int a = 5;
int y[a]; // 变长数组
for (int i = 0; i < a; i++)
x[i] = y[i] = i + 1;
printf("%d\n", sum(5, x));
printf("%d\n", sum(a, y));
}但事情还不止如此。你还可以先把这个尺寸传进去,然后在形参列表里给出对应的维度,从而让 C 知道这个数组是一个特定大小的变长数组:
int sum(int count, int v[count])
{
// ...
}
顺带一提,为上面的函数列出原型有几种写法;如果你不想具体命名变长数组里的那个值,其中一种写法会用到 *。它只是表明该类型是变长数组,而不是普通指针。
变长数组的原型:
void do_something(int count, int v[count]); // 带名字
void do_something(int, int v[*]); // 不带名字再说一次,那个 * 的写法只在原型里有效——在函数本身里,你必须写出明确的尺寸。
现在——我们来玩多维的!好戏从这里开始。
做法和上面一维变长数组的第二种形式一样,只不过这次我们传入两个维度并使用它们。
在下面的示例中,我们构建一个宽度和高度都可变的乘法表矩阵,然后把它传给一个函数打印出来。
#include <stdio.h>
void print_matrix(int h, int w, int m[h][w])
{
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++)
printf("%2d ", m[row][col]);
printf("\n");
}
}
int main(void)
{
int rows = 4;
int cols = 7;
int matrix[rows][cols];
for (int row = 0; row < rows; row++)
for (int col = 0; col < cols; col++)
matrix[row][col] = row * col;
print_matrix(rows, cols, matrix);
}你可以让一部分维度固定、另一部分可变。假设我们把一条记录的长度固定为 5 个元素,但不知道有多少条记录。
#include <stdio.h>
void print_records(int count, int record[count][5])
{
for (int i = 0; i < count; i++) {
for (int j = 0; j < 5; j++)
printf("%2d ", record[i][j]);
printf("\n");
}
}
int main(void)
{
int rec_count = 3;
int records[rec_count][5];
// 填入一些假数据
for (int i = 0; i < rec_count; i++)
for (int j = 0; j < 5; j++)
records[i][j] = (i+1)*(j+2);
print_records(rec_count, records);
}由于变长数组在内存中和普通数组一模一样,完全可以把它们互换着传递……只要维度匹配。
例如,如果我们有一个专门需要 \(3\times5\) 数组的函数,我们仍然可以把变长数组传进去。
int foo(int m[5][3]) {...}
\\ ...
int w = 3, h = 5;
int matrix[h][w];
foo(matrix); // 没问题!同样,如果你有一个接收变长数组的函数,也可以把普通数组传进去:
int foo(int h, int w, int m[h][w]) {...}
\\ ...
int matrix[3][5];
foo(3, 5, matrix); // 没问题!不过要小心:如果维度不匹配,很可能就会出现某种未定义行为。
typedef 与变长数组你可以用 typedef 定义变长数组,但行为可能出乎你的意料。
基本上,typedef 会创建一个新类型,其尺寸取值就是执行 typedef 那一刻的取值。
所以与其说它是变长数组的 typedef,不如说它是由当时的各个维度确定的新固定大小数组类型。
#include <stdio.h>
int main(void)
{
int w = 10;
typedef int goat[w];
// goat 是一个含 10 个 int 的数组
goat x;
// 用各数的平方初始化
for (int i = 0; i < w; i++)
x[i] = i*i;
// 打印它们
for (int i = 0; i < w; i++)
printf("%d\n", x[i]);
// 现在我们改一下 w……
w = 20;
// 但 goat 仍然是一个含 10 个 int 的数组,因为那正是
// typedef 执行时 w 的取值。
}所以它的行为就像固定大小的数组。
但你仍然不能对它使用初始化器列表。
在变长数组附近使用 goto 时必须小心,因为很多做法是不合法的。
而使用 longjmp() 时,存在变长数组导致内存泄漏的情况。
但这两件事我们会在各自对应的章节里讨论。
变长数组已被 Linux 内核禁用,原因有几个:
网上的其他人指出,没有办法检测变长数组分配失败,而遭遇这类问题的程序很可能就直接崩溃了。固定大小的数组也有同样的问题,但比起不小心声明一个比如 30 MB 的固定大小数组,不小心弄出一个尺寸异常的变长数组的可能性要大得多。