☰
C++ STL array容器访问元素的几种方式
2026/10/21 14:06:16 网站建设 项目流程

前言


std::array是 C++11 引入的固定大小数组容器,定义在<array>中。它把原生数组“长度信息会退化丢失”的问题用类型系统补了回来:std::array<int, 5>的类型里就带着长度 5,传给函数时不会悄悄变成指针,size()也总是可用的。它不进行动态分配,元素连续存放,开销与原生数组基本一致。


访问元素的方法看起来很多:operator[]、at()、front()、back()、data()、std::get、迭代器、范围 for、结构化绑定。它们并不是可以随意替换的:有的做边界检查,有的不做;有的在空数组上未定义,有的返回指针,有的只能在编译期确定下标。用错一个,轻则抛异常,重则越界读写。


本文按“容器模型—下标访问—迭代器与结构化绑定—选择建议”的顺序,把std::array的元素访问方式逐个讲清,并指出每种方式在N == 0、越界、常量性等边界上的行为。代码以 C++17 为基准,涉及 C++20 的std::span会单独标注。文中不声称代码经过编译或运行,行为均按标准语义描述。


一、std::array 的模型


std::array<T, N>是一个聚合类型(aggregate),内部持有一个T[N]的数组,但没有用户声明的构造函数。因此它支持聚合初始化:


// 适用于 C++17
#include <array>

std::array<int, 5> a = {1, 2, 3}; // 前三个为 1,2,3,其余为 0
std::array<int, 5> b{}; // 全部为 0
std::array<int, 5> c; // 元素默认初始化,int 的值不确定

c的写法常被误解为“自动清零”。实际上std::array的元素是内置类型时,默认初始化不会赋初值,读取c[0]是未定义行为(undefined behavior, UB)。要清零就写{},或调用fill(0)。


std::array的长度是编译期常量,N必须是常量表达式。它没有push_back、resize之类的接口,因为容量固定。需要运行期改变大小就用std::vector。




访问方式是否检查边界越界行为返回类型



a[i]否UBreference

a.at(i)是抛std::out_of_rangereference

a.front()否空数组时 UBreference

a.back()否空数组时 UBreference

a.data()否不检查,返回首元素指针T*

std::get<I>(a)编译期检查下标越界编译失败reference

迭代器 / 范围 for否解引用 end 是 UB迭代器 / 元素引用



二、下标访问:operator[] 与 at


operator[]是最直接的访问方式,语义与原生数组一致:不检查下标,i >= N时行为未定义。它适合循环边界已经确定、性能敏感的场合:


// 适用于 C++17
#include <array>
#include <iostream>

int main() {
std::array<int, 4> values{10, 20, 30, 40};

for (std::size_t i = 0; i < values.size(); ++i) {
std::cout << values[i] << '\n';
}
return 0;
}

at()做边界检查,下标非法时抛出std::out_of_range:


// 适用于 C++17
#include <array>
#include <iostream>
#include <stdexcept>

int main() {
std::array<int, 4> values{10, 20, 30, 40};

try {
std::cout << values.at(2) << '\n';
std::cout << values.at(4) << '\n'; // 抛出 std::out_of_range
} catch (const std::out_of_range& e) {
std::cerr << "out of range: " << e.what() << '\n';
}
return 0;
}

at()的检查不是免费的,但也不必然带来可观测的开销:如果下标能被编译器证明在范围内,优化器可能消除检查。不要凭直觉断言“at()一定慢多少”,需要的话用std::chrono::steady_clock自己测量,并注意测量结果受优化级别、分支预测和缓存状态影响。


front()和back()分别返回首元素和尾元素的引用,不检查容器是否为空。对std::array<T, 0>调用它们是 UB,因为根本没有元素:


// 适用于 C++17
#include <array>

int main() {
std::array<int, 0> empty{};
// empty.front(); // UB:空数组没有首元素
// empty.back(); // UB:空数组没有尾元素
return empty.empty() ? 0 : 1; // empty() 在 N == 0 时为 true
}

data()返回指向内部数组首元素的指针,适合把数据交给 C 接口:


// 适用于 C++17
#include <array>
#include <cstddef>
#include <iostream>

