
目录
[一、vector 的核心结构](#一、vector 的核心结构)
[1.1 简介作用](#1.1 简介作用)
[1.2 【代码实现】核心结构定义](#1.2 【代码实现】核心结构定义)
[1.3 新手坑点提醒](#1.3 新手坑点提醒)
[2.1 无参构造函数](#2.1 无参构造函数)
[2.2 带 n 个 val 的构造函数](#2.2 带 n 个 val 的构造函数)
[【代码实现】带 n 个 val 的构造函数](#【代码实现】带 n 个 val 的构造函数)
[2.3 迭代器区间构造](#2.3 迭代器区间构造)
[2.4 拷贝构造函数【面试高频考点】](#2.4 拷贝构造函数【面试高频考点】)
[经典 Bug 和坑点](#经典 Bug 和坑点)
[2.5 析构函数](#2.5 析构函数)
[2.6 赋值运算符重载【面试高频考点】](#2.6 赋值运算符重载【面试高频考点】)
[3.1 size()、capacity()、empty()](#3.1 size()、capacity()、empty())
[3.2 reserve ()【面试高频考点】](#3.2 reserve ()【面试高频考点】)
[【代码实现】reserve 扩容函数](#【代码实现】reserve 扩容函数)
[经典 Bug 和坑点](#经典 Bug 和坑点)
[3.3 resize()](#3.3 resize())
[【代码实现】resize 函数](#【代码实现】resize 函数)
[4.1 简介作用](#4.1 简介作用)
[4.2 【代码实现】迭代器定义与接口](#4.2 【代码实现】迭代器定义与接口)
[5.1 push_back () 尾插](#5.1 push_back () 尾插)
[【代码实现】push_back 尾插](#【代码实现】push_back 尾插)
[5.2 pop_back () 尾删](#5.2 pop_back () 尾删)
[【代码实现】pop_back 尾删](#【代码实现】pop_back 尾删)
[5.3 insert () 任意位置插入【面试高频考点】](#5.3 insert () 任意位置插入【面试高频考点】)
[【代码实现】insert 插入函数](#【代码实现】insert 插入函数)
[5.4 erase () 删除元素【面试高频考点】](#5.4 erase () 删除元素【面试高频考点】)
[【代码实现】erase 删除函数](#【代码实现】erase 删除函数)
[5.5 swap () 交换函数](#5.5 swap () 交换函数)
[【代码实现】swap 交换函数](#【代码实现】swap 交换函数)
[5.6 operator \[\] 下标访问](#5.6 operator [] 下标访问)
[【代码实现】operator \[\] 重载](#【代码实现】operator [] 重载)
[6.1 深浅拷贝问题](#6.1 深浅拷贝问题)
[6.2 迭代器失效问题](#6.2 迭代器失效问题)
[6.3 自赋值问题](#6.3 自赋值问题)
[6.4 reserve 扩容的坑](#6.4 reserve 扩容的坑)
前言
大家好,我是你们的小小风呀!临近期末,小风不得不开始恶补学校课程了,哎~~,当初欠下的终究是要还的呀!所以小风的博客只能不定时更新啦!望大家理解,小风也不想挂课呀!言归正传 今天我们来到【从零开始学 C++】专题的第十篇,这篇可以说是 STL 容器学习的重中之重 ------vector 的模拟实现。
为什么要模拟实现 vector?因为:
-
这是面试必考题,90% 的 C++ 面试都会问到
-
能真正理解底层内存管理机制
-
能搞懂深浅拷贝、迭代器失效这些 "坑"
-
为学习其他容器打下坚实基础
本文将手把手带你实现一个完整的 vector,所有代码都可以直接运行,所有面试重点我都会用【面试高频考点】标注出来。新手小白也能轻松看懂!
一、vector 的核心结构
1.1 简介作用
vector 本质上就是一个动态数组,和普通数组的区别就是:它可以自动扩容!
为了管理这个动态数组,vector 内部用了三个指针来维护:
|-----------------|------------------|-----------|
| 指针名 | 作用 | 大白话理解 |
| _start | 指向数组的起始位置 | 数组的 "头" |
| _finish | 指向最后一个有效元素的下一个位置 | 数组的 "尾巴" |
| _endofstorage | 指向整个容量的末尾位置 | 数组的 "天花板" |
这三个指针的关系:
-
size() = _finish - _start(有效元素个数) -
capacity() = _endofstorage - _start(总容量大小) -
永远满足:
_start ≤ _finish ≤ _endofstorage
1.2 【代码实现】核心结构定义
cpp
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace my_vector
{
template<class T>
class vector
{
public:
// 后面会实现各种成员函数...
private:
T* _start; // 指向数组起始位置
T* _finish; // 指向最后一个有效元素的下一个位置
T* _endofstorage; // 指向整个容量的末尾
};
}
1.3 新手坑点提醒
❌ 常见错误 :新手容易把_finish理解成 "指向最后一个元素",这是错的! ✅ 正确理解 :_finish指向的是最后一个元素的下一个位置,这样设计是为了方便计算 size 和判断是否为空。
二、默认成员函数实现
2.1 无参构造函数
简介作用
创建一个空的 vector,三个指针都初始化为 nullptr,表示还没有分配任何内存。
【代码实现】无参构造函数
cpp
// 无参构造函数
vector()
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
2.2 带 n 个 val 的构造函数
简介作用
创建一个 vector,里面有 n 个值为 val 的元素。比如vector<int> v(5, 10)就创建了 5 个 10。
【代码实现】带 n 个 val 的构造函数
cpp
// 带n个val的构造函数
vector(size_t n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
// 先开n个空间
reserve(n);
// 然后插入n个val
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
// 为了避免int类型的匹配问题,再重载一个版本
vector(int n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
新手坑点提醒
❌ 经典 Bug :如果只写vector(size_t n, ...),当你写vector<int> v(5, 10)时,5 是 int 类型,会优先匹配迭代器区间构造,导致奇怪的错误! ✅ 解决方案:重载一个 int 版本的构造函数。
2.3 迭代器区间构造
简介作用
用另一个容器的迭代器区间来初始化 vector,非常灵活,可以接收各种容器的数据。
【代码实现】迭代器区间构造
cpp
// 迭代器区间构造(模板函数)
template <class InputIterator>
vector(InputIterator first, InputIterator last)
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
// 遍历迭代器区间,一个个插入
while (first != last)
{
push_back(*first);
first++;
}
}
2.4 拷贝构造函数【面试高频考点】
简介作用
用一个已有的 vector 来创建新的 vector。这里必须深拷贝,否则会出大问题!
什么是浅拷贝? 就是两个对象共用同一块内存,一个释放了另一个就变成野指针了。 什么是深拷贝? 重新开辟一块内存,把数据完整复制过去,两个对象互不干扰。
【代码实现】拷贝构造函数(深拷贝)
cpp
// 拷贝构造函数(深拷贝)
vector(const vector<T>& v)
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
// 1. 先开辟和v一样大的空间
reserve(v.capacity());
// 2. 把数据一个个拷贝过来
for (auto& e : v)
{
push_back(e);
}
}
经典 Bug 和坑点
❌ 浅拷贝的灾难:如果不写拷贝构造,编译器默认生成的是浅拷贝,会导致:
-
两个对象析构时释放同一块内存(double free)
-
一个对象修改数据会影响另一个对象
-
程序直接崩溃!
✅ 记住:只要涉及资源管理,必须自己写深拷贝!
2.5 析构函数
简介作用
释放 vector 占用的内存,把三个指针置空。
【代码实现】析构函数
cpp
// 析构函数
~vector()
{
if (_start)
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
}
2.6 赋值运算符重载【面试高频考点】
简介作用
实现v1 = v2这样的赋值操作。这里有传统写法 和现代写法两种,面试经常对比!
【代码实现】传统写法
cpp
// 赋值运算符重载 - 传统写法
vector<T>& operator=(const vector<T>& v)
{
// 1. 防止自赋值(v = v)
if (this != &v)
{
// 2. 释放原来的空间
delete[] _start;
// 3. 开辟新空间
_start = new T[v.capacity()];
_finish = _start;
_endofstorage = _start + v.capacity();
// 4. 拷贝数据
for (size_t i = 0; i < v.size(); i++)
{
*(_finish++) = v[i];
}
}
return *this;
}
【代码实现】现代写法(推荐!)
cpp
// 赋值运算符重载 - 现代写法(简洁又安全)
vector<T>& operator=(vector<T> v) // 注意:这里是传值,不是传引用!
{
// 直接交换!就这么简单!
swap(v);
return *this;
}
两种写法对比总结
|-------|--------------|------------------|
| 对比项 | 传统写法 | 现代写法 |
| 代码量 | 多,容易写错 | 极少,3 行搞定 |
| 自赋值检查 | 需要手动检查 | 天然避免(传值已经拷贝了一份) |
| 异常安全 | 可能异常(new 失败) | 异常安全(拷贝失败不影响原对象) |
| 推荐度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
💡 现代写法原理:利用传值参数自动调用拷贝构造生成临时对象,然后交换临时对象和 this 对象的资源,临时对象出作用域自动析构 ------ 完美!
三、容量相关接口实现
3.1 size()、capacity()、empty()
简介作用
这三个是最基础的查询接口:
-
size():返回有效元素个数 -
capacity():返回总容量大小 -
empty():判断是否为空
【代码实现】基础容量查询
cpp
// 返回有效元素个数
size_t size() const
{
return _finish - _start;
}
// 返回总容量
size_t capacity() const
{
return _endofstorage - _start;
}
// 判断是否为空
bool empty() const
{
return _start == _finish;
}
3.2 reserve ()【面试高频考点】
简介作用
扩容核心函数!当空间不够时,开辟更大的空间,把旧数据移过去。
扩容策略:一般是按 2 倍扩容(VS 下是 1.5 倍,g++ 下是 2 倍)。
【代码实现】reserve 扩容函数
cpp
// 扩容函数(只扩不缩)
void reserve(size_t n)
{
// 只有当n大于当前容量时才扩容
if (n > capacity())
{
// 1. 记录原来的有效元素个数(很重要!)
size_t oldSize = size();
// 2. 开辟新空间
T* tmp = new T[n];
// 3. 拷贝旧数据到新空间
if (_start) // 原来有数据才拷贝
{
for (size_t i = 0; i < oldSize; i++)
{
tmp[i] = _start[i];
}
// 4. 释放旧空间
delete[] _start;
}
// 5. 更新三个指针
_start = tmp;
_finish = _start + oldSize; // 这里不能用size()!_start已经变了
_endofstorage = _start + n;
}
}
经典 Bug 和坑点
❌ 致命错误 :_finish = _start + size() 这样写会崩溃! 因为计算size()的时候用的是新的_start减去旧的_finish,结果完全错误!
✅ 正确做法 :扩容前先把oldSize保存下来!
3.3 resize()
简介作用
调整有效元素的个数:
-
如果 n > size ():插入元素,用默认值填充
-
如果 n <size ():删除尾部元素
-
如果 n > capacity ():先扩容再插入
【代码实现】resize 函数
cpp
// 调整有效元素个数
void resize(size_t n, const T& val = T())
{
if (n > size())
{
// 情况1:插入元素,需要先检查扩容
reserve(n);
while (_finish < _start + n)
{
*(_finish++) = val;
}
}
else
{
// 情况2:删除元素,直接移动_finish指针
_finish = _start + n;
}
}
四、迭代器模拟实现
4.1 简介作用
vector 的迭代器本质上就是原生指针!因为 vector 的内存是连续的,指针天然支持 ++、--、* 等操作。
4.2 【代码实现】迭代器定义与接口
cpp
public:
// 正向迭代器(就是原生指针)
typedef T* iterator;
typedef const T* const_iterator;
// 普通迭代器接口
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
// const迭代器接口(只读)
const_iterator cbegin() const
{
return _start;
}
const_iterator cend() const
{
return _finish;
}
💡 小知识:有了迭代器,范围 for 就自动支持了!因为范围 for 底层就是用迭代器实现的。
五、增删查改接口实现
5.1 push_back () 尾插
简介作用
在 vector 尾部插入一个元素,是最常用的接口。
【代码实现】push_back 尾插
cpp
// 尾插
void push_back(const T& x)
{
// 1. 检查是否需要扩容
if (_finish == _endofstorage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
// 2. 插入数据
*_finish = x;
_finish++;
}
5.2 pop_back () 尾删
简介作用
删除 vector 的最后一个元素。
【代码实现】pop_back 尾删
cpp
// 尾删
void pop_back()
{
// 断言:空vector不能尾删
assert(!empty());
_finish--;
}
5.3 insert () 任意位置插入【面试高频考点】
简介作用
在 pos 位置插入一个元素。这里最容易出现迭代器失效问题!
【代码实现】insert 插入函数
cpp
// 在pos位置插入值为x的元素
iterator insert(iterator pos, const T& x)
{
// 检查pos合法性
assert(pos >= _start);
assert(pos <= _finish);
// 1. 检查扩容(这里会导致迭代器失效!)
if (_finish == _endofstorage)
{
// 重点:扩容前记录pos相对于_start的偏移量
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
// 扩容后更新pos!因为_start已经变了
pos = _start + len;
}
// 2. 挪动数据(从后往前挪)
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
// 3. 插入数据
*pos = x;
_finish++;
return pos; // 返回新的迭代器,解决失效问题
}
❌ 为什么会失效? 因为扩容时_start指向了新的内存,原来的 pos 还是指向旧内存,变成野指针了!
✅ 解决方案:
-
扩容前记录
pos - _start的偏移量 -
扩容后用
_start + 偏移量重新计算 pos -
insert 函数返回新的迭代器给用户使用
5.4 erase () 删除元素【面试高频考点】
简介作用
删除 pos 位置的元素。同样存在迭代器失效问题!
【代码实现】erase 删除函数
cpp
// 删除pos位置的元素
iterator erase(iterator pos)
{
// 检查pos合法性
assert(pos >= _start);
assert(pos < _finish);
// 1. 挪动数据覆盖(从前往后挪)
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish--;
return pos; // 返回删除位置的下一个元素的迭代器
}
迭代器失效详解
❌ 经典错误:
cpp
// 错误写法!erase后it失效了,再++会崩溃
for (auto it = v.begin(); it != v.end(); it++)
{
if (*it % 2 == 0)
v.erase(it);
}
✅ 正确写法:
cpp
// 正确写法:用erase的返回值更新迭代器
for (auto it = v.begin(); it != v.end(); )
{
if (*it % 2 == 0)
it = v.erase(it); // 接收返回值
else
it++;
}
5.5 swap () 交换函数
简介作用
交换两个 vector 的内容,效率极高 ------ 只需要交换三个指针就行!
【代码实现】swap 交换函数
cpp
// 交换两个vector
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
5.6 operator \[\] 下标访问
简介作用
像数组一样用[]访问元素,需要提供普通版本和 const 版本。
【代码实现】operator \[\] 重载
cpp
// 普通版本(可读可写)
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
// const版本(只读)
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
六、常见问题与坑点总结
6.1 深浅拷贝问题
问题表现:两个 vector 指向同一块内存,析构时 double free 崩溃。
根本原因:默认拷贝构造是浅拷贝,只复制指针不复制内容。
解决方案:自己实现深拷贝的拷贝构造和赋值重载。
6.2 迭代器失效问题
两种失效场景:
-
insert/erase 导致的失效:插入删除后,原来的迭代器指向的位置已经无效
-
扩容导致的失效:扩容后内存地址变了,所有旧迭代器都变成野指针
解决方案:
-
永远使用 insert/erase 的返回值来更新迭代器
-
扩容后不要使用之前保存的迭代器
6.3 自赋值问题
问题表现 :v = v 自己赋值给自己,传统写法中会先释放内存再拷贝,结果拷贝的是已经释放的内存。
解决方案:
-
传统写法:加上
if (this != &v)判断 -
现代写法:天然避免,推荐使用!
6.4 reserve 扩容的坑
经典错误 :忘记保存 oldSize,直接用_finish = _start + size()导致指针计算错误。
记住:凡是涉及_start 变化的地方,都要先保存偏移量!
结语
恭喜你!到这里,一个完整的 vector 就实现完毕了!
回顾一下今天的【面试高频考点】:
-
✅ 深拷贝的实现(拷贝构造、赋值重载)
-
✅ 赋值重载的传统写法 vs 现代写法
-
✅ reserve 扩容逻辑
-
✅ insert/erase 的迭代器失效处理
这四个点,面试 10 次有 9 次会问到!建议大家亲手把代码敲一遍,理解才会更深刻。
下一篇我们将继续学习 list 的模拟实现,敬请期待!如果这篇文章对你有帮助,别忘了点赞收藏关注三连哦~