C++中 std::vector (动态数组容器)、std::array(静态数组封装)深入解析
在 C++ 开发中,std::vector 是最常用的动态数组容器。
它不仅能够 自动扩容,还提供了 连续的内存存储 和 高效的随机访问。然而,如果不了解其内部机制,很容易写出性能低下的代码。
从数据组织方式来看,vector 与传统的 数组(array) 很相似:
-
都采用连续内存存储
-
都能通过下标进行快速访问
它们最大的区别在于 内存管理的灵活性:
-
数组(array):空间是静态的。一旦分配,大小就固定了。如果需要更大或更小的空间,就必须手动分配一块新内存,将原有数据复制过去,再释放旧内存。这一过程繁琐且容易出错。
-
向量(vector):空间是动态的。随着元素的不断插入,
vector会根据需要自动扩容,将数据迁移到更大的连续内存中。这让我们不再需要预先估算所需大小,也避免了手动搬运数据的麻烦。
因此,std::vector 在 内存利用率 和 使用的灵活性 方面,都比数组更具优势。
1. 基础概念
std::vector 本质上是一块 连续的内存空间,专门用来存储同一类型的对象。它的底层通常包含以下几个核心成员(不同编译器实现可能略有差异):
-
_start:指向存储区域的起始位置
-
_finish:指向当前已存储数据的末尾
-
_end_of_storage:指向已分配空间的末尾
通过这三个指针,vector 能够同时管理“已使用的元素数量”(size)和“总的可用容量”(capacity)。
在 C++ 标准模板库(STL)中,vector 常被称作 容器。虽然中文翻译成“容器”不够精确,但它确实具备类似容器的特性:可以存放对象,并能自动管理内存。
简单来说,std::vector 就是一个能够 动态扩展的数组:
-
它能存放任意类型的对象(包括自定义类型)。
-
它会在需要时自动扩容或缩减存储空间。
-
它兼顾了数组的 高效随机访问 和链表的 灵活增长能力。
因此,vector 被广泛应用于 C++ 开发中,是最常用的 STL 容器之一。
T* _start; // 指向堆上分配的数组起始位置
T* _finish; // 当前存储的元素末尾
T* _end_of_storage; // 已分配的容量尾部
-
_start:实际存储数据的起始地址 -
_finish:当前存储的元素尾部 -
_end_of_storage:已分配的最大容量尾部
vector 提供两个关键概念:
-
size():当前元素数量 -
capacity():当前分配的内存能容纳的最大元素数(不触发扩容)
2. vector 的 push_back 与拷贝机制
当你调用 push_back 添加元素时,如果容量不足,vector 会:
-
分配一块更大的连续内存(通常是原容量的两倍)
-
将原有元素逐一拷贝或移动到新内存
-
释放旧内存
-
将新元素添加到尾部
⚠️ 结果:频繁扩容会触发大量拷贝。
示例
#include <iostream>
#include <vector>
struct Vertex {
float x, y, z;
Vertex(float x, float y, float z) : x(x), y(y), z(z) {}
// 拷贝构造函数
Vertex(const Vertex& other) : x(other.x), y(other.y), z(other.z) {
std::cout << "Copied!" << std::endl;
}
};
int main() {
std::vector<Vertex> vertices;
vertices.push_back({1, 2, 3});
vertices.push_back({4, 5, 6});
vertices.push_back({7, 8, 9});
}
输出结果:
Copied!
Copied!
Copied!
Copied!
Copied!
Copied!
解释:临时对象构造 + 容器扩容导致多次拷贝。
3. 优化 vector 的使用
3.1 提前分配内存
如果你大概知道元素数量,可以调用 reserve() 避免频繁扩容:
std::vector<Vertex> vertices;
vertices.reserve(3); // 分配3个位置
vertices.push_back({1,2,3});
vertices.push_back({4,5,6});
vertices.push_back({7,8,9});
-
一次性分配内存 → 避免反复扩容
-
拷贝次数减少,性能提升
3.2 使用 emplace_back 避免临时对象
push_back 会拷贝临时对象,而 emplace_back 直接在 vector 内存中构造对象:
std::vector<Vertex> vertices;
vertices.reserve(3);
vertices.emplace_back(1,2,3);
vertices.emplace_back(4,5,6);
vertices.emplace_back(7,8,9);
-
不再调用拷贝构造
-
性能最优
4. 遍历与访问
-
普通索引访问:这样写比较直观,但容易出错(比如
i越界),而且代码比较啰嗦。
for(size_t i = 0; i < vertices.size(); ++i)
std::cout << vertices[i].x << ", " << vertices[i].y << ", " << vertices[i].z << std::endl;
-
范围 for 循环(推荐,避免拷贝):
for(const Vertex& v : vertices)
std::cout << v.x << ", " << v.y << ", " << v.z << std::endl;
-
vertices 是一个 std::vector<Vertex>,里面存了一堆 Vertex 对象。
for (Vertex& v : vertices) 表示:依次取出 vertices 里的每一个元素,引用到变量 v 上。
因为用了引用 &,所以 v 不会拷贝,而是直接操作 vector 里存的对象。
-
删除元素:
vertices.erase(vertices.begin() + 2); // 删除第3个元素
-
清空 vector:
vertices.clear();
5. vector 传参优化
-
避免复制 → 使用引用
-
只读访问 → 使用 const 引用
void ProcessVector(const std::vector<Vertex>& vec) {
// do something
}
6. 存储对象 vs 指针
-
存储对象:
-
内存连续 → 缓存友好 → 访问快
-
-
存储指针:
-
扩容仅复制指针值 → 速度快
-
访问需要解引用
-
-
建议:存储对象,除非对象很大或需要共享
7. 总结
-
std::vector是连续内存数组,size 和 capacity 分开管理 -
push_back会触发扩容和拷贝,性能可能下降 -
优化策略:
-
reserve()→ 减少扩容次数 -
emplace_back()→ 避免临时对象拷贝 -
遍历尽量使用引用
-
传参用
const引用
-
通过这些优化,vector 可以在保证简洁代码的同时,接近手写数组的性能。
在 C++ 中,数组是最基本的数据结构之一。传统的 C 风格数组虽然简单高效,但存在一些缺陷:
-
传参时会退化为指针,丢失大小信息。
-
不支持迭代器,难以直接使用 STL 算法。
-
无法自带边界检查,容易越界。
C++ std::array 深入解析
在 C++ 中,数组是最基本的数据结构之一。传统的 C 风格数组虽然简单高效,但存在一些缺陷:
-
传参时会退化为指针,丢失大小信息。
-
不支持迭代器,难以直接使用 STL 算法。
-
无法自带边界检查,容易越界。
C++11 引入了 std::array,它在 保持性能与内存布局一致 的同时,解决了以上问题。本文详细解析 std::array 的使用方法、与原始数组的对比,以及其源码中的关键实现。
1. 基本使用
std::array 是一个 定长数组容器,大小在编译期确定,不能动态增长。
#include <array>
#include <iostream>
int main() {
std::array<int, 5> data; // 大小为5的数组
data[0] = 2;
data[4] = 1;
int dataOld[5]; // C风格数组
}
它和原始数组的主要差别在于:std::array 是一个类模板,拥有更多辅助方法。
2. 数组传参问题
C 风格数组
void PrintArray(int* array, unsigned int size) {
for (int i = 0; i < size; i++) {
// print
}
}
缺点:数组传参会退化为指针,必须额外传入 size,否则无法知道数组长度。
std::array
void PrintArray(const std::array<int, 5>& data) {
for (int i = 0; i < data.size(); i++) {
// print
}
}
优点:数组大小通过模板参数自动传递,不需要额外传入 size。
3. 模板 + 原始数组
如果仍想用 C 风格数组,也可以利用模板避免大小丢失:
template <size_t N>
void PrintArray(int (&array)[N]) { // 引用避免退化成指针。这里 int (&array)[N] 表示:参数是一个 引用到大小为 N 的数组。
for (int i = 0; i < N; i++) {
// print
}
}
int arr[] = {1, 2, 3, 4, 5};
PrintArray(arr); // 自动推导 N=5
等价于:
void PrintArray(int (&array)[5]) { ... } // 专门接收大小为5的数组
4. std::array + 模板
更通用的写法是直接模板化 std::array:
template <size_t N>
void PrintArray(const std::array<int, N>& data) {
for (int i = 0; i < data.size(); i++) {
// print
}
}
或者使用 C++11 的 range-for:
template <size_t N>
void PrintArray(const std::array<int, N>& data) {
for (const auto& item : data) {//这是 C++11 的 范围 for 循环,自动使用 .begin() 和 .end() 来遍历。
// print
}
}
调用方式:
std::array<int, 5> data = {1, 2, 3, 4, 5};
PrintArray(data); // 自动推导 N=5
效率与原始数组相同,且支持 STL 算法。
5. std::array 的优势
-
保留编译期大小信息:
size()直接返回模板参数_Size,无运行时开销。 -
迭代器支持:可直接使用
.begin()和.end(),与 STL 算法无缝结合。 -
栈分配:与 C 数组一样,分配在栈上,效率高。
-
更安全:
at()提供边界检查(调试模式下)。
6. 源码解析
在 MSVC STL 的实现中,可以看到 std::array 的底层定义:
_EXPORT_STD template <class _Ty, size_t _Size>
class array { ... };
// 针对 size=0 的特化
template <class _Ty>
class array<_Ty, 0> { ... };
它本质上就是 一个模板封装的定长数组,并提供了一些成员函数:
-
批量赋值
_CONSTEXPR20 void fill(const _Ty& value) {
_STD fill_n(_Elems, _Size, value);
}
相当于 for (auto& e : arr) e = value;
-
交换内容
_CONSTEXPR20 void swap(array& other) noexcept {
_STD _Swap_ranges_unchecked(_Elems, _Elems + _Size, other._Elems);
}
-
迭代器支持
iterator begin() noexcept { return iterator(_Elems, 0); }
iterator end() noexcept { return iterator(_Elems, _Size); }
-
大小信息
constexpr size_type size() const noexcept { return _Size; }
-
索引访问
reference operator[](size_type pos) noexcept {
return _Elems[pos];
}
其中
_STD、_CONSTEXPR20、_NODISCARD等是宏或注解,用于编译器优化和静态分析。
7. 总结
-
std::array是 定长静态数组,大小在编译期确定。 -
与原始数组相比,它:
-
不会退化成指针,保留大小信息。
-
支持 STL 算法,有迭代器、
size()、fill()等接口。 -
安全性更高,提供
at()的边界检查。
-
-
在性能上,与原始数组无差别,但在功能和可维护性上更胜一筹。
如果需要 固定大小、栈分配 的数组,优先考虑使用 std::array;如果需要 动态大小、堆分配,则用 std::vector。
更多推荐


所有评论(0)