作为C++标准模板库中最基础也最常用的序列式容器,vector的实现原理值得每个C++开发者深入理解。今天我们就来拆解STL vector的核心设计,并完整实现一个简化版的MyVector。这个轮子虽然简单,但能让你真正掌握动态数组的精髓。
我曾在多个高性能计算项目中使用vector作为底层存储,也踩过不少迭代器失效、内存增长的坑。通过这个实现过程,你会看到STL设计者那些精妙的设计取舍,以及在实际工程中如何平衡性能与安全性。
vector本质上是一个能够自动扩容的动态数组。与普通数组相比,它的核心优势在于:
我们来看一个典型的内存布局示例:
code复制[元素1][元素2][元素3][未使用内存]
^ ^ ^
begin() begin()+2 end() capacity()
我们的MyVector需要维护三个核心指针:
cpp复制template <typename T>
class MyVector {
private:
T* _begin; // 指向数组首元素
T* _end; // 指向最后一个元素的下一个位置
T* _capacity; // 指向分配内存的末尾
};
这种设计比单独记录size和capacity更高效,因为:
我们先实现最基础的构造函数和内存分配:
cpp复制explicit MyVector(size_t n = 0) {
_begin = _end = _capacity = nullptr;
if (n > 0) {
_begin = static_cast<T*>(::operator new(n * sizeof(T)));
_end = _begin + n;
_capacity = _end;
// 默认构造每个元素
for (T* p = _begin; p != _end; ++p) {
new (p) T(); // placement new
}
}
}
这里有几个关键点:
注意:直接使用new T[n]会导致默认构造所有元素,这在某些场景下是性能浪费。
让我们实现最常用的push_back操作:
cpp复制void push_back(const T& value) {
if (_end == _capacity) {
reserve(_size() ? _size() * 2 : 1);
}
new (_end) T(value); // 在_end位置构造新元素
++_end;
}
扩容策略采用经典的2倍增长,这是时间和空间的平衡点。实测表明,2倍增长能保证均摊O(1)的时间复杂度。
为了与STL兼容,我们需要提供标准迭代器:
cpp复制typedef T* iterator;
typedef const T* const_iterator;
iterator begin() { return _begin; }
iterator end() { return _end; }
const_iterator cbegin() const { return _begin; }
const_iterator cend() const { return _end; }
这种设计使得:
实现安全的元素访问接口:
cpp复制T& operator[](size_t pos) {
assert(pos < _size());
return _begin[pos];
}
T& at(size_t pos) {
if (pos >= _size()) {
throw std::out_of_range("MyVector::at");
}
return _begin[pos];
}
[]操作符不检查边界以提升性能,at()提供安全的边界检查。
reserve是vector性能的关键:
cpp复制void reserve(size_t new_cap) {
if (new_cap <= _capacity()) return;
T* new_begin = static_cast<T*>(::operator new(new_cap * sizeof(T)));
size_t sz = _size();
// 移动构造元素
for (size_t i = 0; i < sz; ++i) {
new (new_begin + i) T(std::move(_begin[i]));
_begin[i].~T(); // 析构原对象
}
::operator delete(_begin);
_begin = new_begin;
_end = _begin + sz;
_capacity = _begin + new_cap;
}
这里有几个关键优化:
实际工程中,2倍扩容可能不是最优解。我们可以通过模板参数定制增长策略:
cpp复制template <typename T, typename GrowthPolicy = DoubleGrowth>
class MyVector {
// ...
void grow() {
size_t new_cap = GrowthPolicy::calculate(_capacity());
reserve(new_cap);
}
};
struct DoubleGrowth {
static size_t calculate(size_t current) {
return current ? current * 2 : 1;
}
};
struct FixedGrowth {
static size_t calculate(size_t current) {
return current + 1024; // 固定增长1KB
}
};
实现strong exception safety需要特别注意:
cpp复制void push_back(const T& value) {
if (_end == _capacity) {
MyVector temp(*this);
temp.reserve(_size() ? _size() * 2 : 1);
swap(temp); // 无异常操作
}
new (_end) T(value); // 可能抛出异常
++_end;
}
这种实现保证了:
完善析构函数和拷贝控制:
cpp复制~MyVector() {
clear();
::operator delete(_begin);
}
MyVector(const MyVector& other) {
_begin = static_cast<T*>(::operator new(other._capacity()));
_end = _begin + other._size();
_capacity = _begin + other._capacity();
try {
std::uninitialized_copy(other.begin(), other.end(), _begin);
} catch (...) {
::operator delete(_begin);
throw;
}
}
直接构造元素避免临时对象:
cpp复制template <typename... Args>
void emplace_back(Args&&... args) {
if (_end == _capacity) {
reserve(_size() ? _size() * 2 : 1);
}
new (_end) T(std::forward<Args>(args)...);
++_end;
}
使用示例:
cpp复制vector<ComplexObj> vec;
vec.emplace_back(1, 2, "hello"); // 直接构造,无需临时对象
释放多余内存:
cpp复制void shrink_to_fit() {
if (_size() == _capacity()) return;
MyVector temp(*this);
swap(temp);
}
这个实现通过拷贝交换保证了异常安全。
vector操作可能导致迭代器失效的场景:
| 操作 | 失效范围 | 解决方案 |
|---|---|---|
| push_back | 所有迭代器(如果扩容) | 操作后重新获取 |
| insert | 插入点之后的迭代器 | 使用返回值更新 |
| erase | 被删元素之后的迭代器 | 使用返回值更新 |
经验:在循环中修改vector时,务必小心迭代器失效。推荐使用索引或先收集修改再批量处理。
频繁扩容可能导致内存碎片。解决方案:
完整的vector实现需要严格的测试,特别是:
一个简单的测试用例:
cpp复制void test_vector() {
MyVector<int> vec;
assert(vec.empty());
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
assert(vec[i] == i);
}
MyVector<int> vec2 = vec;
assert(vec2.size() == vec.size());
vec2.reserve(1000);
vec2.shrink_to_fit();
}
我们的MyVector简化了以下STL特性:
但在核心功能上完全兼容,可以直接替换STL vector用于学习目的。
实现一个完整的vector容器让我对STL的设计哲学有了更深理解。在实际项目中,除非有非常特殊的性能需求,否则建议直接使用标准库实现。这个练习最大的价值在于,当下次遇到vector相关问题时,你能快速定位到本质原因。