一、为什么我要手写 vector
在 C++ 学习过程中,std::vector 是我们使用频率最高的容器之一。 但**"会用"** 和 "懂它怎么实现",在面试中是两回事。
很多人背过这些结论:
-
vector 底层是连续内存
-
支持随机访问
-
扩容一般是 2 倍
-
插入可能导致迭代器失效
但如果面试官继续追问一句:
"你自己能不能实现一个 vector?"
真正的差距就出现了。
我决定从零开始,完整实现一个具备面试价值的 vector,目标不是复刻 STL 的所有细节,而是做到:
-
内存模型清晰
-
拷贝语义正确
-
接口行为符合 STL 直觉
-
能在面试中经得起追问
二、整体设计思路
1. vector 的核心结构
标准库中的 vector 本质上只维护了三根指针:
cpp
[start, finish) 已使用空间
[start, end_of_storage) 已申请空间
因此我们的 vector 内部结构非常清晰:
cpp
T* _start; // 指向起始位置
T* _finish; // 指向最后一个元素的下一个
T* _end_of_storage; // 指向容量末尾
它们之间的关系决定了:
-
size() = _finish - _start -
capacity() = _end_of_storage - _start
这也是 vector 高效的根本原因。
cpp
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
2. 为什么使用指针而不是下标
使用裸指针有两个优势:
-
和 STL 行为一致 : vector 的 iterator 本质就是
T* -
指针运算天然高效: 无需额外封装,插入/删除逻辑清晰
这也是为什么我们定义:
cpp
typedef T* iterator;
typedef const T* const_iterator;
三、构造函数与析构:资源管理的第一步
1. 默认构造函数
一个合格的 vector 必须支持:
cpp
vector<int> v;
v.push_back(1);
因此默认构造函数必须让指针处于空但安全的状态:
cpp
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
这是后续所有操作的基础。
2. 带参构造函数
构造一个大小为 n、元素初值为 val 的 vector:
cpp
vector(size_t n, const T& val = T())
{
_start = new T[n];
_finish = _start + n;
_end_of_storage = _start + n;
for (size_t i = 0; i < n; ++i)
_start[i] = val;
}
这里有两个重要点:
-
容量 = 大小(不做多余扩容)
-
使用赋值而不是
memcpy,保证对非 POD 类型安全
3. 析构函数
vector 管理的是一整块动态数组,因此析构只需一件事:
cpp
~vector()
{
delete[] _start;
}
这也是 RAII 思想的体现: 资源的申请和释放必须成对出现。
四、拷贝语义:最容易出错的地方
1. 拷贝构造函数(深拷贝)
vector 绝对不能浅拷贝,否则两个对象会共享同一块内存。
正确的做法是:
cpp
vector(const vector& x)
{
size_t n = x.size();
_start = new T[n];
for (size_t i = 0; i < n; ++i)
_start[i] = x._start[i];
_finish = _start + n;
_end_of_storage = _finish;
}
这样每个 vector 都拥有独立的内存资源。
2. 赋值运算符:copy-swap 惯用法
赋值是拷贝构造的升级版,推荐使用 copy-swap:
cpp
vector& operator=(const vector& x)
{
vector<T> tmp(x);
swap(tmp);
return *this;
}
它的好处是:
-
自动处理自赋值
-
异常安全
-
代码简洁、可复用
3. swap 的正确实现
cpp
void swap(vector& x)
{
std::swap(_start, x._start);
std::swap(_finish, x._finish);
std::swap(_end_of_storage, x._end_of_storage);
}
注意: swap 一定是"交换",不是覆盖,这是很多初学者会犯的错误。
五、容量管理:reserve 与 resize
1. reserve:只扩容,不改 size
cpp
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
for (size_t i = 0; i < old_size; ++i)
tmp[i] = _start[i];
delete[] _start;
_start = tmp;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
关键点:
-
只在
n > capacity()时才扩容 -
扩容后 size 不变
-
所有旧 iterator 失效(与 STL 一致)
2. resize:改变 size
cpp
void resize(size_t n, T val = T())
{
if (n > capacity())
reserve(n);
if (n > size())
{
for (size_t i = size(); i < n; ++i)
_start[i] = val;
}
_finish = _start + n;
}
resize 的行为可以总结为:
-
变小:逻辑删除
-
变大:补充新元素
六、元素访问接口
cpp
T& front();
T& back();
T& operator[](size_t i);
以及对应的 const 版本:
cpp
const T& front() const;
const T& back() const;
const T& operator[](size_t i) const;
这些接口与 STL 行为保持一致:
-
不做越界检查
-
由调用者保证前置条件(非空 / 合法下标)
七、push_back 与扩容策略
cpp
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
reserve(new_cap);
}
_start[size()] = x;
++_finish;
}
这里体现了 vector 的经典扩容策略:
-
初始容量:0 → 1
-
后续扩容:2 倍
这样可以保证 均摊 O(1) 的插入复杂度。
八、insert 与 erase:最考验指针理解的地方
1. insert
cpp
iterator insert(iterator pos, const T& val)
{
if (_finish == _end_of_storage)
{
size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
reserve(new_cap);
}
size_t idx = pos - _start;
for (size_t i = size(); i > idx; --i)
_start[i] = _start[i - 1];
_start[idx] = val;
++_finish;
return _start + idx;
}
插入的核心是:
-
从后往前移动元素
-
保证不覆盖数据
-
返回插入位置的 iterator
2. erase
cpp
iterator erase(iterator pos)
{
for (size_t i = pos - _start + 1; i < size(); ++i)
_start[i - 1] = _start[i];
--_finish;
return _start + (pos - _start);
}
删除的本质是:
-
覆盖删除位置
-
size 减 1
-
返回删除后的位置
九、迭代器失效规则(面试高频)
通过实现可以清楚得出:
-
reserve:所有 iterator 失效 -
insert:插入点及之后 iterator 失效 -
erase:删除点及之后 iterator 失效
这正是 STL 文档中的描述。
十、总结
通过完整实现一个 vector,我最大的收获不是代码本身,而是:
-
理解了 连续内存模型
-
真正掌握了 拷贝语义
-
明白了 为什么 vector 插入慢、访问快
-
面试时不再害怕被追问 STL 底层
当你能自己写出 vector 时, STL 不再是黑盒,而是朋友。
十一、完整代码
cpp
#pragma once
#include<iostream>
namespace Vector
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
}
vector(size_t n, const T& val = T())
{
_start = new T[n];
_finish = _start + n;
_end_of_storage = _start + n;
for (size_t i = 0; i < n; ++i)
_start[i] = val;
}
vector(const vector& x)
{
size_t n = x.size();
_start = new T[n];
for (size_t i = 0; i < n; ++i)
_start[i] = x._start[i];
_finish = _start + n;
_end_of_storage = _finish;
}
~vector()
{
delete[] _start;
}
vector& operator= (const vector& x)
{
vector<T> tmp(x);
swap(tmp);
return *this;
}
size_t size() const { return _finish - _start; }
size_t capacity() const { return _end_of_storage - _start; }
iterator begin() { return _start; }
iterator end() { return _finish; }
const_iterator begin() const { return _start; }
const_iterator end() const { return _finish; }
void swap(vector& x)
{
std::swap(_start, x._start);
std::swap(_finish, x._finish);
std::swap(_end_of_storage, x._end_of_storage);
}
bool empty() const
{
return _finish == _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
for (size_t i = 0; i < old_size; ++i)
tmp[i] = _start[i];
delete[] _start;
_start = tmp;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
void resize(size_t n, T val = T())
{
if (n > capacity())
reserve(n);
if (n > size())
{
for (size_t i = size(); i < n; ++i)
_start[i] = val;
}
_finish = _start + n;
}
T& front() { return _start[0]; }
T& back() { return _start[size() - 1]; }
const T& front() const { return _start[0]; }
const T& back() const { return _start[size() - 1]; }
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
reserve(new_cap);
}
_start[size()] = x;
_finish++;
}
T& operator[](size_t i) { return *(_start + i); }
const T& operator[](size_t i) const { return *(_start + i); }
iterator insert(iterator pos, const T& val)
{
if (_finish == _end_of_storage)
{
size_t new_cap = capacity() == 0 ? 1 : capacity() * 2;
reserve(new_cap);
}
size_t idx = pos - _start;
for (size_t i = size(); i > idx; --i)
_start[i] = _start[i - 1];
_start[pos - _start] = val;
_finish++;
return _start + (pos - _start);
}
void pop_back()
{
_finish--;
}
iterator erase(iterator pos)
{
for (size_t i = pos - _start + 1; i < size(); ++i)
_start[i - 1] = _start[i];
_finish--;
return _start + (pos - _start);
}
iterator erase(iterator first, iterator last)
{
size_t len = last - first;
for (size_t i = last - _start; i < size(); ++i)
_start[i - len] = _start[i];
_finish -= len;
return first;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}