C++STL详解 —— vector的模拟实现
- vector各函数接口总览
- vector当中的成员变量介绍
- 默认成员函数
- **构造函数1:**
- **构造函数2**
- **构造函数3**
- **拷贝构造函数**
- 赋值运算符重载函数
- 迭代器相关函数
- begin和end
- 容量和大小相关函数
- size和capacity
- reserve
- resize
- empty
- 修改容器内容相关函数
- push_back
- pop_back
- insert
- erase
- swap
- 访问容器相关函数
- operator[ ]
vector各函数接口总览
namespace qq
{
//模拟实现vector
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//默认成员函数
vector(); //构造函数
vector(size_t n, const T& val); //构造函数
vector(int n, const T& val = T()) //构造函数
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当中的成员变量介绍
在C++标准库中的vector实现中,通常包含三个主要的成员变量:_start
、_finish
和_endofstorage
。这些成员变量共同管理着vector的存储空间和元素。为了方便理解,我们可以将vector想象成一个动态数组,其中:
_start
:指向数组开始的指针,也就是vector中第一个元素的位置。_finish
:指向数组中最后一个有效元素之后的位置,这个位置是新元素插入的地方。_endofstorage
:指向分配的内存空间结束的位置之后的那个位置。这个位置标记了vector可以在不重新分配内存的情况下扩展到的最远位置。
Allocated Memory:从start到end_of_storage的区域是vector当前分配的整个内存空间,它决定了vector能够容纳的最大元素数量(容量)。
Current Size:从start到finish的区域表示vector当前实际包含的元素,即vector的当前大小。
Capacity:是指在需要重新分配内存之前,容器可以容纳的元素总数。这个值至少与vector的当前大小相等,通常更大,因为vector为了减少内存重新分配的次数,会预先分配额外的空间。
默认成员函数
构造函数1:
该构造函数构造出一个空的vector
//默认成员函数
vector()
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{}
构造函数2
vector还支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
while (*first != last)
{
push_back(*first);
first++;
}
}
构造函数3
此外,vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。
vector(size_t n, const T& val)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
该函数需要两个重载函数,若按照以下方法来调用该函数会报如下错误:
这是因为在构造v1的时候,我们希望调用第二个函数来构造,但是第一个函数的参数更加匹配,从而错调了第一个函数,从而报错,所以我们要再提供两个重载函数。
vector(long n, const T& val)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n); //调用reserve函数将容器容量设置为n
for (size_t i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
{
push_back(val);
}
}
vector(int n, const T& val)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n); //调用reserve函数将容器容量设置为n
for (int i = 0; i < n; i++) //尾插n个值为val的数据到容器当中
{
push_back(val);
}
}
拷贝构造函数
在探讨拷贝构造函数之前,我们先探讨下使用memcpy拷贝问题。
假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?
int main()
{
bite::vector<bite::string> v;
v.push_back("1111");
v.push_back("2222");
v.push_back("3333");
return 0;
}
问题分析:
- memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
- 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且
自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。
结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
vector的构造函数涉及深拷贝问题,这里提供两种深拷贝的写法:
写法一:传统写法
拷贝构造的传统写法的思想是我们最容易想到的:先开辟一块与该容器大小相同的空间,然后将该容器当中的数据一个个拷贝过来即可,最后更新_finish和_endofstorage的值即可。
//传统写法
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
_start = new T[v.capacity()];
for (size_t n = 0; n < v.size(); n++)
{
_start[n] = v[n];
}
_finish = _start + v.size();
_endofstorage = _start + v.capacity();
}
写法二:现代写法
拷贝构造函数的现代写法也比较简单,使用范围for(或是其他遍历方式)对容器v进行遍历,在遍历过程中将容器v中存储的数据一个个尾插过来即可。
//现代写法
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(v.capacity());
for (auto e : v)
{
push_back(e);
}
}
注意: 在使用范围for对容器v进行遍历的过程中,变量e就是每一个数据的拷贝,然后将e尾插到构造出来的容器当中。就算容器v当中存储的数据是string类,在e拷贝时也会自动调用string的拷贝构造(深拷贝),所以也能够避免出现与使用memcpy时类似的问题。
赋值运算符重载函数
vector的赋值运算符重载当然也涉及深拷贝问题,我们这里也提供两种深拷贝的写法:
写法一:传统写法
首先判断是否是给自己赋值,若是给自己赋值则无需进行操作。若不是给自己赋值,则先开辟一块和容器v大小相同的空间,然后将容器v当中的数据一个个拷贝过来,最后更新_finish和_endofstorage的值即可。
//传统写法
vector<T>& operator=(const vector<T>& v)
{
if (this != v)
{
delete[] _start; //释放原来的空间
_start = new T[v.capacity()];
for (size_t i = 0; i < v.size(), i++)
{
_start[i] = v[i];
}
_finish = _start + v.size();
_endofstorage = _start + capacity();
}
return *this; //支持连续赋值
}
写法二:现代写法
运算符重载的现代写法运用了传值传参,即是吧实参的拷贝传进来,那么就可以直接使用swap()来进行交换,交换后也不影响被拷贝对象的值。
//现代写法
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
析构函数
对容器进行析构时,首先判断该容器是否为空容器,若为空容器,则无需进行析构操作,若不为空,则先释放容器存储数据的空间,然后将容器的各个成员变量设置为空指针即可。
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
迭代器相关函数
begin和end
我们这里的迭代器是g++的底层,即是原生指针型的迭代器,所以可以直接返回指针就行。
//迭代器相关函数
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;
}
容量和大小相关函数
size和capacity
size和capacity即可由这张图求得,由于两个指针相减的结果,就是这两个指针之间对应类型的数据个数,所以size可以由_finish - _start得到,而capacity可以由_endofstorage - _start得到。
//容量和大小的相关函数
size_t size()const
{
return _finish - _start;
}
size_t capacity()const
{
return _endofstorage - _start;
}
reserve
reserve的实现也就与我们之前实现的string一致,都是先判断是否n大于capacity,若大于,则开辟新的空间,吧之前的值插入到新开辟的空间里,最后再释放掉之前的空间即可。
在reserve函数的实现当中有个地方需要注意:
在进行操作之前需要提前记录当前容器当中有效数据的个数。
因为我们最后需要更新_finish指针的指向,而_finish指针的指向就等于_start指针加容器当中有效数据的个数,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
for (size_t i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = tmp + sz;
_endofstorage = tmp + n;
}
}
resize
根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。
void resize(size_t n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
if (n > capacity())
{
reserve(n);
}
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
在C++当中内置类型也可以看作是一个类,它们也有自己的默认构造函数,所以在给resize函数的参数val设置缺省值时,设置为T( )即可。
empty
empty函数可以直接通过比较容器当中的_start和_finish指针的指向来判断容器是否为空,若所指位置相同,则该容器为空。
bool empty()const
{
return _start == _finish;
}
修改容器内容相关函数
push_back
push_back 时先判断是否容量已满,若满了则更新空间,最后给_finish 的位置上插入x即可,_finish++。
//修改容量的相关函数
void push_back(const T& x)
{
if (_finish == _endofstorage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = x;
_finish++;
}
pop_back
尾删数据之前也得先判断容器是否为空,若为空则做断言处理,若不为空则将_finish–-即可。
void pop_back()
{
assert(!empty());
_finish--;
}
insert
insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
void insert(iterator pos, const T& x)
{
if (_finish == _endofstorage)
{
size_t len = pos - _start;
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + len;
}
iterator it = _finish - 1;
while (it >= pos)
{
*(it + 1) = *it;
it--;
}
*pos = x;
++_finish;
/*iterator it = _finish;
while (it > pos)
{
*(it) = *(it - 1);
it--;
}
*it = val;
++_finish;*/
}
这里实现了两个移动元素的方案,因为_finish先指向最后一个元素的下一个位置,所以it = _finish-1 之后,it指向了最后一个位置,然后给循环的判断条件it >= pos
,即最终是吧pos位置上的元素向后移动之后再退出循环。
第二个方案也与之类似。
erase
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
iterator erase(iterator pos)
{
assert(!empty());
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *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 i)
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i)const
{
assert(i < size());
return _start[i];
}