C++ vector模拟实现:从三指针到深拷贝,彻底理解STL容器核心机制
2026/7/12 1:09:15 网站建设 项目流程

1. 为什么我们要亲手“造轮子”:模拟实现vector的深层价值

如果你正在学习C++,尤其是已经接触到STL(标准模板库)的阶段,那么“vector”这个容器对你来说一定不陌生。它是我们最常用、最顺手的动态数组,push_backpop_back[]操作符用得飞起。但不知道你有没有过这样的疑问:这个“黑盒子”里面到底是怎么工作的?为什么push_back有时会很快,有时又会慢一下?为什么在inserterase之后,我之前保存的迭代器可能会失效,甚至导致程序崩溃?

这些问题,仅仅停留在“使用”层面是找不到答案的。这就好比你会开车,但不懂发动机原理,一旦车子抛锚,你就只能束手无策。而“模拟实现vector”,就是一次亲手拆解这台“发动机”的过程。这不是为了替代标准库中久经考验的std::vector,它的意义在于深度理解。通过从零开始,用代码还原一个简化版的vector,你会彻底明白动态内存管理的精髓、迭代器失效的根源、深浅拷贝的陷阱,以及模板编程的威力。这个过程,是C++从业者从“会用工具”到“理解工具”,乃至未来能“设计工具”的关键一跃。今天,我就带你一起,抛开STL的神秘面纱,用代码和逻辑,深度剖析并亲手实现我们自己的vector

2. 蓝图设计:vector的核心架构与成员变量解析

在动手写代码之前,我们必须先想清楚vector到底是个什么东西。从使用者的角度看,它是一个可以动态增长、支持随机访问的数组。那么,在内存层面,它如何实现“动态”和“连续”这两个核心特性呢?

2.1 三指针定天下:内存管理的基石

几乎所有主流STL实现中,vector的内部都围绕着三个指针来构建。这是我们模拟实现的起点,也是理解其所有行为的关键。

template<class T> class vector { public: typedef T* iterator; // 迭代器就是原生指针 typedef const T* const_iterator; private: iterator _start; // 指向已申请内存块的起始位置 iterator _finish; // 指向最后一个有效元素的下一个位置 iterator _end_of_storage; // 指向已申请内存块末尾的下一个位置 };

这三个指针,清晰地划分了vector内存空间的三个状态区域:

  • _start_finish:这是当前存储的有效元素区间size() = _finish - _start
  • _finish_end_of_storage:这是已申请但尚未使用的空闲容量区间capacity() = _end_of_storage - _start
  • _end_of_storage之后:这是未申请的内存,不属于当前vector对象。

用一个简单的图示来理解:假设我们有一个容量(capacity)为8,当前大小(size)为5的vector<int>

内存布局: [ 1 | 2 | 3 | 4 | 5 | | | ] ^ ^ ^ _start _finish _end_of_storage

这种设计的美妙之处在于,它用最简单的指针运算,高效地管理了动态数组的整个生命周期。begin()返回_startend()返回_finishoperator[]直接基于_start偏移,扩容时也只需要操作这三个指针。

注意:这里将迭代器iterator直接定义为T*类型,是因为对于vector这种连续存储的容器,原生指针完全满足随机访问迭代器的所有要求(解引用、递增、递减、加减整数、比较等)。这简化了实现,也符合STL的常见做法。但切记,并非所有容器的迭代器都是指针(例如list的迭代器就是一个封装过的类对象)。

2.2 模板化设计:容纳万物

我们的类定义以template<class T>开头。这意味着我们的vector是一个类模板T是一个占位符,代表任意类型。当用户写下vector<int>时,编译器会用int替换所有T,生成一个专门处理int的类;写下vector<std::string>时,就生成处理字符串的类。这就是STL“泛型编程”的核心——一份代码,多种类型

这种设计带来了极大的灵活性,但也引入了复杂性。在实现成员函数时,我们必须时刻考虑T可能是内置类型(如int),也可能是自定义的类类型(如MyClass)。这直接影响了我们在拷贝、赋值、析构时的操作选择,后面我们会详细讨论深拷贝与浅拷贝这个关键问题。

3. 从无到有:构造、析构与资源管理

容器对象的生命周期管理是C++的核心课题。一个健壮的vector必须能正确地诞生、复制、赋值和销毁,并妥善管理其动态申请的内存资源。

3.1 构造函数的多样性

一个实用的vector需要提供多种构造方式,以方便用户初始化。

1. 默认构造函数这是最简单的构造,创建一个空的vector。三个指针全部初始化为nullptr

vector() : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) {}

