ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

std::array 与裸数组、vector 怎么选 std::array经常被当成「多此一举的包装」既然int a[5]就够了为什么要包一层答案是两个词零开销和不退化。零开销的意思是sizeof(std::arrayint, 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 -stdc17 -Wall -O2 demo.cpp -o demo #include array #include cstddef #include iostream #include iterator int main() { std::arrayint, 5 a{1, 2, 3, 4, 5}; int raw[5]{1, 2, 3, 4, 5}; std::cout sizeof(std::arrayint,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::arrayint, 5) sizeof(int[5]), 零开销封装); static_assert(std::size(a) 5, std::size 对 std::array 也适用); constexpr std::arrayint, 5 kFixed{1, 2, 3, 4, 5}; static_assert(kFixed.size() 5, size() 是 constexpr); std::arrayint, 5 copy_of_a{a}; // 可以整体拷贝 std::cout 整体拷贝后 copy_of_a[4] copy_of_a[4] \n; return 0; }sizeof(std::arrayint,5) 20 sizeof(int[5]) 20 sizeof(int) * 5 20 整体拷贝后 copy_of_a[4] 520 20 20std::array内部就是一个裸数组作为唯一成员没有任何长度字段、没有虚表、没有指针间接所以 sizeof 完全一致。编译期能力也跟着裸数组走.size()是constexprstatic_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::arrayint, 5 arr) → arr.size() 依然可用 void h(const std::arrayint, N arr) → 模板推导N 也跟着传进去除了退化裸数组还有两个动作做不了不能整体赋值、不能用另一个数组初始化int b[5] a;直接编译错误。std::array是聚合类型有隐含的拷贝构造和拷贝赋值这两个动作都能做。Core Guidelines 的SL.con.1就是把这条写成了规则优先用std::array或std::vector而不是 C 风格数组。// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo #include array #include cstddef #include iostream #include iterator int main() { std::arrayint, 3 a{1, 2, 3}; std::arrayint, 3 b a; // OK整体拷贝 b[0] 100; // b 与 a 各自独立 std::cout a[0] a[0] b[0] b[0] \n; std::arrayint, 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]13. 三者对比与选型先看硬指标。std::array和裸数组的差别只在「类型安全」上和vector的差别在「存储位置」上对比项std::arrayT, NT[N]裸数组std::vectorT大小是否编译期固定是N是模板参数是否运行期可变存储位置栈上随对象走栈上堆上对象本身只有三个指针是否知道自身长度是.size()且是constexpr否退化后丢失是size()运行期查询能否拷贝 / 赋值能整体拷贝不能能深拷贝传给函数不退化长度信息保留退化成指针传引用本身也不退化sizeofsizeof(T) * Nsizeof(T) * N24三个指针实现定义额外开销00一次堆分配 一次指针间接边界检查.at()有[]无都没有.at()有[]无两个结论直接从表里读出来std::array相对裸数组是纯赚——同样的字节数、同样的访问速度换来长度信息、整体拷贝和模板推导能力没有任何代价vector相对两者的代价是「一次堆分配 一层指针间接」但换来运行期可变的大小。只有当数组大小编译期就确定时std::array才成立。按场景选场景选择理由元素个数编译期已知配置表、查表数组、固定维度的矩阵行std::arrayT, N栈上分配、无堆开销、长度信息保留元素个数运行期才知道读文件、网络包、用户输入std::vectorT只有它能运行期增长需要和 C API 交互memcpy/ 三方库 / 系统调用std::array或vector的.data()二者都保证连续存储.data()拿得到裸指针必须使用 C 风格数组类型的地方如T arr[3]的旧接口裸数组没办法但可以包一层std::array再取.data()超大且栈空间紧张几十 MBstd::vectorT栈通常只有几 MB大数组会直接栈溢出官方文档C Core Guidelines · SL.con.1 (isocpp.github.io)4. std::array 作为函数参数长度信息不丢这是std::array最实用的地方。把形参写成template std::size_t N加const std::arrayT, N编译器会把N一并推导出来函数体里既有长度也能遍历而且对任意N都只写一份代码// demo.cpp — 编译: g -stdc17 -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::arrayint, N arr) { std::cout std::array: 元素个数 arr.size() sizeof sizeof(arr) 编译期就知道\n; } int main() { int raw[5]{1, 2, 3, 4, 5}; const std::arrayint, 5 a{1, 2, 3, 4, 5}; const std::arrayint, 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: 元素个数5sizeof20编译期就知道 std::array: 元素个数3sizeof12编译期就知道三种写法的取舍很清楚传参写法长度信息能否接受任意长度代价void f(int* p)丢失是但要额外传长度最危险长度和指针可能对不上void f(int (arr)[5])保留否长度写死每个长度都要重载一份void f(const std::arrayint, 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 -stdc17 -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::arrayint, N arr) { int total 0; for (const int x : arr) total x; return total; } int main() { const std::arrayint, 4 fixed{10, 20, 30, 40}; // 个数编译期已知 → 栈上零堆开销 std::vectorint 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)C17 起对裸数组和容器通用的长度获取方式。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::arrayT, N就是「内嵌了一个T[N]的对象」所以sizeof与裸数组完全相同、访问也没有任何间接却补上了裸数组丢失的长度信息、整体拷贝赋值能力和模板推导能力固定大小编译期已知就用它运行期才知道大小就用std::vector只有要适配 C 风格数组类型的旧接口时才写裸数组。
返回列表