在C++中, 将原生数组传递给函数时, 一个常见的问题是在函数内部无法正确获取数组的原始大小.

例如, 以下代码的输出可能不符合直觉:

#include <iostream>

void analyzeArray(int arr[10]) {
    // 试图在函数内获取大小
    std::cout << "函数内 sizeof(arr): " << sizeof(arr) << std::endl;
}

int main() {
    int my_array[10] = {0};
    // 在定义的作用域内获取大小
    std::cout << "函数外 sizeof(my_array): " << sizeof(my_array) << std::endl;
    
    analyzeArray(my_array);
    return 0;
}

在64位系统上, 典型的输出为:

函数外 sizeof(my_array): 40
函数内 sizeof(arr): 8

sizeof(my_array) 正确地返回了数组的总字节数(4字节/int * 10个元素 = 40字节), 而 sizeof(arr) 返回的却是指针的大小. 这种现象被称为“数组退化” (Array Decay).

数组退化的原理

根据C++标准, 当数组作为函数参数按值传递时, 其类型会被自动调整 (adjusted) 为指向其首元素的指针类型.

因此, void analyzeArray(int arr[10]) 的函数签名在功能上与 void analyzeArray(int* arr) 完全等价. 方括号中的大小 10 会被编译器忽略, 函数实际接收的是一个 int* 指针.

这就是为什么在函数内部 sizeof(arr) 计算的是指针类型的大小, 而不是原始数组的大小. 这个特性源于C语言, 虽然在某些情况下有用, 但也导致了数组尺寸信息的丢失.

解决方案: 使用数组引用防止退化

要解决尺寸信息丢失的问题, 就必须阻止数组退化. 这可以通过将函数参数声明为数组的引用 (Reference to an Array) 来实现.

其语法如下:

T (&a)[N]

这个声明的含义是: a 是一个引用, 它引用的对象是一个大小为 N、元素类型为 T 的数组.

当以引用的方式传递数组时, 函数接收到的是对原始数组对象本身的绑定, 而不是一个指向其首元素的指针. 因此, 数组的完整类型 T[N] 得以保留, 编译器就能从中得知其尺寸.

通用实现: 结合模板在编译期获取大小

为了创建一个可以获取任何类型和大小的数组尺寸的通用函数, 可以将数组引用与函数模板相结合.

#include <cstddef> // for std::size_t

template<class T, std::size_t N>
constexpr std::size_t getArraySize(T (&a)[N]) noexcept
{
    return N;
}

该模板函数的工作机制如下:

  1. 模板参数: 函数模板有两个参数: 类型参数 T 用于匹配数组的元素类型, 非类型模板参数 std::size_t N 用于匹配数组的大小.
  2. 函数参数: T (&a)[N] 确保了只有真正的数组才能作为实参, 并且在传参时不发生退化.
  3. 模板参数推导: 当一个具体数组被传入时, 编译器会进行参数推导. 例如, 对于一个 int numbers[10] 类型的数组, 编译器会将其类型 int[10]T (&a)[N] 进行匹配, 从而成功推导出 Tint, N10.
  4. 编译期计算: 函数返回推导出的 N 值. constexpr 关键字确保了只要传入的数组大小是编译期已知的, 整个函数调用就会在编译时完成, 直接将结果(一个常量)嵌入到代码中, 没有任何运行时开销.

现代化C++实践: 使用 std::size

上述模板函数的实现是一种非常重要的C++编程模式. 自C++17起, 这个功能已被标准化, 并作为 std::size 函数提供在 <array><iterator> 头文件中.

在现代C++项目中, 推荐直接使用 std::size:

#include <iterator> // C++17 or <array> in C++20
#include <iostream>

int main() {
    int numbers[] = {1, 2, 3, 4, 5};
    std::cout << "使用 std::size 获取大小: " << std::size(numbers) << std::endl; // 输出 5
}

std::size 的底层实现原理与我们手动编写的 getArraySize 模板函数是相同的.

总结

  • 数组退化: C++中, 按值传递的数组形参会被调整为指针, 导致尺寸信息丢失.
  • 数组引用: 通过将形参声明为 T (&a)[N], 可以阻止数组退化, 保留完整的类型信息.
  • 模板推导: 结合模板, 可以从不退化的数组类型中自动推导出其大小 N.
  • std::size: C++17标准提供了 std::size 函数, 作为获取原生数组大小的标准、安全且高效的方式.