| 目录 |

30 变长数组(VLA)

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 都懒得定义这个宏,这个办法能派上的用场可能有限。

我们先从一个示例入手,然后再去细节里找魔鬼。

30.1 基础

普通数组用常量大小声明,像这样:

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];

有一些限制:

另外,给数组大小传一个负值会引发未定义行为——至少在这个宇宙里是这样。

30.2 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

30.3 多维变长数组

你可以放手创建各种变长数组,让其中一维或多维由变量决定

int w = 10;
int h = 20;

int x[h][w];
int y[5][w];
int z[10][w][20];

同样,你可以像遍历普通数组那样遍历它们。

30.4 把一维变长数组传给函数

把一维变长数组传进函数,可以和传入普通数组毫无区别。直接上手就行。

#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[*]);            // 不带名字

再说一次,那个 * 的写法只在原型里有效——在函数本身里,你必须写出明确的尺寸。

现在——我们来玩多维的!好戏从这里开始。

30.5 把多维变长数组传给函数

做法和上面一维变长数组的第二种形式一样,只不过这次我们传入两个维度并使用它们。

在下面的示例中,我们构建一个宽度和高度都可变的乘法表矩阵,然后把它传给一个函数打印出来。

#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);
}

30.5.1 部分多维变长数组

你可以让一部分维度固定、另一部分可变。假设我们把一条记录的长度固定为 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);
}

30.6 与普通数组的兼容性

由于变长数组在内存中和普通数组一模一样,完全可以把它们互换着传递……只要维度匹配。

例如,如果我们有一个专门需要 \(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);   // 没问题!

不过要小心:如果维度不匹配,很可能就会出现某种未定义行为。

30.7 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 的取值。
}

所以它的行为就像固定大小的数组。

但你仍然不能对它使用初始化器列表。

30.8 跳转陷阱

在变长数组附近使用 goto 时必须小心,因为很多做法是不合法的。

而使用 longjmp() 时,存在变长数组导致内存泄漏的情况。

但这两件事我们会在各自对应的章节里讨论。

30.9 一般性问题

变长数组已被 Linux 内核禁用,原因有几个:

网上的其他人指出,没有办法检测变长数组分配失败,而遭遇这类问题的程序很可能就直接崩溃了。固定大小的数组也有同样的问题,但比起不小心声明一个比如 30 MB 的固定大小数组,不小心弄出一个尺寸异常的变长数组的可能性要大得多。


| 目录 |