文章目录
前言
本文章对我们stl标准库中的vector容器进行实现
一、模拟前准备
模拟模拟,我们先要知晓其模样是怎样的才能模拟,所以我们首先打开stl库的源码来大概了解一下vector的基本实现模式。
1.源码使用方式
我们使用源码时一行行的看可不行,不信去自己去试一试,大量的typedef会让你晕头转向,而且现阶段理解也不重要。所以我们要按照规律来对源码庖丁解牛,同时对不懂的成员变量和函数也要大胆猜测:
①先找到我们vector的成员变量看看:
protected:
iterator start;
iterator finish;
iterator end_of_storage;
首先它是一个迭代器类型的,我们可以右键速览定义浅浅看看iterator是什么,虽然我们看不懂,但是注释很清楚的告诉我们迭代器就是一个类!具体我们了解迭代器类请点击转到另一篇文章:链接: 【C++、stl】迭代器是什么?迭代器理解!,看完此篇后便了解了vector可以用我们容器原生指针做迭代器。
于是在我们眼中,这三个变量不过是三个指向vector容器空间的指针罢了。
但是这三个指针成员变量和我们熟知的数据结构中的顺序表成员有很大差别啊,那么我们怎么知道它们分别是干甚么的呢?
答案是:先看我们的构造函数,再看一些可以改变我们成员变量的插入等函数。
②再找到我们的构造函数和插入接口以了解成员变量
构造函数:
看我们的构造函数再配合c++文档,我们可以很容易知道这些重载的构造函数的作用,对于第二个用n个value值为函数初始化的构造函数 vector(size type n, const T& value) ,我们再次右键看不懂的函数fill_initialize对其速览:
此时就不要再对allocate_and_fill函数速览了,因为这里涉及空间配置器(内存池)知识,我们当前无需深究,我们只要知道它是开辟空间的函数就可以了,开启我们连蒙带猜模式,allocate_and_fill这几个单词不就是分配和填满吗?结合我们c++文档中了解到的这个构造函数的作用是用n个T类的变量为vector初始化,所以我们很自然想到allocate_and_fill是一个开辟空间n个T的值并返回该空间的函数,所以我们指针start便很自然的指向我们开辟的空间的头位置,finish便是空间的尾位置的指针,结合名字猜测end_of_storage便是我们空间实际容量位置的指针了。
我们再看看我们插入接口验证我们的答案:
if-else这里我们有了初阶数据结构的经验很容易知晓这是我们的判断vector是否满的逻辑。这里如果还是不能验证我们上面的猜测,我们可以把开辟空间的reserve函数和插入insert函数拉出来:
③再找到我们的reserve函数和插入insert函数验证猜测
reserve函数:
insert函数:
二、开始模拟
1.模版参数定义
我们使用模版实现的vector,以至于可以用多种类型实例化vector。
template<class T>
T为我们vector的模版参数。
2.typedef部分
因为我vector的存储空间连续,所以我们模拟时就直接用vector的空间的指针做它的迭代器,用const类的指针做const类的迭代器。
// Vector的迭代器是一个原生指针
typedef T* iterator;
typedef const T* const_iterator;
3.成员变量部分
我们模仿库中的实现,为我们的vector类添加3个成员变量,分别指向vector空间中实际数据的头、尾和实际储存容量的尾。
private:
iterator _start= nullptr; // 指向数据块的开始
iterator _finish= nullptr; // 指向有效数据的尾
iterator _endOfStorage= nullptr; // 指向存储容量的尾
4.构造函数
①默认构造函数
vector()
//这里我们甚至不用为成员变量初始化,因为自从c++11之后有了成员变量的缺省值可用,我们可以直接在成员变量位置加nullptr
//:_start(nullptr)
//,_finish(nullptr) // 指向有效数据的尾
//,_endOfStorage(nullptr)
{}
我们这里可以将注释取消,用初始化列表为我们认成员变量初始化,但cpp11后有了成员变量的缺省值,所以我们在声明成员变量时就就为其加上了一个缺省值,在这里就无需再为变量初始化了。
②用n个T类变量初始化的构造函数
//以下两个构造是文档中第二第三个构造
vector(size_t n, const T& value = T())
{
resize(n, value);
}
这里对resize函数进行了复用。
②用一个迭代器区间初始化的构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first!=last)
{
push_back(*first);
++first;
}
}
这里是什么?它有什么含义?
这很明显是一个模版,我们虽然是在用模版写vector,但是我们在实现vector的函数时也可以用模版写,他的模版参数取名为InputIterator也是因为这里的模版参数之后是要接收迭代器类型的,这样我们就得到了一个迭代器类的区间[first,last),而我们对迭代器区间访问,然后逐个解引用并传给push_back函数,便为vector插入了这个迭代器对象内部成员变量的数据。
注意:
A: 这里就算T类是一个自定义类也是可以插入的,因为我们push_back函数中的(*_finish = x)也调用了自定义类的赋值重载。
B: 这里的模版参数可以是别的容器的迭代器也可以使vector自己的迭代器,甚至可以单纯用一个数组的指针区间做模版参数。
5.拷贝构造函数
普通写法
vector(const vector<T>& v)
{
/*iterator tmp=new T[v.capacity()];
_start = tmp;*/
//不用多此一举,可以改为=》
_start= new T[v.capacity()];
/*memcpy(_start,v._start,sizeof(T)* v.size());*/
//这里要是用memcpy会出现问题,因为memcpy是简单的浅拷贝,扩容逻辑这里也是用的memcpy也需要改一下
//简单浅拷贝的话在用自定义类实现的vector调用该拷贝函数时会出现vector中的自定义类被重复析构的情况。
//所以我们需要用深拷贝实现我们的拷贝函数=》
for (size_t i = 0; i < v.size(); i++)
{
//这里的[]是返回的是类型T的引用,而=又访问的是T类型自己的赋值重载,如在用string实例化的vector中:_start[i]逐步访问了
//每个vector中的string,并调用了string的赋值重载=,实现了对string自定义类型的深拷贝。
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endOfStorage = _start + v.capacity();
}
这里也要注意不要单纯的memcpy内部空间,因为memcpy只是单纯的浅拷贝,这样会出现两个vector变量同时指向一片空间的问题,这是一个很严重的问题,会导致一片空间被析构函数析构两次,编译器毫无疑问无法通过。
现代写法
//或者有另一种现代的拷贝函数的写法:
/* vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto e : v)
{
push_back(e);
}
}*/
//这种拷贝函数调用的是vector的push_back,而push_back函数中的(*_finish = x)也调用了自定义类的赋值重载;
//这里值得注意的是:我们用来实例化的自定义类型一定要有赋值重载,不然就是自己坑自己。
这里的现代写法其实和普通的本质上没什么区别:
A:普通用new开辟空间,而现代的复用了reserve函数,本质也是new的空间。
B:普通的用_start[i] = v._start[i],访问到元素并调用了元素的“=”赋值重载,以实现深拷贝。而现代的调用了push_back函数,它内部也是调用了元素的“=”赋值重载实现了深拷贝。
C(不同点/优化点):现代的push_back函数会自动改变成员变量的值,普通的需要自己改动。
6.赋值重载函数
普通写法
vector<T>& operator= (const vector<T>& v)
{
if(this!=&v)//不能为自己赋值
{
iterator* tmp=new iterator[v.capacity()];//开辟空间
memcpy(tmp,v._start,v.size());//复制空间数据
delete[] _start;//销毁原先数据
_start=tmp;//替换空间tmp是一个临时变量,出作用域自动销毁
_finish=_start+v.size();//改变其他成员
_endOfStorage=_start+v.capacity();
}
}
我们正常开辟一块空间,用v的空间为它初始,然后销毁我们this的成员指向的空间(不用担心销毁空指针的情况,因为delete会自己做判断,当指定销毁的指针是空时不做任何操作。),替换我们的空间,并为改动其他成员的位置。
升级写法
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfStorage, v._endOfStorage);
}
vector<T>& operator= (const vector<T>& v)
{
if(this!=&v)
{
vector<T> tmp(v);
swap(tmp);
}
return *this;
}
复用拷贝构造创建一个临时的vector对象,因为这是一个用v为参数调用拷贝函数创造的临时变量tmp,所以tmp的里面数据和v中的一模一样,调用上面的swap函数,将上面的tmp中的数据和我this中的数据做交换,这样我的this中就有了和v中一样的数据。而函数结束时我们的临时变量tmp会自动调用析构函数,将我们原先*this之中的数据销毁(好一个反客为主的阴暗写法,实在妙极)
现代写法
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfStorage, v._endOfStorage);
}
vector<T>& operator= (vector<T> v)
{
swap(v);
return *this;
}
既然我们升级写法中需要创建临时变量,那我们不如直接传值给函数,这样该函数就自动掉用拷贝函数创建了一个临时变量,并且出函数也自动销毁,我们函数内部便省去了自己创建临时变量的工作了,这便是现代写法的又一升级之处。
7.析构函数
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endOfStorage = nullptr;
}
}
8.迭代器部分
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
//这里我们写迭代器最好不要改它的名字,因为编译器有自动识别的
//如果改了,你自己用自己写的迭代器访问容器还好,但是范围for就用不了了。
//编译器会对范围for做简单的替换,将其替换成用迭代器访问
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
9.有关空间容量等函数
①计算实际数据和容量大小的函数
size_t size() const
{
return _finish-_start;//用地址相减算当前数据的大小
}
size_t capacity() const
{
return _endOfStorage - _start;
}
②开辟空间的函数
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
//这里如果不将size()大小存住的话,在我们vector的首次插入时,_start为空,不会走if
if (_start)
{
//将_start指向的原生数据拷贝给tmp的位置,然后删除原先_start位置的数据
/*memcpy(tmp, _start, sizeof(T) *sz );*/
//这里用memcpy的话会在扩容的时候出问题,因为我们扩容逻辑用的是reserve函数,
//memcpy是简单是浅拷贝,如果是用自定义类实例化的vector,在下面析构原先空间的时候
//因为析构会先调用自定义类的析构函数,然后再释放空间,但我们简单浅拷贝复制到新空
//间中的自定义类中的成员指针指向的就是已经被析构后的地址了。
//所以我们应该用深拷贝的方式=》
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
//用tmp替换_start
_start = tmp;
//于是这里就会出现问题,因为改了_start位置,但是_finish的位置还是空指针,所以这里在调用size()的话就会出现空指针对_start位置的减
//会出现错误,并且报错不明显,很难改。
_finish = _start + sz;
_endOfStorage = _start + n;
}
}
/*void resize(size_t n, const T& value = T())
{
if (n > _endOfStorage)
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}*/
//这里的代码有些错误,因为没有真正理解resize的所有功能
//将代码改为以下=》
void resize(size_t n, const T& value = T())
{
//resize的大小并不是总是大于当前vector的capacity的,有时甚至小于当前存储的数据大小,当n小于当前数据大小
//我们为了控制大小为n,便要舍弃一些数据
if (n < size())
{
_finish = _start + n;
}
//这里的reserve又有人说:可能n小于capacity啊,但是别忘了我们的reserve中自有判断,只有当n大于capacity时才会扩容
else
{
reserve(n);
//这里就是将每个数据逐一初始化为参数value的过程,这里就算是内置类型也可以,因为面向对象将内置类做了升级,
// 可以理解为内置类也有构造函数
while (_finish != _start + n)
{
*_finish = value;
++_finish;
}
}
}
有关空间的函数有一些细节容易出现问题且极难调试,特别是一些深浅拷贝问题,请仔细阅读注释。
10.[]运算符重载函数
//这个pos位置并不是一个迭代器位置,它是供用户使用的函数,它需要用下标访问;
//T& operator[](size_t pos)
T& operator[](size_t pos)
{
assert(pos < size());
//我们对vector使用[]重载时就是相当于对它内部的指针使用了[]
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
我们为vector类重载[]函数重载,为方便用户直接通过该函数访问我们vector空间(_start指针指向的空间)中的元素的引用。需要注意我们的const类的vector需要返回const类元素的引用,因为const类vector对象内部是不能让用户修改数据的。
11.数据元素改动类接口
void push_back(const T& x)
{
//我们正常的push_back函数当然可以这样写
//但是我们要是已经先实现了insert,可以用insert复用
/*if (_finish == _endOfStorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
_finish++;*/
//改为=》
insert(end(), x);
}
void pop_back()
{
--_finish;
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
//这里insert扩容的时候会有迭代器失效的问题,因为传的pos位置是原来位置,但是一旦扩容,vector存储位置改变了,但是
//pos还是原来的位置,就会出现类似对野指针访问的错误情况。
//这里的解决方法是扩容之前先将pos到_start的距离存起来,然后扩容后再将_start+该长度。
if (_finish == _endOfStorage)
{
size_t len = pos - _start;
size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapcacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//这里不用显示的指定调用来使用迭代器,因为这是在类中。
//也不用多此一举的用end()函数来访问_finish位置了,因为这是在类中,可以直接使用成员变量
/*bit::vector<int>::iterator it = end()-1;
while (it>=pos)
{
*(it+1) = *it;
--it;
}*/
*pos = x;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
/*iterator cur = pos;
while (cur != _finish)
{
*cur = *(cur + 1);
cur++;
}*/
//以上的移动代码也可以,虽然它移动了一个_finish位置的未知的值
//但是它后面会_finish--,这个位置数并不会被读到,所以也可以,但是我们最好不要让这种事发生,显得我们很
//很不专业
//改为以下代码=》
iterator cur = pos + 1;
while (cur != _finish)
{
*(cur - 1) = *cur;
++cur;
}
_finish--;
return pos;
}
代码内部注释同样有很多易错点和原理解释,还有许多优化改进部分,请认真阅读注释和源码。
完整源码
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using std::cout;
using std::endl;
namespace bit
{
template<class T>
class vector
{
public:
// Vector的迭代器是一个原生指针
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;
}
// construct and destroy
//构造函数
vector()
//这里我们甚至不用为成员变量初始化,因为自从c++11之后有了成员变量的缺省值可用,我们可以直接在成员变量位置加nullptr
//:_start(nullptr)
//,_finish(nullptr) // 指向有效数据的尾
//,_endOfStorage(nullptr)
{}
//这个是为了使vector<int> (10,1)不冲突
vector(int n, const T& value = T())
{
resize(n, value);
}
//以下两个构造是文档中第二第三个构造
vector(size_t n, const T& value = T())
{
resize(n, value);
}
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first!=last)
{
push_back(*first);
++first;
}
}
vector(const vector<T>& v)
{
/*iterator tmp=new T[v.capacity()];
_start = tmp;*/
//不用多此一举,可以改为=》
_start= new T[v.capacity()];
/*memcpy(_start,v._start,sizeof(T)* v.size());*/
//这里要是用memcpy会出现问题,因为memcpy是简单的浅拷贝,扩容逻辑这里也是用的memcpy也需要改一下
//简单浅拷贝的话在用自定义类实现的vector调用该拷贝函数时会出现vector中的自定义类被重复析构的情况。
//所以我们需要用深拷贝实现我们的拷贝函数=》
for (size_t i = 0; i < v.size(); i++)
{
//这里的[]是返回的是类型T的引用,而=又访问的是T类型自己的赋值重载,如在用string实例化的vector中:_start[i]逐步访问了
//每个vector中的string,并调用了string的赋值重载=,实现了对string自定义类型的深拷贝。
_start[i] = v._start[i];
}
_finish = _start + v.size();
_endOfStorage = _start + v.capacity();
}
//或者有另一种现代的拷贝函数的写法:
/* vector(const vector<T>& v)
{
reserve(v.capacity());
for (auto e : v)
{
push_back(e);
}
}*/
//这种拷贝函数调用的是vector的push_back,而push_back函数中的(*_finish = x)也调用了自定义类的赋值重载;
//这里值得注意的是:我们用来实例化的自定义类型一定要有赋值重载,不然就是自己坑自己。
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endOfStorage, v._endOfStorage);
}
vector<T>& operator= (vector<T> v)
{
swap(v);
return *this;
}
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endOfStorage = nullptr;
}
}
// capacity
size_t size() const
{
return _finish-_start;
}
size_t capacity() const
{
return _endOfStorage - _start;
}
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
//这里如果不将size()大小存住的话,在我们vector的首次插入时,_start为空,不会走if
if (_start)
{
//将_start指向的原生数据拷贝给tmp的位置,然后删除原先_start位置的数据
/*memcpy(tmp, _start, sizeof(T) *sz );*/
//这里用memcpy的话会在扩容的时候出问题,因为我们扩容逻辑用的是reserve函数,
//memcpy是简单是浅拷贝,如果是用自定义类实例化的vector,在下面析构原先空间的时候
//因为析构会先调用自定义类的析构函数,然后再释放空间,但我们简单浅拷贝复制到新空
//间中的自定义类中的成员指针指向的就是已经被析构后的地址了。
//所以我们应该用深拷贝的方式=》
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
//用tmp替换_start
_start = tmp;
//于是这里就会出现问题,因为改了_start位置,但是_finish的位置还是空指针,所以这里在调用size()的话就会出现空指针对_start位置的减
//会出现错误,并且报错不明显,很难改。
_finish = _start + sz;
_endOfStorage = _start + n;
}
}
/*void resize(size_t n, const T& value = T())
{
if (n > _endOfStorage)
{
reserve(n);
}
while (_finish != _start + n)
{
*_finish = val;
++_finish;
}
}*/
//这里的代码有些错误,因为没有真正理解resize的所有功能
//将代码改为以下=》
void resize(size_t n, const T& value = T())
{
//resize的大小并不是总是大于当前vector的capacity的,有时甚至小于当前存储的数据大小,当n小于当前数据大小
//我们为了控制大小为n,便要舍弃一些数据
if (n < size())
{
_finish = _start + n;
}
//这里的reserve又有人说:可能n小于capacity啊,但是别忘了我们的reserve中自有判断,只有当n大于capacity时才会扩容
else
{
reserve(n);
//这里就是将每个数据逐一初始化为参数value的过程,这里就算是内置类型也可以,因为面向对象将内置类做了升级,
// 可以理解为内置类也有构造函数
while (_finish != _start + n)
{
*_finish = value;
++_finish;
}
}
}
///access///
//这个pos位置并不是一个迭代器位置,它是供用户使用的函数,它需要用下标访问;
//T& operator[](size_t pos)
T& operator[](size_t pos)
{
assert(pos < size());
//我们对vector使用[]重载时就是相当于对它内部的指针使用了[]
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
///modify/
void push_back(const T& x)
{
//我们正常的push_back函数当然可以这样写
//但是我们要是已经先实现了insert,可以用insert复用
/*if (_finish == _endOfStorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
_finish++;*/
//改为=》
insert(end(), x);
}
void pop_back()
{
--_finish;
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
//这里insert扩容的时候会有迭代器失效的问题,因为传的pos位置是原来位置,但是一旦扩容,vector存储位置改变了,但是
//pos还是原来的位置,就会出现类似对野指针访问的错误情况。
//这里的解决方法是扩容之前先将pos到_start的距离存起来,然后扩容后再将_start+该长度。
if (_finish == _endOfStorage)
{
size_t len = pos - _start;
size_t newcapcacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapcacity);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
//这里不用显示的指定调用来使用迭代器,因为这是在类中。
//也不用多此一举的用end()函数来访问_finish位置了,因为这是在类中,可以直接使用成员变量
/*bit::vector<int>::iterator it = end()-1;
while (it>=pos)
{
*(it+1) = *it;
--it;
}*/
*pos = x;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
/*iterator cur = pos;
while (cur != _finish)
{
*cur = *(cur + 1);
cur++;
}*/
//以上的移动代码也可以,虽然它移动了一个_finish位置的未知的值
//但是它后面会_finish--,这个位置数并不会被读到,所以也可以,但是我们最好不要让这种事发生,显得我们很
//很不专业
//改为以下代码=》
iterator cur = pos + 1;
while (cur != _finish)
{
*(cur - 1) = *cur;
++cur;
}
_finish--;
return pos;
}
private:
iterator _start= nullptr; // 指向数据块的开始
iterator _finish= nullptr; // 指向有效数据的尾
iterator _endOfStorage= nullptr; // 指向存储容量的尾
};
void print(const vector<int>& v)
{
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
}
总结
本文对c++stl库中的vector容器实现了简单的模拟实现,还简单分析了我们应该如何使用源码做我们模拟实现前的准备。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点:)。