26、变长数组参数:VLA与函数参数传递
说实话,C99 引入变长数组(VLA)那会儿,我挺兴奋的。以前写嵌入式代码,处理不同大小的数组,要么用宏定义最大长度,要么动态 malloc。前者浪费内存,后者容易泄漏。VLA 看起来是个优雅的解决方案——函数参数里直接传一个运行时才确定大小的数组。
但用了几年之后,我得说:VLA 是把双刃剑。用好了代码简洁高效,用不好就是栈溢出的定时炸弹。今天咱们就聊聊 VLA 作为函数参数的那些门道。
VLA 参数的基本写法
先看一个最简单的例子。你想写一个函数,打印一个 int 数组的所有元素。传统写法你得传数组指针和长度两个参数:
void print_array(int *arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
用 VLA 参数,你可以把长度和数组绑定在一起:
void print_array(int len, int arr[len]) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
注意看,arr[len] 这里的 len 必须是前面已经声明的参数。编译器看到这个声明,就知道 arr 是一个 VLA 参数,它的长度由 len 决定。
我个人习惯把长度参数放在前面,数组参数放在后面。这样读代码的时候,先看到长度,再看数组,逻辑上更顺。
多维 VLA 参数
二维数组的传递,在传统 C 里一直是个麻烦事。你必须固定第二维的大小:
void process_matrix(int rows, int cols, int matrix[cols]) {
// 这样写其实是一维数组,不是二维
}
等等,上面这个写法有问题。二维 VLA 参数的正确姿势是这样的:
void process_matrix(int rows, int cols, int matrix[rows][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] *= 2;
}
}
}
这里 matrix[rows][cols] 是一个完整的二维 VLA 参数。编译器知道每一行有 cols 个元素,所以 matrix[i][j] 的地址计算是自动完成的。
我在项目中遇到过一个问题:有人把参数顺序写成 int matrix[cols][rows],结果数组访问全乱了。记住,VLA 参数的维度顺序必须和实际传入的数组维度一致。
VLA 参数的本质
你可能会问:VLA 参数真的是把整个数组拷贝到栈上吗?
不是的。C 语言里,数组作为函数参数,永远退化为指针。VLA 参数也不例外。上面的 int arr[len] 本质上还是 int *arr。那 len 有什么用?
答案是:len 只用于编译器做类型检查和边界信息传递。它不会在栈上分配 len 个 int 的空间。真正的数组空间在调用者那边。
看这个例子你就明白了:
void foo(int n, int arr[n]) {
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出 8(指针大小),不是 4*n
}
嗯,这里要注意:sizeof(arr) 返回的是指针的大小,不是数组的大小。因为 arr 已经退化成指针了。这一点和固定大小数组参数不同——固定大小数组参数也会退化,但很多人误以为不会。
VLA 参数的避坑指南
我曾经在一个实时控制项目里用 VLA 参数,差点把系统搞崩。原因是这样的:
VLA 参数本身不分配空间,但如果你在函数内部声明了一个 VLA 局部变量,那空间是在栈上分配的。栈空间通常只有几 KB 到几十 KB,如果你分配一个很大的 VLA,栈溢出就来了。
不要在函数内部声明大尺寸的 VLA 局部变量。VLA 参数是安全的(因为不分配空间),但 VLA 局部变量是危险的。
另一个坑是:VLA 参数的长度表达式不能是负数。如果 len 传了一个负数,行为是未定义的。我建议在函数入口处加个断言:
void print_array(int len, int arr[len]) {
assert(len > 0);
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
VLA 与 const 限定
你可以在 VLA 参数前加 const,表示不修改数组内容:
void print_array(int len, const int arr[len]) {
// arr[i] = 0; // 编译错误
printf("%d", arr[0]);
}
也可以加 restrict,表示指针没有别名:
void add_arrays(int n, int *restrict a, const int *restrict b, const int *restrict c) {
for (int i = 0; i < n; i++) {
a[i] = b[i] + c[i];
}
}
VLA 参数和 restrict 结合,能让编译器生成更优的代码。我在做音频处理时,用这种方式让循环展开得更彻底,性能提升了大约 15%。
VLA 参数的兼容性
C11 把 VLA 改成了可选特性。也就是说,有些编译器可能不支持 VLA。嵌入式开发中,IAR、ARMCC 等编译器对 VLA 的支持程度不一。
我的建议是:只在明确支持 VLA 的平台上使用。如果你写的是跨平台代码,最好用传统方式:传指针和长度。
| 编译器 | VLA 支持情况 | 备注 |
|---|---|---|
| GCC | 完全支持(C99 起) | 即使 C11 下也默认开启 |
| Clang | 完全支持 | 与 GCC 兼容 |
| MSVC | 不支持 | 需用 alloca 模拟 |
| IAR | 部分支持 | 需检查版本 |
| ARMCC | 不支持 | 建议用传统方式 |
VLA 参数与函数指针
VLA 参数在函数指针类型中也能用,但写法有点绕:
void (*func_ptr)(int, int[*]); // 声明一个接受 VLA 参数的函数指针
实际使用时,你需要提供一个具体的长度:
void my_func(int n, int arr[n]) { /* ... */ }
func_ptr = my_func; // 赋值时,[*] 匹配任何长度
说实话,这种用法我很少见。函数指针加上 VLA 参数,可读性会下降不少。我个人更倾向于用 typedef 把类型包装一下:
typedef void (*array_processor)(int, int *);
// 然后直接用 int * 参数,清晰明了
VLA 参数与 sizeof
前面说了,VLA 参数本身 sizeof 返回指针大小。但如果你有一个 VLA 类型的指针,sizeof 会计算数组大小吗?
不会。因为 VLA 参数已经退化为指针了。但如果你在函数内部声明一个 VLA 变量,sizeof 会正确计算:
void foo(int n) {
int arr[n];
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出 4*n
}
这个特性可以用来在运行时获取数组大小,但要注意栈溢出的风险。
VLA 参数的最佳实践
总结一下我这些年用 VLA 参数的经验:
- 用 VLA 参数提高可读性:把长度和数组绑定在一起,代码自文档化。
- 不要用 VLA 局部变量分配大数组:栈空间有限,大数组请用 malloc。
- 加断言检查长度:防止负数或零长度导致未定义行为。
- 注意平台兼容性:MSVC 和部分嵌入式编译器不支持 VLA。
- VLA 参数不分配空间:它只是语法糖,本质还是指针。
如果你在 GCC 下用 VLA 参数,可以加
-Wvla 编译选项来警告所有 VLA 的使用。这样能帮你发现不小心引入的 VLA 局部变量。
VLA 参数的核心逻辑
下面这张图展示了 VLA 参数在函数调用中的完整流程:
从这张图可以看得很清楚:VLA 参数并没有在函数内部复制数组,它只是把长度信息带进了函数类型系统里。你想想看,这其实是一种「编译时安全,运行时零开销」的设计——除了多传一个整数参数,没有任何额外成本。
好了,关于 VLA 参数的内容就聊到这儿。记住它的本质、它的坑、它的最佳实践,你在项目中用起来就会得心应手。