一、vector 是什么?
vector 本质上是一个:
支持动态扩容的连续内存数组容器。
它和普通数组最大的区别,不是“能下标访问”,而是:
- 连续存储
- 支持尾部高效插入删除
- 空间不够时自动扩容
- 自己管理对象的构造、析构、搬迁和释放
二、vector 的核心特征
手撕 vector 时,脑子里一定先有这几个标签。
1. 连续内存
vector 的元素在内存里是连续排布的,所以它支持:
operator[]data()- 指针偏移访问
- 很好的缓存局部性
这也是它和链表最大的区别。
2. size 和 capacity 分离
vector 一般至少维护两个概念:
size:当前已经构造好的元素个数capacity:当前这块内存最多能容纳多少个元素
也就是说:
size管的是“用了多少”capacity管的是“总共能放多少”
比如:
size = 3capacity = 8
说明这块内存能放 8 个元素,但目前只真正构造了 3 个。
3. 尾插尾删高效
vector 最擅长的操作就是:
push_backpop_back
因为尾部操作一般不需要整体搬移元素。
这也是面试里最常让你重点实现的部分。
4. 扩容会导致数据搬迁
当 size == capacity 时,再 push_back 就必须扩容。
扩容时一般要做这些事:
- 申请一块更大的原始内存
- 把旧元素逐个搬到新内存
- 销毁旧内存中的对象
- 释放旧内存
- 更新指针和容量
这里就会牵涉到:
- 拷贝构造 / 移动构造
- 异常安全
- 回滚机制
这正是手撕 vector 最容易写错的地方。
5. 分离“内存分配”和“对象构造”
这一点非常关键。
vector 管理的不是简单字节,而是 对象。
所以要分清:
内存分配
只拿到一块“能放对象的裸内存”,但对象还没构造出来。
比如:
::operator new(sizeof(T) * n);
对象构造
在某块已经分配好的内存上,真正构造对象。
比如:
new (address) T(args...);
对象析构
销毁对象本身,但不释放那块原始内存。
比如:
ptr->~T();
内存释放
把整块原始内存还给系统。
比如:
::operator delete(ptr);
所以你要始终记住一句话:
分配内存 != 构造对象
析构对象 != 释放内存
三、一个 vector 通常包含什么?
最小实现里,通常就这几个成员。
T* data_;
size_t size_;
size_t capacity_;
它们分别表示:
data_:指向连续存储区的起始地址size_:当前有效元素个数capacity_:当前容量
这三个字段基本就是 vector 的核心骨架。
四、手撕 vector 需要实现哪些基础功能?
如果是面试手撕,我建议分成 基础版 和 进阶版 来记。
五、基础版 vector:先实现什么?
基础版的目标不是“完全对标 STL”,而是:
先把容器最核心的行为跑通。
基础版应实现的功能
1. 构造与析构
至少要有:
- 默认构造
- 析构函数
默认构造负责把容器初始化为空。
析构函数负责:
- 调用所有已构造元素的析构函数
- 释放整块内存
2. 基本访问接口
至少要有:
size()capacity()empty()operator[]back()
这些接口让这个容器先“像个容器”。
3. push_back
这是最核心的方法之一。
要处理两种情况:
情况 1:容量够
直接在尾部构造新元素。
情况 2:容量不够
先扩容,再把元素放进去。
这一步本质考的是你会不会:
- 判断扩容
- 搬迁旧元素
- 构造新元素
- 更新
size_
4. pop_back
这是另一个核心方法。
逻辑相对简单:
- 判断容器是否为空
- 对最后一个元素调用析构
size_--
注意:
pop_back 一般 不缩容
因为频繁缩容会让性能很差。
5. reserve
reserve 的作用是:
提前申请足够大的容量,避免频繁扩容。
它只改变 capacity,不改变 size。
这个函数通常是扩容逻辑的底层支撑。
6. clear
作用:
- 析构所有已构造元素
- 把
size_置为 0 - 一般不释放容量
这样后续还能继续复用内存。
六、基础版 vector 的核心方法有哪些?
如果让我列一个“最小可用版”的方法清单,我会写成这样:
Vector();
~Vector();
size_t size() const;
size_t capacity() const;
bool empty() const;
T& operator[](size_t index);
const T& operator[](size_t index) const;
T& back();
const T& back() const;
void push_back(const T& value);
void push_back(T&& value);
void pop_back();
void reserve(size_t new_capacity);
void clear();
这套接口已经足够支撑一个基础版手撕 vector。
七、为什么 push_back 和 pop_back 是重点?
因为这两个函数几乎把 vector 的本质全覆盖了。
1. push_back 考什么?
push_back 表面是在尾部插入一个元素,实际上考的是:
内存管理
- 容量够不够
- 什么时候扩容
- 扩容后旧内存怎么处理
对象生命周期
- 新元素如何构造
- 旧元素如何搬迁
- 旧对象何时析构
移动语义
- 扩容时优先移动还是拷贝
- 插入右值时是否用移动构造
异常安全
- 元素搬迁过程中抛异常怎么办
- 已构造的新对象如何回滚
- 旧数据能不能保持不变
所以面试官让你写 push_back,本质是在看你会不会写一个“真正的容器”。
2. pop_back 考什么?
pop_back 看起来简单,但它考的是你有没有正确理解:
size和capacity的区别- 析构对象和释放内存的区别
- 容器删除元素时只需要销毁对象,不一定要回收整块空间
它主要是在考你的基本功。
八、push_back 的标准思路
你可以把它记成一个固定模板。
情况一:还有空位
如果 size_ < capacity_:
- 在
data_ + size_的位置用 placement new 构造对象 size_++
例如:
new (data_ + size_) T(value);
++size_;
情况二:没有空位,需要扩容
如果 size_ == capacity_:
第一步:计算新容量
一般是:
- 初始从 1 开始
- 后续按 2 倍扩容
例如:
new_capacity = (capacity_ == 0 ? 1 : capacity_ * 2);
第二步:申请新内存
这里只分配原始内存,不构造对象。
第三步:把旧元素搬到新内存
通常逐个构造:
- 优先移动构造
- 不行再拷贝构造
第四步:在新内存尾部构造新元素
第五步:销毁旧元素并释放旧内存
第六步:更新 data_ / capacity_ / size_
九、pop_back 的标准思路
pop_back 的逻辑可以背下来:
if (size_ == 0) return;
data_[size_ - 1].~T();
--size_;
注意这里的顺序一般是:
- 找到最后一个元素
- 调析构
- size 减一
十、手撕 vector 时必须理解的几个底层点
1. 为什么不能直接用 new T[n] 代替所有逻辑?
因为 vector 需要的是:
- 分配一块原始内存
- 按需逐个构造元素
- 按需逐个析构元素
而 new T[n] 会:
- 一次性构造 n 个对象
这就不符合 vector 的语义,因为:
capacity_ 只是“能放多少”,不是“已经构造了多少”。
所以 vector 更适合用:
::operator new分配裸内存placement new按需构造对象
2. 为什么 pop_back 不释放内存?
因为 vector 的设计目标是:
减少频繁分配和释放带来的开销。
如果每删一个元素就缩容,那插入/删除频繁时性能会非常差。
所以标准思路是:
pop_back只析构对象- 不动
capacity_
3. 为什么扩容通常按 2 倍增长?
因为这样可以把均摊复杂度控制在较低水平。
虽然某一次扩容很贵,但整体看:
push_back 的均摊时间复杂度是 O(1) 。
十二、vector 手撕时最容易写错的点
1. 把“分配内存”和“构造对象”混了
这是最常见错误。
记住:
operator new:只分配内存- placement new:真正构造对象
2. 忘记析构旧元素
扩容后旧内存不能直接 delete 就完了。
如果旧内存中已经有对象,必须先逐个析构,再释放原始内存。
3. size_ 和 capacity_ 更新时机写错
尤其是扩容时,顺序一乱就容易出 bug。
4. pop_back 只减 size,不析构对象
这对基础类型可能看不出来,但对带资源的对象就会出问题。
5. 扩容时直接 memcpy
这对非平凡类型是错的。
对象类型可能有:
- 指针成员
- 文件句柄
- 智能指针
- 自定义析构逻辑
这种类型必须走:
- 移动构造
- 或拷贝构造
不能粗暴字节拷贝。
6. 扩容异常时没有回滚
这是进阶题里很关键的点。
十三、面试里你可以怎么概括一个手撕 vector
你可以这样说:
vector本质是一个动态连续数组,核心成员一般是data_、size_、capacity_。它的关键在于分离内存分配和对象构造,通过operator new获取原始内存,再用 placement new 按需构造元素。基础版至少要实现构造析构、访问接口、push_back、pop_back、reserve、clear。其中push_back是重点,因为它会涉及扩容、元素搬迁、移动语义和异常安全;pop_back则主要体现对象析构和容量管理的基本思想。`
如果想再多说一句加分,可以补:
进阶版还可以继续补拷贝控制、移动语义、
emplace_back、resize和异常安全保证。
十四、一个适合复习的“最小实现清单”
你复习时可以直接记这个版本。
成员变量
T* data_;
size_t size_;
size_t capacity_;
基础接口
Vector();
~Vector();
size_t size() const;
size_t capacity() const;
bool empty() const;
T& operator[](size_t index);
const T& operator[](size_t index) const;
T& back();
const T& back() const;
void push_back(const T& value);
void push_back(T&& value);
void pop_back();
void reserve(size_t new_capacity);
void clear();