[STL]vector模拟实现
文章目录
- [STL]vector模拟实现
- 1. 整体结构总览
- 2. 成员变量解析
- 3. 默认成员函数
- 构造函数1
- 构造函数2
- 构造函数3
- 拷贝构造函数
- 析构函数
- 4. 迭代器相关函数
- begin函数
- end函数
- begin函数const版本
- end函数const版本
- 5.容量相关函数
- size函数
- capacity函数
- reserve函数
- resize函数
- empty函数
- 6. 数据修改函数
- push_back函数
- pop_back函数
- insert函数
- erase函数
- 7. 数据访问函数
- []运算符重载
- []运算符重载const版本
- =运算符重载const版本
- 8. 完整源码链接
1. 整体结构总览
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector(); //默认构造函数
vector(size_t n, const T& val = T());
vector(int n, const T& val = T());
template <class InputIterator>
vector(InputIterator first, InputIterator last); //迭代器初始化
vector(const vector<T>& v);//拷贝构造
~vector();//析构函数
iterator begin();//返回指向数据开头的迭代器
iterator end(); //返回指向最后一个数据的下一个数据的位置的迭代器
const_iterator begin() const;//const迭代器版本
const_iterator end() const;//const迭代器版本
size_t size()const; //获取容器内有效数据的个数
size_t capacity()const;//获取容器容量
void reserve(size_t n); //扩容函数
void resize(size_t n, T val = T());//扩容函数
bool empty();//检查容器是否为空
void push_back(const T& x); //尾插函数
void pop_back();//尾删函数
iterator insert(iterator pos, const T& val);//插入函数
iterator erase(iterator pos);//删除函数
T& operator[](size_t n); //[]重载
const T& operator[](size_t n) const; //const变量版本[]重载
vector<T>& operator=(const vector<T>& v);//传统写法
//vector<T>& operator=(vector<T> v);//现代写法
void swap(vector<T>& v);//交换函数
private:
iterator _start; //指向数据开头的位置
iterator _finish; //指向最后一个有效数据的下一个数据的位置
iterator _end_of_storage; //指向容量空间的末尾位置
};
2. 成员变量解析
_start 指向数据的开始位置,_finish指向最后一个有效数据的后一个数据的位置,_end_of_storage指向容量空间的末尾位置。3.
3. 默认成员函数
构造函数1
首先是无参数的默认的构造函数,构造的是一个空容器,因此成员变量赋空值就行。
vector()
:_start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{}
构造函数2
此构造函数实现的功能是在定义时开一定空间,并且将这段空间全是相同的数据,由于模拟实现这个构造函数复用了其他函数,要注意一定要初始化变量,否则会影响正常复用其他函数。
vector(size_t n, const T& val = T())//const 引用可以延长匿名对象的寿命,使该匿名对象的寿命和引用相当
: _start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
reserve(n);//提前扩容,提高效率
for (size_t i = 0; i < n; ++i)
{
push_back(val);
}
}
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);
}
}
构造函数3
此构造函数的功能是利用不同类型迭代器初始化容器,只要迭代器指向数据和容器数据类型能匹配上就能使用该函数,包括指向数组的原生指针可看作迭代器,可以作为参数传入。
template <class InputIterator>
vector(InputIterator first, InputIterator last)//指向数组的原生指针可看作迭代器,可以作为参数传入
: _start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
拷贝构造函数
拷贝构造函数的实现需要注意深浅拷贝的问题,首先要注意自身空间需要深拷贝,拷贝构造构造容器时为自身需要申请独立的空间,然后将数据进行拷贝,而不是将相应成员变量(_start等成员变量)直接赋值,两个容器指向同一块空间,造成问题。
其次要注意的是如果容器存储的是需要申请空间的自定义类型,在拷贝构造时,也需要单独为它们申请空间然后拷贝数据,而不是将要拷贝的容器的数据直接赋值过来,比如存储string类,直接赋值会导致两个容器存储的string对象指向相同的空间。
传统写法:
申请新的空间然后将数据一个一个拷贝过来。
vector(const vector<T>& v)
{
//传统写法
_start = new T[v.capacity()]; //为容器申请独立空间
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];//如果是内置类型直接赋值,如果是自定义类型,自定义类型会重载=运算符,满足深拷贝的需求,比如string类的=运算符就是对数据进行深拷贝
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
现代写法:
创建一个新的容器拷贝数据,然后将该容器的空间转让过来。
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(const vector<T>& v)
: _start(nullptr),
_finish(nullptr),
_end_of_storage(nullptr)
{
//现代写法
vector<T> tmp(v.begin(), v.end());//用迭代器初始化创建一个和v数据相同的vector
swap(tmp);
}
另外值得注意的是由于是利用自定义类型会重载=运算符,满足深拷贝的需求,因此需要实现=运算符重载,并且是对数据深拷贝,否则使用vector<vector<T>>类型时也会因为浅拷贝出现问题。
析构函数
由于空间是new出来的连续空间,因此只需要用delete关键字释放就可以,然后将各成员变量置空。
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
4. 迭代器相关函数
begin函数
begin函数的功能是返回指向数据开始位置的迭代器,因此只需要返回_start变量就可以。
iterator begin()
{
return _start;
}
end函数
end函数的功能是返回指向最后一个有效数据的下一个数据的位置,因此只需要返回_finish变量就可以。
iterator end()
{
return _finish;
}
begin函数const版本
const版本的begin函数是提供给const类型的容器使用,因此this指针加const才能适配,并且返回值得是const版本的迭代器。
const_iterator begin() const
{
return _start;
}
end函数const版本
和const版本的begin函数同理,this指针加const,并且返回值是const版本的迭代器。
const_iterator end() const
{
return _finish;
}
5.容量相关函数
size函数
由于申请的空间是连续的,因此只需要用_finish减_start就可以得到容器内有效数据的个数。
size_t size()const
{
return _finish - _start;
}
capacity函数
和size函数原理相同,只需要用_end_of_storage减_start即可获取容器的容量。
size_t capacity()const
{
return _end_of_storage - _start;
}
reserve函数
reserve规则
1. 如果n小于capacity,什么都不做。
1. 如果n大于capacity,就将capacity扩容到n。
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n]; //申请能存储n个T类型数据的空间
size_t sz = size();//提前保存原有size大小,后续size函数会失效
if (_start) //如果原有空间存在,需要拷贝原有空间的数据
{
for (size_t i = 0; i < sz; ++i)
{
tmp[i] = _start[i];//如果容器存储的是自定义类型,使用=运算符,利用自定义类型自身的实现,避免浅拷贝
}
delete[] _start; //将原有空间释放
}
_start = tmp;//指向新空间的数据开头
_finish = _start + sz; //指向新空间最后一个有效数据的下一个位置
_end_of_storage = _start + n;//指向新空间的末尾位置
}
}
注意:原有有效数据个数需在_start指向修改前保存,否则由于_start指向的改变导致size函数的结果为错误的,进而导致_finish指向的位置不正确。
resize函数
resize规则:
1. 当n小于当前的size时,将size缩小到n。
1. 当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
void (size_t n, T val = T())
{
if (n < size())//n是无符号整形,不用担心n为负数造成_finish指向位置为非法位置
{
_finish = _start + n;
}
else
{
if (n > capacity())//n大于capacity需要扩容
{
reserve(n);
}
while (_finish != _start + n)//将原有有效数据位置到第n个数据位置设置为val
{
*_finish = val;
++_finish;
}
}
}
注意: 由于需要兼容模板, 因此在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。
empty函数
empty函数的作用是检查容器是否为空,当_start和_finish指向的位置相同时就是空。
bool empty()
{
return _finish == start;
}
6. 数据修改函数
push_back函数
容器已满就先扩容再尾插,容器未满就直接尾插,由于_finish指向的是最后一个有效数据的下一个数据的位置,因此只需要对其解引用就可以尾插数据。
void push_back(const T& x)
{
if (_finish == _end_of_storage) //容器已满,需要扩容
{
size_t newcapacity = capacity == 0 ? 4 : capacity() * 2; //扩容空间至原有空间的2倍
reserve(newcapacity);
}
//容器未满,直接尾插
*_finish = x;
++_finish;
}
pop_back函数
删除数据前要保证有数据,因此断言判断容器不为空,删除数据只需要将_finish的指向改变就可以。
void vector<T>::pop_back()
{
assert(!empty());//检查是否有数据
--_finish;
}
insert函数
insert函数的功能是将数据插入到迭代器指向的位置,因此迭代器的指向的正确性是很重要的。首先要断言判断插入位置是否越界;其次要注意容器是否有空间插入数据,因为如果空间不够,需要利用申请新的空间的方法来实现扩容,申请新的空间,会导致形参传入的指向插入数据位置的迭代器失效,具体讲就是迭代器指向原有空间,而原有空间已经释放了,导致迭代器失效,因此在容量不足时要单独判断,并且保证迭代器指向正确的位置才能完成正确插入的。由于空间不论怎么变,指向的数据和数据开头的偏移量是不变的,因此需要利用偏移量来保证迭代器指向位置的正确即可,保证迭代器指向正确后,只需要挪动数据,将要插入的数据写入正确位置即可。返回插入位置的迭代器。
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start);//检查插入位置是否越界
assert(pos <= _finish);
size_t len = pos - _start;//记录偏移量
if (_finish == _start)//容器已满
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;//使得pos指向新空间的相应位置
}
iterator end = _finish - 1;
while (pos <= end)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
return pos;
}
erase函数
删除数据首先要确保删除的数据是有效数据,因此断言判断迭代器指向的位置,然后只需要将删除数据后的数据向前挪动覆盖即可,最后别忘了将指向最后一个有效数据的下一个位置的_finish修正。
iterator erase(iterator pos)
{
assert(pos >= _start);//判断删除位置是否越界
assert(pos < _finish);
iterator start = pos + 1;
while (start != _finish)
{
*(start - 1) = *start;
++start;
}
--_finish;
return pos;
}
7. 数据访问函数
[]运算符重载
申请的是连续的空间,因此只需要利用偏移量找到对应的位置解引用即可,由于[]需要支持修改数据内容,返回类型必须是引用。
T& operator[](size_t n)
{
assert(n < size());//检查是否越界访问
return _start[n];
}
[]运算符重载const版本
const版本的[]运算符重载是提供给const类型的容器使用,因此this指针加const才能适配,并且返回值得是const类型的数据。
const T& operator[](size_t n) const
{
return _start[n];
}
=运算符重载const版本
传统写法:
申请相应大小的新的空间然后将数据依次拷贝。
vector<T>& operator=(const vector<T>& v)
{
if (_start != v._start) //避免v1 = v1出错
{
delete[] _start;
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(); ++i)
{
_start[i] = v._start[i];
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
return *this;
}
现代写法:
利用形参会拷贝构造实参,然后将形参的空间交换过来。
vector<T>&operator=(vector<T> v)
{
//现代写法
swap(v);
return *this;
}
8. 完整源码链接
STL/vector/vector/vector.h · 钱雪明/日常代码 - 码云 - 开源中国 (gitee.com)