手撕STL——vector
目录
引言
1,了解 STL 中的 vector
2,先来一个简易版跑起来
2_1,构造函数
2_2,扩容reserve()
2_3,push_back()
2_4,pop_back()
2_5,测试一下
3,迭代器
3_1,如何设计iterator
3_2,begin(),end()一锅端
4,功能完善
4_1,operator[ ]()
4_2,size() 和 capacity()
4_3,resize(size_t n, T x = T())
4_4,insert()
4_5,erase()
4_6,构造函数重载——迭代器
4_7,拷贝构造函数
4_8,赋值运算符重载
4_9,析构函数
5,总结
革命尚未成功,同志仍须努力
引言
上一期我们手撕了list,这一期我们就来手撕一个vector(代码在最后),vector手撕起来就比list简单很多啦。
1,了解 STL 中的 vector
在学习初阶数据结构的时候,顺序表咱们也撕过好多次了,有静态的,也有动态的,在STL中,vector是一个动态开辟的顺序表,但是它与咱们在数据结构阶段实现的顺序表是有差异的,在数据结构阶段实现的顺序表中,我们是通过 typename* a, int size, int capacity,作为成员变量
#define typename intstruct node
{typename* a;int size;int capacity;
}
但是哈,在vector中,就变得不一样啦。
template <typename T>
class vector
{private:T* _start;T* _finish;T* _end_of_storage;
}
STL 中的 vector,用了三个指针当成员变量,为什么要这样设计呢?
在C中,咱们用的是,struct,struct的成员变量咱们是可以直接访问的,但是在C++的class中,为了保护成员变量,咱们将其设置成private成员,咱们在外面是不能直接访问的,vector中有size()和capacity()函数,来获取size和capacity。但是其他的思路大致是不变的,那咱们就开始手撕吧。
2,先来一个简易版跑起来
2_1,构造函数
2_2,扩容reserve()
vector中数据是存在一段连续的空间,不能像list那样插入前new一个空间出来,我们必须提前开好空间,保证插入数据的时候空间一个是足够的。
reserve的实现过程很简单,在数据结构初阶阶段我们用realloc进行扩容,其大致原理就是,如果这段连续的空间后面的空间够就直接原地扩容,如果不够就找一个空间够的地方,重新开辟空间,并将原有的数据拷贝过去,最后将原有的空间释放。
我们直接用new和delete进行异地扩容,开辟好空间后再进行数据拷贝,拷贝完数据记得释放原来的空间,避免内存泄漏。
2_3,push_back()
因为顺序表头插需要挪动数据,代价是比较大的,STL中的vector也没有提供头插接口,所以这里只来一个头插
尾插分3步
1,检查需不需要扩容
2,将 _finish 位置赋值
3,++finish
2_4,pop_back()
尾删就比较简单啦,直接 --_finish,就可以了
2_5,测试一下
咱们目前还没有完成其他的函数,为了测试,咱们写一个打印函数print()
ok,没有问题,是可以跑起来了的。
3,迭代器
3_1,如何设计iterator
迭代器在STL中是一个非常重要的设计,它观察STL,当然在vector中的迭代器是比较简单的。
因为vector中的数据是存储在一段连续的空间中,咱们的iterator就可以直接由指针typedef得来。
typedef T* iterator;
typedef const T* const_iterator;
3_2,begin(),end()一锅端
4,功能完善
4_1,operator[ ]()
中括号重载是一个比较重要的函数,有他的存在我们就能很方便的插入修改数据。
4_2,size() 和 capacity()
直接用指针相减的中间元素个数的知识实现
4_3,resize(size_t n, T x = T())
resize()的功能大家一定要清楚,他的注意功能是改变size。
如果 n < size() 就直接删除数据,通过改变 _finish 实现
如果 size() < n < capacity (),改变size,并进行初始化
如果 capacity () < n,先开空间,在改变size,并进行初始化
4_4,insert()
insert 大致分三步
1,检查是否需要扩容
2,挪动数据
3,插入
这里要注意一个问题,当我们进行扩容后,原来的迭代器所指向的位置已经被delete清理了,这时候的iterator实际上是一个野指针。
为了防止出错,我们要在最开始通过指针的减法记录迭代器的位置,后面通过加法还原迭代器。
4_5,erase()
这里也用注意迭代器失效的问题
虽然我们这里没有扩容,进行删除后,迭代器仍然指向原来的位置
但是还是会有一点点风险,为了确保万无一失,咱们这里还是进行了存储迭代器位置,还原迭代器的操作。
4_6,构造函数重载——迭代器
在STL中,我们常可以看见用迭代器去构造一个容器,vector当然也可以,咱们就来实现一下
其实也是比较简单啊,通过迭代器,我们可以变量它,得到它的数据,再通过push_back()插入,遍历完了之后,vector也构造完了。为了可以使所有的迭代器都可以对其进行传参构造,这里还需要用到模板。
4_7,拷贝构造函数
根据参数直接构造。
先根据 被拷贝的对象 的 capacity 对要拷贝的对象进行扩容,再进行遍历和赋值。
4_8,赋值运算符重载
还是有传统写法与现代写法。
传统写法是 遍历参数的数据 给需要被赋值的对象进行数据的赋值
咱们这里写现代写法。
传参的时候不传引用,编译器根据咱们的拷贝构造函数进行拷贝,使用swap函数进行交换,对于参数 x 而言,它的数据存储的空间再堆上,但是x的三个成员变量会在赋值运算符重载这个函数结束的时候进行空间的回收,但是在堆的那一部分数据还是存在的。
使用 swap 函数进行交互 只是交换了 参数 x 和 待赋值的对象的三个指针,让待赋值的对象的三个成员遍历指向堆存储的数据的位置。
传统写法和现代写法原理大差不差的,只不过现代写法把拷贝数据的工作交给了编译器,实现起来更为简洁
4_9,析构函数
因为vector的数据存储在一段连续的区域,直接delete就可以了
5,总结
虽然咱们手撕了一个 vector 但是 咱们撕的只能说是一个简易版,我只是把 vector 里面常用,经典的函数带着大家手撕了一下,库里面的 vector的功能是非常强大的,而且大多函数都有多种重载。
大家可以通过这篇博客感受一下vector 的创建,也可以继续完善咱们自己的vector
革命尚未成功,同志仍须努力
#pragma once
#include <assert.h>
#include <iostream>
namespace ssy
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;vector(): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){}vector(size_t n, T data = T()): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){resize(n, data);}template<class InputIterator>vector(InputIterator first, InputIterator last): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){while (first != last){push_back(*first);++first;}}vector(const vector<int>& v): _start(nullptr), _finish(nullptr), _end_of_storage(nullptr){_start = new T[v.capacity()];size_t sz = v.size();for (int i = 0; i < sz; i++){_start[i] = v._start[i];}_finish = _start + sz;_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<T>& operator=(const vector<T> x){swap(x);return *this;}~vector(){if (_start){delete _start;_start = _finish = _end_of_storage = nullptr;}}size_t size(){return _finish - _start;}size_t capacity(){return _end_of_storage - _start;}void resize(size_t n, T data = T()){size_t sz = size();if (n < capacity()){_finish = _start + n;for (int i = sz; i < n; i++){_start[i] = data;}}else{reserve(n);for (int i = sz; i < n; i++){_start[i] = data;}_finish = _start + n;}}void reserve(size_t n){if (n > capacity()){T* tmp = new T[n];int size = _finish - _start;if (_start){for (int i = 0; i < size; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + size;_end_of_storage = _start + n;}}void push_back(const T& x){if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;}void pop_back(){--_finish;}T& operator[](size_t pos){assert(pos < capacity());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < capacity());return _start[pos];}iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}iterator insert(iterator pos, const T& data){size_t ppos = pos - _start;assert(ppos <= size());if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : 2 * capacity());}size_t end = size();while (end > ppos){_start[end] = _start[end - 1];--end;}_start[end] = data;++_finish;return _start + ppos;}iterator erase(iterator pos){size_t ppos = pos - _start;assert(ppos < size());size_t len = ppos;size_t end = size();while (ppos < end){_start[ppos] = _start[ppos + 1];ppos++;}--_finish;return _start + len;}void print(){int n = _finish - _start;for (int i = 0; i < n; i++){cout << _start[i] << " ";}cout << endl;}private:T* _start;T* _finish;T* _end_of_storage;};
}