int main() {
std::array<int, 5> values{1, 2, 3, 4, 5};
int* raw = values.data();

for (std::size_t i = 0; i < values.size(); ++i) {
std::cout << raw[i] << ' ';
}
std::cout << '\n';
return 0;
}

对N == 0的std::array,data()的返回值不保证是空指针,也不应被解引用。data()返回的指针在数组对象存活期间有效;std::array不能重新分配,所以不存在std::vector那种扩容导致指针失效的问题,但对象本身被销毁后指针同样悬垂。


三、编译期下标:std::get 与结构化绑定


std::get<I>(a)要求I是编译期常量,返回第I个元素的引用。I >= N时是编译错误,而不是运行期异常:


// 适用于 C++17
#include <array>
#include <iostream>
#include <string>

int main() {
std::array<std::string, 3> names{"Alice", "Bob", "Carol"};

std::cout << std::get<0>(names) << '\n';
std::cout << std::get<2>(names) << '\n';
// std::get<3>(names); // 编译错误:下标越界

std::get<1>(names) = "Bobby";
std::cout << names[1] << '\n';
return 0;
}

std::get也支持 const 和右值重载,对const std::array返回const T&,对右值返回T&&。它的价值在于下标在编译期确定,配合模板元编程或结构化绑定时很自然。


C++17 的结构化绑定可以直接把std::array拆成多个变量:


// 适用于 C++17
#include <array>
#include <iostream>

int main() {
std::array<int, 3> point{3, 4, 5};
auto [x, y, z] = point; // 拷贝出三个 int

std::cout << x << ' ' << y << ' ' << z << '\n';

auto& [rx, ry, rz] = point; // 引用绑定,可修改原数组
ry = 40;
std::cout << point[1] << '\n';
return 0;
}

结构化绑定的变量个数必须与N一致,否则编译失败。auto [x, y, z] = point;产生的是三个副本;要修改原数组必须写auto&。这一点和范围 for 里auto与auto&的区别是同一个道理。


四、迭代器、范围 for 与算法


std::array提供begin()/end()、cbegin()/cend()、rbegin()/rend(),迭代器在常见实现里就是指针,支持随机访问。范围 for 是最简洁的遍历方式:


// 适用于 C++17
#include <algorithm>
#include <array>
#include <iostream>

int main() {
std::array<int, 6> scores{72, 88, 95, 60, 88, 100};

for (const auto& score : scores) {
std::cout << score << ' ';
}
std::cout << '\n';

std::sort(scores.begin(), scores.end());

auto it = std::find(scores.begin(), scores.end(), 95);
if (it != scores.end()) {
std::cout << "index = " << (it - scores.begin()) << '\n';
}

std::cout << "size = " << scores.size() << '\n';
return 0;
}

std::sort需要随机访问迭代器,std::array满足;std::find返回迭代器,找不到时返回end()。用it - scores.begin()得到下标,类型是std::ptrdiff_t,输出到std::cout没有问题。


只读遍历时用const auto&避免每个元素一次拷贝。如果元素是廉价的int,按值auto也可以;如果是std::string或大结构体,按值遍历会产生明显复制。


std::array还可以配合<numeric>的算法,例如std::accumulate:


// 适用于 C++17
#include <array>
#include <numeric>

int main() {
std::array<int, 5> values{1, 2, 3, 4, 5};
int total = std::accumulate(values.begin(), values.end(), 0);
return total == 15 ? 0 : 1;
}

注意accumulate的初值0是int,累加结果也按int计算;如果元素是double,初值要写0.0,否则每次累加都可能发生截断。


作为函数参数


按值传递std::array会复制整个数组;按引用传递保留长度信息:


// 适用于 C++17
#include <array>
#include <cstddef>

template <std::size_t N>
void print_all(const std::array<int, N>& values) {
for (std::size_t i = 0; i < N; ++i) {
// 读取 values[i]
}
}

void print_six(const std::array<int, 6>& values) {
// 只接受长度为 6 的数组
}

模板参数N让函数对任意长度工作,同时保留类型安全;写死6则只接受那一种长度。不要把std::array传给期望const int*的函数,否则又会退化成指针,长度信息丢失。


