一. vector是什么?
1. vector的定义与概念
vector是 C++ 标准模板库(STL)中的动态数组容器,支持动态调整大小、快速随机访问和灵活的插入与删除操作。可以将vector看成是一个动态顺序表或者是可变长数组。不过,与数据结构中的顺序表不同的是,vector支持不同类型的vector容器的对象同时存在。而它能支持这个功能是因为vector是底层是一个模板类。
cpp
template < class T, class Alloc = allocator<T> > class vector; // generic template
2. vector的实际意义
vector在实际中的应用很广泛,无论是工程项目,还是算法竞赛都会频繁的使用到vector。vector的主要的应用场景就是顺序的存储数据,例如:用户信息的存储,算法竞赛中测试数据的存放等等。
二. vector的基本用法
1. vector的构造和遍历
1.1 vector的构造
vector的构造方法共有4个(这里主要讲解C++98版本的vector),其构造方式如下图

⚠ 注意:vector的成员函数中有关于
allocator_type的参数代表的都是与内存池和空间配置器相关,而C++标准库中对vector等STL的容器都设有默认的空间配置器,也就是缺省值,所以我们不需要关注allocator_type类型的参数。
-
默认构造
vector的默认构造其实就是构造了一个没有任何数据的vector容器
cpp// 默认构造 vector<int> v1; vector<double> v2; -
初始时构造n个元素
vector在构造时可以直接初始化为具有n个数据的容器,同时这n个数据的值也可以被设定,默认数值为
value_type()其实也就是0,其构造方式如下cpp// v1初始化构造了10个元素,默认的初始值都是0 vector<int> v1(10); // v2初始化也构造了10个元素,默认的初始值都是1 vector<int> v2(10, 1); -
迭代器区间构造
vector可以使用相同类型的vector对象的迭代器区间进行构造,其构造效果就是将这段迭代器区间的值顺序插入在vector中
cpp// v1中有10个3 vector<int> v1(10, 3); // v2中有5个3 vector<int> v2(v1.begin() + 5, v1.end()); -
拷贝构造
vector中还有封装好的拷贝构造,能进行深拷贝
cppvector<int> v1(5, 1); vector<int> v2(v1);
1.2 vector的遍历
vector的遍历方式基本上和string的遍历方式一摸一样,在宏观上看只有两种遍历方式,分别是使用for循环+operator[]遍历和迭代器遍历。而使用迭代器遍历又分为两种方式,一种是直接使用iterator遍历,另外一种是使用范围for进行遍历,下面以v1进行演示
cpp
vector<int> v1;
v1.push_back(1); //在v1的尾部插入元素
v1.push_back(2);
v1.push_back(3);
-
for循环+operator\[\]
cppfor(size_t i = 0; i < v1.size(); i++) { //cout << v1.operator[](i) << endl; cout << v1[i] << " "; } // 输出:1 2 3 -
迭代器
-
使用iterator遍历
cppvector<int>::iterator it = v1.begin(); while(it != v1.end()) { cout << *it << " "; ++it; } // 输出:1 2 3 -
范围for
cppfor(auto& e : v1) { cout << e << " "; } // 输出:1 2 3
-
2. vector的增删查改
在C++98中,vector较为常用的修改元素的成员函数有push_back,pop_back,insert,erase,这些函数我们在string的使用部分都见过了,它们的功能和使用方法都大同小异(后文会讲解)。而C++11又引入了两个新的修改相关的成员函数emplace_back和emplace其作用和push_back,insert差不多,但是底层原理和涉及的知识却非常的多,这里两个成员函数我们在之后的篇章进行详细的讲解。

-
push_back/pop_back
这两个成员函数分别对应着尾插和尾删,它们的功能和使用和string中的push_back,pop_back完全一样,下面进行演示
cppvector<int> v1; v1.push_back(1); //尾部插入1 v1.push_back(2); //尾部插入2 v1.pop_back(); //尾部删除2 v1.pop_back(); //尾部删除1 -
insert
insert的函数功能就是在vector的任意位置插入一个数据或者是一段区间范围的数据。vector的insert(之后用vector::insert)与string的insert(之后用string::insert)不同的是string::insert是通过传递插入位置的下标或下标区间进行插入数据,而vector::insert是通过传递插入位置的迭代器或迭代器区间进行插入数据。vector::insert函数重载图如下

vector::insert使用演示
cppvector<int> v1; v1.insert(v1.begin(), 10); //传递初始位置的迭代器,相当于是头插 v1.insert(v1.end(), 100); //传递末尾位置的迭代器,相当于是尾插 v1.insert(v1.begin() + 1, 10, 2); //在v1[1]位置插入10个1 vector<int> v2; v2.insert(v2.begin(), v1.begin(), v1.end()); //往v2的起始位置插入一段迭代器区间的值 // v1中的数据:10 2 2 2 2 2 2 2 2 2 2 100 // v2中的数据:10 2 2 2 2 2 2 2 2 2 2 100⚠ 注意:使用vector::insert时,传递的插入位置的迭代器是有范围限制的,其只能传递
[begin(), end()]区间的迭代器,超出这个区间,程序会断言报错。 -
erase
erase的使用和insert的使用一样,都是使用迭代器或迭代器区间来确定函数需要作用的位置。如果传递一个有效的迭代器,则erase会删除这个迭代器指向位置的元素,如果传递的是一个迭代器区间,则erase会删除这个迭代器区间的所有的元素。需要注意的是,erase删除的迭代器区间范围是
[first, last),last指向位置的元素并不会被删除。所以,如果想删除vector容器中所有的值,erase的调用逻辑应该是erase(v.begin(), v.end()),而不是erase(v.begin(), v.end() - 1)
erase使用演示
cppvector<int> v1(10, 2); v1.erase(v1.begin()); //只删除begin指向位置的元素,相当于头删 v1.erase(v1.begin(), v1.end()); //删除v1中所有的数值
3. vector的拓展用法
-
initializer_list
vector的构造方法不仅仅有上面讲的那4种方式,还有一种较为常见的构造方法,使用
initializer_list容器构造。其构造方法如下。不难发现,使用initializer_list的构造方法和静态数组的初始化非常相似(int a[] = {1,2,3};),而这是因为initializer_list本身就像是一个静态数组一样的东西,下面我们来看一下std::initializer_list的定义。cpp// 使用initializer_list构造初始化vector vector<int> v1({1,2,3,4,5}); for(auto& e : v1) { cout << e << " "; } cout << endl; // 输出:1 2 3 4 5initializer_list

从标准库中给出的
initializer_list的说明(which is a list of elements of type const T)得出,它其实就是一个可以存放任意类型列静态数组,比起普通数组,只是initializer_list被封装成了类模板,但是其不存在什么扩容机制。所以,vector的初始化可以使用{数据}的样式,其本质还是构造了一个initializer_list的临时对象来初始化vector。
-
emplace_back
emplace_back我们暂且可以将他当作和push_back一样的东西,但是它和push_back还是有些差异的,这些差异主要体现在对于自定义类型的插入数据的操作。比较明显的差异有两个。
cppstruct A { A(int a1, int a2) :_a1(a1) ,_a2(a2) { cout << "A(int a1, int a2)" << endl; } A(const A& aa) :_a1(aa._a1) ,_a2(aa._a2) { cout << "A(const A& aa)" << endl; } int _a1; int _a2; };-
使用方法方面
在【C++】类和对象(下)讲过,对于具有单参数构造函数的类类型,可以直接使用参数隐式类型转换为该类型,如果要使用多参数的构造函数进行隐式类型转换,多个参数需要使用花括号将参数按顺序括起来(
{参数1,参数2......参数n})。所以,对于一个存放A类型的vector,push_back的插入方式需要使用花括号,但是emplace_back不能使用花括号将参数括起来。cppvector<A> v1; int a1 = 1, a2 = 2; // 使用多参数的构造进行隐式类型转换,参数需要使用{}括起来 v1.push_back({a1, a2}); // emplace_back的特殊性 v1.emplace_back(a1, a2); -
效率方面
既然emplace_back在使用上弄了特殊,那肯定是有原因的,这原因就体现在了emplace_back插入数据的效率上。我们可以创建如下场景进行检验。从输出结果可以发现,使用
emplace_back(3, 4)的效率最高,只有一次拷贝构造。cppvector<A> v1; A aa(10, 20); // push_back; v1.push_back(aa); cout << endl; v1.push_back(A(1, 2)); cout << endl; v1.push_back({ 3,4 }); cout << endl << endl; // emplace v1.emplace_back(aa); cout << endl; v1.emplace_back(A(1, 2)); cout << endl; v1.emplace_back(3, 4);
-
注意:使用emplace_back插入数据并不是所有的场景下都会有效率的提升。例如内置类型的插入数据就没有什么实质性的效率提升,而自定义类型也不是任意时候都有效率提升,上面代码中的效率提升仅仅是限于在那个特殊的场景。
三. vector的模拟实现
1. vector源码的简单分析
前面我们说了,vector本质上可以看成数据结构中的动态顺序表,那么vector的底层实现是否就是实现这么一个简单的顺序表呢?我们暂时先不盖棺定论,可以先看vector的源码,分析vector其底层是如何实现的。而分析源码上也有一些技巧,我把它分为了3个步骤
注:源码来源于侯捷老师的《STL源码剖析》
-
先找到核心的类的实现,再看类的核心------成员变量
- 首先是在源码的各个文件中找到核心的类的实现的文件,以vector源码为例,这里我们可以通过文件名直接锁定到
stl_vector.h文件中
cpp#ifndef __SGI_STL_VECTOR #define __SGI_STL_VECTOR #include <stl_algobase.h> #include <stl_alloc.h> #include <stl_construct.h> #include <stl_uninitialized.h> #include <stl_vector.h> // 核心类的实现 #include <stl_bvector.h> #endif /* __SGI_STL_VECTOR */ // Local Variables: // mode:C++ // End:-
接着就在
stl_vector.h中vector类的核心,也就是成员变量
- 首先是在源码的各个文件中找到核心的类的实现的文件,以vector源码为例,这里我们可以通过文件名直接锁定到
-
锁定成员变量后,连蒙带猜的理解/确定成员变量的意思
通过源码我们发现,这里的成员变量的定义并不是我们预想的那样(动态顺序表的成员),而是3个迭代器,那这里3个迭代器又代表了什么意思呢?前面我们说了vector本质上是一个动态顺序表,那么不管它的底层是如何实现的,源码的版本是什么,其成员变量一定要能表示出动态顺序表的基础:数组/容器( _array),元素个数( _size),容量( _capacity)。
所以,这里我们就可以猜测,start是指向数组/容器的起始位置的迭代器,finish是指向容器最后一个有效元素下一个位置的迭代器,end_of_storage是指向容器最后一个有效空间下一个位置的迭代器。那么如何进行验证呢?我们可以通过成员函数
begin(), end(), capacity()来验证猜测。

从begin和end函数可以直接确定start和finish的猜测是正确的,因为
指向容器最后一个有效空间下一个位置的迭代器 - 容器起始位置的迭代器 = 容器的总容量,所以可以确定end_of_storage的猜测正确。 -
大概理解类中核心成员函数的实现,弄懂这个类的核心作用
有了上面的铺垫,到这一步其实已经很轻松了,上面确定了的成员变量的各个含义,那么容器的大小size可以通过
finish - start得到,容器的容量capacity可以通过end_of_storage - start得到。因为动态顺序表的各种操作都是基于size和capatity实现的,所以这里不难推断出vector成员函数的实现是基于3个成员变量间相互的运算。知道了这个规律,vector的底层实现就可以类比动态顺序表的实现,代码编写也就变得容易了。
⚠ 注意:我们这里分析vector的源码这么轻松是因为我们提前就知道了vector的作用且这里只有一个核心的类,但是实际的项目情况远没有这么简单,项目中可能会有上百个类,它们各自都对应着你不知道的功能。所以,我们对于源码的阅读能力还需要慢慢加强,精进。最后要记住,读源码时切记不要死磕细节,不要拿到源代码后按顺序一点一点的去看,主要是理解大框架与核心。
2. vector的模拟实现
vector类的主体框架
| 成员变量 | 含义 | 直接相关函数 |
|---|---|---|
iterator _start |
指向数组/容器的起始位置的迭代器 | begin() = _start |
iterator _finish |
指向容器最后一个有效元素下一个位置的迭代器 | end() = _finish |
iterator _end_of_storage |
指向容器最后一个有效空间下一个位置的迭代器 | capacity() = _end_of_storage - _star |
cpp
// vector.h
namespace yzx
{
template<class T>
class vector
{
public:
typedef T* iterator; //为了框架易懂,这里提前透露迭代器的底层`^`
vector() {}
成员函数......
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
前面有了【C++】string的模拟实现(感兴趣的读者可以去阅读,那里讲过的很多基础的这里就不会再讲了)的基础后,这里命名空间的作用就不再多说了。唯一需要注意的是,因为vector是类模板,而类模板的声明和定义不能分离到不同的文件 ,所以vector的成员函数的实现全都要写在vector.h中。
构造函数的实现
构造函数的目的就是需要对成员变量进行初始化,但是这里的3个成员变量都是迭代器,那要如何初始化呢?前面我们说了,迭代器是像指针一样的东西,但是并不一定是指针(vs2022编译器下vector的迭代器就不是指针)。但是,这里为了方便起见,就直接使用T类型的指针作为迭代器的底层。
cpp
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
但是这样实现其实还是有些问题,如果不做修改的话,之后每写一个构造函数,都要显式的使用初始化列表将3个迭代器赋值为nullptr,而这样实现太麻烦了,这里可以使用【C++】类和对象(下)讲的内容,使用缺省值初始化成员变量。如下所示
cpp
class vector
{
public:
vector(){}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
使用initializer_list构造
为了后面测试方便,这里我们先实现使用initializer_list对象做参数的构造函数。当然,这里就需要提前透支一下push_back和reserve了,文章后面会实现的^ _ ^
cpp
vector(initializer_list<T>& li)
{
reserve(li.size());
for(const auto& e : li)
{
push_back(e);
}
}
注:这里先只实现两个构造函数的重载,其它的构造函数等后面实现了一些成员函数后,直接复用成员函数,实现起来会更加方便。
迭代器相关函数的实现
-
iterator
cppiterator begin() { return _start; }cppiterator end() { return _finish; } -
const_iterator
iterator只能用于普通的vector对象的遍历,当遇到const修饰的vector对象时,就需要const迭代器,也就是
const_iterator。因为const迭代器不能修改其指向的位置的值(简单来讲就是不能修改*const迭代器),所以const_iterator的实质为const T*。cpptypedef const T* const_iterator; const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } -
泛化编程(模板遍历)
实现了迭代器之后,我们就可以使用范围for来遍历vector容器,为了方便后续测试成员函数,可以将遍历vector容器的功能封装成一个函数
Print,如下所示cppvoid Print(const yzx::vector<int>& v) { for(const auto& e : v) { cout << e << " "; } cout << endl; }封装完这样一个简单的遍历函数,看似是没有任何问题了,但是,其实这个函数透露出很多不方便的地方。这个Print函数用于yzx::vector类型的对象遍历是足够了,那如果后续我需要遍历的是std::vector类型的对象要怎么办呢?就不说不同的容器,这个函数甚至连yzx::vector类型的对象都遍历不了。所以,这就可以看出这个函数的缺陷了,就是耦合性很高,很不方便。
当编写一个程序时,应该尽可能的让程序耦合性降低,让一个函数可以被多种类型使用,一个类可以同时实例化多个不同类型的对象,尽可能的让代码更具有通用性,也就是泛化编程。那要如何改造这个Print函数使得它更具有通用性,可以被其它类型的容器使用呢?很简单,使用函数模板即可,代码如下所示。将Print改成函数模板后,只要是有迭代器的容器,就都可以使用这个函数进行遍历了。
cpptemplate<class container> void Print(const container& con) { for(const auto& e : con) { cout << e << " "; } cout << endl; }
增删查改的函数实现
以下主要模拟实现vector核心的增删查改的成员函数,在实现增删查改的成员函数时,会带出一些相关的其它成员函数,如:reserve, size, capacity, resize等。
扩容导致_finish位置出错问题
-
size和capacity
这里先实现两个最基本的成员函数
size和capacity方便后面的函数调用。cppsize_t size() const { return _finish - _start; } size_t capacity() const { return _end_of_storage - _start; } -
push_back和reserve
push_back就是简单的在vector容器末尾位置插入一个元素,函数的逻辑很简单(对比string::push_back的实现都简单很多)这里就不再啰嗦了。不过,需要注意的是,在插入元素前需要判断容器有没有满,如果满了的话就需要扩容,所以这里还需要写一个扩容的函数
reserve。cpp// vector.h void reserve(size_t n) { if(n > capacity()) { T* tmp = new T[n]; if(_start) { memcpy(tmp, _start, sizeof(T) * size()); delete[] _start; } _start = tmp; _finish = _start + size(); _end_of_storage = _start + n; } } void push_back(const T& val) { if(_finish == _end_of_storage) { reserve(capacity() == 0 ? 4 : 2 * capacity()); } *_finish = val; ++_finish; }cpp// test.cpp int main() { yzx::vector<int> v1; v1.push_back(1); v1.push_back(2); v1.push_back(3); v1.push_back(4); Print(v1); return 0; } // 输出:1 2 3 4我们通过插入4个数据的结果得出来这个push_back和reserve函数的正确性没有什么问题,但是这里真的没有问题吗?其实你可以尝试一下插入5个数据,问题自然就出现了。而问题就在
reserve中(其实从4个数据没有问题而5个就有问题的状况也可以猜出)。我们来看reserve中的这两行代码。当tmp扩容完成且将_start的数据拷贝到tmp中后,此时将tmp赋值给 _start ,这个 _start就变成新的 _start了。而 _finish是用新的 _start + 有效数据元素个数,这里逻辑上没有问题,但是
size()确出现问题了 。我们知道,当函数执行到第2行时 _start已经是新的 _start了,此时size的计算逻辑是旧的_finsh - 新的_start,所以这里的size计算出来的结果肯定是有问题的。cpp_start = tmp; _finish = _start + size();这里就可以得出,_finish的值不能使用实时的
size()计算,解决方案就是提前记录好有效元素的个数old_size,后面都使用old_size更新结果。cppvoid reserve(size_t n) { if(n > capacity()) { size_t old_size = size(); T* tmp = new T[n]; if(_start) { memcpy(tmp, _start, sizeof(T) * old_size); delete[] _start; } _start = tmp; _finish = _start + old_size; _end_of_storage = _start + n; } } -
pop_back和empty
cppbool empty() const { return _start != _finish; } void pop_back() { assert(!empty()); --_finish; }
扩容导致迭代器位置错误问题
-
insert
insert和之前唯一不同的点就是,vector::insert是传递插入数据位置的迭代器,迭代器的范围要在
[_start, _finish]中。cppvoid insert(iterator pos, const T& val) { assert(pos >= _start); assert(pos <= _finish); // 判断是否需要扩容 if(_finish == _end_of_storage) { reserve(capacity() == 0 ? 4 : 2 * capacity()); } // 挪动数据 iterator end = _finish; while(end > pos) { *end = *(end - 1); --end; } *pos = val; ++_finish; }这里直接给出结论,这段代码是有问题的,且无法预期输出结果。这里的问题其实和上面的问题类似,都是扩容引发的问题。我们知道,容器扩容之后,整个容器的内存地址会改变,也就是在新地址上了,而上面实现的insert函数中的pos迭代器指向的还是旧的内存地址。从底层来看,此时pos就是一个野指针,从表现上看,这个pos就是一个无效的迭代器。而使用pos进行的各种操作肯定都是有问题的。
这里的解决方案和前面的差不多,就是提前记录是要在哪个位置插入数据,在扩容完之后,将更新pos更新成有效的迭代器即可。
cppvoid insert(iterator pos, const T& val) { assert(pos >= _start); assert(pos <= _finish); // 判断是否需要扩容 if(_finish == _end_of_storage) { size_t old_pos = pos - _start; //扩容前记录好是要在第几个位置插入数据 reserve(capacity() == 0 ? 4 : 2 * capacity()); pos = _start + old_pos; //使用新的_start更新pos } // 挪动数据 iterator end = _finish; while(end > pos) { *end = *(end - 1); --end; } *pos = val; ++_finish; } -
erase
erase的实现没有那么多坑,但是判断pos需要注意,因为_finish指向的位置是没有有效元素的,所以pos要在区间
[_start, _finish)中。cppvoid erase(iterator pos) { assert(pos >= _start); assert(pos < _finish); // 注意这里不能使用<= iterator end = pos + 1; while(end < _finish) { *(end - 1) = *end; ++end; } }
insert/erase导致迭代器失效问题(重点)
由上面我们知道了,扩容会导致原先的迭代器指向的位置失效,那来看下面这段代码。问:这里it作为insert的参数过后失效了吗?
cpp
yzx::vector<int> v1 = { 1,2,3,4,5 };
auto it = find(v1.begin(), v1.end(), 3);
v1.insert(it, 10);
或许有的人会说有可能失效,主要是看insert有没有发生扩容。实际情况也确实是这样,但是,这里我们必须认为它失效了,且如果没有给it赋新的值,就不能再使用它了。++而由insert使得it失效了的现象就叫做迭代器失效++。
注:这里给出it一定失效的理由。不同的编译器的扩容机制是不一样的,不说你有没有计算当时容器中有效的元素个数,就算知道了有效元素的个数也不能确定编译器在什么时候会扩容,所以,这里统一的认为,在insert过后,it一定失效。
-
删除所有偶数情景
现在再看一个情景:删除v1中的所有偶数。如下代码所示。这里迭代器it作为erase的参数之后还继续使用了,而且得出来的结果还是正确的结果,或许有人会觉得这是因为erase不会涉及到扩容,得出erase不会使得迭代器失效的结论。但是,这里迭代器it也失效了,不管结果如何,我们都必须认为it失效了。我们可以通过vs 2022验证这个it是否失效。
cppyzx::vector<int> v1 = { 1,2,3,4,5 }; auto it = v1.begin(); while(it != v1.end()) { if(*it % 2 == 0) { erase(it); }else{ ++it; } } Print(v1); // 输出:1 3 5 -
vs2022下迭代器失效的严格检查
在vs 2022下对于迭代器失效具有严格的检查,而要触发这个检查,就需要使用std::vector完成上面一系列的操作。
cppstd::vector<int> v1 = { 1,2,3,4,5 }; auto it = v1.begin(); while(it != v1.end()) { if(*it % 2 == 0) { erase(it); }else{ ++it; } } Print(v1); // 结果:程序崩溃

由std::vector引发的结果就可以得出,erase也会使得迭代器失效。那万一真的需要继续使用插入/删除位置的迭代器要怎么办?C++标准库给出了解决方案。
-
迭代器失效解决方案:函数返回值
因为迭代器在作insert/erase的参数之后被严格规定了迭代器失效,如果遇到需要继续使用该位置的迭代器的情况就会很麻烦,所以,C++标准库规定,将插入位置/删除位置的有效的迭代器作为insert/erase的返回值(即返回it的位置的有效的迭代器)。这个其实可以通过标准库中insert和erase的函数接口验证。


所以,我们上面实现的insert和erase函数也需要略微的修改,如下所示
cpp
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
// 判断是否需要扩容
if(_finish == _end_of_storage)
{
size_t old_pos = pos - _start; //扩容前记录好是要在第几个位置插入数据
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + old_pos; //使用新的_start更新pos
}
// 挪动数据
iterator end = _finish;
while(end > pos)
{
*end = *(end - 1);
--end;
}
*pos = val;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish); // 注意这里不能使用<=
iterator end = pos + 1;
while(end < _finish)
{
*(end - 1) = *end;
++end;
}
return pos;
}
默认构造做缺省参数
-
resize
resize可以调整容器有效元素个数,如果是增加有效元素的个数,可以指定添加的元素的初始值,而如果没有指定添加的元素的初始值,就使用默认值进行初始化。使用默认值初始化的这个默认值也就是缺省值,那么问题来了,如何设定缺省值呢?因为我们写的vector是一个类模板,所以缺省参数val可以是任意的类型,那是否有什么值能适配所有类型的初始化呢?
cppvoid resize(size_t n, const T& val = ?);答案当然是没有的,这里也不使用那"万能的数值",而是使用T类型的默认构造做缺省值。这里有的人会有些疑问,自定义类型一定会有默认构造能理解,那内置类型哪里来的默认构造函数啊?
cppvoid resize(size_t n, const T& val = T());其实,C++对内置类型进行了升级,内置类型也具有构造函数。这里使用以下几种初始化方式进行验证。
cppint a(2); int b = int(); int c(a); cout << a << " " << b << " " << c; // 输出:2 0 2cppvoid resize(size_t n, const T& val = T()) { if(n > size()) { reserve(n); while(_finish < _start + n) { push_back(val); } } else { _finish = _start + n; } }
再探构造函数
-
拷贝构造
cppvector(const vector<T>& v) { reserve(v.capacity()); for(const auto& e : v) { push_back(e); } } -
迭代器区间构造
前面使用std::vector讲过,vector是支持使用一段迭代器区间进行初始化的,在这里,我们也可以写一个使用迭代器区间进行构造的函数,如下所示。
cppvector(iterator first, iterator last) { assert(first < last); size_t len = last - first; reserve(len); while(first != last) { push_back(*first); } }现在,yzx::vector是支持使用同类型的迭代器区间构造初始化了,但是它有一个缺陷就是,仅仅只能使用同类型的对象进行初始化。上面写的使用迭代器区间进行构造的函数,对于yzx::vector类型,只能使用yzx::vector类型进行构造,如果想使用yzx::vector,string等任意的其它的类型构造都不行。
cppyzx::vector<int> v1 = { 1,2,3,4,5 }; yzx::vector<int> v2(v1.begin() + 1, v1.end() - 1);cppstring s("change world"); yzx::vector<int> v3(s.begin(), s.end()); // 编译器会报错如果仅仅是这样的迭代器区间构造,那这个函数就太low了。所以,这里就需要使用前面所说的泛化编程。因为我们想要的功能是可以使用任意类型的迭代器区间进行构造,而不管什么类型的迭代器在遍历容器时都是一样的步骤,所以我们可以将这个使用迭代器区间构造的函数写成一个函数模板,让这个函数更具有通用性,函数模板的实现如下。
模板参数中
enable_if_t<_Is_iterator_v<InputIterator>, int> = 0是为了防止当前这个构造函数和使用n个数据直接构造的函数在调用时发生歧义,加上这段语句后,就只有当使用迭代器区间对vector初始化时,才会调用这个构造函数。cpptemplate<class InputIterator, enable_if_t<_Is_iterator_v<InputIterator>, int> = 0> vector(InputIterator first, InputIterator last) { assert(first < last); size_t len = last - first; reserve(len); while(first != last) { push_back(*first); } }其实,标准库中的迭代器区间构造的函数写的也是函数模板,如下所示

-
n个数据直接构造
cppvector(size_t n, const T& val = T()) { reserve(n); for(size_t i = 0; i < n; i++) { push_back(val); } }
析构函数与赋值运算符重载
-
析构函数
cpp~vector() { if(_start) { delete[] _start; _start = _finish = _end_of_storage = nullptr; } } -
赋值运算符重载
cpp// 成员函数swap void swap(vector<T>& v) { std::swap(_start, v._start); std::swap(_finish, v._finish); std::swap(_end_of_storage, v._end_of_storage); } // 赋值运算符重载的现代写法 vector<T>& operator=(const vector<T>& v) { vector<T> tmp(v); swap(tmp); return *this; }
深层次的深浅拷贝问题(重点)
到这里,vector的模拟实现已经完成的差不多了,各个函数的功能也都没有什么问题,但是这里其实还隐藏一个大问题,我们来看下面这段程序。按我们之前实现的vector的逻辑来看,因为vector是类模板,可以支持任意的类型实例化,所以这段代码应该是没有任何问题的。可以运行一下这段代码进行测试,结果如下图,程序直接崩溃了。
cpp
yzx::vector<string> v1 = { "12345","12345","12345","12345" };
Print(v1);
v1.push_back("12345");
Print(v1);

这里程序为什么会崩溃?这里直接给出答案,其实就是深层次的深浅拷贝问题,对于这段代码来说,就是string和string之间的拷贝出了问题。通过测试会发现,使用string实例化vector,当插入4个数据程序没有任何的问题,但是插入5个数据程序就会崩溃,所以这里不难想到,又是reserve出了问题。那么问题是什么呢?来看下面reserve拷贝的逻辑的核心代码。
cpp
// reserve函数内部代码片段
size_t old_size = size();
T* tmp = new T[n];
if(_start)
{
memcpy(tmp, _start, sizeof(T) * old_size);
delete[] _start;
}
代码中,tmp申请了新的空间之后,需要将_start中的数据拷贝过来,但是这里拷贝数据使用的是memcpy,而问题就出在这里。我们知道,memcpy的拷贝逻辑是一个字节一个字节的拷贝,那这里等于是直接将string中的指针_str进行值拷贝,而并没有_str指向的字符数组开辟空间进行深拷贝,所以导致拷贝完之后,tmp中的string和 _start中的string指向的是同一块内存空间,如下图所示。这就导致string和string之间进行的是浅拷贝。

注意,这里不是vector和vector之间是浅拷贝,而是string和string之间是浅拷贝,是更深层次的深浅拷贝问题。
这个问题的解决方案就是使用string的operator=,利用string的赋值运算符重载具有深拷贝的特性进行string和string之间的深拷贝。reserve最终版的代码如下。
cpp
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
// 拷贝
if (_start)
{
// 使用这个如果遇到本身具有动态内存的类作为类型程序会崩溃
// 因为使用memcpy对于容器内部的类型来说是浅拷贝
// memcpy(tmp, _start, sizeof(T) * old_size);
for (size_t i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + old_size;
_the_end_of_storage = _start + n;
}
}