C++ STL vector模拟实现:从三指针模型到迭代器失效的深度解析

1. 项目概述:为什么我们要“手撕”一个vector?

如果你正在学习C++,尤其是学到STL(标准模板库)这一块,那么“模拟实现vector”几乎是一个绕不开的经典练习。这就像学木工,光看别人做精美的榫卯结构没用,你得自己拿起刨子、凿子,亲手做一遍,才能真正理解木头纹理的走向、榫头的松紧度。模拟实现vector,就是C++学习路上的“木工活”。

vector是STL中最基础、最常用的序列容器,它背后封装了动态数组的复杂逻辑——自动扩容、元素搬移、迭代器管理、异常安全等等。很多面试官喜欢问vector的底层原理,比如“插入元素时发生了什么?”、“迭代器为什么会失效?”。如果你只是停留在调用push_backbegin的层面,这些问题会让你一头雾水。但如果你亲手实现过一个简陋但五脏俱全的MyVector,这些问题就会变得异常清晰,因为每一个坑你都亲手踩过,每一个边界条件你都反复斟酌过。

这个项目,就是带你从零开始,构建一个属于你自己的MyVector类。我们将从三个指针(start,finish,end_of_storage)开始,一步步实现构造、析构、增删改查、迭代器,并直面令人头疼的迭代器失效和异常安全问题。最终,你得到的不仅是一个可以运行的代码,更是一份对C++内存管理、模板编程、RAII(资源获取即初始化)等核心概念的深刻理解。无论你是为了夯实基础、应对面试,还是纯粹享受“造轮子”的乐趣,这都是一趟值得投入的旅程。

2. 核心架构设计:理解vector的“三指针”模型

在开始写代码之前,我们必须先想清楚vector在内存中长什么样。标准库的实现细节各编译器略有不同,但核心思想一致:用一段连续的堆内存来存储元素,并用三个指针(或等价物)来标记这块内存的使用状态。

2.1 “三指针”模型详解

这是模拟实现vector最核心、最经典的设计模式,务必理解透彻。

  • _start:指向已申请内存块(storage)的起始位置。这是数组的“头”。
  • _finish:指向当前已存储的最后一个元素的下一个位置。也就是说,[_start, _finish)这个左闭右开区间内存放着所有有效元素。_finish - _start就等于当前容器的大小(size())。
  • _end_of_storage:指向已申请内存块的末尾的下一个位置。[_start, _end_of_storage)表示整个容器的容量(capacity())。

为什么是三个指针?两个不行吗(比如一个指针+两个表示大小和容量的整数)?当然可以,但指针方案在计算地址偏移、进行内存操作时更为直观和高效。例如,判断是否需扩容就是比较_finish_end_of_storage;插入元素就是在_finish处构造新对象并移动_finish

