数组与 std::array
定义与作用
C++ 提供两种固定大小数组:
| 类型 | 来源 | 特点 |
|---|---|---|
T[N] 原生数组 | C 语言 | 会退化为指针,丢失大小信息 |
std::array<T, N> | C++11 | 封装类,保留大小,支持 STL 接口 |
原生数组的最大问题是退化(decay)——当数组作为函数参数时,它退化为首元素指针,长度信息完全丢失。std::array 通过封装彻底解决了这一问题,同时保持了与原生数组相同的性能(零开销抽象)。
核心原理
数组退化示意
内存布局对比
两者内存布局完全一致——连续存储,无额外开销。
std::array的.size()和.begin()/.end()在编译期确定,零运行时开销。
完整示例
示例一:原生数组退化陷阱与修复
场景说明:演示原生数组作为函数参数时的退化问题,以及 std::array 如何解决。
#include <iostream>
#include <array>
#include <algorithm>
#include <numeric>
// ---- 原生数组的问题 ----
// 这三个函数声明是等价的!都退化为 int*
void process_1(int arr[10]) { // 10 被忽略
std::cout << "sizeof(arr) in process_1: " << sizeof(arr) << std::endl;
}
void process_2(int arr[]) { // 完全等价
std::cout << "sizeof(arr) in process_2: " << sizeof(arr) << std::endl;
}
void process_3(int* arr) { // 同上
std::cout << "sizeof(arr) in process_3: " << sizeof(arr) << std::endl;
}
// 需要额外传递长度参数
double average(const int* arr, size_t size) {
return std::accumulate(arr, arr + size, 0.0) / size;
}
// ---- std::array 的优势 ----
template <size_t N>
double average_array(const std::array<int, N>& arr) {
if (N == 0) return 0.0;
return std::accumulate(arr.begin(), arr.end(), 0.0) / N;
}
// 或者用 std::array 的方法直接访问大小
double average_v2(const std::array<int, 5>& arr) {
return std::accumulate(arr.begin(), arr.end(), 0.0) / arr.size();
}
int main() {
// 原生数组
int raw_arr[5] = {10, 20, 30, 40, 50};
std::cout << "===== 原生数组 =====" << std::endl;
std::cout << "sizeof(raw_arr) in main: " << sizeof(raw_arr) << std::endl;
process_1(raw_arr); // 打印指针大小(8 字节)
std::cout << "平均分: " << average(raw_arr, 5) << std::endl;
// std::array
std::array<int, 5> std_arr = {10, 20, 30, 40, 50};
std::cout << "\n===== std::array =====" << std::endl;
std::cout << "sizeof(std_arr) in main: " << sizeof(std_arr) << std::endl;
std::cout << "std_arr.size(): " << std_arr.size() << std::endl;
std::cout << "平均分: " << average_array(std_arr) << std::endl;
// STL 兼容性:range-based for
std::cout << "\n遍历: ";
for (const auto& score : std_arr) {
std::cout << score << " ";
}
std::cout << std::endl;
// STL 兼容性:算法
auto [min_it, max_it] = std::minmax_element(std_arr.begin(), std_arr.end());
std::cout << "最低分: " << *min_it << ", 最高分: " << *max_it << std::endl;
// 边界检查(at 抛出异常,operator[] 不检查)
std::cout << "std_arr.at(2) = " << std_arr.at(2) << std::endl;
try {
std::cout << std_arr.at(10); // 抛出 std::out_of_range
} catch (const std::out_of_range& e) {
std::cout << "越界访问被捕获: " << e.what() << std::endl;
}
return 0;
}
预期输出:
===== 原生数组 =====
sizeof(raw_arr) in main: 20
sizeof(arr) in process_1: 8
sizeof(arr) in process_2: 8
sizeof(arr) in process_3: 8
平均分: 30
===== std::array =====
sizeof(std_arr) in main: 20
std_arr.size(): 5
平均分: 30
遍历: 10 20 30 40 50
最低分: 10, 最高分: 50
std_arr.at(2) = 30
越界访问被捕获: array::at: __n (which is 10) >= _Nm (which is 5)
逐段分析:
sizeof(raw_arr)在 main 中是 20(5 个 int),在函数内部变为 8(64 位指针大小)——退化已发生process_1/process_2/process_3三个函数签名完全等价,都只是int*std::array<int, 5>在函数间传递时保留大小信息,可通过模板参数N或.size()获取std::array完全兼容 STL 算法(begin/end、minmax_element等).at()提供带边界检查的访问,越界抛出std::out_of_range;operator[]与原生数组一样无检查
示例二:二维数组与 std::array 嵌套
场景说明:实现一个固定大小的矩阵类,对比原生二维数组和 std::array 嵌套。
#include <iostream>
#include <array>
#include <iomanip>
// 使用 std::array 实现 3x3 矩阵
using Matrix3 = std::array<std::array<double, 3>, 3>;
// 原生数组版本:退化严重,无法传递
void print_matrix_raw(const double matrix[3][3]) {
// matrix 是 double (*)[3],不是 double[3][3]
// 但第一维的 3 仍被忽略,实际是 double (*)[3]
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << std::setw(8) << matrix[i][j];
}
std::cout << std::endl;
}
}
void print_matrix(const Matrix3& matrix) {
for (const auto& row : matrix) {
for (double val : row) {
std::cout << std::setw(8) << std::setprecision(2)
<< std::fixed << val;
}
std::cout << std::endl;
}
}
Matrix3 add_matrices(const Matrix3& a, const Matrix3& b) {
Matrix3 result{};
for (size_t i = 0; i < result.size(); ++i) {
for (size_t j = 0; j < result[i].size(); ++j) {
result[i][j] = a[i][j] + b[i][j];
}
}
return result;
}
int main() {
Matrix3 m1 = {{{1.0, 2.0, 3.0},
{4.0, 5.0, 6.0},
{7.0, 8.0, 9.0}}};
Matrix3 m2 = {{{9.0, 8.0, 7.0},
{6.0, 5.0, 4.0},
{3.0, 2.0, 1.0}}};
std::cout << "矩阵 m1:" << std::endl;
print_matrix(m1);
std::cout << "\n矩阵 m2:" << std::endl;
print_matrix(m2);
auto m3 = add_matrices(m1, m2);
std::cout << "\nm1 + m2:" << std::endl;
print_matrix(m3);
// 属性查询
std::cout << "\n矩阵属性:" << std::endl;
std::cout << "行数: " << m3.size() << std::endl;
std::cout << "列数: " << m3[0].size() << std::endl;
std::cout << "总元素: " << m3.size() * m3[0].size() << std::endl;
return 0;
}
预期输出:
矩阵 m1:
1.00 2.00 3.00
4.00 5.00 6.00
7.00 8.00 9.00
矩阵 m2:
9.00 8.00 7.00
6.00 5.00 4.00
3.00 2.00 1.00
m1 + m2:
10.00 10.00 10.00
10.00 10.00 10.00
10.00 10.00 10.00
矩阵属性:
行数: 3
列数: 3
总元素: 9
逐段分析:
std::array<std::array<T, Cols>, Rows>是固定大小矩阵的零开销实现add_matrices按值返回——由于std::array是固定大小,编译器可以优化掉拷贝(RVO).size()在每一层返回对应维度的大小,编译期确定using Matrix3 = std::array<...>类型别名提高可读性
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
函数参数写 int arr[N] | N 被忽略,实际是 int* | 用 int (&arr)[N](数组引用)或 std::array<int,N>& |
sizeof(arr) 在函数内使用 | 返回指针大小而非数组大小 | 用 std::array 或显式传递长度 |
std::array 越界用 [] | 未定义行为,无错误提示 | 调试期用 .at(),发布期用 [](性能优先) |
| 大数组放在栈上 | 栈溢出 | 大数组用 std::vector 或堆分配 |
试图用 auto 推导数组 | auto arr2 = arr 得到 int* | 用 auto& arr2 = arr 或 std::array |
常见面试问题
原生数组作为函数参数时发生了什么?——退化为指针,第一维的长度信息丢失。
void f(int arr[10])等价于void f(int* arr)。如何防止数组退化?——使用数组引用
int (&arr)[N]或std::array<int, N>。后者更推荐。std::array和std::vector的区别?什么场景用哪个?——array固定大小、栈分配、无运行时开销;vector动态大小、堆分配、支持扩容。编译期已知大小且不大时用array。std::array的.size()有运行时开销吗?——无。size()返回模板参数N,编译期即可确定,通常被优化为常量。多维
std::array和原生多维数组的内存布局是否相同?——完全相同,都是连续的行优先(row-major)布局。std::array<std::array<T,Cols>, Rows>保证与T[Rows][Cols]相同的内存排布。
小结
- 原生数组会退化为指针,丢失长度信息;用
std::array彻底解决 std::array零开销抽象——内存布局与原生数组一致,.size()编译期求值- 支持 STL 接口:迭代器、
begin()/end()、at()边界检查 - 函数参数尽量用
std::array或数组引用T (&)[N] - 编译期未知大小或需要动态扩容时用
std::vector