std::array:C 数组会的它都会,还知道自己多大
C 风格数组能干活吗?当然能,这个问题咱们前一节说过了,实际上咱们从学 C 语言的第一天起就在用它。(不熟悉C的朋友仓库教程也有比较详细的C教程!)
但是,C 数组咱们用起来实在太容易吃到一坨大的了:传给函数就退化成指针、丢失长度信息、不能直接赋值、不能作为函数返回值、没有边界检查。这些问题不是"写的时候小心一点"就能避免的,它们是 C 数组设计上的固有缺陷。
std::array 就是为了解决这些问题而生的。它在栈上分配内存,和 C 数组一样紧凑高效,但拥有真正的值语义:可以拷贝、赋值、传参、返回,而且始终知道自己的大小。接下来咱们就来看看,为什么从 C++11 开始,固定大小的数组应该优先使用 std::array。
std::array 基本用法
咱们看 std::array 的定义:在 <array> 头文件中,需要两个模板参数:元素类型和固定大小。大小必须是编译期常量,和 C 数组一样,std::array 不会动态增长,它就是一块固定大小的连续内存。
#include <array>
#include <iostream>
int main()
{
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::cout << "大小: " << arr.size() << "\n";
std::cout << "为空? " << (arr.empty() ? "是" : "否") << "\n";
std::cout << "最大大小: " << arr.max_size() << "\n";
return 0;
}大小: 5
为空? 否
最大大小: 5size()、max_size()、empty() 这几个函数对固定大小的 std::array 来说看起来有点多余。它们存在的意义在于统一接口:让 std::array 和 std::vector 等容器拥有相同的访问方式,咱们写泛型代码时就不需要关心底层到底是固定大小还是动态大小。
std::array<int, 0>是合法的,此时empty()返回true。但大小为 0 的std::array在实际代码里极少出现。您要是需要一个"可能为空"的容器,请用std::vector。
访问元素
std::array 提供了多种元素访问方式,咱们最常用的是 [] 和安全的 at(),还有直接获取首尾元素和底层指针的便捷接口:
#include <array>
#include <iostream>
int main()
{
std::array<int, 5> arr = {10, 20, 30, 40, 50};
std::cout << "arr[0] = " << arr[0] << "\n"; // 无边界检查
std::cout << "arr.at(2) = " << arr.at(2) << "\n"; // 越界抛异常
std::cout << "front = " << arr.front() << "\n";
std::cout << "back = " << arr.back() << "\n";
int* p = arr.data(); // 获取裸指针
std::cout << "data()[3] = " << p[3] << "\n";
return 0;
}arr[0] = 10
arr.at(2) = 30
front = 10
back = 50
data()[3] = 40[] 和 at() 的区别很重要:arr[10] 在这个 5 元素的数组上是未定义行为——可能读到垃圾、可能崩溃、也可能表面上没事但数据已被悄悄破坏。而 arr.at(10) 会抛出 std::out_of_range 异常,咱们可以捕获它、处理它。
笔者的小PS:在开发阶段,建议对可能越界的下标使用
at()。发布版本里可以换回[]来避免异常开销——不过现代编译器在非越界情况下at()的额外开销几乎为零。或者全程用[],然后靠 AddressSanitizer 来抓越界 bug。
data() 返回指向底层元素存储的裸指针,咱们在与接受 int* 参数的 C 库函数交互时直接传入即可。
值语义:std::array 的核心优势
前面铺垫了这么多基本用法,接下来才是 std::array 真正甩开 C 数组的地方:它拥有值语义。咱们可以像操作 int 或 std::string 一样操作它:拷贝、赋值、传参、返回,全都没问题。
展开代码收起代码共 33 行
#include <array>
#include <iostream>
// 直接返回 std::array——C 数组做不到这一点
std::array<int, 5> make_array()
{
std::array<int, 5> result = {1, 2, 3, 4, 5};
return result;
}
// 按值传参——不会丢失大小信息
void print_array(std::array<int, 5> arr)
{
for (int x : arr) {
std::cout << x << " ";
}
std::cout << "\n函数内大小: " << arr.size() << "\n";
}
int main()
{
auto arr1 = make_array();
auto arr2 = arr1; // 直接拷贝——C 数组做不到
arr2[0] = 99;
std::cout << "arr1[0] = " << arr1[0] << "\n"; // 1,不受 arr2 影响
std::cout << "arr2[0] = " << arr2[0] << "\n"; // 99
print_array(arr1);
print_array(arr2);
return 0;
}arr1[0] = 1
arr2[0] = 99
1 2 3 4 5
函数内大小: 5
99 2 3 4 5
函数内大小: 5咱们看上面每一行都是 C 数组做不到的事。C 数组不能直接赋值(a = b 编译都过不了),不能作为函数返回值,作为函数参数时会退化成指针从而丢失长度。std::array 之所以能做到这些,是因为它是一个类,封装了内部的 C 数组,并提供了拷贝构造函数和拷贝赋值运算符。编译器知道如何复制这个对象,也知道它的大小,从根本上消除了数组退化问题。
按值传递
std::array会拷贝整个数组内容。咱们的数组要是很大(比如std::array<int, 10000>),应该用const引用:void process(const std::array<int, 10000>& arr)。对于小数组,按值传递的开销基本可以忽略。
C 数组 vs std::array:直接对比
咱们把 C 数组和 std::array 在常见操作上做一个直接对比:
| 操作 | C 数组 | std::array |
|---|---|---|
| 声明 | int arr[5]; | std::array<int, 5> arr; |
| 获取大小 | sizeof(arr)/sizeof(arr[0])(传参后失效) | arr.size()(始终有效) |
| 赋值 | 不支持 | arr2 = arr1 |
| 拷贝 | 手动 memcpy | auto copy = arr; |
| 传参 | 退化为指针,丢失大小 | 按值保留大小,或传引用 |
| 返回值 | 不可能 | 可以 |
| 边界检查 | 无 | arr.at(i) 抛异常 |
| 获取裸指针 | 自动退化 | arr.data()(显式) |
| 零开销 | 是 | 是 |
咱们看最后一行是关键:std::array 和 C 数组在内存布局和运行时性能上完全等价。所有额外能力(size()、at()、data()、值语义)全是编译期的零开销抽象,运行时没有任何额外的内存分配或函数调用开销。
您要是有兴趣,可以加
-O2编译一个分别使用 C 数组和std::array的遍历程序,对比汇编输出——两者生成的指令几乎一模一样。零开销抽象不是一句空话。
填充、交换与遍历
std::array 还有几个咱们会用到的实用操作,配上 STL 算法可以直接用:
展开代码收起代码共 31 行
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array<int, 5> a = {1, 2, 3, 4, 5};
std::array<int, 5> b = {10, 20, 30, 40, 50};
// fill —— 全部设为同一值
a.fill(0);
std::cout << "fill 后: ";
for (int x : a) { std::cout << x << " "; }
std::cout << "\n";
// swap —— 交换两个 array 的内容
a = {1, 2, 3, 4, 5};
a.swap(b);
std::cout << "swap 后 a: ";
for (int x : a) { std::cout << x << " "; }
std::cout << "\n";
// 配合 <algorithm>
std::array<int, 5> c = {5, 3, 1, 4, 2};
std::sort(c.begin(), c.end());
std::cout << "排序后: ";
for (int x : c) { std::cout << x << " "; }
std::cout << "\n";
return 0;
}fill 后: 0 0 0 0 0
swap 后 a: 10 20 30 40 50
排序后: 1 2 3 4 5fill() 在需要重置缓冲区时非常方便,一行搞定。swap() 的底层是逐元素交换,时间复杂度 O(n)。整个 STL 算法库都可以直接用在 std::array 上,std::sort、std::find、std::reverse,咱们传 begin() 和 end() 即可。
实战:用 std::array 重写 C 数组代码
咱们把之前用 C 数组完成的操作用 std::array 重新实现一遍,直观感受改进在哪里:
展开代码收起代码共 44 行
#include <algorithm>
#include <array>
#include <iostream>
// 函数签名清晰——类型和大小一目了然,不需要额外传长度
void print_stats(const std::array<int, 5>& data)
{
std::cout << "元素个数: " << data.size() << "\n";
auto [min_it, max_it] = std::minmax_element(data.begin(), data.end());
std::cout << "最小值: " << *min_it << "\n";
std::cout << "最大值: " << *max_it << "\n";
int sum = 0;
for (int x : data) { sum += x; }
std::cout << "平均: " << static_cast<double>(sum) / data.size() << "\n";
}
int main()
{
std::array<int, 5> scores = {85, 92, 78, 96, 88};
std::cout << "原始数据: ";
for (int x : scores) { std::cout << x << " "; }
std::cout << "\n\n";
print_stats(scores);
std::sort(scores.begin(), scores.end());
std::cout << "\n排序后: ";
for (int x : scores) { std::cout << x << " "; }
auto it = std::find(scores.begin(), scores.end(), 88);
if (it != scores.end()) {
std::cout << "\n找到 88,下标: " << (it - scores.begin());
}
std::reverse(scores.begin(), scores.end());
std::cout << "\n反转后: ";
for (int x : scores) { std::cout << x << " "; }
std::cout << "\n";
return 0;
}编译运行:
g++ -Wall -Wextra -std=c++17 std_array.cpp -o std_array && ./std_array原始数据: 85 92 78 96 88
元素个数: 5
最小值: 78
最大值: 96
平均: 87.8
排序后: 78 85 88 92 96
找到 88,下标: 2
反转后: 96 92 88 85 78咱们看改进是全方位的:print_stats 的参数是 const std::array<int, 5>&,类型和大小一目了然;所有 STL 算法直接使用,排序、查找、反转、求极值,都是一行调用;而且永远不会遇到数组退化丢失长度的问题。
咱们在代码中要是看到
void func(int arr[], int n)这种 C 风格签名,建议改成void func(const std::array<int, N>& arr)(大小固定)或void func(std::span<int> arr)(大小运行时确定)。两种方式都不会丢失长度信息,比手传n安全得多。
练习
练习一:重写 C 数组练习
请您把之前用 C 数组写的练习全部用 std::array 重写:声明、初始化、遍历、传参、查找最大值,体会两种写法在清晰度和安全性上的差别。
练习一自行练习就好。
练习二:成绩排序与统计
请您创建 std::array<int, 8> 存放一组成绩,使用 std::sort 排序,然后输出最高分、最低分和平均分。所有统计操作要求使用 <algorithm> 中的函数。
参考答案
展开代码收起代码共 32 行
#include <algorithm>
#include <array>
#include <iostream>
int main()
{
std::array<int, 8> arr = {32, 76, 43, 10, 54, 65, 87, 21};
std::cout << "原始数据: ";
int sum = 0;
for (int x : arr)
{
std::cout << x << " ";
sum += x;
}
double average = static_cast<double>(sum) / arr.size();
std::cout << "\n";
std::sort(arr.begin(), arr.end());
std::cout << "排序后数据: ";
for (int x : arr)
{
std::cout << x << " ";
}
std::cout << "\n";
auto [min_it, max_it] = std::minmax_element(arr.begin(), arr.end());
std::cout << "最小值: " << *min_it << "\n";
std::cout << "最大值: " << *max_it << "\n";
std::cout << "平均值: " << average << "\n";
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
原始数据: 32 76 43 10 54 65 87 21
排序后数据: 10 21 32 43 54 65 76 87
最小值: 10
最大值: 87
平均值: 48.5练习三:判断元素是否存在
请您编写 bool contains(const std::array<int, 5>& arr, int value),用 std::find 判断数组中是否包含指定值。在 main 中分别测试存在和不存在的值。
参考答案
#include <algorithm>
#include <array>
#include <iostream>
bool contains(const std::array<int, 5>& arr, int value)
{
return std::find(arr.begin(), arr.end(), value) != arr.end();
}
int main()
{
std::array<int, 5> arr = {32, 76, 43, 10, 54};
std::cout << "数组是否包含 43: " << (contains(arr, 43) ? "是" : "否") << "\n";
std::cout << "数组是否包含 99: " << (contains(arr, 99) ? "是" : "否") << "\n";
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra main.cpp -o main &&./main运行结果:
数组是否包含 43: 是
数组是否包含 99: 否下一篇:
std::array搞定了固定大小的容器,但字符串呢?C 风格字符串的那些坑(手动管理'\0'、容易越界、没有值语义)和 C 数组如出一辙。接下来咱们就认识std::string,看它怎么解决这些问题。