template<class T> class MyVector { public: // ... 成员函数 private: T* _start = nullptr; // 指向内存块开始 T* _finish = nullptr; // 指向最后一个有效元素的下一个位置 T* _end_of_storage = nullptr; // 指向内存块末尾的下一个位置 };

2.2 关键设计决策与权衡

  1. 迭代器类型:我们选择原生指针T*作为迭代器类型。这是因为对于连续存储的vector,其迭代器本质就是指针,支持随机访问(+,-,[])。这简化了实现,也符合标准(std::vector<T>::iterator通常就是T*的别名)。如果为了教学,你也可以封装一个类来实现迭代器,以理解迭代器类的设计模式(如重载++,*,->等运算符)。

  2. 扩容策略:这是vector性能的关键。常见的策略是“倍增”(geometric growth),即每次容量不足时,新容量 = 旧容量 * 2(或1.5)。为什么是2?这是一个经验值,在内存使用效率和减少重新分配次数之间取得了较好的平衡。倍数太小(如1.2),会导致频繁扩容,搬移元素开销大;倍数太大(如3),可能导致内存浪费。标准库并未规定具体倍数,但主流实现(如GCC的libstdc++, MSVC)通常使用2或1.5。在我们的实现中,我们将采用经典的2倍扩容。

  3. 异常安全:这是一个高级话题,但必须考虑。我们的目标是提供“基本异常安全”(basic exception safety)保证:如果操作因异常失败,容器应保持在一个有效状态,且不会发生资源泄漏(如内存泄漏)。例如,在push_back中,如果构造新元素时抛出异常,我们需要确保_finish指针没有前移,容器大小不变,已申请的内存正确释放。

3. 基础框架与资源管理

有了清晰的设计图,我们就可以开始打地基了。第一步是搭建类的骨架,并处理好最根本的资源——内存的获取与释放。

3.1 类模板声明与成员变量

vector必须是模板类,以存储任意类型的元素。

// MyVector.h #pragma once #include <cassert> // 用于断言检查 #include <algorithm> // 用于std::copy等算法,后续自己实现 namespace my { template<class T> class vector { public: // 类型别名,增加可读性并与STL风格保持一致 typedef T* iterator; typedef const T* const_iterator; // 构造函数系列 vector(); explicit vector(size_t n, const T& val = T()); // 防止隐式转换 template<class InputIterator> vector(InputIterator first, InputIterator last); // 迭代器范围构造 // 拷贝控制:拷贝构造、赋值运算符、析构函数 vector(const vector<T>& v); vector<T>& operator=(vector<T> v); // 注意,这里使用“拷贝交换”技法,参数为值传递 ~vector(); // 迭代器 iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } // 容量相关 size_t size() const { return _finish - _start; } size_t capacity() const { return _end_of_storage - _start; } bool empty() const { return _start == _finish; } void reserve(size_t n); // 重点:扩容函数 void resize(size_t n, const T& val = T()); // 调整大小 // 元素访问 T& operator[](size_t pos); const T& operator[](size_t pos) const; T& front() { return *_start; } T& back() { return *(_finish - 1); } // 修改操作 void push_back(const T& x); void pop_back(); iterator insert(iterator pos, const T& x); iterator erase(iterator pos); void clear(); void swap(vector<T>& v); private: iterator _start = nullptr; iterator _finish = nullptr; iterator _end_of_storage = nullptr; }; }

3.2 构造函数、析构函数与初始内存分配

构造函数负责初始化状态,析构函数负责清理资源。这是RAII思想的直接体现。

默认构造函数:最简单,将所有指针置为nullptr,表示一个空容器。

template<class T> vector<T>::vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}

带大小和初始值的构造函数:这里有一个初学者极易踩坑的地方。注意第二个参数const T& val = T()提供了一个默认值T(),即类型T的默认构造值。对于内置类型如intint()就是0。

