---
title: "C++中vector的一些理解"
date: 2026-09-03
category: "C++"
tags: ["C++", "STL", "vector", "C++20", "template"]
summary: "引言\\n\\nvector应该是c++标准库中最常用的容器了，但我此前对它的认识基本停留在“可变长数组”这个层面，初始化方式的细节、size与capacity的区别、各个修改函数的语义，其实都没有认真梳理过。\\n\\n在此篇文章中，我将记录下自己通过一个方法示例实验对vector的理解，以供思考，如有不足之处欢迎指正。…"
series:
  name: "C++STL容器及附属"
  order: 0
---

## 引言

vector应该是c++标准库中最常用的容器了，但我此前对它的认识基本停留在“可变长数组”这个层面，初始化方式的细节、size与capacity的区别、各个修改函数的语义，其实都没有认真梳理过。

于是我写了一个vector方法的示例实验，把初始化、访问、修改三块内容分别放进三个函数里逐个验证。

在此篇文章中，我将记录下自己对vector的理解，以供思考，如有不足之处欢迎指正。

## 正文

### 初始化的多种方式

为了观察，先写了两个模板打印函数：

```cpp
template<typename T>
void print(const vector<T> &vec)//T!=bool，vector<bool>被认为不属于vector容器
{
    cout << vec.size() << ':';
    for (const T &i: vec) { cout << i << " "; }
    cout << endl;
}
```

然后是各种初始化方式的对决：

```cpp
vector<int> empty{};        //默认构造，空vector
vector<double> list{1, 2, 3, 4};//列表构造
vector ctad{'a', 'b', 'c'}; //CTAD(c++17)自动推导元素类型为char
vector<int> v1(6);          //直接初始化显式构造，6个元素默认初始化为0
vector<int> v2{6};          //列表初始化优先于直接初始化，是1个元素6
```

这里有一个很容易踩的坑：`vector<int> v1(6)`和`vector<int> v2{6}`外形只差一个括号，结果却是一个6元素的零值vector和一个单元素vector。
原因是**列表初始化的优先级高于直接初始化，只要花括号能匹配列表构造，就不会走单参数构造函数**。

实验里还验证了`vector<int> v = 10;`是编译不过的，10既不匹配列表构造，而单参数构造函数本身是explicit的，也不能匹配复制初始化。

> 类的成员默认初始化器不支持直接初始化，只能写成`vector<int> v5 = {vector<int>(7)};`这种复制列表初始化的形式，算是一个冷门但真实的限制。

### 元素访问

访问方式里，最重要的是`[]`与`at()`的区别：

```cpp
cout << vec[2] << endl;//无边界检查，越界是未定义行为
cout << vec.at(3) << ' ';//有安全检查，越界抛出std::out_of_range
```

at()的代价是一次检查，换来的是确定的行为，我认为大多数场景下这点开销是值得的。

另外几个取大小的方法也有讲究：

```cpp
cout << vec.size() << endl;//成员方法，返回无符号的size_type
cout << std::size(vec) << endl;//c++17，还能接受c风格数组
cout << std::ssize(vec) << endl;//c++20，返回有符号整型std::ptrdiff_t
```

size()返回无符号数这一点，在做`vec.size() - 1`之类的运算时很容易因为下溢出问题，所以c++20补了ssize()。

索引类型最好用`std::size_t`，其他整数类型在传给at()时会发生类型窄化警告。此外`data()`能拿到底层c风格数组的裸指针，`front()`和`back()`取首尾元素，这些和string是共通的。

### 大小与容量

这是vector作为动态数组最核心的机制：

```cpp
vec.resize(1000);//运行时调整vector大小
vec.resize(5);   //缩小回来，但容量不一定缩
cout << vec.capacity() << endl;//容量，已分配的内存，不会小于size
vec.shrink_to_fit();//请求去除未使用的容量（非强制）
vec.reserve(10);   //重新分配容量，不改变size
```

我的理解是，**size是装了多少东西，capacity是预留了多少空间**。
vector扩容时需要重新分配内存并搬移元素，代价不小，所以提前reserve()避免反复扩容是常见的优化手段。

> 从之前写生命周期实验的经验看，reserve()导致的重新分配还会使所有迭代器失效，这一点和下面要说的erase类似。

### 增删与交换

修改操作里，最值得对比的是push_back()与emplace_back()：

```cpp
vec.push_back(4);//构造好元素再放入
vec.emplace_back(5);//在容器内直接构造，适合临时对象或explicit构造
vec.pop_back();//模拟出栈
vec.clear();  //清空
vec.assign(6, {1});//重新赋值为6个1
```

insert()和erase()都以迭代器为参数，且erase()返回被删元素的下一个位置的迭代器：

```cpp
auto it = vec.insert(vec.begin(), 2);//返回指向新元素的迭代器
vec.erase(it);
vec.swap(vec2);//交换两个vector，指针级别的交换，开销很小
```

swap()只交换内部的指针和元信息，我认为这也是vector很多操作（包括前面的copy and swap惯用法）能高效实现的基础。

### 遍历与ranges

除了普通for循环，实验里还试了c++20的ranges：

```cpp
template<typename T>
void print_reverse(const vector<T> &vec)
{
    for (const T &i: std::views::reverse(vec)) { cout << i << " "; }//反向视图
}
```

std::views::reverse()生成一个反向视图， range-based for直接就能倒着遍历，不需要手动拿reverse_iterator，写起来确实舒服。

### 补充要点

实验注释里记了几条容易忽略的限制：vector的元素类型不能是const（即`vector<const int>`不行），只能把vector本身声明为const；vector\<bool>在标准库中被特殊对待，不被认为属于vector容器，应该尽量避免使用。

## 小结

vector的方法本身并不难记，难的是背后那一层语义：花括号与圆括号决定了列表构造还是元素构造，size与capacity决定了内存行为，insert与erase决定了迭代器的生死。把这些理顺之后，vector就从“能用的可变长数组”变成了一个行为完全可预期的工具。

标准库容器的学习方法大概都是如此，逐个方法敲一遍，比只看文档记得牢得多。

本文所用的测试代码收录于[Learncpp](https://github.com/ManJin03/Learncpp)，如有不妥之处欢迎指正。
