在C++中把数组传给函数,是初学者最早碰到的“反直觉”问题之一。你可能以为函数会拿到整个数组的拷贝,实际上函数拿到的只是一个指向首元素的指针。正因如此,C++允许三种看似不同、实则完全等价的数组形参声明写法。理解这三种写法为何等价、各自适合什么场景,是写出让代码意图更清晰、更少踩坑的函数接口的基础。本文将围绕数组形参的三种声明方式展开,从底层退化机制讲起,逐一分析优缺点,并给出更现代的替代方案。

一、数组退化为指针:三种写法等价的根本原因
要理解数组传参,必须先理解一个核心规则:数组名在表达式中会退化为指向首元素的指针。这条规则几乎适用于所有把数组名当作值来使用的场景,函数调用时的实参传递也不例外。
当调用func(arr)时,arr并不是按值拷贝整个数组内容,而是退化成&arr[0]传过去。因此无论形参写成什么形式,函数内部真正接收到的都是一个指针。这就解释了为什么下面三种声明在编译器眼里完全相同:
void func(int *a); // 写法一:指针形式 void func(int a[]); // 写法二:不定长度数组形式 void func(int a[10]); // 写法三:定长数组形式 // 三者完全等价,编译器都会把形参处理成 int*
这三种声明在C++标准中被明确视为相同的函数签名,它们声明的是同一个函数,而不是三个重载版本。如果在同一个作用域里同时声明这三个版本,编译器会直接报错,提示函数重复定义。
还有一个直接后果值得强调:由于形参实际是指针,函数内部用sizeof(a)得到的是指针的大小(在64位系统上是8字节),而不是数组的总字节数。很多人在函数内写sizeof(a)/sizeof(a[0])来计算长度,结果得到一个离谱的数字,就是掉进了这个陷阱。
二、三种声明方式的语法细节与适用场景
1. 指针形式 int *a
这是最直白的写法,明确表达了“传进来的是一个地址”。它的优点是没有任何歧义,缺点是无法体现“这个指针指向的是一段连续的多个元素”。调用者看到int *a,无法判断它指向单个整数还是一串整数,语义上有所损失。不过在接口需要同时接受单个对象地址和数组首地址的场合,这种写法反而最灵活。
2. 不定长度数组形式 int a[]
这种写法在语义上更贴近意图:它告诉阅读者“这里期望接收一个数组”。虽然编译器内部仍按指针处理,但代码可读性更好。它常与一个额外的长度参数搭配使用:
// 常见的组合:数组首地址 + 元素个数
double average(const double a[], int n) {
double sum = 0;
for (int i = 0; i < n; ++i) {
sum += a[i];
}
return n > 0 ? sum / n : 0;
}
int main() {
double scores[5] = {88.5, 92.0, 76.5, 95.0, 81.0};
std::cout << average(scores, 5); // 输出 86.6
return 0;
}这种“指针加长度”的组合是C风格代码中最经典的模式,C++标准库的很多接口(如std::accumulate的早期用法)也遵循这一惯例。它的问题在于长度参数和数组参数之间没有强制的关联约束,调用者传错长度时编译器不会察觉,只能靠运行时的谨慎或断言来兜底。
3. 定长数组形式 int a[10]
这是最容易被误解的写法。方括号里的10在编译器看来毫无意义,任何长度的int数组甚至一个int*指针都能传进来,编译器不做任何检查。因此这种写法不仅不能保证长度正确,反而可能给读者错误的暗示,以为函数要求恰好10个元素。一般建议避免单独使用这种写法,除非配合其他手段。
三、多维数组的传递:退化规则同样适用
二维数组的传参比一维复杂一些,因为退化只发生在最外层维度。一个int matrix[3][4]传给函数时,退化成指向int[4]的指针,也就是int (*)[4]。因此形参必须保留除第一维以外的所有维度:
// 正确的二维数组形参写法 void printMatrix(int m[][4], int rows); // 写法一 void printMatrix(int (*m)[4], int rows); // 写法二,与写法一等价 // 错误写法:编译不通过 // void printMatrix(int m[][]); // 第二维不能省略 // void printMatrix(int **m); // 类型不匹配,int** 与 int (*)[4] 不同
注意int **m不能用来接收二维数组。二级指针指向的是指针数组,而二维数组在内存中是连续存放的,两者内存布局完全不同,强行传参会导致编译错误。如果想接受任意列宽的矩阵,通常要么用一维数组加手动计算下标,要么改用std::vector<std::vector<int>>。
四、现代C++的替代方案:让长度信息不被丢弃
传统三种写法的共同缺陷是长度信息在传递时丢失了。现代C++提供了几种更好的选择。
第一种是数组引用形参。通过引用传递数组,可以完整保留数组的类型和长度信息:
void func(int (&a)[5]); // 接收恰好含5个int的数组的引用
// 结合模板自动推导长度,同时获得首尾指针
template <typename T, size_t N>
void printAll(T (&a)[N]) {
for (size_t i = 0; i < N; ++i) {
std::cout << a[i] << ' ';
}
std::cout << '\n';
}模板版本特别实用,编译器会自动推导数组长度N,函数内部可以直接遍历,不需要额外的长度参数。配合C++17的范围for循环和std::begin/std::end,代码非常简洁。不过数组引用有一个限制:实参必须是真正的数组,不能传指针,且不同长度的数组会实例化出不同的函数版本,可能带来代码膨胀。
第二种是使用标准容器。std::array适合编译期确定长度的场景,它可以按值传递、按引用传递,本身知道自己的大小;std::vector适合动态长度场景,配合span(C++20)还可以实现零拷贝的只读视图传递:
#include <vector>
#include <iostream>
// 按const引用传递vector,避免拷贝,且长度信息完整
void process(const std::vector<int>& data) {
std::cout << "元素个数: " << data.size() << '\n';
}五、选择建议与总结
综合来看,三种传统写法的选择可以遵循这样的原则:如果函数语义上接受的是“一段数据的首地址”,用指针形式最直白;如果想强调接收的是数组,用不定长数组形式并配合长度参数;定长数组形式基本可以放弃,因为它既不提供检查又误导读者。
而在新项目中,优先考虑现代方案:长度固定的数据用std::array或模板数组引用,长度可变的用std::vector或std::span。这些方案的核心优势是把“长度”这一关键信息留在类型系统里,让编译器替你检查错误,而不是依赖运行时的自觉。理解数组退化这一底层机制,无论使用哪种写法,都能帮助你准确预判函数内部的行为边界,写出更可靠的C++代码。