c++基础篇STLvector类的介绍及其模拟实现
Posted 东条希尔薇
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++基础篇STLvector类的介绍及其模拟实现相关的知识,希望对你有一定的参考价值。
作者介绍:
关于作者:东条希尔薇,一名喜欢编程的在校大学生
主攻方向:c++和linux
码云主页点我
本系列仓库直通车
作者CSDN主页地址
vector的官方文档定义如下:
简单翻译:
- vector是一个可以动态增加容量的一个顺序表
- vector容器通过一块连续的物理空间存放元素,所以它同样可以支持随机访问(下标访问)
通过以上翻译,我们可以初步认定vector的本质其实就是我们c语言ds阶段学习的动态顺序表,利用一块动态开辟的数组来存放我们的元素
目录
vector的迭代器
库文件定义
为什么要先讲迭代器?我们往下看
vector中同样提供了普通迭代器(iterator),常量迭代器(const_iterator)和反转迭代器(reverse_iterator)
其中常量迭代器只能使用于容器具有常性的情况:例如(const vector &)
反转迭代器可以实现反着遍历容器
我们可以把迭代器理解为指针,它可以指向容器中的某元素
它有一些固定的位置,利用成员函数进行了封装
迭代器的对应关系函数如下:
我们可以利用迭代器来遍历我们的容器
vector<int>v1=1,2,3,4,5;
for(vector<int>::iterator it=v1.begin();it!=v1.end();it++)
//遍历元素
cout<<*it<<" ";
cout<<endl;
for(vector<int>::reverse_iterator it=v1.rbegin();it!=v1.rend();it++)
//反向遍历迭代器的元素
cout<<*it<<" ";
//其中反向迭代器中,++会使迭代器往前移
cout<<endl;//输出54321
模拟实现和库模板的vector定义
vector迭代器其实也是一个原生指针,它直接就是一个模板参数的指针,可以指向数组中元素的指针,通过typedef形成的
假设我们vector的模板参数为T,就有
//迭代器定义
typedef T* iterator;
至于vector类的实现大致框架,我们可以参照SGI30版的stl源代码
通过查看它的成员来了解它的大致实现
第一行是空间适配器,我们暂时不做讲解
我们发现它的成员包含三个迭代器,中文意思分别是:开始位置,结束位置,容量结束位置
还记得我们c语言实现的顺序表有哪三个成员变量吗?
T* _arr;//存储元素的数组,动态开辟
size_t size;//表示元素个数
size_t capacity;//表示容量大小
这里的实现在c语言基础上进行了一些改进:
- 我们用数组开始位置的迭代器_start来表示一个数组
- finish迭代器指向有效元素末尾的下一个位置,利用指针-指针=中间元素个数和指针+整数代表跳过元素的个数,我们可以算出上面的size()
- end_of_storage同理,可以算出capacity
关系图如下:
注:迭代器遵循左闭右开的原则,所以我们要把end写到下一个位置
所以我们可以把我们的vector也快速写出来
template<class T>//类模板参数,可以表示各种各样的数据类型
class vector
public:
typedef T* iterator;
typedef const T* const_iterator;//常量迭代器
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
;
那么,通过迭代器的定义,我们也可以快速的把对应位置的函数写出来
iterator begin()
return _start;
iterator end()
return _finish;
const_iterator cbegin()const
return _start;
const_iterator cend()const
return _finish;
vector的构造与析构
库实现
库中有四种构造方法,c++11中增加了一种,所以共5种
默认构造
构造出一个空的vector,没有任何元素
vector<int>v;
填充构造
可以通过n个val值来填充我们的容器
**(size_t n,const T& val=T())**第一个参数表示个数,第二个参数表示值
vector<int>v(10,1);//构造出有10个1的vector
拷贝构造
我们可以使用另一个容器来初始化我们的新容器
vector(const vector& v)
vector<int>v2(v1);
迭代器构造方法
我们可以传入任意容器的迭代器来初始化我们的容器
vector(inputIterator first,inputIterator last)
list<int>l1;//用list来初始化
vector<int>(l1.begin(),l1.end());
c++11构造方法
c++11中我们允许像数组一样初始化我们的vector
vector<int>v=1,2,3,4,5;
补充:c++11之前的初始化常用方式:迭代器方式
int arr[]= 1,2,3,4,5,6,7,8;
int n=sizeof(arr)/sizeof(arr[0]);
vector<int>v1(arr,arr+n);
模拟实现
默认构造很简单,空容器把所有迭代器置空即可
vector()
:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
填充构造我们需要先开辟n的大小的空间,然后再通过容器存储的数据对象的常引用来一个位置一个位置的引用,这里的val是个缺省值,默认给这个对象的默认构造
补充知识点:对一个匿名对象进行常引用,它的生命周期将会延长,一直持续到这个引用被销毁
通过这个匿名对象,我们能实现一个自定义对象的缺省值
当然,内置类型也可以这样初始化
例如:
int();//默认是0
int(10);
实现代码:
vector(int n, const T& val = T())
:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr)
reserve(n);//开辟n个大小空间的成员函数,后文讲解
for (size_t i = 0; i < n; i++)
_start[i] = val;//一个一个的进行赋值(拷贝构造)
_finish = _start + n;//更新结束位置
重点:迭代器实现和迭代器类型
我们查到标准文档中实现迭代器时需要一个inputIterator的迭代器,那么这是什么迭代器?又有哪些迭代器?
可以初分为inputIterator和outputIterator
而我们容器一般都是InputIterator,也就是输入性迭代器
而输入性迭代器根据不同的容器类型,又可细分为:
- forwardit,单向迭代器,只能进行++操作的迭代器,其中单链表符合这一种迭代器
- bidirectionalIt,双向迭代器,能进行++和–的迭代器,其中链表,哈希表,红黑树都符合,一般用于不连续的空间
- randomaccessIt,随机迭代器,这种迭代器不仅能进行++和–,还支持随机访问,一般用于连续的空间,vector,string,deque等符合
我们可以观察到,列表中由上到下,实现的功能越来越多,而偏下行的迭代器又包含偏上行迭代器所有的功能
向实现功能最少的迭代器看齐,只要我们把最少功能的实现了,那么基本所有的容器我们都能实现初始化,进而达到可以利用所有容器初始化的目的
template <class InputIterator>//模板参数,传入任何input迭代器
vector(InputIterator first, InputIterator last)
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
while (first != last)
push_back(*first);//尾插元素,后文讲解
first++;
补充:为什么我的填充版本的第一个参数类型与文档不符(不是size_t而是int)?
因为我一般都用int类型来测试我的vector,那么在我同时使用填充构造和迭代器构造时:编译器会把我的填充构造识别成迭代器构造而发生错误
因为迭代器构造为(int,int)而填充构造为(size_t,int)前者比后者更符合规则
拷贝构造,因为每个vector都有一块单独的数组空间,所以我们必须要进行深拷贝,不然会因为重复析构而程序崩溃
而深拷贝我们可以复用我们迭代器实现的构造版本来帮我们进行深拷贝
vector(const vector<T>& v)
:_start(nullptr), _finish(nullptr), _end_of_storage(nullptr)
vector<T>tmp(v.cbegin(), v.cend());
swap(tmp);//交换两个容器,后文讲解
//开辟的临时容器会立即销毁,不用担心
有了拷贝构造,我们就能立刻写出=的重载,把等号右边的容器拷贝到左边
vector<T>& operator=(vector<T> v)//利用了传参拷贝构造
swap(v);
return *this;
析构函数特别简单,释放_start数组,然后把成员变量全部置为空
~vector()
delete[]_start;
_start = _finish = _end_of_storage = nullptr;
容量操作
reserve调整容量
我们可以使用reserve来调整我们容器的容量
reserve(size_t n),如果n比原容量小,不进行任何操作
vector<int>v;
v.reserve(100);
**模拟实现:**我们可以向库的定义靠齐,要扩大容量时,需要在堆区上另外开辟一块符合大小的空间,然后把原来空间的数据拷贝在新空间中,再释放原空间
有几点说明:
这里的拷贝只能一个个的进行赋值拷贝,不能使用memcpy直接拷贝,因为memcpy是浅拷贝,如果我们存储的对象也是需要深拷贝的对象,在释放原空间后,新空间就会成为野指针,会发生崩溃
这里的实现需要记录原来数组的有效大小,不然释放空间后,新空间的实际有效大小将无法算出
void reserve(size_t n)
if(n>capacity())
size_t oldSize=_finish-_start;
T* tmp=new T[n];
if(_start)//只有非空才需要拷贝
for(size_t i=0;i<oldSize;i++)
tmp[i]=_start[i];
delete[]_start;
_start=tmp;
_finish=_start+oldSize;
_end_of_storage=_start+n;
调整有效数据个数resize
resize能接受两个参数,第一个参数是需要调整的个数,第二个是需要填充的元素对象的值,默认是这个对象的默认构造
如果resize比原size小,就将后面的数据清除,如果较大,就用传入的元素值来进行填充
vector<int>v;
v.resize(5,0);//vector将会变成0 0 0 0 0
v.resize(2);//vector调整为0 0
模拟实现中也分为几种情况:需要调整的个数大于原有效大小但小于总容量、大于总容量、和小于有效数据大小
大于的话,首先检查需不需要开辟空间,然后再从原来的结束位置开始一个个的往后进行赋值构造
小于的话就直接调整_finish即可
void resize(size_t n,const T& val=T())
if(n>size())
if(n>capacity())
reserve(n);
iterator cur=_finish;//记录原数据结束
_finish=_start+n;//调整finish
//一个个进行赋值拷贝
while(cur!=_finish)
*cur=val;
cur++;
else
//直接调整_finish即可
_finish=_start+n;
插入函数
任意位置插入(重点:迭代器失效)
库函数的insert需要接受两个参数,第一个是需要插入位置的迭代器,第二个是需要插入的值
insert(iterator pos,const T& x)
//通常使用算法库中的查找函数来找出我们需要插入的位置
vector<int>v=1,2,3,4,5,6;
vector<int>::iterator it=find(v.begin(),v.end(),3);
v.insert(it,9);//在3处插入一个数字9
模拟实现我们像c语言顺序表实现一样,如果容量不够了,我们就增容
if(_finish==_end_of_storage)
reserve(capacity()==0?4:2*capacity());
//这里我们实现的是2倍增容,与SGI版本靠齐
这样增容真的完了吗?我们的_start等指针都指向了正确的位置,而我们的参数pos呢?
参数pos仍然指向的是原来已经被释放的空间!所以这里的pos变成了野指针
像这样由于某些操作导致迭代器失去了原来的含义或成为野指针的现象,我们就叫做迭代器失效
所以这里,我们在增容后,需要修改pos的位置
具体修改方法是先算出保存原来pos的偏移量,调整空间后再算出新的pos位置
if(_finish==_end_of_storage)
size_t len=pos-_start;//先保存
reserve(capacity()==0?4:2*capacity());
pos=_start+len;//然后就能算出新的pos了
别急,问题还没有真正解决!
我们这里的迭代器是传值调用,所以修改了函数中的pos,但是我们传入的pos没有被修改,仍然是失效状态!
库里面的解决办法是:insert设置一个返回值,就返回我们新的pos值
至于增加操作,很简单,前面已经讲了很多次,一个个往后挪动数据即可
//迭代器失效问题:
//返回值为iterator防止外部迭代器失效,返回插入位置
iterator insert(iterator pos, const T& x)
assert(pos >= _start);
assert(pos <= _finish);//检查传入的位置是否有效
if (_finish == _end_of_storage)
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len;//防止迭代器失效,所以增容要更新
iterator end = _finish - 1;
while (end >= pos)
*(end + 1) = *end;
end--;
*pos = x;
_finish++;
return pos;
尾插函数
尾插函数很简单,直接在容器的尾部插入我们的指定元素即可
push_back(const T& x)
v1.push_back(1);
v1.push_back(2);
至于实现直接复用insert即可
void push_back(const T& x)
insert(end(),x);
删除函数
与插入函数的参数差不多,需要传入我们需要删除位置的迭代器
erase(iterator pos)
vector<int>v=1,2,3,4,5,6;
vector<int>::iterator it=find(v.begin(),v.end(),3);
v.erase(it);//删除后变成1,2,4,5,6
删除的迭代器失效问题
问题引入:
假如我们需要利用erase删除所有的偶数,可以写出以下的函数
auto it=v.begin();
while(it!=v.end())
if(*it%2==0)
v.erase(it);
it++;
大家可以去测试一下,如果我们这样删除,删除以下的数据会导致崩溃
1,2,3,4//以及其他尾部是偶数的数
我们走读代码可以发现,在我们删除最后一个偶数后,迭代器仍然在往后走,就会导致迭代器的越界
其实,这样不管删没删除数据,迭代器都会一直往后走,迭代器的意义就改变了
所以,解决方案为erase的返回值设置为被删除位置的下一个位置,并且使用赋值迭代的方式来改变迭代器
所以,正确的代码如下
auto it=v.begin();
while(it!=v.end())
if(*it%2==0)
it=v.erase(it);
else
it++;
删除实现就需要返回pos位置,删除的逻辑不用多说,一个个挪动数据即可
iterator erase(iterator pos)
assert(pos >= _start);
assert(pos <= _finish);
iterator cur = pos + 1;
while (cur != _finish)
*(cur - 1) = *cur;
cur++;
_finish--;
return pos;
尾删函数
直接删除尾部数据
pop_back()
直接复用即可
void pop_back()
erase(--end());
访问操作,operator[]重载
vector支持用户像访问数组一样访问vector中的元素
实现分为两个版本,一个是访问常容器,一个是正常访问
T& operator[](size_t n)
assert(n < size());
return _start[n];
const T& operator[](size_t n)const
assert(n < size());
return _start[n];
其它简单操作
这些函数我只放实现和使用方法,不讲原理
判断一个容器是否为空
bool empty()const
return _start == _finish;
计算容器有效元素大小
size_t size()const
return _finish - _start;
计算容器总共容量大小
size_t capacity()const
return _end_of_storage - _start;
交换两个容器
void swap(vector<T>& v)
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
清除容器所有元素
void clear()
_finish = _start;
vector实现全代码
#pragma once
#include<cassert>
namespace my
template<class T>
class vector以上是关于c++基础篇STLvector类的介绍及其模拟实现的主要内容,如果未能解决你的问题,请参考以下文章