变长数组(VLA):C99 引入的特性,有什么坑?

变长数组,英文叫 Variable-Length Array,简称 VLA。这是 C99 标准引入的一个特性。说白了,就是允许你在定义数组时,用变量来指定数组的大小。

比如这样:

void func(int n) {
    int arr[n];  // n 是变量,数组长度在运行时确定
    // 使用 arr...
}

看起来挺方便对吧?不用 malloc,不用 free,直接在栈上分配。我刚开始接触这个特性时也觉得挺香的。但后来在项目中踩过几次坑之后,我的态度就变成了:能用静态数组就用静态数组,实在不行再用 malloc,VLA 能不用就不用

为什么?下面我一条一条跟你说。

坑一:栈溢出风险

VLA 是在栈上分配内存的。你想想看,栈空间通常有多大?嵌入式系统里可能就几 KB,PC 上也就几 MB。如果用户传了一个很大的 n,比如 10 万,那你的程序直接就崩了。

⚠️ 注意:VLA 没有错误返回值。如果分配失败,不会返回 NULL,而是直接导致栈溢出,程序崩溃。

我曾经在一个通信协议解析模块里看到过这样的代码:

void parse_packet(int payload_len) {
    uint8_t buffer[payload_len];  // 危险!
    // 处理数据...
}

如果攻击者构造了一个超大 payload_len 的恶意包,那这个函数就直接把栈给撑爆了。嗯,这就是一个典型的安全漏洞。

坑二:sizeof 的行为变了

对于普通数组,sizeof 是在编译期计算的。但对于 VLA,sizeof 是在运行时计算的。这会导致一些意想不到的问题。

void func(int n) {
    int arr[n];
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 运行时计算,结果是 n * sizeof(int)
    
    // 但如果你这样写:
    int *p = arr;
    printf("sizeof(p) = %zu\n", sizeof(p));      // 始终是指针大小,8 或 4
}

更坑的是,如果你把 VLA 作为函数参数传递,sizeof 的行为会变得非常诡异:

void process(int arr[static 10]) {
    // 这里 arr 实际上是指针,sizeof(arr) 是指针大小
}

void func(int n) {
    int vla[n];
    process(vla);  // 传进去后,sizeof 就变了
}

坑三:不能用在结构体里

C99 标准不允许在结构体中使用 VLA。你可能会想:

struct Packet {
    int len;
    char data[];   // 这是柔性数组成员,不是 VLA
};

struct BadPacket {
    int len;
    char data[len];  // ❌ 编译错误!
};

柔性数组成员(Flexible Array Member)是 C99 的另一个特性,它和 VLA 是两码事。柔性数组成员只能放在结构体的最后一个字段,而且只能用 [] 而不是 [len]

坑四:goto 不能跳过 VLA 的声明

这个坑比较隐蔽。C 标准规定,goto 不能跳过一个 VLA 的声明,因为 VLA 的声明涉及到运行时内存分配和释放。

void func(int flag) {
    if (flag) {
        goto skip;  // ❌ 错误!跳过了 VLA 的声明
    }
    
    int arr[100];
    
skip:
    // 这里 arr 可能没有被正确初始化
    return;
}

编译器会报错:jump into scope of identifier with variably modified type。嗯,这个错误信息我第一次看到时还愣了半天。

坑五:C11 把它变成了可选特性

到了 C11 标准,VLA 被标记为可选特性。这意味着编译器可以不支持 VLA。比如 MSVC(Visual Studio 的 C 编译器)就从来不支持 VLA。如果你写跨平台代码,用了 VLA,那在 Windows 上就编译不过。

重要提示:从 C11 开始,检查编译器是否支持 VLA 的方法是看 __STDC_NO_VLA__ 宏是否被定义。如果定义了,就说明不支持。

#ifdef __STDC_NO_VLA__
    // 不支持 VLA,用 malloc 替代
    int *arr = malloc(n * sizeof(int));
#else
    int arr[n];  // 支持 VLA
#endif

坑六:调试困难

VLA 的大小在编译期是未知的,这给调试器带来了麻烦。我在调试一个 VLA 相关的 bug 时,调试器里看到的数组长度是 ???,根本没法查看数组内容。最后只能改成固定大小数组才把问题定位出来。

什么时候可以用 VLA?

说了这么多坑,那 VLA 就一无是处吗?也不是。在一些场景下,VLA 还是有它的优势的:

场景 推荐做法 说明
小型临时数组(长度可控) 可以用 VLA 比如长度不超过 256 字节的缓冲区
性能敏感的代码 优先用 VLA 避免 malloc/free 的开销
跨平台代码 不要用 VLA MSVC 不支持,C11 后是可选特性
安全关键系统 不要用 VLA 栈溢出风险,无法优雅处理错误
大型数组(>1KB) 不要用 VLA 栈空间有限,建议用 malloc

避坑指南

我个人习惯的做法:

  • 在嵌入式项目中,完全禁用 VLA。栈空间本来就紧张,经不起折腾。
  • 在 PC 端工具类代码中,如果数组长度有明确上限(比如不超过 100),偶尔用一下 VLA 也无妨。
  • 如果要用 VLA,一定在函数开头检查长度是否合理:if (n > MAX_SIZE) return ERROR;

我曾经在一个网络协议栈的代码里看到过这样的写法:

void handle_request(int data_len) {
    if (data_len > 4096) {
        // 拒绝处理
        return;
    }
    char buffer[data_len];  // 安全了,因为 data_len 被限制在 4096 以内
    // ...
}

这种做法虽然限制了长度,但 4096 字节的栈分配在嵌入式系统里还是太大了。我建议改成 mallocfree,或者用静态缓冲区加分段处理。

总结

VLA 这个特性,C99 引入的时候确实解决了一些痛点,但带来的问题也不少。我的建议是:

  • 新项目:除非你有充分的理由,否则不要用 VLA。
  • 老项目:如果代码里已经用了 VLA,确保长度可控,并且只在少数几个地方使用。
  • 面试题:面试官问 VLA 的坑,你就把上面这六条说出来,保证加分。

嗯,关于 VLA 就聊这么多。记住一句话:栈上分配,风险自担

VLA 变长数组核心问题 VLA 的六大坑 ① 栈溢出风险 ② sizeof 行为异常 ③ 结构体不能用 ④ goto 不能跳过 ⑤ C11 可选特性 ⑥ 调试困难 避坑建议 ✅ 嵌入式项目完全禁用 VLA ✅ 使用前检查长度上限 ✅ 跨平台代码用 malloc 替代

公众号:蓝海资料掘金营,微信 deep3321