第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]怎么找到正确的位置?

但这样写有个问题:函数只能处理固定列数的二维数组。如果想处理任意列数呢?

有两种常见做法:

  1. 使用指针数组
  2. 使用一维数组模拟二维数组

我个人更推荐第二种方式:

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 本章知识体系

下面这张图展示了本章的核心逻辑:

数组作为函数参数——知识体系 数组作为函数参数 数组退化为指针(核心概念) sizeof(arr) ≠ 数组大小 传递首地址(效率高) 函数内无法获知长度 传递数组长度 void func(int arr[], int size) 二维数组作为参数 必须指定列数 可展平为一维处理 变长数组(VLA) 运行时确定长度 栈上分配(注意大小) C11中为可选特性

12.6 避坑指南

最后,我总结几个常见的坑:

坑1:在函数内部使用sizeof(arr)计算数组长度。记住,arr已经退化为指针了。

坑2:二维数组作为参数时忘记指定列数。编译器会报错,但新手经常搞不清楚原因。

坑3:VLA分配过大导致栈溢出。我曾经在一个递归函数里用了VLA,结果递归深度一深,栈就爆了。排查了好久才发现是VLA的问题。

嗯,数组作为函数参数这个话题,说难不难,说简单也不简单。关键是要理解“数组退化”这个核心概念,然后记住:传数组时一定要把长度也传过去。二维数组的处理方式要灵活,VLA要用得谨慎。

掌握了这些,你在C语言数组的使用上就能少踩很多坑。


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