vector模拟实现
- 一、vector函数接口总览
- 二、vector当中的成员介绍
- 三、list模拟实现
- 1、默认成员函数
- (1)构造函数1
- (2)构造函数2
- (3)构造函数3
- 2、拷贝构造函数
- (1)写法一:老式写法
- (2)写法二:现代写法
- 3、赋值运算符重载函数
- (1)写法一:传统写法
- (2)写法二:现代写法
- 4、析构函数
- 四、迭代器相关函数
- 1、begin和end
- 2、const对象的begin和end
- 3、使用演示
- 五、容器和大小相关函数
- 1、size和capacity
- 2、reserve
- 细节问题1、在进行操作之前需要提前记录当前容器当中有效数据的个数。
- 细节问题2、拷贝容器当中的数据时,不能使用memcpy函数进行拷贝。
- 3、resize
- 4、empty
- 六、修改容器内的相关函数
- 1、push_back
- 2、pop_back
- 3、insert
- 4、erase
- 5、swap
- 七、访问容器相关函数
- 1、operator[]
- 2、const operator[]
一、vector函数接口总览
namespace JRH
{
// vector
template<class T>
class vector
{
public:
// 1、默认的函数
vector(); // 构造函数1
vector(size_t n, const T& val); // 构造函数2
template<class InputIterator>
vector(InputIterator first, InputIterator last); // 构造函数3
vector(const vector<T>& vt); // 拷贝构造
vector<T>& operator=(const vector<T>& vt); // 运算符赋值重载函数
~vector(); // 析构函数
// 2、迭代器相关函数
iterator begin(); // 头
iterator end(); // 尾
const_iterator begin() const; // const头
const_iterator end() const; // const尾
// 3、容器相关的函数
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; // 判空
// 4、修改容器内容相关函数
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);
// 5、访问容器相关函数
T& operator[](size_t i);
const T& operator[](size_t i)const;
private:
iterator _start; //指向容器的头
iterator _finish; //指向有效数据的尾
iterator _endofstorage; //指向容器的尾
};
}
二、vector当中的成员介绍
在vector当中有三个成员变量_start、_finish、_endofstorage。
_start指向容器的头,_finish指向容器当中有效数据的尾,_endofstorage指向整个容器的尾。
三、list模拟实现
1、默认成员函数
(1)构造函数1
首先支持一个无参的构造函数,我们只需要将其三个变量都定为nullptr即可。
vector() // 构造函数1
:_start(nullptr)
, _finish(nullptr)
, _endofstorge(nullptr)
{}
(2)构造函数2
vector还支持构造这样一种容器,该容器当中含有n个值为val的数据。对于该构造函数,我们可以先使用reserve函数将容器容量先设置为n,然后使用push_back函数尾插n个值为val的数据到容器当中即可。
vector(size_t n, const T& val) // 构造函数2
:_start(nullptr)
,_finish(nullptr)
,_endofstorage(nullptr)
{
reserve(n); // 调用下面写的reverse函数将其空间先定义为n个
// 尾插n个val值进入到vector中
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
注意:
- 这个reverse函数有大用,我们要开好刚好的n个的空间,因为避免调用push_back函数时需要增容多次,导致效率降低。
- 该构造函数还需要实现两个重载函数(int和long)。
vector(int n, const T& val) // 构造函数2 -- 重载int
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n); // 调用下面写的reverse函数将其空间先定义为n个
// 尾插n个val值进入到vector中
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
vector(long n, const T& val) // 构造函数2 -- 重载long
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n); // 调用下面写的reverse函数将其空间先定义为n个
// 尾插n个val值进入到vector中
for (long i = 0; i < n; i++)
{
push_back(val);
}
}
(3)构造函数3
vector还支持使用一段迭代器区间进行对象的构造。因为该迭代器区间可以是其他容器的迭代器区间,也就是说该函数接收到的迭代器的类型是不确定的,所以我们这里需要将该构造函数设计为一个函数模板,在函数体内将该迭代器区间的数据一个个尾插到容器当中即可。
template<class InputIterator>
vector(InputIterator first, InputIterator last) // 构造函数3
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
// 将迭代器中的所有区间的值全部都放进vector中
while (first != last)
{
// this->push_back(*first);
push_back(*first);
first++;
}
}
2、拷贝构造函数
(1)写法一:老式写法
先开辟一块与该容器大小相同的空间,然后将该容器当中的数据一个个拷贝过来即可,最后更新_finish和_endofstorage的值即可。
vector(const vector<T>& vt) // 拷贝构造
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
// 先开一块相同大小的空间
_start = new T[vt.capacity()];
for (size_t i = 0; i < vt.size(); i++) // 一个个拷贝过去
{
_start[i] = vt[i];
}
_finish = _start + vt.size(); // 有效数据的尾
_endofstorage = _start + vt.capacity(); // 整个数组的尾
}
注意: 将容器当中的数据一个个拷贝过来时不能使用memcpy函数,当vector存储的数据是内置类型或无需进行深拷贝的自定义类型时,使用memcpy函数是没什么问题的,但当vector存储的数据是需要进行深拷贝的自定义类型时,使用memcpy函数的弊端就体现出来了。例如,当vector存储的数据是string类的时候。
并且vector当中存储的每一个string都指向自己所存储的字符串。
如果此时我们使用的是memcpy函数进行拷贝构造的话,那么拷贝构造出来的vector当中存储的每个string的成员变量的值,将与被拷贝的vector当中存储的每个string的成员变量的值相同,即两个vector当中的每个对应的string成员都指向同一个字符串空间。
这结果是肯定错误的,因为都指向同一块空间了,假如销毁原字符串用memcpy拷贝构造的函数也销毁了,那么我们看一下代码是怎么实现的:
代码中看似是使用普通的“=”将容器当中的数据一个个拷贝过来,实际上是调用了所存元素的赋值运算符重载函数,而string类的赋值运算符重载函数就是深拷贝,所以拷贝结果是这样的:
- 如果vector当中存储的元素类型是内置类型(int)或浅拷贝的自定义类型(Date),使用memcpy函数进行进行拷贝构造是没问题的,但如果vector当中存储的元素类型是深拷贝的自定义类型(string),则使用memcpy函数将不能达到我们想要的效果。
(2)写法二:现代写法
现代写法比较简单且暴力,直接一个for循环将其一个个放到this指针的push_back中即可,也就是一个个尾插过来即可。
在使用范围for对容器v进行遍历的过程中,变量e就是每一个数据的拷贝,然后将e尾插到构造出来的容器当中。就算容器v当中存储的数据是string类,在e拷贝时也会自动调用string的拷贝构造(深拷贝),所以也能够避免出现与使用memcpy时类似的问题。
vector(const vector<T>& vt) // 拷贝构造 -- 现代写法
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(vt.capacity()); // 开辟一段vt等大的空间
for (auto e : vt)
{
push_back(e); // 将容器vt中的数据一个个尾插进来
}
}
3、赋值运算符重载函数
(1)写法一:传统写法
首先判断是否是给自己赋值,若是给自己赋值则无需进行操作。若不是给自己赋值,则先开辟一块和容器v大小相同的空间,然后将容器v当中的数据一个个拷贝过来,最后更新_finish和_endofstorage的值即可。
同样也不能用memcpy函数进行拷贝,其原理与上面的拷贝构造函数是一样的。
vector<T>& operator=(const vector<T>& vt) // 运算符赋值重载函数 -- 传统写法
{
if (this != &vt) // 不能自己给自己赋值
{
delete[] _start; // 释放原来的空间
_start = new T[vt.capacity()]; // 开辟一块与vt等大的空间
for (size_t i = 0; i < vt.size(); i++)
{
_start[i] = vt[i]; // 将vt里面的数据一个个拷贝过来
}
_finish = _start + vt.size(); // 有效数据的大小
_endofstorage = _start + vt.capacity(); // 整个数组的大小
}
return *this; // 支持连续赋值
}
(2)写法二:现代写法
首先在右值传参时并没有使用引用传参,因为这样可以间接调用vector的拷贝构造函数,然后将这个拷贝构造出来的容器v与左值进行交换,此时就相当于完成了赋值操作,而容器v会在该函数调用结束时自动析构。
赋值运算符重载的现代写法也是进行的深拷贝,只不过是调用的vector的拷贝构造函数进行的深拷贝,在赋值运算符重载函数当中仅仅是将深拷贝出来的对象与左值进行了交换而已。
vector<T>& operator=(const vector<T> vt) // 运算符赋值重载函数 -- 现代写法
{
std::swap(vt); // 交换这两个值
return *this; // 支持连续赋值
}
4、析构函数
析构函数很简单,但有细节,是析构函数首先要判断是不是空指针,不可释放空指针,非空指针后先释放原空间,再将三个变量置空即可。
~vector() // 析构函数
{
if (_start != nullptr) // 避免对空指针进行曹组
{
delete[] _start; // 释放空间
_start = nullptr;
_finish = nullptr;
_endofstorage = nullptr;
}
}
四、迭代器相关函数
我们的迭代器就是容器当中所存储数据类型的指针,我们首先定义两个迭代器,一个是iterator,另一个是const_iterator,我们看一下下面的定义:
typedef T* iterator;
typedef const T* const_iterator;
1、begin和end
begin()就是返回该vector的首元素的地址,end()返回容器当中有效数据的下一个数据的地址。我们看一下下面的代码:
iterator begin() // 头
{
return _start; // 返回头元素地址
}
iterator end() // 尾
{
return _finish; // 返回有效数据的下一个元素地址
}
2、const对象的begin和end
const对象的begin和end函数是和上面一样的,只不过重载了上面的两个函数,我们直接显示代码即可:
const_iterator begin() const // const头
{
return _start; // 返回头元素地址
}
const_iterator end() const // const尾
{
return _finish; // 返回有效数据的下一个元素地址
}
3、使用演示
第一种我们可以用迭代器iterator来进行演示:
for (size_t i = 0; i < v.size(); ++i)
{
std::cout << v[i] << " ";
}
std::cout << std::endl;
第二种我们用for循环来进行演示,因为for循环可以用的情况下也就是底层的是迭代器也可以进行使用。
for (auto e : v)
{
std::cout << e << " ";
}
std::cout << std::endl;
五、容器和大小相关函数
1、size和capacity
vector当中三个成员遍历各自的指向,我们可以很容易得出当前容器中的有效数据个数和最大容量。
size()就是已用空间的大小,也就是_finish-_start;capacity()就是总空间的大小,也就是_endofstorage-_start。
size_t size() const // 大小
{
return _finish - _start;
}
size_t capacity() const // 容量
{
return _endofstorage - _start;
}
2、reserve
- 当n大于对象当前的capacity时,将capacity扩大到n或大于n。
- 当n小于对象当前的capacity时,什么也不做。
reserve函数的实现思路也是很简单的,先判断所给n是否大于当前容器的最大容量(否则无需进行任何操作),操作时直接开辟一块可以容纳n个数据的空间,然后将原容器当中的有效数据拷贝到该空间,之后将原容器存储数据的空间释放,并将新开辟的空间交给该容器维护,最好更新容器当中各个成员变量的值即可。
void reserve(size_t n) // 保持容量
{
if (n > capacity()) // 判断是否需要进行操作
{
size_t sz = size(); // 保存一下有效数据的个数
T* tmp = new T[n]; // 开辟一个可以容纳n个空间的tmp
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i]; // 将原本容器中的所有数据都拷贝到tmp临时空间中
}
delete[] _start; // 释放原空间
}
_start = tmp; // 将tmp临时空间中的数据全都放到对象容器当中
_finish = _start + sz; // 有效数据个数
_endofstorage = _start + n; // 总容量大小
}
}
细节问题1、在进行操作之前需要提前记录当前容器当中有效数据的个数。
我们在进入循环记录了一下有效数据的大小sz,这是因为我们最后需要更新_finish指针的指向,而_finish指针的指向就等于_start指针加容器当中有效数据的个数,当_start指针的指向改变后我们再调用size函数通过_finish - _start计算出的有效数据的个数就是一个随机值了。
细节问题2、拷贝容器当中的数据时,不能使用memcpy函数进行拷贝。
可能你会想,当vector当中存储的是string的时候,虽然使用memcpy函数reserve出来的容器与原容器当中每个对应的string成员都指向同一个字符串空间,但是原容器存储数据的空间不是已经被释放了,相当于现在只有一个容器维护这这些字符串空间,这还有什么影响。
但是不要忘了,当你释放原容器空间的时候,原容器当中存储的每个string在释放时会调用string的析构函数,将其指向的字符串也进行释放,所以使用memcpy函数reserve出来的容器当中的每一个string所指向的字符串实际上是一块已经被释放的空间,访问该容器时就是对内存空间进行非法访问。
所以还是用for循环和=将其一个个赋值过来才是王道:
3、resize
- 当n大于当前的size时,将size扩大到n,扩大的数据为val,若val未给出,则默认为容器所存储类型的默认构造函数所构造出来的值。
- 当n小于当前的size时,将size缩小到n。
根据resize函数的规则,进入函数我们可以先判断所给n是否小于容器当前的size,若小于,则通过改变_finish的指向,直接将容器的size缩小到n即可,否则先判断该容器是否需要增容,然后再将扩大的数据赋值为val即可。
void resize(size_t n, const T& val = T()// 模板缺省值) // 扩容
{
if (n < size()) // 当n小于当前的size时
{
_finish = _start + n; // 将size缩小到n
}
else // 当n大于当前的size时
{
if (n > capacity()) // 判断是否需要增容
{
reserve(n);
}
while (_finish < _start + n) // 将size扩大到n
{
*_finish = val;
_finish++;
}
}
}
4、empty
只需要比较_start和_finish的位置是否相同即可,相同则空了,不相同则非空。
bool empty() const // 判空
{
return _start == _finish;
}
六、修改容器内的相关函数
1、push_back
简单,判断是否容器已经满了,满了就扩容,然后将数据尾插到_finish指向的位置,再将_finish++即可。
void push_back(const T& x)
{
if (_finish == _endofstorage) // 到容量底了
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
_finish++;
}
2、pop_back
简单,先进行判空,这里用assert断言即可,方便快捷,非空则直接将_finish--
即可。
void pop_back()
{
assert(!empty());
--_finish;
}
3、insert
insert函数可以在所给迭代器pos位置插入数据,在插入数据前先判断是否需要增容,然后将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入,最后将数据插入到pos位置即可。
void insert(iterator pos, const T& x)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _endofstorage)
{
size_t len = pos - _start; // 记录一下相差大小
// 扩容
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1; // 将pos位置及其之后的数据统一向后挪动一位,以留出pos位置进行插入
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x; // 插入
++_finish; // 往后移一位
}
4、erase
erase函数可以删除所给迭代器pos位置的数据,在删除数据前需要判断容器释放为空,若为空则需做断言处理,删除数据时直接将pos位置之后的数据统一向前挪动一位,将pos位置的数据覆盖即可。
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator start = pos + 1;
// 将pos位置之后的数据统一向前挪动一位,以覆盖pos位置的数据
while (start != _finish)
{
*(start - 1) = *start;
++start;
}
--_finish;
}
5、swap
直接用std库中的swap函数即可,用其进行交换三个变量,这里的std::域作用限定符告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)。
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
七、访问容器相关函数
只有一个operator[]来进行访问,实现时直接返回对应位置的数据即可。
1、operator[]
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
2、const operator[]
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
这里的权限只需要读,不能写。