大家好呀!欢迎来到我的C++系列学习笔记!~上一篇是有关STL标准模板库的string的理解到深入,传送门在这里哦:【C++】从使用string类到模拟实现string类_柒海啦的博客-CSDN博客_c++引入string
vector同样是STL里的一个容器,其功能就是存放任意数据的一个动态数组。这篇文章我会从会用vector的容器开始,理解到最后的模拟实现vector的基本框架,以便达到我们对此容器的一个熟练操作的目的。

目录
因为是STL,所以里面的容器操作还是熟悉的操作,我们只需要明白其容器的大致框架,然后具体的对这些进行模拟实现即可,所以我们针对这些框架去熟练使用vector。

vector的理解流程大致如上图所示。首先自然了解其构造函数,明确内部所拥有的成员变量,其次也要明确析构。
然后容量来说就是对整个空间进行申请或者初始化操作了,运算符重载要么就是方便使用,要么就是重载一些比较常用且重要的(比如赋值重载)。操作就是一个动态线性表一般的操作,随机插入,随机删除,尾插,尾删.......
接下来我会用具体代码来对对应的模块进行演示,帮助我们一起理解vector这个容器。
比较好的C++文档在这里:https://cplusplus.com/reference/vector/vector/
空数组、几个相同的数构造、拷贝构造、迭代器构造。

参考代码:
- void test1()
- {
- vector<int> v1; // 空
- vector<int> v2(6, 6); // 初始化6个6
- vector<int> v3(v2); // 或者 vector
v3 = v2 // 拷贝构造 - vector<int> v4(v3.begin(), v3.end()); // 迭代器构造
- }


可以发现v1 v2 v3 v4对象均构造成功,v2对应的就是构造6个6,v3就是拷贝构造,v4用了两个迭代器进行构造。(一般线性顺序表的迭代器的底层实际上就是指针)
既然构造成功了,那么我们就需要对其进行操作了。
首先,可以简单的看一下我们耳熟能详的push_back:(push_back实际上底层也就是调用的insert())。

同样的,在vector里面也支持[]运算符重载,所以我们就可以通过下标的方式去遍历vector,自然同样存在size()是统计当前数据的个数的。


参考代码:
- void test2()
- {
- // 插入 遍历
- vector<int> v1(6, 1);
- v1.push_back(2); // 尾插
- // 利用下标去遍历数据
- for (size_t i = 0; i < v1.size(); ++i)
- {
- cout << v1[i] << " ";
- }
- cout << endl;
-
- }

上述取出数据我们使用的是[]重载,实际上,我们取出数据的方式有很多种,这里介绍两种:一个取出头的数据front(),一个取出尾的数据back()。
![]()
插入数据的话自然也就是有insert之类的函数:

当然,和插入对应的就是删除了,erase()即随机删除:

也存在尾删pop_back():

需要注意的是,上述传入的参数并不是size_t或者int的下标,而是迭代器iterator哦~
提到迭代器自然就不会忘记end(),begin(),rend(),rbegin()...... 并且实际上此迭代器的实现底层也就是简单的指针,且类型应该是:std::vector
那么我们可以利用其进行遍历--同时就可以使用范围for,然后进行一些基本的随机插入删除等操作:
- void test3()
- {
- // 插入遍历
- // 迭代器及其迭代器遍历:
- vector<int> v(3, 2);
- vector<int>::iterator it = v.begin();
- while (it != v.end()) // end为最后的元素的下一个
- {
- cout << *it << " "; // 像指针一样的进行访问
- ++(*it); // 同样的可以进行修改
- ++it;
- }
- cout << endl;
-
- // 支持迭代器,也就支持范围for
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
-
- v[0] = 1;
- v[2] = 0;
- // 打印头尾,不用[]重载
- cout << "头:" << v.front() << endl;
- cout << "尾:" << v.back() << endl;
-
- // 随机插入 先寻找
- it = find(v.begin(), v.end(), 3);
- // 在3前插入 2
- v.insert(it, 2);
-
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
-
- // 随机删除
- //v.erase(it); // 在把此位置删除 直接这样会出现失效的问题,所以必须重新寻找或者接收insert的返回值
- it = find(v.begin(), v.end(), 2);
- v.erase(it);
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
-
- }