这里使用了初始化列表,这是C++中初始化类成员的首选方式,效率高于在构造函数体内赋值。

2. 填充构造函数创建一个包含n个元素,每个元素都是val的vector。这里有一个非常重要的细节和坑点。

// 版本一:使用size_t vector(size_t n, const T& val = T()) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) { reserve(n); // 先预留空间 for (size_t i = 0; i < n; ++i) { push_back(val); // 再填充元素 } } // 版本二:使用int(关键!) vector(int n, const T& val = T()) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) { reserve(n); for (int i = 0; i < n; ++i) { push_back(val); } }

为什么需要两个版本?关键在于模板的匹配优先级。假设我们写vector<int> v(10, 5);,编译器会尝试匹配构造函数。它有两个候选:

  • vector(size_t n, const T& val = T()):需要将int类型的10转换成size_t
  • vector(InputIterator first, InputIterator last)(后面会讲):两个参数都是int,完全匹配。

编译器认为“完全匹配”优于“需要转换的匹配”,因此会选择迭代器区间构造函数,这会导致将105当作迭代器去解引用(*first),引发非法内存访问。提供一个int版本的构造函数,就是为了避免这个陷阱,让vector<int> v(10, 5)这种常见写法能正确匹配到填充构造。

3. 迭代器区间构造函数这是一个函数模板,它接受两个迭代器[first, last),将区间内的元素拷贝到新vector中。这种设计使得vector可以用任何容器的迭代器来初始化,体现了STL的通用性。

