C99/C11新特性:可变长数组 (VLA - Variable Length Array)

在C99标准之前,C语言要求数组的长度必须在编译时确定,即数组长度必须是一个常量表达式。这给编程带来了一些不便,例如当数组大小依赖于运行时计算的值时,程序员通常不得不使用动态内存分配(如 malloc)或者分配一个足够大的固定大小数组以应对最坏情况。

C99标准引入了可变长数组 (Variable Length Array, VLA),允许在自动存储期(通常是在函数内部的栈上分配)的数组的长度由一个在运行时才确定的变量来指定。

VLA 的基本概念与用法

VLA 的声明看起来与普通数组类似,但其长度可以是一个非常量表达式。

示例:

 #include <stdio.h>
 #include <stdlib.h> // for rand()
 
 void process_array(int size) {
     if (size <= 0) {
         printf("Invalid size for VLA: %d\n", size);
         return;
     }
     printf("Creating a VLA of size %d\n", size);
     int vla_array[size]; // 声明一个可变长数组
     // 注意:size 的值在运行时确定
 
     // 初始化VLA
     for (int i = 0; i < size; ++i) {
         vla_array[i] = i * 10;
     }
 
     // 使用VLA
     printf("VLA contents: ");
     for (int i = 0; i < size; ++i) {
         printf("%d ", vla_array[i]);
     }
     printf("\n");
 
     printf("sizeof(vla_array) = %zu bytes\n", sizeof(vla_array));
     // sizeof 操作符在VLA上是运行时计算的
 
 } // vla_array 在函数退出时自动销毁
 
 int main() {
     int n1 = 5;
     process_array(n1);
 
     int n2;
     printf("Enter a size for another VLA: ");
     if (scanf("%d", &n2) == 1) {
         process_array(n2);
     } else {
         printf("Invalid input.\n");
     }
 
     // VLA不能在文件作用域(全局)或作为静态局部变量声明
     // static int global_vla[n1]; // 错误!n1不是常量表达式,且VLA不能是static
 
     return 0;
 }

关键点:

  1. 自动存储期:VLA 只能具有自动存储期。这意味着它们通常在栈上分配,并且其生命周期仅限于声明它们的代码块。
  2. 不能在文件作用域(全局)声明VLA。
  3. 不能将VLA声明为 static(无论是文件作用域还是块作用域)。
  4. 长度在运行时确定:数组的长度表达式在运行时求值。这个表达式的值不必是编译时常量。
  5. sizeof 运算符:当 sizeof 应用于VLA时,它会在运行时计算数组的大小(长度 * sizeof(元素类型))。这与普通数组不同,普通数组的 sizeof 是在编译时计算的。
  6. 不能是结构体或联合的成员:结构体或联合的成员大小必须在编译时确定,因此它们不能包含VLA。
  7. 不能被初始化:VLA在声明时不能使用初始化列表进行初始化。
  8. int n = 5;
    // int vla[n] = {1, 2, 3, 4, 5}; // 错误!VLA不能使用初始化列表
    int vla[n]; // 正确
    for(int i=0; i<n; ++i) vla[i] = i+1; // 必须在声明后手动初始化

VLA 作为函数参数