template<class T> vector<T>::vector(size_t n, const T& val) { _start = new T[n]; // 第一步:申请原始内存 _finish = _start + n; _end_of_storage = _finish; // 第二步:在已申请的内存上构造n个对象,值为val for (size_t i = 0; i < n; ++i) { // 使用“placement new”在指定地址构造对象 // 这是关键!直接赋值(_start[i] = val)对于非平凡类型可能不安全。 new(_start + i) T(val); } }

注意:这里直接使用了new T[n]。对于拥有非平凡构造/析构函数的类类型,这实际上已经调用了n次默认构造函数。然后我们又用placement new和拷贝构造去覆盖它们,这存在一定的效率浪费。更精细的实现可以先用operator new分配原始字节内存,再循环placement new。但为了代码清晰和教学目的,我们采用这种稍显简单的方式,并假设T的默认构造函数开销不大。这是一个重要的取舍点。

迭代器范围构造函数:这是一个模板函数,允许用任何输入迭代器范围来初始化vector,非常强大。

template<class T> template<class InputIterator> vector<T>::vector(InputIterator first, InputIterator last) { // 计算范围大小(注意:InputIterator不一定支持减法,所以需要遍历) size_t n = 0; InputIterator it = first; while (it != last) { ++n; ++it; } // 分配内存 _start = new T[n]; _finish = _start + n; _end_of_storage = _finish; // 拷贝元素 iterator dest = _start; for (InputIterator src = first; src != last; ++src, ++dest) { new(dest) T(*src); // placement new + 拷贝构造 } }

析构函数:必须手动调用每个有效元素的析构函数,然后释放内存。

template<class T> vector<T>::~vector() { if (_start) { // 1. 析构所有有效对象 for (iterator i = _start; i != _finish; ++i) { i->~T(); // 显式调用析构函数 } // 2. 释放内存块 delete[] _start; _start = _finish = _end_of_storage = nullptr; } }

关键点:为什么不能直接delete[] _start?因为对于类类型,delete[]会先调用每个元素的析构函数,再释放内存。我们这里显式调用析构是为了和构造函数中使用的placement new配对,并且逻辑更清晰。实际上,对于用new T[n]分配的内存,直接delete[]是安全的。但为了保持模式统一(特别是如果我们后续改用operator new分配原始内存),显式析构是更好的习惯。

4. 核心功能实现:增删改查与迭代器

基础框架搭好后,我们来实现最让vector“好用”的那些功能。

4.1 扩容机制 reserve() 的实现

这是vector动态性的核心。当size()即将超过capacity()时,就需要扩容。

template<class T> void vector<T>::reserve(size_t n) { if (n > capacity()) { // 1. 申请新的、更大的内存块 T* new_start = new T[n]; size_t old_size = size(); // 2. 将旧数据“移动”到新内存(拷贝构造) // 这里必须用拷贝构造,而不是简单的内存拷贝(memcpy)。 // 因为对于某些类,其拷贝构造函数和赋值运算符可能有特殊语义。 for (size_t i = 0; i < old_size; ++i) { new(new_start + i) T(_start[i]); // placement new + 拷贝构造 } // 3. 析构旧对象并释放旧内存 for (size_t i = 0; i < old_size; ++i) { _start[i].~T(); } delete[] _start; // 4. 更新指针 _start = new_start; _finish = new_start + old_size; _end_of_storage = new_start + n; } // 如果n <= capacity(),则什么都不做 }

踩坑记录:我曾在这里犯过一个错误,试图用memcpy(new_start, _start, sizeof(T) * old_size)来搬运数据。对于像intdouble这样的平凡类型(POD),这确实可以。但对于包含指针、动态内存的类(如std::string),memcpy进行的是浅拷贝,会导致新旧两个对象内部的指针指向同一块内存,析构时同一块内存被释放两次,引发程序崩溃。必须使用拷贝构造或移动语义来转移资源。

4.2 push_back() 与 pop_back()

push_back:在尾部添加元素,是vector最常用的操作。

template<class T> void vector<T>::push_back(const T& x) { // 检查是否需要扩容 if (_finish == _end_of_storage) { // 计算新容量:如果当前容量为0,则扩容到1;否则倍增。 size_t new_capacity = capacity() == 0 ? 1 : capacity() * 2; reserve(new_capacity); } // 在_finish指向的位置构造新对象 new(_finish) T(x); // placement new ++_finish; // 更新大小 }

pop_back:删除尾部元素,相对简单。

template<class T> void vector<T>::pop_back() { assert(!empty()); // 必须确保容器非空 --_finish; _finish->~T(); // 显式调用析构函数,销毁对象 }

4.3 随机访问 operator[] 与 at()

operator[]不进行边界检查,效率高,但需要调用者自己保证安全。

template<class T> T& vector<T>::operator[](size_t pos) { assert(pos < size()); // 使用断言在调试期检查 return _start[pos]; } template<class T> const T& vector<T>::operator[](size_t pos) const { assert(pos < size()); return _start[pos]; }

标准库的vector::at()会进行边界检查,越界时抛出std::out_of_range异常。我们可以类似实现:

template<class T> T& vector<T>::at(size_t pos) { if (pos >= size()) { throw std::out_of_range("vector::at"); } return _start[pos]; }

4.4 insert() 与 erase() 及其引发的迭代器失效问题

这是模拟实现中最复杂、最体现功力的部分,因为它们直接导致了经典的“迭代器失效”问题。

insert在指定位置插入元素

template<class T> typename vector<T>::iterator vector<T>::insert(iterator pos, const T& x) { // 断言检查pos是否在有效范围[_start, _finish]内 assert(pos >= _start && pos <= _finish); // 1. 检查扩容 if (_finish == _end_of_storage) { // !!!关键步骤:扩容会导致原有内存被释放,pos指针失效!!! // 我们必须记录pos相对于_start的偏移量。 size_t offset = pos - _start; size_t new_capacity = capacity() == 0 ? 1 : capacity() * 2; reserve(new_capacity); // 扩容后,更新pos为新内存中的正确位置 pos = _start + offset; } // 2. 将pos及其后的所有元素向后移动一位 // 必须从后往前移动,避免覆盖未移动的元素 iterator end = _finish; while (end > pos) { // 在end位置构造一个由前一个位置移动而来的对象 // 使用移动语义(std::move)可以避免不必要的拷贝,提高效率。 new(end) T(std::move(*(end - 1))); // 析构前一个位置的旧对象(其资源已被移走) (end - 1)->~T(); --end; } // 3. 在pos位置构造新对象 new(pos) T(x); // 4. 更新_finish ++_finish; // 5. 返回指向新插入元素的迭代器 return pos; }

迭代器失效详解:注意代码中的注释。如果在插入前发生了扩容(reserve),那么pos这个迭代器(本质是指针)指向的是旧内存地址。旧内存被释放后,pos就成了一个“野指针”(dangling pointer)。后续对pos的解引用或操作都是未定义行为,通常会导致程序崩溃。我们的解决方案是:在扩容前,计算pos相对于容器头部的偏移量(offset = pos - _start);扩容后,用新的_start加上这个偏移量,得到在新内存中对应的正确位置,并更新pos这就是为什么标准规定,在vector中插入元素可能导致所有迭代器失效的原因。即使不扩容,插入点之后的迭代器也会因为元素移动而失效。

erase删除指定位置元素

template<class T> typename vector<T>::iterator vector<T>::erase(iterator pos) { assert(pos >= _start && pos < _finish); // pos不能等于_finish // 1. 将pos+1及其后的元素向前移动一位,覆盖pos位置的元素 iterator it = pos + 1; while (it != _finish) { // 在it-1位置构造一个由it位置移动而来的对象 new(it - 1) T(std::move(*it)); // 析构it位置的旧对象 it->~T(); ++it; } // 2. 更新_finish --_finish; // 3. 返回指向被删除元素下一个位置的迭代器 // 注意:如果删除的是最后一个元素,则返回end() return pos; }

迭代器失效详解:对于erase,被删除元素及其之后的所有迭代器都会失效。因为后面的元素都向前移动了。标准库的erase会返回一个新的迭代器,指向被删除元素原来所在位置的下一个元素(移动后的新元素)。这个设计是为了方便在循环中连续删除元素:

// 正确用法:使用erase的返回值更新迭代器 for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) { it = v.erase(it); // it指向下一个元素 } else { ++it; } } // 错误用法:在循环中直接++it,会导致迭代器失效 for (auto it = v.begin(); it != v.end(); ++it) { if (*it % 2 == 0) { v.erase(it); // 错误!erase后it失效,再++it行为未定义 } }

4.5 resize() 调整容器大小

resize用于增大或缩小容器。如果新大小n大于当前size(),则用val填充多出的位置;如果小于,则丢弃尾部的元素。

template<class T> void vector<T>::resize(size_t n, const T& val) { if (n > size()) { // 需要增大 if (n > capacity()) { // 如果新大小超过容量,需要扩容 reserve(n); } // 在[_finish, _start+n)区间内构造新元素 for (iterator i = _finish; i != _start + n; ++i) { new(i) T(val); } _finish = _start + n; } else if (n < size()) { // 需要缩小 // 析构尾部多余的元素 for (iterator i = _start + n; i != _finish; ++i) { i->~T(); } _finish = _start + n; } // n == size() 时,什么都不做 }

5. 进阶实现:拷贝控制与“拷贝交换”技法

让我们的MyVector能够像内置类型一样正确地进行拷贝和赋值,是成为合格容器的关键。这里我们实现一个高效且异常安全的方案。

5.1 传统的拷贝构造与赋值重载

拷贝构造函数:深拷贝。为新对象申请一块独立的内存,并将原对象的所有元素拷贝过来。

template<class T> vector<T>::vector(const vector<T>& v) { _start = new T[v.capacity()]; // 按v的容量申请内存 _finish = _start + v.size(); _end_of_storage = _start + v.capacity(); // 拷贝元素 for (size_t i = 0; i < v.size(); ++i) { new(_start + i) T(v._start[i]); // 拷贝构造 } }

赋值运算符重载(传统写法)

template<class T> vector<T>& vector<T>::operator=(const vector<T>& v) { if (this != &v) { // 防止自赋值 // 1. 释放当前资源 for (iterator i = _start; i != _finish; ++i) { i->~T(); } delete[] _start; // 2. 申请新资源并拷贝 _start = new T[v.capacity()]; _finish = _start + v.size(); _end_of_storage = _start + v.capacity(); for (size_t i = 0; i < v.size(); ++i) { new(_start + i) T(v._start[i]); } } return *this; }

传统写法的问题在于:代码重复(和拷贝构造类似),且不是异常安全的。如果在申请新内存或拷贝元素过程中抛出异常,*this对象原有的资源已被释放,对象将处于一个无效状态。

5.2 更优雅的“拷贝交换”技法(Copy-and-Swap Idiom)

这是一种现代C++中非常优雅的实现方式,能自动提供强异常安全保证,并且代码简洁。

template<class T> vector<T>& vector<T>::operator=(vector<T> v) { // 注意!参数是值传递,会调用拷贝构造函数 swap(v); // 交换*this和临时对象v的内容 return *this; // 函数结束,临时对象v被销毁,其析构函数会释放原先*this持有的旧资源。 }

工作原理

  1. vector<T> v是值参数,传入时会调用拷贝构造函数,生成一个原对象的完整副本。如果拷贝构造失败(比如内存不足),异常会在修改*this之前抛出,*this保持不变——强异常安全
  2. 调用swap函数,交换*this和临时对象v的内部指针。这个操作不会抛出异常(交换指针是简单的赋值)。
  3. 函数返回,临时对象v(现在持有*this的旧数据)被析构,自动清理旧资源。

我们需要实现一个高效的swap成员函数:

template<class T> void vector<T>::swap(vector<T>& v) { // 只需交换三个指针,效率极高 std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_end_of_storage, v._end_of_storage); }

同时,我们还可以在命名空间作用域提供一个非成员函数的swap重载,这符合STL的惯例,并且能被一些标准库算法更高效地使用。

namespace my { template<class T> void swap(vector<T>& lhs, vector<T>& rhs) { lhs.swap(rhs); } }

6. 调试、测试与常见问题实录

代码写完了,但让它正确运行才是真正的挑战。下面分享一些我在调试这个MyVector时遇到的典型问题和解决方法。

6.1 基础功能测试用例

编写全面的测试是保证代码质量的关键。以下是一些核心测试场景:

// test_myvector.cpp #include "MyVector.h" #include <iostream> #include <string> using namespace my; using std::cout; using std::endl; void Test1_Basic() { cout << "=== Test 1: 基础构造与push_back ===" << endl; vector<int> v1; cout << "size: " << v1.size() << ", capacity: " << v1.capacity() << endl; // 0, 0 v1.push_back(1); v1.push_back(2); v1.push_back(3); cout << "size: " << v1.size() << ", capacity: " << v1.capacity() << endl; // 3, 4 (假设初始扩到1,然后2,然后4) for (size_t i = 0; i < v1.size(); ++i) { cout << v1[i] << " "; // 1 2 3 } cout << endl; } void Test2_IteratorAndRangeFor() { cout << "\n=== Test 2: 迭代器与范围for ===" << endl; vector<std::string> v2; v2.push_back("Hello"); v2.push_back("World"); v2.push_back("!"); // 使用迭代器 for (vector<std::string>::iterator it = v2.begin(); it != v2.end(); ++it) { cout << *it << " "; } cout << endl; // 使用范围for (依赖于begin()和end()) for (const auto& s : v2) { cout << s << " "; } cout << endl; } void Test3_InsertEraseAndIteratorInvalidation() { cout << "\n=== Test 3: Insert/Erase与迭代器失效 ===" << endl; vector<int> v3; for (int i = 0; i < 5; ++i) v3.push_back(i); // 0 1 2 3 4 auto it = v3.begin() + 2; // it指向元素2 cout << "*it before insert: " << *it << endl; // 插入,可能导致扩容 v3.insert(v3.begin() + 1, 99); // 在位置1插入99 -> 0 99 1 2 3 4 // 此时it可能失效!不能直接使用。 // 正确做法是使用insert返回的新迭代器,或者重新计算 it = v3.begin() + 3; // 重新获取指向原来元素2(现在在位置3)的迭代器 cout << "*it after insert: " << *it << endl; // 应该是2 // 测试erase it = v3.erase(v3.begin()); // 删除第一个元素0,it指向新的第一个元素99 cout << "After erase begin, *returned_it: " << *it << endl; for (auto num : v3) cout << num << " "; // 99 1 2 3 4 cout << endl; } void Test4_CopyAndSwap() { cout << "\n=== Test 4: 拷贝构造与赋值 ===" << endl; vector<int> v4; v4.push_back(10); v4.push_back(20); vector<int> v5(v4); // 拷贝构造 cout << "v5 (copy of v4): "; for (auto n : v5) cout << n << " "; // 10 20 cout << endl; vector<int> v6; v6 = v5; // 赋值运算符 cout << "v6 (assigned from v5): "; for (auto n : v6) cout << n << " "; // 10 20 cout << endl; // 测试自赋值 v6 = v6; cout << "After self-assignment, v6: "; for (auto n : v6) cout << n << " "; // 应该还是 10 20 cout << endl; } int main() { Test1_Basic(); Test2_IteratorAndRangeFor(); Test3_InsertEraseAndIteratorInvalidation(); Test4_CopyAndSwap(); return 0; }

6.2 典型问题与排查技巧

  1. 程序崩溃(Segmentation Fault)

    • 可能原因1:访问越界。operator[]atfrontbackinserterase中,没有对传入的位置参数进行有效性检查。解决:在调试版本中使用assert,在at中使用异常。
    • 可能原因2:迭代器失效后仍在使用。最常见于insert/erase之后,或者在循环中修改容器。解决:牢记insert/erase后,相关迭代器失效的规则,使用函数返回的新迭代器。
    • 可能原因3:内存管理错误。newdelete不匹配(如用new[]分配却用delete释放),或对已释放的内存进行操作。解决:确保new[]对应delete[],并在析构函数和reserve中正确配对placement new和显式析构调用。
  2. 内存泄漏(Memory Leak)

    • 可能原因:析构函数没有正确释放内存,或者在reserveresize、赋值运算符中,申请了新内存但忘记释放旧内存。解决:使用Valgrind(Linux)或Dr. Memory(Windows)等工具检测。确保每个new[]都有对应的delete[],并且在重新分配前释放旧资源。
  3. 元素值错误或重复析构

    • 可能原因:reserve或拷贝时错误地使用了memcpy,导致浅拷贝。对于管理资源的类(如std::stringstd::vector),这会导致多个对象内部指针指向同一块内存,一个对象析构后,其他对象内部的指针就悬空了。解决永远不要对非平凡(non-trivial)类型使用memcpy进行“拷贝”。坚持使用拷贝构造或移动语义。
  4. 扩容策略导致的性能问题

    • 现象:在大量push_back时,程序速度很慢。排查:在reserve函数中加入打印语句,观察扩容频率。如果初始容量为0,插入N个元素,采用倍增策略,大约会发生log2(N)次扩容和元素搬移。如果觉得性能可以接受,则策略合理。如果对性能有极致要求,可以考虑在构造时用reserve预分配足够大的空间,避免中间多次扩容。

6.3 性能优化思考

我们实现的只是一个教学版的vector,离标准库的实现还有距离。以下是一些可以继续优化的方向:

  1. 移动语义(C++11):为类添加移动构造函数和移动赋值运算符,当传入右值(如临时对象)时,可以“窃取”其资源,避免深拷贝,大幅提升效率。

    vector(vector&& v) noexcept; // 移动构造 vector& operator=(vector&& v) noexcept; // 移动赋值

    同时,push_back可以重载一个接受右值引用的版本void push_back(T&& x),使用std::move构造新元素。

  2. 完美转发(C++11)emplace_back函数可以直接在容器尾部构造对象,无需先创建临时对象再拷贝/移动。这需要用到可变参数模板和完美转发。

    template <class... Args> void emplace_back(Args&&... args) { // 检查扩容... new (_finish) T(std::forward<Args>(args)...); // 原地构造 ++_finish; }
  3. 使用std::allocator:标准库的容器通常不直接使用new/delete,而是通过一个“分配器”(allocator)来分配和释放内存。这提供了更大的灵活性,可以将内存分配与对象构造分离,也支持自定义内存池。

  4. 更精细的异常安全:我们的实现提供了基本异常安全。标准库的实现通常追求“强异常安全”(strong exception safety),即操作要么成功,要么完全失败,容器状态保持不变。这需要更复杂的资源管理技巧,如“先拷贝后交换”。

亲手实现一遍vector,就像完成了一次对C++核心机制的深度解剖。你不再是一个STL的普通用户,而是成为了它的“知情者”。下次当你再使用std::vector时,你脑海中会清晰地浮现出那三个指针的舞动,理解每一次push_back背后可能发生的扩容拷贝,并对迭代器失效保持本能的警惕。这份从底层构建起来的认知,是阅读任何教科书都无法替代的。