一.vector介绍
vector是表示可变大小数组的序列容器
就像数组一样,vector也采用连续的存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
二. vector的模拟实现
1. 基本框架
vector是个类模板,这样它就可以存储不同数据类型的变量,其迭代器是数据类型的指针。
template<class T> class vector { public: typedef T* iterator; //普通迭代器 typedef const T* const_iterator;//const迭代器 private: iterator _first; //指向第一个有效数据的指针 iterator _finish; //指向有效数据的尾(尾不存储有效数据) iterator _endofstorage; //指向存储容量的尾(尾是未开辟的空间) };
2. 迭代器相关接口
2.1 begin和cbegin
begin
原型:iterator begin() noexcept;
作用:返回一个指向第一个数据的普通迭代器
cbegin
原型:const_iterator cbegin() const noexcept;
作用:返回一个指向第一个数据的const迭代器
template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; // begin iterator begin() { return _first; } // cbegin const_iterator cbegin()const { return _first; } private: iterator _first; iterator _finish; iterator _endofstorage; };
2.2 end和cend
end
原型:iterator end();
作用:返回一个指向最后一个数据的下一个位置的普通迭代器
cend
原型:const_iterator cend() const noexcept;
作用:返回一个指向最后一个数据的下一个位置的const迭代器
template<class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; //end iterator end() { return _finish; } //cend const_iterator cend()const { return _finish; } private: iterator _first; iterator _finish; iterator _endofstorage; };
3. 容量操作接口
3.1 size和capacity
size
原型:size_type size() const;
作用:返回vector对象有效数据的个数
capacity
原型:size_type capacity() const;
作用:返回vector对象的容量大小
注意指针减指针的结果并不是单纯的数字减法的结果,编译器会根据指针类型把数字减法的结果除以其权重 sizeof(T),得到对应类型的有效数据的个数。
指针减指针运算:1.计算间隔的字节数,2、除以权重( sizeof(T))。
//size size_t size()const { return _finish - _first; } //capacity size_t capacity()const { return _endofstorage - _first; }
3.2 reserve
原型:void reserve (size_type n);
作用:调整vector对象的容量
n > 对象的容量大小:开新空间扩容(还要拷贝旧空间的数据)
n <= 对象的容量大小:啥事都不干
void reserve(size_t n) { if (n > capacity()) { size_t len = size(); // 1.开新空间 T* tmp = new T[n]; // 2.拷贝数据 for (size_t i = 0; i < len; ++i) { tmp[i] = _first[i]; } // 3.释放旧空间,并更新成员变量 delete[] _first; _first = tmp; _finish = tmp + len; _endofstorage = tmp + n; } }
问:拷贝数据时能否用memecpy?
在回答这个问题之前我们先来看看memcpy的拷贝原理,其实就是按一个字节一个字节这样的来完成深拷贝。
void* memcpy(void* dest, const void* src, size_t num)//传入数据可能是各种类型的,所以用void*接收 { //断言,判断指针的有效性,防止野指针 assert(dest!=NULL); assert(src!=NULL); //void* //可以接收所有类型的指针 //不可以进行解引用和加减的操作,但可以比较大小 void* tmp = dest; while (num--) { //把指针类型转化为char*在解引用和+1/-1时可以访问一个字节 *(char*)dest = *(char*)src; ((char*)dest)++; ((char*)src)++; } return tmp; }
如果我们是内置类型还好,但是像string类对象那样有指向一块空间的指针的成员变量的话就会造成浅拷贝
3.3 resize
原型:void resize (size_type n, value_type val = value_type());
作用:调整vector有效数据的大小
n > 有效元素个数:多出来的有效空间用val填补,如果不传那就填补对应数据类型默认的值value_type(),如果大于capacity()还会扩容
n < 等于有效元素个数:会截断多出来的有效数据
void resize(size_t n, const T& val = T()) { // 1.当n大于其有效数据长度的处理 if (n > size()) { if (n > capacity()) { reserve(n); } iterator finish = _first + n*sizeof(T); while (_finish != finish) { *_finish = val; ++_finish; } } else// 2.当n小于等于其有效数据长度的处理 { _finish = _first + n * sizeof(T); } }
内置类型的构造函数
我们resize参数列表中有个形参const T& val = T(),这里的T是传入数据的类型,T()就是这个类型的一个匿名对象。如果T是自定义类型的话确实有它的默认的构造函数,但如果T是内置类型的话像int, double,char他们也有自己的默认构造函数吗?
4. 默认成员函数
4.1 构造函数
类型一
原型:vector()
作用:构造一个空对象
什么都没传就是空对象。没有开辟空间,其成员变量都为nullptr
vector() :_first(nullptr) ,_finish(nullptr) ,_endofstorage(nullptr)
类型二
原型:vector(size_type n, const value_type& val = value_type())
作用:构造并初始化n个val
vector(size_t n, const T& val = T()) // 1.先把成员变量初始化为空 :_first(nullptr) ,_finish(nullptr) ,_endofstorage(nullptr) { // 2.开空间 reserve(n); // 3.给空间赋值 while (n--) { *_finish = val; ++_finish; } }
问:既然是初始化构造n个val,直接在函数体里构造就行了,在初始化列表把成员变量初始化为空有必要吗?
4.2 拷贝构造
vector(const vector& v) :_first(nullptr) ,_finish(nullptr) ,_endofstorage(nullptr) { // 1.开和v一样大的空间 reserve(v.capacity()); // 2.遍历v,利用迭代器拷贝数据 const_iterator it = v.begin(); const_iterator finish = v.cend(); while (it != finish) { *_finish++ = *it++; } }
拷贝构造的几点说明
4.3 赋值重载
vector<T>& operator=(vector<T> v)//注意这里的参数v是拷贝构造得来的 { swap(v);//这里复用了成员函数swap return *this; } //成员函数swap void swap(vector<T>& v) { //相互交换各自指向的地址 //下面的swap是std的swap std::swap(_first, v._first); std::swap(_finish, v._finish); std::swap(_endofstorage, v._endofstorage); }
成员函数swap和std::swap
赋值重载的几点说明
5. 修改操作接口
5.1 insert
原型:iterator insert (iterator position, const value_type& val);
作用:在pos位置插入一个数据
iterator insert(iterator pos, const T& val) { // 插入之前检查是否需要扩容 if (_finish == _endofstorage) { size_t len = pos-_first; size_t newcapacity = capacity() == 0 ? 2 : capacity() * 2; reserve(newcapacity); // 更新迭代器(下面图里有说明) pos = _first + len; } // end为最后一个有效数据的迭代器 iterator end = _finish-1; // 挪动数据 while (pos <= end) { *(end + 1) = *end; --end; } // 插入数据 *pos = val; ++_finish; return pos; }
insert涉及到的迭代器失效问题
①增容导致外面传入的迭代器pos变成野指针
因为增容的过程是开新空间、拷贝数据、释放旧空间。在增容后外面的pos指向的空间被释,自己放变成了野指针。造成这个问题的原因是原空间被释放,而外面的pos不能及时更新。
当然对于pos我们是传值进来的,虽然我们在函数里面更新了它,但是外面的pos其实还是失效的,它指向一块已经被释放的空间,变成了野指针。如果是传引用的话就可以解决,不过STL库并没有这样做。
②插入导致外面传入的迭代器pos意义改变
这是在没有增容的情况发生的迭代器失效问题,插入后外面的迭代器pos指向的值变成了新插入的val。造成这个问题的原因是vector的存储空间是连续的,外面的pos永远指向那个位置,而insert就是要挪动pos以及pos位置以后的数据,最后在pos位置上插入新的数据val。
insert迭代器失效问题总结
在使用insert后,原来的pos的最好别用了,因为我们不确定是否增容导致它变成野指针还是,还是没增容但是它的意义改变了。要用的话就更新它,让它接收insert的返回值,这个返回值一定是有效的,但是我们要清楚返回的迭代器它的值是指向新插入那个数据的。
问:插入一个数据,能否用memset?
memcpy和memset拷贝数据总结
拷贝数据时慎用memcpy和memset,前者如果拷贝的是涉及到内存管理的数据类型比如string会造成浅拷贝,后者只是按一个字节的内容来拷贝的。最保险的拷贝方式还是遍历一遍单独拿出每个数据以来赋值完成拷贝。
5.2 erase
iterator erase(iterator pos) { //把pos后面的数据一个一个地往前挪 iterator begin = pos + 1; while (begin != _finish) { *(begin - 1) = *begin; ++begin; } --_finish; return pos; }
erase涉及到的迭代器失效问题
①缩容导致外面传入的迭代器pos变成野指针
有的编译器,如果你一直删除数据,而导致太多空间空不出不同,编译器会把你的容量缩小:开辟小空间、拷贝数据、释放旧的大空间。和insert一样外面的迭代器pos变成了野指针。
②:删除导致外面传入的迭代器pos意义改变
在未缩容的情况下,erase删除一个数据后,外面pos的值变成了那个被删除数据的后一个数据,这时因为vector存储空间是连续的而pos永远指向空间的那个位置,删除一个数据就是把后面位置的数据往前挪。
关于erase写法的几点说明