第12章:数组作为函数参数——数组退化为指针、传递数组长度的必要性、二维数组作为参数、变长数组(VLA)
数组作为函数参数,这个话题我每次讲课时都会多花些时间。为什么?因为这里藏着C语言最容易被忽视的“陷阱”之一。很多初学者甚至工作两三年的工程师,都在这上面栽过跟头。
说白了,数组和指针在参数传递时,并不是你想的那样“数组就是数组”。它们之间有一条微妙的界线,一旦跨过,数组就“退化”了。
12.1 数组退化为指针
我们先看一个最简单的例子:
#include <stdio.h>
void print_array(int arr[]) {
printf("arr size inside function: %zu\n", sizeof(arr));
}
int main() {
int numbers[10] = {1,2,3,4,5,6,7,8,9,10};
printf("numbers size in main: %zu\n", sizeof(numbers));
print_array(numbers);
return 0;
}
猜猜输出是什么?
numbers size in main: 40
arr size inside function: 8
看到了吗?在main函数里,sizeof(numbers)是40字节(10个int × 4字节)。但在print_array函数里,sizeof(arr)变成了8字节(64位系统上指针的大小)。
为什么会这样?
因为数组作为函数参数时,它“退化”成了指针。你写int arr[],编译器实际上把它当作int *arr来处理。这是C语言的设计决策——为了效率,函数调用时只传递数组的首地址,而不是复制整个数组。
核心要点:数组名作为函数参数时,会隐式转换为指向数组第一个元素的指针。这就是所谓的“数组退化”。
我在项目中遇到过这样一个bug:有个同事写了一个函数,想通过sizeof(arr)/sizeof(arr[0])来计算数组长度,结果在函数内部永远得到的是指针大小除以元素大小,算出来总是1或2。排查了半天才发现是数组退化的问题。
12.2 传递数组长度的必要性
既然数组退化为指针,那函数内部就无法知道数组的实际长度了。怎么办?
答案很简单:把数组长度作为另一个参数传进去。
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
int numbers[] = {1,2,3,4,5,6,7,8,9,10};
int size = sizeof(numbers) / sizeof(numbers[0]);
print_array(numbers, size);
return 0;
}
这是C语言的标准做法。我个人习惯把长度参数放在数组参数后面,这样代码读起来更自然。
注意:千万不要在函数内部用sizeof(arr)/sizeof(arr[0])来计算数组长度!这得到的是指针大小除以元素大小,结果永远是1(32位系统)或2(64位系统)。
我曾经在一个嵌入式项目中看到这样的代码:
void process_data(int data[]) {
int len = sizeof(data) / sizeof(data[0]); // 错误!
// 处理数据...
}
这个bug导致程序只处理了前两个元素,后面的数据全被忽略了。排查起来特别痛苦,因为逻辑上看起来完全正确。
12.3 二维数组作为参数
二维数组作为参数时,情况更复杂一些。我们先看基本语法:
void print_matrix(int matrix[][3], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int mat[2][3] = {{1,2,3}, {4,5,6}};
print_matrix(mat, 2);
return 0;
}
注意看,二维数组作为参数时,第二维(列数)必须指定。为什么?因为编译器需要知道每行有多少个元素,才能正确计算地址偏移。
你想想看,如果不知道列数,matrix[i][j]怎么找到正确的位置?
但这样写有个问题:函数只能处理固定列数的二维数组。如果想处理任意列数呢?
有两种常见做法:
- 使用指针数组
- 使用一维数组模拟二维数组
我个人更推荐第二种方式:
void print_matrix_generic(int *matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i * cols + j]);
}
printf("\n");
}
}
int main() {
int mat[2][3] = {{1,2,3}, {4,5,6}};
print_matrix_generic(&mat[0][0], 2, 3);
return 0;
}
这种方式把二维数组“展平”成一维数组来处理,灵活性更高。我在处理图像数据时经常用这种方法。
12.4 变长数组(VLA)
C99标准引入了变长数组(Variable Length Array,VLA)。它允许数组的长度在运行时确定,而不是编译时固定。
void process_vla(int n) {
int arr[n]; // VLA:长度由变量n决定
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
// 使用arr...
}
VLA也可以作为函数参数:
void print_vla(int rows, int cols, int matrix[rows][cols]) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
注意参数的顺序:长度参数必须出现在数组参数之前。这样编译器才能知道rows和cols的值。
小技巧:使用VLA作为函数参数时,可以把长度参数放在最前面,这样代码更清晰。比如:void func(int n, int arr[n])。
不过,VLA也有一些限制和争议:
- 栈空间有限:VLA分配在栈上,如果长度太大,会导致栈溢出。
- C11中变为可选特性:C11标准把VLA改为可选特性,有些编译器可能不支持。
- 调试困难:VLA的大小在运行时才确定,调试时不太方便。
我在嵌入式开发中很少使用VLA,因为嵌入式系统的栈空间通常很小(几KB到几十KB),用VLA很容易出问题。但在PC端开发中,VLA确实能简化代码。
12.5 本章知识体系
下面这张图展示了本章的核心逻辑:
12.6 避坑指南
最后,我总结几个常见的坑:
坑1:在函数内部使用sizeof(arr)计算数组长度。记住,arr已经退化为指针了。
坑2:二维数组作为参数时忘记指定列数。编译器会报错,但新手经常搞不清楚原因。
坑3:VLA分配过大导致栈溢出。我曾经在一个递归函数里用了VLA,结果递归深度一深,栈就爆了。排查了好久才发现是VLA的问题。
嗯,数组作为函数参数这个话题,说难不难,说简单也不简单。关键是要理解“数组退化”这个核心概念,然后记住:传数组时一定要把长度也传过去。二维数组的处理方式要灵活,VLA要用得谨慎。
掌握了这些,你在C语言数组的使用上就能少踩很多坑。
公众号:蓝海资料掘金营,微信deep3321