news 2026/11/5 6:52:03

std::array 与裸数组、vector 怎么选

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
std::array 与裸数组、vector 怎么选

std::array经常被当成「多此一举的包装」:既然int a[5]就够了,为什么要包一层?答案是两个词:零开销和不退化。零开销的意思是sizeof(std::array<int, 5>) == sizeof(int[5]),一个字节都没多;不退化指的是它把裸数组最容易丢掉的东西(长度)留住了。这篇用sizeof实测、用退化过程图说明裸数组的坑,最后给一张按场景选容器的决策表。

官方文档:std::array (cppreference)

1. 引子:一个「传进函数就变指针」的数组

void print_size(int arr[5]) { // 这里的 5 是骗人的 std::cout << sizeof(arr) << '\n'; // 打印的是指针大小,不是 20 }

形参写成int arr[5],编译器会把它改写成int* arr——那个5被彻底忽略。于是函数里既不知道长度,也没法遍历。std::array解决的正是这个问题,而且不额外收费。先验证「不额外收费」这件事:

// demo.cpp — 编译: g++ -std=c++17 -Wall -O2 demo.cpp -o demo #include <array> #include <cstddef> #include <iostream> #include <iterator> int main() { std::array<int, 5> a{1, 2, 3, 4, 5}; int raw[5]{1, 2, 3, 4, 5}; std::cout << "sizeof(std::array<int,5>) = " << sizeof(a) << '\n'; std::cout << "sizeof(int[5]) = " << sizeof(raw) << '\n'; std::cout << "sizeof(int) * 5 = " << sizeof(int) * 5 << '\n'; // 零开销:封装没有引入任何额外字节 static_assert(sizeof(std::array<int, 5>) == sizeof(int[5]), "零开销封装"); static_assert(std::size(a) == 5, "std::size 对 std::array 也适用"); constexpr std::array<int, 5> kFixed{1, 2, 3, 4, 5}; static_assert(kFixed.size() == 5, "size() 是 constexpr"); std::array<int, 5> copy_of_a{a}; // 可以整体拷贝 std::cout << "整体拷贝后 copy_of_a[4] = " << copy_of_a[4] << '\n'; return 0; }
sizeof(std::array<int,5>) = 20 sizeof(int[5]) = 20 sizeof(int) * 5 = 20 整体拷贝后 copy_of_a[4] = 5

20 == 20 == 20:std::array内部就是一个裸数组作为唯一成员,没有任何长度字段、没有虚表、没有指针间接,所以 sizeof 完全一致。编译期能力也跟着裸数组走:.size()是constexpr,static_assert能在编译期就把它验掉。

2. 裸数组的坑:退化与不能赋值

退化(decay)是裸数组最本质的问题:在大多数表达式里,数组名会自动转换成指向首元素的指针,长度信息随之消失。

裸数组退化过程 main 里: int raw[5] = {1, 2, 3, 4, 5}; ┌────┬────┬────┬────┬────┐ │ 1 │ 2 │ 3 │ 4 │ 5 │ sizeof(raw) == 20 └────┴────┴────┴────┴────┘ std::size(raw) == 5 ▲ │ 传参:形参写成 int* 或 int[] 时发生退化 │ void f(int* p) ← 函数只收到一个地址 ┌────┐ sizeof(p) │ 8 │ = 指针大小(64 位),长度信息丢了 └────┘ std::array 走的是同一条内存布局,但它是一个「对象」: void g(const std::array<int, 5>& arr) → arr.size() 依然可用 void h(const std::array<int, N>& arr) → 模板推导,N 也跟着传进去

除了退化,裸数组还有两个动作做不了:不能整体赋值、不能用另一个数组初始化(int b[5] = a;直接编译错误)。std::array是聚合类型,有隐含的拷贝构造和拷贝赋值,这两个动作都能做。Core Guidelines 的SL.con.1就是把这条写成了规则:优先用std::array或std::vector,而不是 C 风格数组。

// demo.cpp — 编译: g++ -std=c++17 -Wall -O2 demo.cpp -o demo #include <array> #include <cstddef> #include <iostream> #include <iterator> int main() { std::array<int, 3> a{1, 2, 3}; std::array<int, 3> b = a; // OK:整体拷贝 b[0] = 100; // b 与 a 各自独立 std::cout << "a[0]=" << a[0] << " b[0]=" << b[0] << '\n'; std::array<int, 3> c{}; c = a; // OK:可以赋值 std::cout << "赋值后 c[0]=" << c[0] << '\n'; int raw1[3]{1, 2, 3}; int raw2[3]{}; // int raw3[3] = raw1; // 反例,不要这么写:编译错误,裸数组不能这样初始化 // raw2 = raw1; // 反例,不要这么写:编译错误,裸数组不能整体赋值 for (std::size_t i = 0; i < std::size(raw1); ++i) raw2[i] = raw1[i]; // 只能逐元素搬 std::cout << "裸数组逐元素拷贝后 raw2[0]=" << raw2[0] << '\n'; return 0; }
a[0]=1 b[0]=100 赋值后 c[0]=1 裸数组逐元素拷贝后 raw2[0]=1

3. 三者对比与选型

先看硬指标。std::array和裸数组的差别只在「类型安全」上,和vector的差别在「存储位置」上:

对比项std::array<T, N>T[N](裸数组)std::vector<T>
大小是否编译期固定是,N是模板参数是否,运行期可变
存储位置栈上(随对象走)栈上堆上(对象本身只有三个指针)
是否知道自身长度是,.size()且是constexpr否,退化后丢失是,size()运行期查询
能否拷贝 / 赋值能(整体拷贝)不能能(深拷贝)
传给函数不退化,长度信息保留退化成指针传引用,本身也不退化
sizeofsizeof(T) * Nsizeof(T) * N24(三个指针,实现定义)
额外开销00一次堆分配 + 一次指针间接
边界检查.at()有,[]无都没有.at()有,[]无

两个结论直接从表里读出来:std::array相对裸数组是纯赚——同样的字节数、同样的访问速度,换来长度信息、整体拷贝和模板推导能力,没有任何代价;vector相对两者的代价是「一次堆分配 + 一层指针间接」,但换来运行期可变的大小。只有当数组大小编译期就确定时,std::array才成立。

按场景选:

场景选择理由
元素个数编译期已知(配置表、查表数组、固定维度的矩阵行)std::array<T, N>栈上分配、无堆开销、长度信息保留
元素个数运行期才知道(读文件、网络包、用户输入)std::vector<T>只有它能运行期增长
需要和 C API 交互(memcpy/ 三方库 / 系统调用)std::array或vector的.data()二者都保证连续存储,.data()拿得到裸指针
必须使用 C 风格数组类型的地方(如T arr[3]的旧接口)裸数组没办法;但可以包一层std::array再取.data()
超大且栈空间紧张(几十 MB)std::vector<T>栈通常只有几 MB,大数组会直接栈溢出

官方文档:C++ Core Guidelines · SL.con.1 (isocpp.github.io)

4. std::array 作为函数参数:长度信息不丢

这是std::array最实用的地方。把形参写成template <std::size_t N>加const std::array<T, N>&,编译器会把N一并推导出来,函数体里既有长度也能遍历,而且对任意N都只写一份代码:

// demo.cpp — 编译: g++ -std=c++17 -Wall -O2 demo.cpp -o demo #include <array> #include <cstddef> #include <iostream> #include <iterator> // 裸数组按值传参 → 退化成指针,长度信息丢失 void take_raw_ptr(int* p) { std::cout << "裸数组(指针): sizeof(p)=" << sizeof(p) << "(指针大小,不是数组大小)\n"; } // 传引用能保住长度,但长度被写死成 5 void take_raw_ref(int (&arr)[5]) { std::cout << "裸数组(引用): 元素个数=" << std::size(arr) << ",只能接受恰好 5 个元素的数组\n"; } // std::array + 模板参数 N:长度信息保留,且任意 N 都能用 template <std::size_t N> void take_array(const std::array<int, N>& arr) { std::cout << "std::array: 元素个数=" << arr.size() << ",sizeof=" << sizeof(arr) << "(编译期就知道)\n"; } int main() { int raw[5]{1, 2, 3, 4, 5}; const std::array<int, 5> a{1, 2, 3, 4, 5}; const std::array<int, 3> b{1, 2, 3}; std::cout << "main 里 sizeof(raw)=" << sizeof(raw) << '\n'; take_raw_ptr(raw); take_raw_ref(raw); take_array(a); take_array(b); // 同一份模板,换 N 直接复用 return 0; }
main 里 sizeof(raw)=20 裸数组(指针): sizeof(p)=8(指针大小,不是数组大小) 裸数组(引用): 元素个数=5,只能接受恰好 5 个元素的数组 std::array: 元素个数=5,sizeof=20(编译期就知道) std::array: 元素个数=3,sizeof=12(编译期就知道)

三种写法的取舍很清楚:

传参写法长度信息能否接受任意长度代价
void f(int* p)丢失是(但要额外传长度)最危险:长度和指针可能对不上
void f(int (&arr)[5])保留否,长度写死每个长度都要重载一份
void f(const std::array<int, N>&)保留是,N自动推导模板实例化,代码量随 N 增长

take_raw_ptr那种「裸指针 + 额外长度参数」的写法还有个隐患:两个参数各说各话,调用方传错也没人拦。std::array把长度编码进类型,sizeof、size()、边界检查全都在编译期就确定了,这也是 Core GuidelinesES.27(用std::array或stack_array表示栈上的数组)想表达的东西。

官方文档:std::size (cppreference)

5. 完整示例:三种容器在同一个程序里各司其职

一个程序里同时出现三者是很正常的:固定个数的配置用std::array,运行期才知道多少条的数据用std::vector,最后统一用.data()交给只认「指针 + 长度」的 C 风格函数:

// demo.cpp — 编译: g++ -std=c++17 -Wall -O2 demo.cpp -o demo #include <array> #include <cstddef> #include <iostream> #include <numeric> #include <vector> // 模拟一个 C API:只认「指针 + 长度」 int c_style_sum(const int* data, std::size_t n) { int total = 0; for (std::size_t i = 0; i < n; ++i) total += data[i]; return total; } // 长度信息写在类型里:N 由编译器推导 template <std::size_t N> int sum_all(const std::array<int, N>& arr) { int total = 0; for (const int x : arr) total += x; return total; } int main() { const std::array<int, 4> fixed{10, 20, 30, 40}; // 个数编译期已知 → 栈上,零堆开销 std::vector<int> dynamic; // 个数运行期才知道 → 堆上 for (int i = 1; i <= 5; ++i) dynamic.push_back(i * 100); std::cout << "std::array 求和 = " << sum_all(fixed) << '\n'; std::cout << "vector 求和 = " << std::accumulate(dynamic.begin(), dynamic.end(), 0) << '\n'; std::cout << "两者一起交给 C API = " << c_style_sum(fixed.data(), fixed.size()) + c_style_sum(dynamic.data(), dynamic.size()) << '\n'; return 0; }
std::array 求和 = 100 vector 求和 = 1500 两者一起交给 C API = 1600

三个容器共存的方式就是这样:能用std::array的地方绝不用vector(省掉堆分配和指针间接,数据就在栈上、缓存友好);只有长度运行期可变时才用vector;两者都能用.data()卸掉封装去适配 C 接口,std::array的.data()返回的就是内部那个裸数组的首地址。

官方文档:std::array::data (cppreference)

6. 延伸阅读

  • std::array (cppreference):成员列表与「不退化、可拷贝」的说明,注意它是聚合类型。
  • Array declaration (cppreference):裸数组的退化规则(array-to-pointer conversion)原文。
  • std::vector (cppreference):堆分配、可增长,与这篇的选型对照看。
  • std::size (cppreference):C++17 起对裸数组和容器通用的长度获取方式。
  • C++ Core Guidelines (isocpp.github.io):SL.con.1(容器优先于 C 数组)、ES.27(栈数组用std::array)。

本知识库内的相关篇目:

  • 《vector 扩容策略与迭代器失效全解》 —— vector 的 size 与 capacity 为什么分开、push_back 触发的扩容到底做了什么
  • 《std::string 高频用法与 SSO 小字符串优化》 —— 为什么创建一个小字符串完全不碰堆、string 对象里那 32 个字节都装了什么?这篇用内存布局图和「数
  • 《list 与 forward_list:链表真的比 vector 快吗》 —— 用计数分配器和实测耗时把 std::list / std::forward_list 的真实开销算清楚

7. 一句话总结

std::array<T, N>就是「内嵌了一个T[N]的对象」,所以sizeof与裸数组完全相同、访问也没有任何间接,却补上了裸数组丢失的长度信息、整体拷贝赋值能力和模板推导能力;固定大小编译期已知就用它,运行期才知道大小就用std::vector,只有要适配 C 风格数组类型的旧接口时才写裸数组。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/1 19:08:37

点估计推导过程详解:从矩估计到最大似然估计

1. 什么是点估计现实中我们经常不知道总体参数&#xff0c;却能抽到样本。例如&#xff1a;想知道某厂灯泡的平均寿命 μ&#xff08;总体均值&#xff09;&#xff0c;不可能把所有灯泡都点亮测一遍&#xff1b;想知道一枚硬币是否均匀&#xff0c;即正面概率 p&#xff0c;只…

作者头像 李华
网站建设 2026/10/24 21:56:28

闪灵卡组残缺怎么办?三套实测替代方案解析

1. 项目概述&#xff1a;当“闪灵”卡组缺了那张决定胜负的卡&#xff0c;你该怎么办&#xff1f;“闪灵卡组”这个词&#xff0c;在当前主流卡牌对战圈里已经不是新鲜概念&#xff0c;它特指以《游戏王》中「闪灵」系列怪兽为核心构建的战术体系——主打高速展开、资源循环与压…

作者头像 李华
网站建设 2026/10/24 9:27:18

ARM交叉编译踩坑:-march参数写错导致Illegal instruction

1. 从一条编译报错说起&#xff1a;为什么-march写错会让人抓狂第一次在 ARM 平台上做交叉编译的人&#xff0c;大概率都会经历这样一个瞬间&#xff1a;明明代码在 x86 主机上编译得好好的&#xff0c;换成交叉工具链之后&#xff0c;要么直接报unrecognized command line opt…

作者头像 李华
网站建设 2026/10/22 20:07:13

大文件上传超时?分片上传解决方案全解析

在日常做后台管理、网盘系统、素材上传功能时&#xff0c;很多前端同学都遇到过同一个头疼问题&#xff1a;小文件上传秒成功&#xff0c;一旦上传几十MB甚至几百MB的大文件&#xff0c;大概率直接报错。要么接口请求超时&#xff0c;要么网络波动导致上传失败&#xff0c;用户…

作者头像 李华
网站建设 2026/10/25 13:06:32

C语言入门:从输入输出到条件判断

昨天讲了系统安装&#xff0c;今天可以继续讲编写程序了。昨日的作业要先去做下, 昨日这个作业, 是去编写C语言程序, 然后输出以下这样的图形。程序如下&#xff1a;怎么样&#xff1f;很简单吧&#xff1f;去重新复习一回, 每一行当中第二个双引号的前面的那个 \n 究竟是什么,…

作者头像 李华