03 / 80 · C++11 · 约 8 分钟
数组参数退化:声明调整与 array-to-pointer conversion
数组并不是指针,但数组形式的函数形参会被调整成指针类型,普通调用中数组实参也常转换为首元素指针。边界不会自动跟随;要保留长度,使用数组引用、容器或 C++20 的 span。
两条规则产生相似外观
void f(int a[10]) 中的形参类型会调整为 int*,与 void f(int* a) 声明同一个函数。括号里的 10 不构成调用时的长度检查,传入只有两个元素的数组也不会因这个数字被拒绝。函数体里 a 从一开始就是指针变量。
这是声明层面的参数调整;调用处的数组到指针转换是另一条规则。真正的数组对象拥有固定数量的元素,指针对象只是保存指向位置。把二者统称为“数组就是指针”,会遗漏赋值、sizeof、取地址以及模板推导方面的重要区别。
用类型保留边界
template<std::size_t N> void f(int (&a)[N]) 接收数组引用,N 可以从实参推导,引用绑定不丢失数组类型。固定长度接口也能写成 int (&a)[3],编译器会拒绝不匹配的长度。示例用这个规则计算元素数,不依赖任何指针宽度。
std::array<T, N> 可以按值复制整个数组,也能按引用传入;std::vector 保存运行时长度。C++20 的 std::span<T> 表示不拥有元素的连续区间,兼顾指针接口的轻量与长度信息,但仍需调用者保证被引用存储存活。
多维数组只转换最外层
int grid[2][3] 转换后是指向一行的 int (*)[3],不是 int**。行宽参与指针运算,指针加一前进一整行;二维指针通常指向另外一组指针对象,表示方式完全不同。不能用强制转换修复错误的接口模型。
sizeof、对数组取地址以及绑定数组引用等场景会保留数组身份。设计接口时先问是否需要所有权、是否需要修改和是否需要长度,再选择容器引用或视图;不要把原本清晰的数组边界先丢掉,随后在函数内部猜回来。
容易答错的地方
- 对数组形式形参使用 sizeof 得到的是调整后指针类型的大小,除以元素大小也不能恢复元素数。
- 返回指向局部数组的指针或 span 不会延长数组生命期;保留长度不等于保留存储。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <cstddef>
#include <iostream>
#include <type_traits>
void set_first(int values[3]) {
static_assert(std::is_same<decltype(values), int*>::value, "adjusted");
values[0] = 9;
}
template<class T, std::size_t N>
constexpr std::size_t count_of(const T (&)[N]) { return N; }
int main() {
int values[3] = {1, 2, 3};
static_assert(count_of(values) == 3, "bound retained");
set_first(values);
assert(values[0] == 9);
std::cout << count_of(values) << ' ' << values[0] << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-array-decay.cpp -o example && ./example预期结果
3 9
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为 int matrix[2][3] 写一个只读取它、同时保留行列数的函数模板参数,并说明为什么 int** 不合适。
查看参考答案
写成 template<std::size_t R, std::size_t C> void inspect(const int (&m)[R][C]),调用时推导 R 为 2、C 为 3。矩阵直接连续保存六个 int,并不保存两个行指针;int** 的间接寻址要求存在指针对象,因此不是这一布局的正确类型。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。