这里提到了迭代器失效的情况,那么这个是什么情况呢?
所谓迭代器失效,也就是在进行insert或者erase,即移动空间的时候,就可能会产生当前位置地址被释放掉,转移到其他地方去了,那么此时地址也就是野指针了,这就是迭代器失效。
- void test4()
- {
- vector<int> v(2, 2);
- auto it = v.begin(); // 头结点值地址
- printf("%p\n", it);
- v.erase(it);
- //验证it是否失效
- printf("%p\n", it);
- printf("%p\n", v.begin());
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
- }

此时可以发现三者的地址完全不一样,说明发生了变化,也就是所谓的迭代器失效了。
其实,在不同的平台下,编译器对此迭代器失效的方法也存在不一样的地方:
比如,我们测试程序12345,对此数组中的偶数进行删除:在linuxg++和Windows下的vs2022分别进行测试:
- void test5()
- {
- // 迭代器遍历随机删除
- vector<int> v;
- v.push_back(1);
- v.push_back(2);
- v.push_back(3);
- v.push_back(4);
- v.push_back(5);
-
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
-
- vector<int>::iterator it = v.begin();
- // 迭代器失效
- while (it != v.end())
- {
- if (*it % 2 == 0)
- {
- // 偶数删除
- v.erase(it);
- }
- ++it;
- }
- for (auto& e : v)
- {
- cout << e << " ";
- }
- cout << endl;
- }
vs2022 直接崩溃

Linux下的g++却没有。
上述情况也就说明了:
VS下和Linux下可能会不一样:STL只是一个规范-->不同的平台下面实现是不同的。
VS下对迭代器会进行强制检查,如果原本指向的数据被清除了,下次在对此处指向(存在数据,不是野指针),但是会检查报错
Linux下:12345没有问题(删去偶数,不正确使用)1235
-- 数据排列的偶然性(最后一个不是偶数,没有连续的偶数)
但是当我们遍历插入或者删除的时候就会产生很多不便,那么insert以及erase要如何处理才能优化好呢?对返回值进行操作:

insert返回的就是新插入的那个元素位置的迭代器,而erase返回的就是被删除元素的下一个元素的迭代器。
知晓这些之后,我们就可以稍微修改一下上面的代码,使其能在Windows平台下也能够跑起来:
- while (it != v.end())
- {
- if (*it % 2 == 0)
- {
- // 偶数删除
- it = v.erase(it);
- }
- else
- ++it;
- }

此时就能够在Windows平台上跑了。
跟string这个类类似,实际上在STL内基本都有这个扩容以及初始化的reserve和resize,用法都基本一致,一个就是一开始申请多大空间,另一个就是申请的同时也初始化多少数据。


其实我们发现resize默认给的是value_type(),其实有的时候存储的数据并不是内置类型,而是自定义类型,所以就会如此设计,这在之后的模拟实现也会涉及。
参考代码:
- void test6()
- {
- // reserve
- vector<int> v1;
- v1.reserve(10);
- cout << v1.capacity() << endl;
-
- // resize
- vector<int> v2;
- v2.resize(6, 2); // 初始化6个数据,均为2
- for (auto e : v2)
- {
- cout << e << " ";
- }
- cout << endl;
- v2.resize(10, 1); // 当申请空间比原本数据个数大,那么会将没填充的数据填为对应数据
- for (auto e : v2)
- {
- cout << e << " ";
- }
- cout << endl;
- v2.resize(2, 0); // 如果比原本数据小,会发生截断,不会填充数据
- for (auto e : v2)
- {
- cout << e << " ";
- }
- cout << endl;
- }

在了解一些基本并且熟练的时候,就可以对vector进行更加深入的模拟实现了:
大致了解后,我们想要进一步深入理解vector的话,就要对其进行模拟实现。既然只是简单的去模拟实现,那么我们只需要实现一个大致框架即可:
准备工作:
我们首先可以将其实现代码放在一个命名空间内,这样就可以不用和std内官方的vector相互冲突。当然,此类可以定义在一个头文件内,比如叫做vector.h,这样方便其他程序进行调用此头文件。

