如果你翻过那本薄薄的《The C Programming Language》。
你大概率见过一句话。
a[i] 等价于 *(a + i)
它看起来像一句小技巧。
但它其实是在交底。
在 C 的世界里。
数组不是“容器”。
数组更像一段地址。
再配上一句承诺。
我从这里起步。
你按步长走。
你就能找到第 i 个元素。
至于这段路到底有多长。
那是你自己的事。
这不是谁心狠。
是年代本来就紧。
早年的 Unix 机器。
内存按 KB 计。
拷贝一次。
就心疼一次。
所以数组必须连续。
地址必须可预测。
于是数组和指针绑在一起。
绑得很牢。
牢到你在函数参数里写 int a[10]。
它也能面不改色地变成 int*。
尺寸。
就在那一刻悄悄溜走。
后来 C++ 来了。
它先背着“兼容 C”这个包袱。
又慢慢学会谈“抽象”。
再后来。
STL 把话说得更直。
算法不该只服务一种容器。
它应该服务“序列”。
连续的。
不连续的。
只要你能迭代。
就都能吃。
可偏偏。
有一类序列一直别扭。
它固定长度。
它通常在栈上。
它不想分配。
但它又想当个现代对象。
想要 size()。
想要 begin()。
想要能拷贝。
想要能赋值。
想要能把“长度”写进接口里。
这条伏脉。
先在 Boost 里活过一段。
又借 TR1 过了桥。
最后才在 C++11 正名。
名字叫 std::array。
而我第一次吃“数组”的亏。
也正是从这段历史里长出来的。
不是越界。
是尺寸。
那会儿我刚进项目。
师傅丢给我一句话。
数组传参别写错长度。
我当时还挺自信。
数组嘛。
int a[10]。
长度就在那里。
谁还能写错。
后来我才知道。
你在函数参数里写的 int a[10]。
只是个写法。
它进去就变成了指针。
然后你就会发现。
长度这件事。
得靠人记。
C 风格数组:快
它真的很快。
内存就是一段连续的 T[N]。
没有分配。
没有间接层。
CPU 看到它。
就像看到一条直路。
但它也很“冷”。
冷到你一不小心。
就得拿手去摸雷。
先看这个最常见的坑。
#include <cstddef>
void dump(const int* p, std::size_t n);
void f(int a[10]) {
dump(a, 10);
}
你以为 a 是“长度为 10 的数组”。
不是。
这里的 a 实际类型是 int*。
10 只是给读代码的人看的。
编译器不会替你守住它。
你把 10 改成 8。
代码照样过。
然后你就等着线上来教你做人。
再看另一个老毛病。
数组不像“对象”。
它很多时候连赋值都不配。
int a[3] = {1, 2, 3};
int b[3] = {4, 5, 6};
// a = b; // 这句不让写
你想要的是“把三元素拷过去”。
语言给你的是沉默。
早年的折中:boost::array
后来 C++ 社区就开始自己补洞。
Boost 时代。
大家很务实。
能用库解决。
就先用库解决。
boost::array 就是那时候的老朋友。
它的想法特别朴素。
把 T[N] 包一层。
让它变成一个真正的类型。
有 begin/end。
有 size()。
能拷贝。
能赋值。
而且不引入额外开销。
这类东西后来进入 TR1。
再后来。
C++11 把它正儿八经收编。
名字叫 std::array。
std::array:像容器
你先别急着抬杠。
它确实还是固定长度。
它也确实不会 push_back。
但它终于把“数组”这件事。
从一种语法。
变成了一个类型。
#include <array>
std::array<int, 3> a{1, 2, 3};
你现在可以把它当对象用。
#include <array>
std::array<int, 3> a{1, 2, 3};
std::array<int, 3> b{4, 5, 6};
a = b;
这句 a = b 不是魔法。
就是把三个元素都拷过去。
你读起来心里有数。
编译器也能帮你兜底。
第一个真实场景:别再“丢尺寸”
工程里最常见的数组用途。
就是“固定长度的缓冲区”。
比如做协议。
做哈希。
做指纹。
你需要 16 字节。
就永远是 16 字节。
你不想要动态分配。
也不想把长度写成两份。
#include <array>
#include <cstdint>
using Digest = std::array<std::uint8_t, 16>;
void print_digest(const Digest& d);
这里我故意用 const Digest&。
因为它是对象。
我可以把“长度”藏在类型里。
调用点也不用再传一个 16。
少一个参数。
少一种出错方式。
你如果硬要拿到底层指针。
也行。
但你得自己说清楚。
#include <array>
#include <cstddef>
#include <cstdint>
void c_api_write(const std::uint8_t* p, std::size_t n);
void send(const std::array<std::uint8_t, 16>& d) {
c_api_write(d.data(), d.size());
}
data() 给你指针。
size() 给你长度。
你不用再猜。
也不用再靠注释续命。
第二个场景:静态表
有些表。
就是不会变。
你用 std::vector 也能写。
但你会多出“初始化”和“分配”的戏。
我做过一个老系统。
每次启动都要跑一堆初始化。
最后发现。
很多数据其实是常量表。
只是当年随手写成了 vector。
#include <array>
constexpr std::array<int, 4> dx{1, 0, -1, 0};
constexpr std::array<int, 4> dy{0, 1, 0, -1};
你看着它。
就像看着一块钉死的铁。
它不会在运行时“悄悄变大”。
也不会在某次重构里“突然需要 allocator”。
它就在那里。
简单。
可靠。
第三个场景:当它是成员时
很多人只把数组当局部变量。
但真正难受的。
是把数组放进结构体。
用 C 风格数组。
你很快会遇到复制语义的问题。
你想让它可拷贝。
可移动。
可比较。
#include <array>
#include <cstdint>
struct Header {
std::array<std::uint8_t, 4> magic;
std::uint16_t version;
};
这种时候。
std::array 很像“标准件”。
你不需要自己写拷贝。
也不用担心某个成员数组把默认操作删掉。
初始化这点小脾气
std::array 的初始化很讲道理。
但你得记住它还是聚合体。
#include <array>
std::array<int, 3> a{};
这句会把三个元素都值初始化。
对 int 来说。
就是全 0。
你想把它填成同一个值。
可以用 fill。
#include <array>
std::array<int, 3> a{};
a.fill(7);
这比你写三次 7。
更像工程代码。
它到底“零开销”吗
我见过有人不信。
说你包了一层。
怎么可能没开销。
这话放在别的库上。
我也会谨慎。
但 std::array 的目标就是。
把 T[N] 放进一个标准接口里。
它不会替你分配。
不会替你间接访问。
它的内存布局就是连续的 N 个 T。
你写 a[i]。
跟写 C 数组一样直接。
和 std::vector 的边界
std::vector 是“可变长”。
std::array 是“定死”。
所以它们不是谁替代谁。
更多时候。
是你要先回答一个问题。
这个长度到底是不是设计的一部分。
如果是。
那就把它放进类型里。
如果不是。
那就别装。
用 vector。
小尾声:它其实是在帮你写接口
写 C++ 写久了。
你会越来越在意一件事。
不是怎么把代码写得更炫。
是怎么把“错误的空间”写得更小。
std::array 最值钱的地方。
不是 size()。
也不是能和算法库一起玩。
是它让“长度”从注释。
变成类型的一部分。
这就是老工程师喜欢它的原因。
不是浪漫。
是省命。