VLA 也可以用于函数参数声明,这在处理多维数组时特别有用,允许函数接受不同大小的数组。

 #include <stdio.h>
 
 // rows 和 cols 在参数列表中必须出现在 VLA 声明之前
 void print_2d_vla(int rows, int cols, int array[rows][cols]) {
     printf("Printing 2D VLA (%d x %d):\n", rows, cols);
     for (int i = 0; i < rows; ++i) {
         for (int j = 0; j < cols; ++j) {
             printf("%3d ", array[i][j]);
         }
         printf("\n");
     }
 }
 
 // 另一种声明方式,使用 '*' 表示维度是可变的 (C99)
 // 这种方式下,rows 和 cols 仍然需要传递,但数组声明更简洁
 void print_2d_vla_alt(int rows, int cols, int array[][*]) { // cols 可以用 * 代替
     // 或者 int array[][cols] 也是合法的,只要cols在前面声明
     printf("Printing 2D VLA ALT (%d x %d):\n", rows, cols);
     for (int i = 0; i < rows; ++i) {
         for (int j = 0; j < cols; ++j) {
             printf("%3d ", array[i][j]);
         }
         printf("\n");
     }
 }
 
 int main() {
     int r1 = 2, c1 = 3;
     int matrix1[r1][c1];
     for (int i = 0; i < r1; ++i) {
         for (int j = 0; j < c1; ++j) {
             matrix1[i][j] = (i + 1) * 10 + (j + 1);
         }
     }
     print_2d_vla(r1, c1, matrix1);
     print_2d_vla_alt(r1, c1, matrix1);
 
     printf("\n");
 
     int r2 = 3, c2 = 2;
     int matrix2[r2][c2];
     for (int i = 0; i < r2; ++i) {
         for (int j = 0; j < c2; ++j) {
             matrix2[i][j] = (i + 1) * 100 + (j + 1) * 10;
         }
     }
     print_2d_vla(r2, c2, matrix2);
     print_2d_vla_alt(r2, c2, matrix2);
 
     return 0;
 }

注意: 当VLA作为函数参数时,其长度参数(如 rows, cols)必须在参数列表中出现在VLA本身之前,这样编译器才能知道如何计算VLA的维度。

VLA 与 typedef

typedef 不能直接用于创建可变长度的数组类型,因为 typedef 定义的是类型,而类型的大小必须在编译时确定。

 int n = 5;
 // typedef int VLA_Type[n]; // 错误!n不是常量表达式

但是,你可以 typedef 一个指向VLA的指针类型,如果VLA的维度(除了第一个)是固定的。

 void process_vla_ptr(int n, int (*arr_ptr)[n]) {
     // arr_ptr 是一个指向包含 n 个 int 的数组的指针
     // (*arr_ptr)[i] 可以访问元素
     for (int i = 0; i < n; ++i) {
         printf("%d ", (*arr_ptr)[i]);
     }
     printf("\n");
 }
 
 int main() {
     int size = 3;
     int my_vla[size];
     for(int i=0; i<size; ++i) my_vla[i] = i*100;
 
     process_vla_ptr(size, &my_vla);
     return 0;
 }

VLA 的优点

  1. 方便性:允许根据运行时需求在栈上创建大小合适的数组,避免了手动进行动态内存分配和释放的麻烦。
  2. 代码简洁:对于某些情况,使用VLA比 malloc/free 更简洁。
  3. 自动内存管理:由于VLA在栈上分配,其内存在函数返回时自动释放,减少了内存泄漏的风险。

VLA 的缺点与风险

  • 栈溢出风险:VLA在栈上分配内存。如果请求的数组大小过大,可能会耗尽栈空间,导致栈溢出 (stack overflow),进而程序崩溃。栈空间通常比堆空间小得多。
  • // 潜在的危险代码
    int very_large_size;
    scanf("%d", &very_large_size); // 如果用户输入一个巨大的数
    char vla_buffer[very_large_size]; // 极易导致栈溢出
  • 因此,在使用VLA时,必须对数组大小进行合理性检查,确保它不会过大。
  • 错误处理:如果VLA分配失败(例如,请求的大小过大导致栈溢出),C标准并没有规定标准的错误报告机制。程序通常会直接崩溃。这与 malloc 不同,malloc 在失败时会返回 NULL,允许程序进行错误处理。
  • 性能:VLA的分配和 sizeof 的运行时计算可能会带来轻微的性能开销,尽管通常不显著。
  • 可移植性与编译器支持
    • VLA是C99标准引入的特性。
    • C11标准将VLA列为可选特性 (optional feature)。这意味着符合C11标准的编译器不一定需要支持VLA。编译器通过预定义宏 __STDC_NO_VLA__ (如果定义为1) 来指示它不支持VLA。
    • C++ 不支持VLA (尽管某些编译器如GCC可能作为扩展支持)。
    • Microsoft的C编译器 (MSVC) 长期以来不完全支持C99的VLA(尤其是在C模式下,其C++模式下有时通过扩展支持类似行为,但不是标准的VLA)。较新版本的MSVC可能有所改进,但传统上这是一个痛点。 由于这些原因,如果代码需要高度可移植性,尤其是在多种编译器或C++环境中使用,过度依赖VLA可能不是最佳选择。
  • 安全性:由于栈溢出和缺乏标准错误处理机制,不受信任的输入(例如,来自用户的数组大小)用于VLA的长度可能导致安全漏洞。

