目录
vector各接口总览
namespace nxbw
{
//模拟实现vector
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//默认成员函数
vector(); //构造函数
vector(size_t n, const T& val); //构造函数
template<class InputIterator>
vector(InputIterator first, InputIterator last); //构造函数
vector(const vector<T>& v); //拷贝构造函数
vector<T>& operator=(const vector<T>& v); //赋值运算符重载函数
~vector(); //析构函数
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;
//容量和大小相关函数
size_t size()const;
size_t capacity()const;
void reserve(size_t n);
void resize(size_t n, const T& val = T());
bool empty()const;
//修改容器内容相关函数
void push_back(const T& x);
void pop_back();
void insert(iterator pos, const T& x);
iterator erase(iterator pos);
void swap(vector<T>& v);
//访问容器相关函数
T& operator[](size_t i);
const T& operator[](size_t i)const;
private:
iterator _start; //指向容器的头
iterator _finish; //指向有效数据的尾
iterator _endofstorage; //指向容器的尾
};
}
注:为了避免和标准库中的元素相冲突,这里我们使用将vector的实现放入命名空间中
在vector中有三个成员变量,如下图所示:
start指向容器的头,finish指向容器有效数据的尾,end_of_storage指向容器的尾
构造函数1
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{}
构造函数2
vector还支持使用迭代器进行初始化(范围可以是一个区间),因为可以使用一个对象的迭代器对另一个对象赋值,所以迭代器的类型是不确定的,这里需要为这个函数设计一个函数模板,让它可以接收任意类型的迭代器
//可使用任意寄存器类型
template<class inputiterator>
vector(inputiterator first, inputiterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
构造函数3
这段构造函数的作用是开辟一个n大小的空间并初始化为val,这里复用reserve来开辟空间,然后使用push_back将val放入空间即可
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);
}
}
注意:
1.如果我们对使用所需空间的大小是可知的,可以先使用reserve开辟好空间,避免使用push_back时频繁开辟空间,导致效率降低
2.这个函数还需要开一个函数重载
vector(int 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);
}
}
可以观察到该函数只有n的类型与上面函数类型不相同,但是这样是必要的,我们来看一下下面这段构造函数调用会出现什么问题
vector<int> v(20,8) //编译器会优先匹配最合适的函数跟帮它初始化
如果没有以上重载函数,v就会去调用最匹配的构造函数2进行初始化,但是2上会解引用first,int不能解引用(vs编译器就会报错)
拷贝构造函数
传统方法:
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
//memcpy不行!!!
//memcpy(_start, v._start, sizeof(T) * v.size());
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
这里不能使用memcpy,拷贝内置类型或这使用浅拷贝的自定义类型不会出现浅拷贝问题,例如:当拷贝的对象是string类型
使用memcpy对vector进行深拷贝,vector进行深拷贝,但是拷贝的是string数组,对string对象进行的是浅拷贝
浅拷贝的缺点:
1.一个被修改会影响另一个
2.会析构两两次
所以这里不能使用memcpy来进行深拷贝
解决方法:
如果T是string这种深拷贝类型的对象,那么就使用string的赋值重载,对string对象进行深拷贝
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v[i];
}
现代方法:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(v._capacity());
for(auto e : v)
{
push_back(e);
}
}
总结:
如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。
operator=
传统方法:
//传统写法
vector<T>& operator=(vector<T> v)
{
if (*this != &v) //防止自己给自己赋值
{
delete[] _start; //释放之前的空间
_start = new vector<T>[v.capacity()]; //申请和v一样大小的空间
//进行深拷贝
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v[i];
}
_finish = _start + v.size(); //指向有效数据的尾部
_end_of_storage = _start + v.capacity(); //指向整个容器的尾部
}
return *this //支持连续赋值
}
现代写法:
首先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构。
//现代方法
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
注意: 赋值运算符重载的现代写法也是进行的深拷贝,只不过是调用的vector的拷贝构造函数进行的深拷贝,在赋值运算符重载函数当中仅仅是将深拷贝出来的对象与左值进行了交换而已。
析构函数
~vector()
{
//判断_start是否为空,为空就没必要析构
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
迭代器相关函数
vector中的迭代器其实就是容器当中所存储数据类型指针
typedef T* iterator;
typedef const T* const_iterator;
begin和end
//迭代器相关的函数
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
此时再让我们来看看vector使用迭代器的代码也就一目了然了,实际上就是使用指针遍历容器。
vector<int> v(5, 3);
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
it++;
}
cout << endl;
现在我们实现了迭代器,实际上也就可以使用范围for遍历容器了,因为编译器在编译时会自动将范围for替换为迭代器的形式。
vector<int> v(5, 3);
//范围for进行遍历
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
容量和大小相关函数
size和capacity
size_t capacity() const
{
return _end_of_storage - _start;
}
size_t size() const
{
return _finish - _start;
}
resize和reserve
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
reserve函数的实现思路也是很简单的,先判断所给n是否大于当前容器的最大容量(否则无需进行任何操作),操作时直接开辟一块可以容纳n个数据的空间,然后将原容器当中的有效数据拷贝到该空间,之后将原容器存储数据的空间释放,并将新开辟的空间交给该容器维护,最好更新容器当中各个成员变量的值即可。
void reserve(size_t n)
{
if(n > capacity())
{
T* tmp = new T[n];
if(_start)
{
for(size_t i = 0; i < size(); ++i)
{
tmp[i] = _start[i];
}
delete[] _start; //注意:这里需要释放_start这里不释放,后面就没机会释放了
}
_start = tmp; //这里调用的赋值重载,进行的深拷贝
_finish = _start + size();
_end_of_storage = _start + n; //这里是增容之后指向的容器尾部
}
}
这里进行的深拷贝不能使用memcpy进行,这里是申请一个扩容好的动态资源tmp,首先去释放_start的原空间,然后让_start指向tmp新空间
如图:
resize规则:
1、当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
2、当n小于当前的size时,将size缩小到n。
根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。
void resize(size_t n, const T& val = T())
{
if(n < size())
{
_finish = _start + n;
}
else
{
reserve(n); //reserve n小于capacity是不会扩容的
while(_finish != _edn_of_storage)
{
*_finish = val;
++_finish;
}
}
}
注:在C++引入模板之后,对内置类型进行了特殊的处理,内置类型现在有了自己的构造函数
所以我们给模板类型变量初始化可以给匿名类型,它会去调用自己的构造函数进行初始化
empty
empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。
bool empty()
{
return _start == _finish;
}
push_back
要尾插数据首先得判断容器是否已满,若已满则需要先进行增容,然后将数据尾插到_finish指向的位置,再将_finish++即可。
//尾插数据
void push_back(const T& x)
{
if (_finish == _endofstorage) //判断是否需要增容
{
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity(); //将容量扩大为原来的两倍
reserve(newcapacity); //增容
}
*_finish = x; //尾插数据
_finish++; //_finish指针后移
}
pop_back
尾删数据之前也得先判断容器是否为空,若为空则做断言处理,若不为空则将_finish–即可。
//尾删数据
void pop_back()
{
assert(!empty()); //容器为空则断言
_finish--; //_finish指针前移
}
insert
insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
//pos是一个地址,有效地址的空间不可能为0
iterator insert(iterator pos, const T& x)
{
//判断pos位置的合理性
assert(pos >= _start && pos <= _end_of_storage);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
T* newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
//解决迭代器失效问题(不能访问迭代器我们认为它失效,访问结果是未定义)
pos = _start + len;
}
iterator end = _finish;
while (end >= pos)
{
*end = *(end - 1);
--end;
}
*pos = x;
++_finish;
//间接改变pos位置元素的值
return pos;
}
注意: 若需要增容,则需要在增容前记录pos与_start之间的间隔,然后通过该间隔确定在增容后的容器当中pos的指向,否则pos还指向原来被释放的空间,会导致迭代器失效。
erase
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
iterator erase(iterator pos)
{
//判断pos位置的合理性
assert(pos >= _start && pos <= _end_of_storage);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
swap
swap函数用于交换两个容器的数据,我们可以直接调用库当中的swap函数将两个容器当中的各个成员变量进行交换即可。
//交换两个容器的数据
void swap(vector<T>& v)
{
//交换容器当中的各个成员变量
::swap(_start, v._start);
::swap(_finish, v._finish);
::swap(_endofstorage, v._endofstorage);
}
注意: 在此处调用库当中的swap需要在swap之前加上“::”(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。
访问容器相关函数
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容器通过“下标+[ ]”获取到的数据只允许进行读操作,不能对数据进行修改。