前言
还是那句话:模拟实现不是为了比库里面更好,而是去学习它的一些底层,能够让自己有更深的了解,为了更好的熟用!
vector底层图
由上图可以看出,它的底层实际上就是原生的指针T*,只不过会存在三个标记不同的位置,start标记的是有效数据的开始,finish标志的有效数据的结尾,end_of_storage标记的是整个空间的最尾部!
一、接口概述
namespace Ve
{template<class T>class vector{public:// vector的迭代器是一个原生指针,所以这里统一叫iteratortypedef T* iterator;typedef const T* const_iterator;// 默认成员函数vector();vector(size_t n, const T& value = T());template<class InputIterator>vector(InputIterator first, InputIterator last);vector(const vector<T>& v);vector<T>& operator= (vector<T> v);~vector();// capacitysize_t size() const;size_t capacity() const;void reserve(size_t n);void resize(size_t n, const T& value = T());bool empty();//迭代器与遍历iterator begin()iterator end()const_iterator begin()constconst_iterator end() constT& operator[](size_t pos);const T& operator[](size_t pos)const;///修改/void push_back(const T& x);void pop_back();void swap(vector<T>& v);void insert(iterator pos, const T& x);void erase(iterator pos);private:iterator _start; // 指向数据块的开始iterator _finish; // 指向有效数据的尾iterator _endOfStorage; // 指向存储容量的尾};
}
注意:这里采用命名空间的原因的string的模拟实现是类似的,同时也可以看到vector实际上是一个类模板,T可以是任意类型!
二、各接口具体实现
Ⅰ、默认成员函数
1.、构造函数
- 无参构造
vector():_start(nullptr),_finish(nullptr),_endOfStorage(nullptr) {}
- 特定值初始化
vector(size_t n, const T& value = T()) {reserve(n);//这里为了防止容量不够而进行的扩容操作,即使小了也不影响for (int i = 0; i < n; i++){push_back(value);} }
这里需要注意一个参数:const T& value = T(),这样写是因为T可以是任意类型,也就是说可能会是string这样的自定义类型,这时T()就相当于string(),一个匿名对象构造,那么就会去调用对应自定义类型的默认成员函数完成构造初始化工作!
而对于内置类型int,double等等,它们实际也存在着析构、默认构造等默认成员函数!这里可以证明一下,看代码
int main() {int i=1;int j=int();int k=int(2);return 0; } k=? j=?
结果:
- 迭代器区间初始化
template<class InputIterator> vector(InputIterator first, InputIterator last) {while (first != last){push_back(*first);++first;} }
操作十分的简单,就是一个尾插操作即可!同时需要注意类成员函数也可以是函数模板!
构造函数这里还有一点注意的地方:迭代器区间初始化和特定值初始化
vector(size_t n, const T& value = T()) {reserve(n);for (int i = 0; i < n; i++){push_back(value);} }//类成员函数也可以是函数模板 template<class InputIterator> vector(InputIterator first, InputIterator last) {while (first != last){push_back(*first);++first;} }
上面这两段代码在vector<int> v(10,1)情况下会发生冲突,因为两个都可以匹配,它会匹配到下面的那个模板函数,因为更加的符合,所以在底层会将第一段代码在写一个int的重载函数
vector(int n, const T& val = T()) {reserve(n);for (int i = 0; i < n; i++){push_back(val);} }
这里就能对vector<int> v(10,1)进行很好的处理!
2、拷贝构造
vector(const vector<T>& v){reserve(v.capacity());for (auto& a : v)//这里加个引用,就是因为T的类型未知,可能是string等其他自定义类型,最好加上{push_back(a);} }
同样,为了防止当前对象容量不够,所以最好先弄一个和拷贝对象一样大的空间,然后直接尾插即可!
3、赋值重载
这里直接采用和string类一样的现代写法,复用拷贝构造函数!!再交换两者即可
vector<T>& operator= (vector<T> v)//引用返回支持连续赋值
{swap(v);return *this;
}
4、析构函数
~vector()
{delete[] _start;_start = _finish = _endOfStorage = nullptr;
}
Ⅱ、容量
1.size()
从上述的vector底层图可以看出,size实际上就是两指针相减,即finish-start!
size_t size() const {return _finish - _start; }
2.capacity()
size_t capacity() const {return _endOfStorage-_start; }
3.reserve()
再回顾一下规则:如果要调整的空间小于原空间那就什么都不做,否则,就扩到和要调整空间一样大!
细节点:
①扩容实际是重新开一个空间,再将原数据拷贝过去,释放原来的空间!那么当前的三个成员变量都得更新,关键_finish指针该怎么去更新?按底层图来看应该是start+当前有效的数据个数,所以应该要先去保存原来的有效数据个数!
②数据的拷贝问题,我们会使用memcpy,但是呢实际上memcpy只是简单的值拷贝,但是对于string这样的需要深拷贝的类型来说必定会出现问题!!所以这里的解决方案也很简单,使用for循环将原数据一个一个放到新空间即可!
基于上述规则和细节,具体实现如下:
void reserve(size_t n) {if (n > capacity()){T* tmp = new T[n];//临时空间size_t old_size = size();//这里要保存原来的值,因为后面_start 和 _finish改变空间了,那么原来size就会消失!//memcpy(tmp, _start, old_size * sizeof(T));//值得注意的是,这里memcpy会存在一个问题,就是它是按照字节去拷贝的,也就是值拷贝,那对于string这样的需要深拷贝的类型必定出错!//所以可以改成循环的方式for (int i = 0; i < old_size; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_endOfStorage = tmp+n;} }
这一操作后,一定要注意迭代器问题,要使用需更新!!!
4.resize()
再来回顾规则:如果小于当前的size(),那就缩到与目标size()一样大,反之就扩大!如果n不仅仅大于原来的size(),还大于当前的capacity(),那就要考虑扩容操作了!
void resize(size_t n, const T& value = T()) {if(n > size()){reserve(n);//保险起见最好扩容//直接尾插数据即可while (_finish < _start + n){(*_finish) = value;_finish++;}}//小于直接改变finish的指向即可!else{_finish = _start + n;} }
可以看到一点:reserve只负责开空间,resize()负责开空间+初始化
5.empty()
bool empty()
{return _start == _finish;
}
Ⅲ、迭代器与遍历
begin+end
iterator begin() {return _start; } iterator end() {return _finish; } const_iterator begin()const {return _start; } const_iterator end() const {return _finish; }
这里一个是专门为const对象提供的const迭代器!
这里注意个细节问题:
这里都是传值返回,返回的是临时对象,具有常性,可读不可写,意味着不能begin()++,++要改变的返回的那个临时对象,由于常性的特点只能begin()+1;
operator【】
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对象设置了一个!
Ⅳ、增删查改操作
1.insert()
在pos位置前进行插入操作。首先最需要判断的就是位置的合法性,然后就是容量问题,当finish=endofstorage的时候,那就需要进行扩容操作了!紧接着就是向后挪动数据,结束条件就是把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要更新pos = _start + len;}iterator it = _finish - 1;while (it >= pos){*(it + 1) = (*it);--it;}*pos = x;_finish++;}
注意:在此之后使用迭代器也是要更新的!
2.erase()
删除pos位置的值。实际上就是将后面的元素向前挪动去覆盖pos位置的值即可。注意位置的合法性!
代码如下:
void erase(iterator pos) {assert(pos >= _start);assert(pos <= _finish);iterator it = pos + 1;while (it < _finish){*(it - 1) = (*it);it++;}--_finish; }
3.push_back()
直接复用insert即可。
void push_back(const T& x)
{insert(_finish, x);
}
4.pop_back()
finish指针向前挪动即可!即不把最后一个元素看在眼里!
void pop_back()
{assert(!empty());--_finish;
}
5.swap()
这里的swap实际上就是引用全局的swap进行交换!
void swap(vector<T>& v) {std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_endOfStorage, v._endOfStorage); }
今天的分享就到这里。感谢你的观看!