然后,此类是一个模板类,因为里面的存储的数据可不只有int一种,自然使用template
- namespace QiHai
- {
- // 类模板
- template<typename T>
- class vector
- {
- //...
- }
- //...
- }
首先看我们的私有成员。因为这是一个顺序结构,所以内存地址是连续的,也就是普通的在堆上申请的动态数组。结合std官方的实现,我们可以定义如下三个指针:(同时将指针重命名为迭代器类型)
- public:
- typedef T* iterator; // 定义迭代器类型为iterator
- typedef const T* const_iterator;
- private:
- iterator _start; // 存储数据的开始位置
- iterator _finish; // 存储数据的最后一个位置的后一个位置
- iterator _end_of_storage; // 存储数据的空间最后一个地址
三者在某一时候关系可以如图所示:

那么此时你的内心是否存在一个疑惑:那么这些是如何进行控制的呢?其实也就是保证开空间和插入的时候控制其在对应的位置即可,这些后面均会实现,现在这里可以进行一个简单的演示:(假设第一次插入开8个空间)

和string的实现不一样,我们对外提供的接口也是迭代器,所以首先提供一些基本的迭代器接口:比如begin()、end()等。
- iterator begin()
- {
- return _start;
- }
-
- iterator end()
- {
- return _finish;
- }
-
- const_iterator begin() const
- {
- return _start;
- }
-
- const_iterator end() const
- {
- return _finish;
- }
然后就是最基本的构造函数也是最常用的构造函数。如上述演示一样,我们只需控制三个变量初始化为nullptr即可。
- vector()
- :_start(nullptr)
- ,_finish(nullptr)
- ,_end_of_storage(nullptr)
- {}
为了方便后序的拷贝构造函数方便调用,并且也是一种构造函数方式,即对传入的类似于地址使用(即*解应用就是数据),要使用模板函数,结构如下:(push_back还未写,下面会进行实现)--(传入的迭代器,就有点类似于迭代器遍历)
- template <class InputIterator>
- vector(InputIterator first, InputIterator last)
- :_start(nullptr)
- ,_finish(nullptr)
- ,_end_of_storage(nullptr)
- {
- while (first != last)
- {
- push_back(*first);
- ++first;
- }
- }
拷贝构造也分为现代写法和原始写法,原始写法就是重新建立一个空间,然后一一把传入的对象的数据拷贝到新空间上去,否则默认的拷贝构造实现的是浅拷贝(注意此深拷贝不能使用memcpy进行数据替换,因为当储存对象本身也是一个数组的时候,即内部存的同样也是地址,那么此时就会出现问题,这就是深度拷贝,后面也会具体讲,需要一个一个进行赋值)
当然,除了原始写法,现代写法就高明的多, 会让一个具体的对象帮我们做事,然后将地址进行交换即可。这个对象也就是我们上面的迭代器区间构造,而地址交换就需要我们自己实现一个。
交换函数很简单,只需要执行外部的交换函数将我们每一个变量所存的地址交换即可。
(注意传入的参数必须是一个类型,vector可不是一个具体的类型,而是一个模板,所以需要
- void swap(vector
& v) - {
- ::swap(_start, v._start);
- ::swap(_finish, v._finish);
- ::swap(_end_of_storage, v._end_of_storage);
- }
拷贝构造现代写法:
- vector(const vector
& v) - :_start(nullptr)
- , _finish(nullptr)
- , _end_of_storage(nullptr)
- {
- // 现代做法,请打工人
- vector
tmp(v.begin(), v.end()) ; // 请到的打工人 - swap(tmp); // 两者交换,它的就是我的了
- }
析构函数很简单,因为我们变量的申请的空间由堆而来(new/malloc),所以我们将头指针释放掉,然后将其所有变量置为空即可。
- ~vector()
- {
- delete[] _start;
- _start = _finish = _end_of_storage = nullptr;
- }
为了统计当前数据个数以及容量大小,结合之前创建变量的介绍,这里可以轻松的解决:
- size_t size() const
- {
- return _finish - _start;
- }
-
- size_t capacity() const
- {
- return _end_of_storage - _start;
- }
赋值重载和拷贝构造函数有点类似,只不过一个是新创建一个对象,而赋值是两个以及存在的对象让另一个的值拷贝给另一个。
也有原始写法和现代写法,原始写法同样注意深度拷贝的问题,而现代写法就只需要让形参做工具人,因为此时传参就会发生拷贝构造,然后与其形参交换数据即可。同时注意返回对象的本身。
- vector
& operator=(vector v) - {
- swap(v); // 把形参当做打工人
- return *this;
- }
此重载也就是方便像数组一样的对下标进行访问,方便快捷。实际上也就是根据传入的下标,根据首元素地址加减获得,因为可以修改数据,那么就可传出引用,const对象加上const即可:
- T& operator[](size_t n)
- {
- assert(n < size());
- return _start[n];
- }
-
- const T& operator[](size_t n) const
- {
- assert(n < size());
- return _start[n];
- }
开空间是插入的基础。首先就需要存在空间才能进行插入操作,也才能真正的构造一个数组。如上面的动画演示一样,我们可以对这个函数传入n表示我们要开n个对应数据类型的空间。
所谓开空间,也就是原来开的的一串地址空间不够了或者没有,然后需要开辟一个比原来大的空间供数据进行操作。既然是新的空间,那么数据就要进行搬家。数据搬家也就是要进行拷贝。在拷贝的同时我们要注意到数据类型不再是像string那样简单的char类型了,而是T。这个T可以是内置类型,当然也可以是vector
- void reserve(size_t n)
- {
- if (n > capacity())
- {
- iterator new_start = new T[n];
- size_t len = size();
-
- if (_start) // 有可能是空数据进行
- {
- // 普通的进行内存拷贝的话,存在更深层次无法拷贝
- //memcpy(new_start, _start, sizeof(T) * capacity());
- // 深层次拷贝 一个一个进行拷贝 方便调用其赋值进行深拷贝
- for (size_t i = 0; i < len; i++)
- new_start[i] = _start[i];
- delete[] _start;
- }
-
- _start = new_start;
- _finish = _start + len;
- _end_of_storage = _start + n;
- }
- }
开空间并且初始化实际上分为两个步骤,一个进行开空间,另一个负责初始化数据。注意缺省参数是一个匿名构造,C++为了迎合自定义类型初始数据的问题,也给内置类型定义了一个匿名对象,所以使用匿名对象对内置类型同样适合。
- void resize(size_t n, const T& val = T()) // 缺省值给T的一个匿名对象 (C++为了迎合自定义类型,也给内置类型增加了无参构造)
- {
- // 1. n > capacity()
- if (n > capacity())
- {
- reserve(n); // 扩容
- }
- for (size_t i = size(); i < n; i++)
- _start[i] = val;
-
- _finish = _start + n;
- }
front获得头数据,back获得尾数据,只需要像数组那样访问即可,记得传出引用即可:
- T& front()
- {
- assert(size() > 0);
- return *(_start);
- }
-
- T& back()
- {
- assert(size() > 0);
- return *(_finish - 1);
- }
-
- bool empty()
- {
- return _start == _finish;
- }
只有插入我们才能对数据进行管理。随机插入可以复用于尾插,随机删除同样可以。需要注意的是在挪动空间,原本传入的迭代器就可能出现失效的问题,那么我们只需控制insert返回插入后新数据所在的位置指针,删除数据的下一个数据位置指针即可。(插入空间满申请空间,然后进行移动,删除进行移动操作)--注意检查边界问题哦~
- // 随机插入
- iterator insert(iterator pos, const T& val)
- {
- assert(pos >= _start);
- assert(pos <= _finish);
- // 满了就需要扩容
- if (_finish == _end_of_storage)
- {
- size_t len = pos - _start;
- reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩容后,原本地址会发生改变
- pos = _start + len; // pos也发生变化
- }
-
- iterator temp = _finish - 1;
- while (temp >= pos)
- {
- *(temp + 1) = *temp;
- --temp;
- }
- *pos = val;
- ++_finish;
-
- return pos; // 返回改变地址后的迭代器位置
- }
-
- // 随机删除 返回删除位置的下一个位置的迭代器
- iterator erase(iterator pos)
- {
- assert(pos < _finish);
- assert(pos >= _start);
-
- iterator temp = pos + 1;
- while (temp <= _finish)
- {
- *(temp - 1) = *(temp);
- ++temp;
- }
-
- _finish--;
-
- return pos;
- }
此时尾插尾删复用即可。
仅供参考,有误请大佬指正!
- // vector.h
- #pragma once
- #include
- #include
- #include
- #include
-
- using namespace std;
-
- // 模拟实现vector
-
- namespace QiHai
- {
- // 类模板
- template<typename T>
- class vector
- {
- public:
- typedef T* iterator; // 定义迭代器类型为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;
- }
-
- // 类中交换函数
- void swap(vector
& v) - {
- ::swap(_start, v._start);
- ::swap(_finish, v._finish);
- ::swap(_end_of_storage, v._end_of_storage);
- }
-
- // 构造函数
- vector()
- :_start(nullptr)
- ,_finish(nullptr)
- ,_end_of_storage(nullptr)
- {}
-
- // 迭代器区间进行初始化构造
- template <class InputIterator>
- vector(InputIterator first, InputIterator last)
- :_start(nullptr)
- ,_finish(nullptr)
- ,_end_of_storage(nullptr)
- {
- while (first != last)
- {
- push_back(*first);
- ++first;
- }
- }
-
- // 拷贝构造
- vector(const vector
& v) - :_start(nullptr)
- , _finish(nullptr)
- , _end_of_storage(nullptr)
- {
- // 现代做法,请打工人
- vector
tmp(v.begin(), v.end()) ; // 请到的打工人 - swap(tmp); // 两者交换,它的就是我的了
- }
-
-
- // 析构函数
- ~vector()
- {
- delete[] _start;
- _start = _finish = _end_of_storage = nullptr;
- }
-
- // 数据个数
- size_t size() const
- {
- return _finish - _start;
- }
-
- size_t capacity() const
- {
- return _end_of_storage - _start;
- }
-
- // 重载
- // 赋值重载
- vector
& operator=(vector v) - {
- swap(v); // 把形参当做打工人
- return *this;
- }
-
- // []重载
- T& operator[](size_t n)
- {
- assert(n < size());
- return _start[n];
- }
-
- const T& operator[](size_t n) const
- {
- assert(n < size());
- return _start[n];
- }
-
- // 开空间
- void reserve(size_t n)
- {
- if (n > capacity())
- {
- iterator new_start = new T[n];
- size_t len = size();
-
- if (_start) // 有可能是空数据进行
- {
- // 普通的进行内存拷贝的话,存在更深层次无法拷贝
- //memcpy(new_start, _start, sizeof(T) * capacity());
- // 深层次拷贝 一个一个进行拷贝 方便调用其赋值进行深拷贝
- for (size_t i = 0; i < len; i++)
- new_start[i] = _start[i];
- delete[] _start;
- }
-
- _start = new_start;
- _finish = _start + len;
- _end_of_storage = _start + n;
- }
- }
-
- // 开空间并且初始化
- void resize(size_t n, const T& val = T()) // 缺省值给T的一个匿名对象 (C++为了迎合自定义类型,也给内置类型增加了无参构造)
- {
- // 1. n > capacity()
- if (n > capacity())
- {
- reserve(n); // 扩容
- }
- for (size_t i = size(); i < n; i++)
- _start[i] = val;
-
- _finish = _start + n;
- }
-
- T& front()
- {
- assert(size() > 0);
- return *(_start);
- }
-
- T& back()
- {
- assert(size() > 0);
- return *(_finish - 1);
- }
-
- bool empty()
- {
- return _start == _finish;
- }
-
- void pop_back()
- {
- assert(_finish > _start);
- --_finish;
- }
-
- // 操作
- // 尾插
- void push_back(const T& val)
- {
- // 直接复用insert()即可
- insert(end(), val);
- }
-
- // 随机插入
- iterator insert(iterator pos, const T& val)
- {
- assert(pos >= _start);
- assert(pos <= _finish);
- // 满了就需要扩容
- if (_finish == _end_of_storage)
- {
- size_t len = pos - _start;
- reserve(capacity() == 0 ? 4 : capacity() * 2); // 扩容后,原本地址会发生改变
- pos = _start + len; // pos也发生变化
- }
-
- iterator temp = _finish - 1;
- while (temp >= pos)
- {
- *(temp + 1) = *temp;
- --temp;
- }
- *pos = val;
- ++_finish;
-
- return pos; // 返回改变地址后的迭代器位置
- }
-
- // 随机删除 返回删除位置的下一个位置的迭代器
- iterator erase(iterator pos)
- {
- assert(pos < _finish);
- assert(pos >= _start);
-
- iterator temp = pos + 1;
- while (temp <= _finish)
- {
- *(temp - 1) = *(temp);
- ++temp;
- }
-
- _finish--;
-
- return pos;
- }
-
- private:
- iterator _start; // 存储数据的开始位置
- iterator _finish; // 存储数据的最后一个位置的后一个位置
- iterator _end_of_storage; // 存储数据的空间最后一个地址
- };
- }
谢谢观看~