C++20 引入了std::span,可以用一个轻量视图同时表达“指针 + 长度”,适合接受任意连续序列:


// 需要 C++20 及以上
#include <cstddef>
#include <span>

void print(std::span<const int> values) {
for (std::size_t i = 0; i < values.size(); ++i) {
// 读取 values[i]
}
}

std::span不拥有数据,调用者必须保证底层数组在调用期间存活。它需要 C++20 及以上的标准库支持,使用前应确认工具链。


完整示例:统计分数分布


// 适用于 C++17
#include <algorithm>
#include <array>
#include <cstddef>
#include <iostream>

int main() {
std::array<int, 10> scores{72, 88, 95, 60, 88, 100, 45, 77, 88, 91};

std::array<int, 5> buckets{}; // 0-59, 60-69, 70-79, 80-89, 90-100

for (int score : scores) {
std::size_t index = 0;
if (score >= 90) {
index = 4;
} else if (score >= 80) {
index = 3;
} else if (score >= 70) {
index = 2;
} else if (score >= 60) {
index = 1;
}
++buckets[index];
}

const std::array<const char*, 5> labels{
"0-59", "60-69", "70-79", "80-89", "90-100"};

for (std::size_t i = 0; i < buckets.size(); ++i) {
std::cout << labels[i] << ": " << buckets[i] << '\n';
}

std::sort(scores.begin(), scores.end());
std::cout << "lowest = " << scores.front() << '\n';
std::cout << "highest = " << scores.back() << '\n';
std::cout << "median = " << scores[scores.size() / 2] << '\n';

return 0;
}

这里buckets用{}清零,labels用聚合初始化;scores.front()和back()在排序后分别是最小值和最大值。scores.size() / 2一定小于 10,所以operator[]安全。若数组长度可能为 0,就不能这样调用front()/back()。


常见坑点



  1. ❌ 用values[values.size()]访问尾后位置,认为它和end()一样安全。


✅operator[]不检查边界,尾后位置是 UB;遍历用i < size(),需要检查时用at()。



  1. ❌ 声明std::array<int, 5> a;后直接读取元素,以为会自动清零。


✅ 用std::array<int, 5> a{};值初始化,或调用a.fill(0)。



  1. ❌ 对std::array<T, 0>调用front()、back()或解引用data()。


✅ 先用empty()判断;空数组只做遍历(此时begin() == end())或传递,不解引用。



  1. ❌ 写std::get<5>(a)期望运行期抛异常。


✅std::get的下标必须是编译期常量,越界直接编译失败;运行期检查用at()。



  1. ❌for (auto value : arr)后修改value,期望改变数组元素。


✅ 要修改写for (auto& value : arr);只读用const auto&。



  1. ❌ 把std::array传给void f(const int* p),在函数里用sizeof(p)求长度。


✅ 传const std::array<T, N>&或std::span(C++20),保留长度信息。



  1. ❌ 保存data()返回的指针后,认为它永远有效。


✅ 指针只在数组对象存活期间有效;对象销毁后立即失效,不要跨作用域保存。



  1. ❌ 用at()却不处理std::out_of_range,异常一路逃逸导致程序终止。


✅ 在可能越界的入口捕获并处理,或先用size()校验下标。


总结




需求推荐方式注意



已确认下标合法的高频访问operator[]越界是 UB

需要运行期边界检查at()越界抛std::out_of_range

首元素 / 尾元素front()/back()空数组时 UB

编译期已知下标std::get<I>越界编译失败

拆成多个变量结构化绑定(C++17)数量必须等于N

交给 C 接口data()不检查长度,注意空数组

遍历范围 for +const auto&修改元素用auto&

传参保留长度const std::array<T, N>&或模板避免退化为指针



std::array的访问方式没有绝对优劣,关键是把“边界是否已知”“是否需要检查”“是否要修改”三个问题先回答清楚。下标确定且经过验证时用operator[],边界来自外部输入时用at(),编译期固定位置用std::get,遍历时根据是否需要修改选择auto&或const auto&。只要不越界、不读未初始化元素、不在空数组上取首尾,std::array就是一个几乎没有额外成本、又比原生数组更安全的固定长度容器。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询