VLA vs. malloc

特性

VLA (可变长数组)

malloc (动态内存分配)

内存位置

栈 (Stack)

堆 (Heap)

生命周期

自动 (块作用域)

手动 (mallocfree)

大小限制

受限于栈大小 (通常较小)

受限于可用堆内存 (通常较大)

分配失败

通常导致程序崩溃 (栈溢出)

返回 NULL,可检查并处理错误

初始化

不能在声明时使用初始化列表

分配的内存未初始化 (除非用 calloc)

sizeof

运行时计算

sizeof(pointer),不反映分配的大小

可移植性

C99必须,C11可选,C++不支持

C/C++ 标准库函数,广泛支持

使用便利

简单数组声明,自动释放

需要 malloc/free,指针操作

何时考虑使用VLA?

  • 当数组大小在运行时确定,但大小相对较小且可预测,不太可能导致栈溢出时。
  • 当希望避免手动 malloc/free 的复杂性,并且数组生命周期与函数作用域一致时。
  • 在编写接受多维数组且维度可变的函数原型时。

替代方案:

  • 对于较大的数组或大小不确定的情况,或者需要更长的生命周期,始终优先考虑使用 malloc (或 calloc, realloc) 进行动态内存分配
  • 如果只是为了传递多维数组,可以考虑使用指向指针的指针(例如 int **array),并手动管理行指针的分配。

检查编译器是否支持VLA (C11及以后)

 #include <stdio.h>
 
 int main() {
 #ifdef __STDC_NO_VLA__
     #if __STDC_NO_VLA__ == 1
         printf("Compiler does NOT support VLA (as per C11 __STDC_NO_VLA__).
 ");
     #else
         // 这个分支理论上不应该出现,__STDC_NO_VLA__ 定义了就是1
         printf("Compiler might support VLA (__STDC_NO_VLA__ is defined but not 1).
 "); 
     #endif
 #else
     printf("Compiler likely supports VLA (or __STDC_NO_VLA__ is not defined).
 ");
     // 尝试一个小VLA来实际测试 (某些编译器即使定义了__STDC_NO_VLA__也可能通过扩展支持)
     int n = 5;
     int test_vla[n];
     test_vla[0] = 1;
     printf("Small VLA test successful, element 0: %d\n", test_vla[0]);
 #endif
     return 0;
 }

总结

VLA是C99引入的一个特性,它允许在栈上创建长度在运行时确定的数组。这在某些情况下提供了便利,但也带来了栈溢出的风险,并且在C11中被降级为可选特性,影响了其可移植性。

在使用VLA时,务必:

  1. 谨慎评估数组大小,防止栈溢出。
  2. 考虑可移植性,如果目标编译器或标准(如C++)不支持VLA,则应避免使用。
  3. 对于任何不确定大小或可能较大的数组,优先使用动态内存分配 (malloc)

尽管VLA在某些场景下很方便,但由于其潜在的风险和可移植性问题,许多现代C编程指南建议谨慎使用或避免使用VLA,特别是在需要高可靠性和可移植性的代码中。

原文链接:,转发请注明来源!