template <class InputIterator> vector(InputIterator first, InputIterator last) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) { while (first != last) { push_back(*first); // 依次取出迭代器指向的元素并插入 ++first; } }

你可以这样使用它:

int arr[] = {1, 2, 3, 4, 5}; vector<int> v1(arr, arr + 5); // 用数组指针(也是一种迭代器)初始化 std::list<int> myList = {6, 7, 8}; vector<int> v2(myList.begin(), myList.end()); // 用list的迭代器初始化

3.2 析构函数:安全释放资源

由于我们在堆上动态申请了内存(new T[n]),析构函数必须负责释放,否则会造成内存泄漏。

~vector() { if (_start) { // 判断是否申请过内存 delete[] _start; // 使用delete[]释放数组 _start = _finish = _end_of_storage = nullptr; // 置空,防止野指针 } }

这里有两个关键点:

  1. delete[]对应new[]:我们使用new T[n]申请数组,必须使用delete[]释放。如果误用delete,行为是未定义的,通常会导致程序崩溃。
  2. 置空指针:释放后立即将指针置为nullptr是一个好习惯。这可以防止后续代码误用这些“悬空指针”,同时if(_start)的判断也能在多次析构时保证安全。

4. 拷贝控制:深拷贝与浅拷贝的生死抉择

这是模拟实现中最容易出错,也最能体现C++功底的部分。默认情况下,编译器会为我们生成拷贝构造函数和赋值运算符,但它们执行的是浅拷贝(按位拷贝)。对于管理动态内存的类,这绝对是灾难。

4.1 拷贝构造函数:从零开始复制一个对象

浅拷贝只会复制三个指针的值,导致两个vector对象指向同一块内存。当其中一个对象析构释放内存后,另一个对象的指针就变成了野指针,再次析构或访问都会导致严重错误。

// 错误的默认行为(浅拷贝) vector(const vector<T>& v) { _start = v._start; // 直接复制指针,指向同一块内存 _finish = v._finish; _end_of_storage = v._end_of_storage; } // v1和v2的指针指向同一地址,析构时同一内存被释放两次!

我们必须实现深拷贝,为新对象申请独立的内存,并逐个复制元素。

vector(const vector<T>& v) : _start(nullptr), _finish(nullptr), _end_of_storage(nullptr) // 先初始化为空 { reserve(v.capacity()); // 申请与v一样大的空间 for (size_t i = 0; i < v.size(); ++i) { _start[i] = v._start[i]; // 关键:逐个元素赋值 } _finish = _start + v.size(); _end_of_storage = _start + v.capacity(); }

这里为什么用_start[i] = v._start[i];而不是memcpy?因为T可能是自定义类型。memcpy是内存的二进制拷贝,如果Tstd::string这类带有内部指针的类,memcpy只会复制指针值,造成两个string对象共享同一块字符缓冲区,这依然是浅拷贝,会导致双重释放。而operator=(对于内置类型就是赋值)会调用对象自己的拷贝赋值运算符,确保真正的深拷贝发生。

4.2 赋值运算符重载:处理自赋值与资源复用

赋值操作v1 = v2;需要先释放v1原有的资源,再像拷贝构造一样复制v2的内容。这里有一个经典陷阱:自赋值,即v1 = v1;。如果不加检查,先delete[] _start,就把自己的数据也删除了。

vector<T>& operator=(const vector<T>& v) { if (this != &v) { // 1. 检查自赋值 // 2. 释放原有资源 delete[] _start; // 3. 申请新空间并拷贝数据 _start = new T[v.capacity()]; for (size_t i = 0; i < v.size(); ++i) { _start[i] = v._start[i]; } _finish = _start + v.size(); _end_of_storage = _start + v.capacity(); } return *this; // 4. 返回自身引用以支持连续赋值 (a = b = c) }

上述实现是可行的,但不够高效。因为它总是先释放再申请。一个更优的“拷贝并交换”写法在现代C++中更受欢迎,它利用了移动语义,但为了理解基本原理,我们先掌握这个版本。

实操心得:在实现管理资源的类时(如vector、string),“三大件”(拷贝构造、赋值运算符、析构)通常需要同时定义或禁用。如果你定义了其中一个,往往意味着你需要处理深拷贝问题,另外两个也应该手动定义。这就是著名的“Rule of Three”(C++11后发展为“Rule of Five”,增加了移动构造和移动赋值)。

5. 容量管理:reserve与resize的玄机

容量管理是vector动态性的核心。reserve负责保证容量,resize负责改变大小,两者逻辑不同,极易混淆。

5.1 reserve:预分配内存,避免多次扩容

reserve(n)保证capacity()至少为n。如果当前容量已足够,它什么都不做;如果不足,则重新申请一块更大的内存,将旧数据搬过去,然后释放旧内存。

void reserve(size_t n) { if (n > capacity()) { // 只有需要扩容时才操作 size_t old_sz = size(); // 【关键步骤】在改变_start前保存当前元素个数 T* tmp = new T[n]; // 申请新空间 if (_start) { for (size_t i = 0; i < old_sz; ++i) { tmp[i] = _start[i]; // 深拷贝旧数据 } delete[] _start; // 释放旧空间 } // 更新指针 _start = tmp; _finish = _start + old_sz; // 使用之前保存的old_sz _end_of_storage = _start + n; } }

这里有一个极其隐蔽的坑:为什么一定要在if外面先用old_sz = size()保存大小?因为size()的计算依赖于_start_finish。一旦我们执行了_start = tmp;_finish(此时还指向旧内存)和新的_start之间的减法运算_finish - _start就失去了意义,结果未定义。如果先计算并保存old_sz,逻辑就清晰了。

5.2 resize:调整有效元素个数

resize(n, val)将元素个数改为n。如果n小于当前大小,就丢弃尾部多余的元素(析构它们);如果n大于当前大小,则在尾部添加值为val的新元素;如果n大于当前容量,则需要先扩容。

void resize(size_t n, const T& val = T()) { if (n < size()) { // 缩小:只需调整_finish指针。注意,对于自定义类型,被“丢弃”的元素应该被析构。 // 简化实现中,我们仅移动指针。更严格的实现需要调用析构函数。 _finish = _start + n; } else { // 扩大 if (n > capacity()) { reserve(n); // 确保容量足够 } iterator it = _finish; _finish = _start + n; // 先将_finish指向目标位置 while (it != _finish) { *it = val; // 在未初始化的空间上赋值(对于内置类型是初始化,对于类类型调用赋值运算符) ++it; } // 注意:更精确的做法是,对于[_finish, _start+n)的“新”空间,应该用placement new构造,而不是赋值。 // 这里简化处理,假设T的默认赋值操作是安全的。 } }

resize的默认值val = T()利用了C++的一个特性:对于任意类型TT()会生成一个值初始化的临时对象。对于内置类型如intint()就是0;对于类类型,就是调用其默认构造函数。这保证了resize行为的合理性。

6. 元素访问:迭代器与operator[]的实现

访问元素是容器最基本的功能。vector提供两种主要方式:迭代器和下标。

6.1 迭代器:让算法与容器分离

STL的精髓之一就是用迭代器作为算法和容器之间的桥梁。对于vector,迭代器就是原生指针T*,因为指针天然支持随机访问。

typedef T* iterator; typedef const T* const_iterator; iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; }

我们提供了普通版本和const版本。const版本的迭代器指向的元素不可修改,用于const vector对象。这使得我们可以写出这样的代码:

void printVector(const vector<int>& v) { for (vector<int>::const_iterator it = v.begin(); it != v.end(); ++it) { cout << *it << " "; // 可以读 // *it = 10; // 错误!不能通过const_iterator修改元素 } }

6.2 operator[]:像数组一样随机访问

重载下标运算符让我们可以像使用内置数组一样使用vector。

T& operator[](size_t pos) { assert(pos < size()); // 越界检查,非常重要! return _start[pos]; } const T& operator[](size_t pos) const { assert(pos < size()); return _start[pos]; }

同样提供了const和非const版本。assert在调试阶段能快速定位越界访问错误。在release版本中,assert通常被定义为空,为了更健壮,商业代码可能会选择抛出异常(如std::out_of_range)。

7. 增删元素:push_back、insert、erase与迭代器失效陷阱

这是vector最核心、也最需要小心使用的部分,尤其是涉及到迭代器失效的问题。

7.1 push_back 与 pop_back:尾部的快速操作

push_back在尾部添加一个元素。如果空间不足(_finish == _end_of_storage),需要先扩容。

void push_back(const T& x) { if (_finish == _end_of_storage) { // 扩容策略:如果容量为0,扩到4;否则扩为2倍。 // 这是许多STL实现的常见策略,在空间和时间效率上取得平衡。 size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2; reserve(new_capacity); } *_finish = x; // 在_finish位置构造/赋值新元素 ++_finish; }

pop_back则简单得多,只需将_finish向前移动一位。但必须确保容器非空。

void pop_back() { assert(!empty()); // 防止对空vector调用pop_back --_finish; // 注意:对于自定义类型,这里应该调用析构函数销毁被“弹出”的元素。 // 简化实现中,我们只移动指针,真正的析构发生在vector整体析构或内存被覆盖时。 }

7.2 insert:在任意位置插入,迭代器失效的根源

insert(pos, val)在迭代器pos指向的位置前插入新元素val。这是导致迭代器失效的典型操作。

iterator insert(iterator pos, const T& val) { assert(pos >= _start && pos <= _finish); // 检查pos合法性 // 1. 检查容量,可能扩容 if (_finish == _end_of_storage) { size_t len = pos - _start; // 【关键】保存pos的相对位置 size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2; reserve(new_capacity); pos = _start + len; // 扩容后,pos指向旧内存,失效了!必须更新。 } // 2. 移动元素,腾出pos位置 iterator end = _finish - 1; while (end >= pos) { *(end + 1) = *end; // 从后向前,逐个后移 --end; } // 3. 插入新元素 *pos = val; ++_finish; return pos; // 返回指向新插入元素的迭代器 }

为什么需要len = pos - _start因为reserve会申请新内存,释放旧内存。参数pos是用户传入的,它指向旧内存中的一个地址。扩容后,旧内存被释放,pos变成了一个野指针(悬垂迭代器)。如果我们直接使用这个失效的pos去访问或移动数据,程序必然崩溃。所以,我们必须先计算出pos相对于起始位置_start的偏移量len,在扩容并更新_start后,用_start + len计算出在新内存中的对应位置,更新pos。这就是处理迭代器失效的核心逻辑。

7.3 erase:删除元素,另一种失效场景

erase(pos)删除pos位置的元素。

iterator erase(iterator pos) { assert(pos >= _start && pos < _finish); iterator begin = pos + 1; while (begin != _finish) { *(begin - 1) = *begin; // 从前向后,逐个前移覆盖 ++begin; } --_finish; return pos; // 注意:返回的pos指向被删除元素的下一个位置(原pos+1的位置) }

erase也会导致迭代器失效,但原因和insert不同。erase后,pos及其之后的所有迭代器都失效了(因为元素位置发生了移动)。标准规定,erase返回一个指向被删除元素之后位置的迭代器,方便用户继续操作。例如,在循环中删除满足条件的元素,应该这样写:

vector<int> v = {1, 2, 3, 4, 5}; for (auto it = v.begin(); it != v.end(); ) { if (*it % 2 == 0) { it = v.erase(it); // 用返回值更新it,it指向被删除元素的下一个 } else { ++it; } } // 错误写法:v.erase(it); ++it; // erase后it已失效,再++是未定义行为

避坑指南:迭代器失效总结

  1. 扩容导致失效:任何引起reserve的操作(如push_back导致扩容、insert导致扩容),所有迭代器、指针、引用都会失效。
  2. 插入导致失效:在pos位置insertpos及其之后的所有迭代器、指针、引用都会失效(因为元素后移了)。
  3. 删除导致失效:在pos位置erasepos及其之后的所有迭代器、指针、引用都会失效(因为元素前移了)。 牢记:一旦发生可能引起内存重新分配或元素移动的操作,之前获取的迭代器就不要再使用了。这是使用vector(以及其他序列容器)时必须时刻绷紧的一根弦。

8. 模拟实现中的进阶思考与优化方向

我们实现了一个简化但功能完整的vector。然而,与标准库的std::vector相比,它还有很多可以优化和深入的地方。理解这些差异,能让你对C++的理解再上一个台阶。

8.1 异常安全与强异常保证

我们的实现假设T的拷贝构造函数和赋值运算符不会抛出异常。但在现实中,自定义类型的这些操作可能抛出异常(如内存不足)。一个工业级的实现需要考虑异常安全。例如,在reserve中,如果new T[n]失败抛出std::bad_alloc,我们的函数会直接抛出异常,但此时旧数据尚未被破坏,这提供了基本异常保证。如果要提供强异常保证(操作要么成功,要么完全不影响原状态),实现会复杂得多,通常需要“先分配新内存并拷贝,成功后再替换指针”的策略,并妥善处理拷贝过程中的异常。

8.2 迭代器类型与traits技术

我们将迭代器简单定义为T*。标准库的迭代器是一个更为复杂的体系,它通过迭代器标签traits(特性萃取)技术来区分不同类型的迭代器(输入、输出、前向、双向、随机访问)。这使得STL算法可以根据迭代器能力选择最高效的实现。例如,std::sort要求随机访问迭代器,所以它可以用于vectordeque,但不能用于list

8.3 使用allocator分配器

我们直接使用new[]delete[]进行内存分配。标准库的std::vector模板的第二个参数就是一个分配器(Allocator),默认为std::allocator<T>。分配器将内存分配和对象构造分离开来。它先分配一块原始内存,然后在需要的位置用placement new构造对象;删除时,先调用析构函数,再释放内存。这提供了更大的灵活性,允许用户自定义内存管理策略(如使用内存池)。

8.4 移动语义(C++11)

现代C++引入了移动语义。对于vector这样的资源管理类,实现移动构造函数移动赋值运算符可以大幅提升性能。移动操作“窃取”右值临时对象的资源(如内部指针),而不是进行深拷贝,避免了不必要的内存分配和数据复制。

// 移动构造函数示例 vector(vector<T>&& v) noexcept : _start(v._start), _finish(v._finish), _end_of_storage(v._end_of_storage) { v._start = v._finish = v._end_of_storage = nullptr; // 将源对象置于有效但空的状态 }

当发生vector<int> v2 = std::move(v1);时,会调用移动构造,v2直接接管v1的内存,v1变为空。这比深拷贝高效得多。

8.5 更精确的元素生命周期管理

在我们的简化resizepop_back中,对于被“删除”的元素,我们只是移动了指针,并没有显式调用其析构函数。对于内置类型这没问题,但对于自定义类型,这可能造成资源泄漏(例如,元素是一个管理文件句柄的类)。更严格的实现应该在减少size时,显式调用需要销毁的元素的析构函数。同样,在reserve拷贝数据时,更优的做法是在新内存上构造新对象(使用std::uninitialized_copyplacement new),而不是先默认构造再赋值。

9. 常见问题与调试技巧实录

在亲手实现和调试vector的过程中,你几乎一定会遇到下面这些问题。我把它们和排查思路记录下来,希望能帮你节省时间。

9.1 问题一:程序在push_back后崩溃,报错“double free or corruption”

现象:使用了拷贝构造或赋值后,程序在析构时崩溃。根因浅拷贝问题。你没有实现拷贝构造函数和赋值运算符,或者实现有误(如用了memcpy),导致两个vector对象内部的指针指向同一块内存。当第一个对象析构释放内存后,第二个对象的指针变成野指针。第二个对象析构时,对野指针执行delete[],导致双重释放。排查:检查是否实现了“三大件”(拷贝构造、赋值运算符、析构)。确保在拷贝时进行了深拷贝(使用循环赋值,而非内存拷贝)。

9.2 问题二:insert或erase操作后,使用之前的迭代器程序崩溃

现象:在insertpush_back(导致扩容)后,继续使用之前保存的迭代器访问数据,程序访问了非法内存。根因迭代器失效。你没有理解或处理好扩容或元素移动导致的迭代器失效问题。排查

  1. insert中,检查扩容(reserve)之后,是否更新了传入的pos迭代器(通过保存偏移量len)。
  2. 提醒使用者,在inserterase、可能导致扩容的push_back之后,之前获取的所有迭代器、指针、引用都应视为失效,不可再使用。

9.3 问题三:使用vector<int> v(10, 5)构造时编译报错或运行时崩溃

现象:编译器报错“非法间接寻址”或运行时出现访问异常。根因构造函数匹配错误。你只提供了vector(size_t n, const T& val),当使用两个int参数调用时,编译器优先匹配了迭代器区间构造函数模板。解决:按照前面所述,额外提供一个vector(int n, const T& val)的重载版本。

9.4 问题四:resize或赋值后,元素的值出现乱码或非预期值

现象:特别是当T是自定义类时,新添加的元素状态不对。根因未初始化的内存访问。在resize扩大容量,或reserve后,新分配的内存空间是“原始”的,对于类类型对象,需要调用构造函数进行初始化,而不是直接赋值。排查:检查resize中对于新增元素的部分,是直接*it = val;(赋值),还是应该先构造。严格来说,对于从未构造过对象的内存,应该使用placement new进行构造:new (address) T(value);。我们的简化实现假设T的赋值操作对未构造内存是安全的,这对于有默认构造且赋值操作简单的类可能可行,但不是最规范的做法。

9.5 调试技巧:可视化与单元测试

  1. 打印状态:在关键函数(如reserve,insert, 析构)的开始和结束处,打印三个指针的地址和容器size/capacity。这能帮你直观看到内存何时被重新分配。
  2. 使用简单元素类型:初期测试使用intdouble,排除自定义类型拷贝控制逻辑错误的干扰。
  3. 编写单元测试:系统性地测试各种边界情况。
    • 默认构造、填充构造、迭代器构造。
    • 拷贝构造和赋值(测试深拷贝)。
    • 连续push_back触发多次扩容。
    • insert在开头、中间、末尾插入,并触发扩容。
    • erase删除开头、中间、末尾元素,以及循环删除。
    • 混合操作,并检查迭代器失效情况。
  4. 使用Valgrind或AddressSanitizer:这些工具能检测内存泄漏、越界访问、使用未初始化内存等问题,是C/C++程序员的神器。

亲手实现一遍vector,就像完成了一次对动态数组和C++资源管理的深度解剖。你不再只是STL的使用者,你看到了华丽接口下的齿轮是如何咬合的,明白了效率与安全的权衡,更深刻地理解了迭代器、模板、异常安全这些概念。这份理解,会让你在未来使用任何容器和编写资源管理类时,都更加得心应手,避免踩坑。虽然我们实现的版本距离std::vector还有距离,但核心思想已经完整呈现。接下来,你可以尝试挑战listmap的模拟实现,那时你会对迭代器的封装、树形结构有更深的认识。编程之路,知其然,更要知其所以然,共勉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询