vector是STL容器的一种,和我们在数据结构中所学的顺序表结构相似,其使用和属性可以仿照顺序表的形式。vector的本质是封装了一个动态大小的数组,支持动态管理容量、数据的顺序存储以及随机访问。
1.前言说明
vector作为容器,应该支持任意数据类型的使用,所以vector使用模板类来实现,在使用的时候对模板类进行实例化即可。
我们需要首先明确vector的迭代器如何实现。对于如vector和上一篇文章中的string而言,他们的存储空间是连续的,所以使用原生指针即可完成遍历和访问操作。在这里对迭代器的实现进行说明,一般而言,存储物理空间连续的容器,迭代器使用原生指针就可以了,因为此时迭代器的自增、begin和end等行为指针可以完美驾驭;但是对于物理空间不连续的容器而言,如链表,就不可以使用原生指针作为迭代器了,因为此时物理空间不再连续,导致自增等行为会出现异常,所以需要额外封装一个迭代器类,具体详情会在下一篇文章中详细说明。
对于vector而言,我们贴合库中的形式进行实现,所以使用迭代器变量的形式来作为vector类的成员变量。也就是使用指针作为成员变量来表示vector的始末位置以及容量。
namespace m_vector //为自己实现的vector定义一个命名空间
{template<class T> //使用类模板class vector{public:typedef T* iterator;typedef const T* const_iterator; //定义迭代器为T*类型private: //成员变量都是迭代器变量的形式(指针),尽量贴合库中的形式iterator _start = nullptr; //vector起始地址iterator _finish = nullptr; //vector最后一个数据的下一个地址iterator _endofstorage = nullptr; //vector开辟的空间的下一个地址//全部给定缺省值为空指针};
}
2. 构造函数
2.1 无参构造(默认构造)
对于无参构造,将所以成员全部初始化为空指针,初始化缺省值在成员变量声明处给出。
//构造函数://1.无参构造,成员参数全部初始化为空指针vector(){}
2.2 拷贝构造
拷贝构造复用已有的成员函数即可,无需自己再手动进行拷贝,先使用reserve扩容后,依次尾插元素即可。
//2.拷贝构造
//拷贝构造需要一些成员函数的辅助
vector(const vector<T>& v)
{//拷贝构造:先扩容,然后将元素依次尾插reserve(v.capacity());for (auto& e : v){push_back(e);}
}
2.3 范围构造
使用迭代区间对数组内容进行初始化,构造出来的数组具有[first,last)迭代区间的元素。
template <class InputIterator>
vector (InputIterator first, InputIterator last);
std::string s("abcdef");vector<char> v4(s.begin() + 1, s.end() - 1);for (auto e : v4){std::cout << e << ' ';}std::cout << std::endl;
对于范围构造,传递的迭代器参数理应支持任何容器的迭代器作为参数,所以考虑使用函数模板。迭代器的出现,就是为了可以忽略内部细节,而使用统一的方式格式进行访问。因此,在使用迭代器的时候无需关注具体实现细节,只需按照套路使用就好。
//3.迭代区间构造,构造的数组使用迭代区间的内容初始化// template <class InputIterator> // vector (InputIterator first, InputIterator last);// 类模板的成员函数可以是函数模板,使用函数模板以满足各种数据类型迭代区间的构造template <class InputIterator>vector(InputIterator first, InputIterator last){//由于任何数据类型都实现并支持迭代器,所以可以使用迭代器进行访问while (first != last){push_back(*first);++first;}}
2.4 填充构造
这个构造函数具有两个参数,将vector的前n个元素使用val进行初始化。模拟实现使用复用resize函数即可,但是值得一提的是它的参数缺省值。
vector (size_type n, const value_type& val = value_type());
发现val的缺省值是一个匿名对象,这是因为vector类模板实例化对象可能是内置类型,也可能是自定义类型。C++语法为了方便这种缺省值的情况,规定内置类型也有一个伪构造函数,这样就可以兼顾到内置类型和自定义类型两种情况,当value_type是内置类型时,就会被当做对象调用构造函数,如果是自定义类型就会使用匿名对象缺省值。
//3.n个元素构造,构造的vector初始化为具有n个val元素// vector (size_type n, const value_type& val = value_type());vector(size_t n, const T& val = T()) //以匿名对象T()作为缺省值//使用T()作为缺省值,T()是T类的匿名对象,被用来赋值的时候会调用其类的构造函数生成对象//使用了匿名对象可以保证对自定义类型的缺省//对于内置类型,C++为其定义一个伪构造函数,即当T是内置类型时,内置类型会被当做对象调用构造函数,如int类型会被默认初始化为0{resize(n, val);}
需要注意的是,由于存在范围构造,并且填充构造中的参数n类型为size_t,int变为size_t发生类型转换,因为调用范围构造并且将类型推导为int更贴合,所以如vector v(2,3)的(int,int)类的构造就会调用范围构造。所以为了避免这种情况需要为(int,int)再重载一个构造。
vector(int n, const T& val = T()){resize(n, val);}
2.5 初始化列表构造
这是C++11中支持的构造函数。在C++11中新增了初始化列表类,在初始化列表部分所介绍的多参数隐式类型转换实际上就是使用了这种初始化列表类对象进行初始化。
初始化列表:template<class T> class initializer_list;
初始化列表类实际是一个类模板,所以可以实例化定义类对象,其形式类似于数组。
//实例化初始化列表类auto a = { 1,2,3,4,5 };std::initializer_list<int> b = { 2,3,4 };vector<int> v5(a);vector<int> v6{ 1,2,9 };vector<int> v7({ 2,3,5 });vector<int> v8 = { 1,1,4,5,1,4 };//单参数构造支持隐式类型转换
这个类只实现了迭代器和size()功能,仅仅是为了作为一种特殊的类方便构造等操作。
//5.使用初始化列表进行构造//初始化列表:template<class T> class initializer_list;//这是C++11中新增的类型,也是一种类模板。在初始化列表时使用过,使用大括号进行表示。//这个类模板的函数很少。只满足了迭代器和size()vector(std::initializer_list<T> il){reserve(il.size());for (auto& e : il) //e使用引用,减小拷贝开销{push_back(e);}}
3.析构函数
释放空间,并且将指针置空。
//析构函数~vector(){delete[] _start;_start = _finish = _endofstorage = nullptr;}
4.vector遍历
4.1 size与capacity
通过size()函数可以得到数组的大小;通过capacity()函数可以得到数组的容量。
//size,返回vector的数组大小size_t size() const{return _finish - _start; //指针-指针得到之间元素个数}//capacity,返回vector的容量大小size_t capacity() const{return _endofstorage - _start;}
4.2 [ ]重载元素访问
重载[ ]操作符以满足任意下标访问操作,因为需要进行访问与修改,所以返回值是引用。对于[ ]同样有const与非const版本。
//[]运算符重载,重载const和非const版本T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < size());return _start[pos];}
4.3 迭代器
vector的迭代器我们在文章开篇已经进行了介绍,vector由于其连续的物理存储空间的特性,所以可以直接使用原生指针作为迭代器。我们模拟实现时只需要重载const和非const版本即可。
//非const迭代器iterator begin(){return _start;}iterator end(){return _finish;}//const迭代器const_iterator begin() const{return _start;}const_iterator end() const{return _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);}//赋值运算符重载vector<T>& operator=(vector<T> v){//传参时存在拷贝,将拷贝结果与this交换,原先的this被释放swap(v);return *this;}
6.数组大小与容量调整
6.1 reserve函数
reserve实现的还是对capacity的控制,对于reserve的参数n,当n>capacity的时候,会进行扩容操作,将容量扩展至n;当n<=capacity时,函数不做任何动作,直接返回。
在reserve中需要强调的是扩容后数据拷贝的问题。因为C++扩容是另外开辟一个空间,将原来的数据进行拷贝。但是与string不同,string的内容只会是字符,所以memcpy完全可以应付。但是vector的元素可能是任意类型的,不只是内置类型,也有可能是自定义类型。
当类模板实例化为string、vector等类型时,这时候单纯的对vector进行memcpy就是一个浅拷贝,这就会导致拷贝的内容只是对应自定义类型的成员变量的值,而这些值实际上是指向了开辟的空间。当如此浅拷贝之后,释放原空间这些成员变量就变成了野指针,并且在对象生命周期结束后还会进行一次析构,这样就会引起错误。所以为了避免这种问题,可以在新旧空间之间使用赋值运算符进行手动拷贝,因为是使用了赋值运算符,只要类型T正确重载了合适的赋值运算符,那么就不会出错。
扩容后由于空间发生了变化,原本的成员变量指针就会失效,所以需要先保存好size,之后释放了原空间仍然可以对成员变量进行正确赋值。
//reserve,调整容量// n>capacity:扩容到n// n<=capacity:不做处理void reserve(size_t n){if (n > capacity()){size_t old_size = size(); //需要先存储原数组size//开辟新的容量的空间tmp,将原数组内容拷贝到tmp中,并释放原空间T* tmp = new T[n];//memcpy(tmp, _start, size() * sizeof(T)); //memcpy属于浅拷贝,在遇到类似于string、vector对象作为元素时,拷贝后的结果仅仅是将要被释放的无效的地址//所以要在此处完成深拷贝,即将原数组中的内容一一赋值给tmpfor (size_t i = 0; i < old_size; i++){tmp[i] = _start[i]; //此处实际调用的是T类型对应的赋值运算符,如数组元素为string,这时只需要保证string的赋值运算符重载是深拷贝,此处调用后完成的就是深拷贝}delete[] _start;//成员变量是指针类型,所以在重新分配空间后地址发生变化,所有成员变量都需要进行更新_start = tmp;_finish = tmp + old_size; //提前计算size大小,否则当_start变化后,就无法再获得size了_endofstorage = tmp + n;}}
6.2 resize函数
resize函数用于修改对象的size。当参数n大于对象的size时,则会将size扩大至n,扩大的部分则由参数val来填充,此处val的缺省值是一个匿名对象,这和我们上文在填充构造处的作用一致。当参数n小于等于size时,则会将数组的size缩小为n。
//resize,调整大小// n>size:将size扩大至n,扩展出的空间使用参数进行填充// n<=size:将size缩小至n,截断void resize(size_t n, const T& val = T())//使用T()作为缺省值,T()是T类的匿名对象,被用来赋值的时候会调用其类的构造函数生成对象//使用了匿名对象可以保证对自定义类型的缺省//对于内置类型,C++为其定义一个伪构造函数,即当T是内置类型时,内置类型会被当做对象调用构造函数,如int类型会被默认初始化为0{if (n > size()){reserve(n);while (_finish < _start + n){*_finish = val;++_finish;}}else{_finish = _start + n;}}
7.增删查改
7.1 插入
7.1.1 push_back尾插
顺序表尾插元素使用push_back函数。模拟实现注意reserve函数调用扩容逻辑,然后赋值调整成员变量。
//push_back:尾插void push_back(const T& val){//检查扩容if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = val;++_finish;}
7.1.2 insert插入
insert函数支持pos位置前进行插入。注意观察insert的参数,其pos参数是迭代器类型,所以传参的时候需要使用迭代器进行传参。
插入必然涉及到检查容量,但是需要注意的是,如果发生了扩容,那么就说明空间发生了变化,所以就代表这原指针会失效,所以pos就不可以再使用了。所以需要扩容前记录pos的相对位置,在扩容后对pos进行恢复。
//insert:在pos位置前插入void insert(iterator pos, const T& val){//检查扩容if (_finish == _endofstorage){size_t old_len = pos - _start; //pos是iterator类型参数,所以当扩容后整个数组的地址会发生改变,所以pos的值就失去了意义,因此记录相对位置,并使pos随数组做出修改reserve(capacity() == 0 ? 4 : 2 * capacity());pos = _start + old_len;}iterator it = _finish;while (it != pos){*it = *(it - 1);--it;}*pos = val;++_finish;}
7.1.3 迭代器失效
插入逻辑也是因为存在扩容的机制,所以可能会产生迭代器失效的问题。原本的迭代器值,可能在对象调用了插入函数之后,扩容导致空间更改,从而再调用迭代器发生迭代器失效。为了避免这种情况,一般应避免在调用了插入函数后再使用之前的迭代器,如果要使用,则应该进行更新。
//迭代器失效://①在insert、push_back操作后可能会导致迭代器实现,这是由于扩容更改空间导致的vector<int> v2;v2.push_back(1);v2.push_back(2);v2.push_back(3);v2.push_back(4);vector<int>::iterator it1 = v2.begin() + 2;std::cout << *it1 << std::endl;v2.push_back(5);std::cout << *it1 << std::endl; //打印了随机值,所以it1失效了,insert同理//针对这种迭代器失效,无法避免,所以一般在insert或push_back操作后,就不再使用之前的it//如有需求,可以在插入操作后对it进行更新it1 = v2.begin() + 2;std::cout << *it1 << std::endl;
7.2 删除
7.2.1 pop_back尾删
pop_back的作用就是尾删。
//empty:判空bool empty(){return _start == _finish;}//pop_back:尾删void pop_back(){assert(!empty());--_finish;}
7.2.2 erase删除
erase的作用是删除pos位置的值。
//erase:删除pos位置的值//void erase(iterator pos)iterator erase(iterator pos) //erase可能导致迭代器失效,所以返回新的迭代器值,来更新原先的迭代器{assert(pos < _finish && pos >= _start);iterator it = pos;while (it != _finish){*it = *(it + 1);++it;}--_finish;return pos;}
7.2.3 迭代器失效
删除元素同样会导致迭代器失效。erase的迭代器失效原因是因为可能会触发缩容,同时也可能在遍历中删除配合不协调跳过元素的情况。
我们针对这个问题,做出了小细节优化。细心观察发现erase有一个iterator的返回值,这是为了方便我们更新迭代器,避免迭代器失效。
除此之外,对于遍历时存在erase的情况,需要判断如果erase执行了就不可以在使迭代器自增了,否则会出现跳过元素的可能。
//②在erase后可能会导致迭代器失效//一方面是由于缩容导致的//另一方面则是erase函数和遍历时的协调问题,我们可以进行改进优化//由于我们所实现的erase会直接使用后面的值覆盖,但同时在迭代器遍历时又会让it++,这就使得it跳过了一部分元素,甚至是顺序表的end()导致越界崩溃vector<int> v3({ 1,2,3,4,4,5,6 });vector<int>::iterator it2 = v3.begin();while (it2 != v3.end()){if (*it2 % 2 == 0){it2 = v3.erase(it2); //为了避免第一种可能,所以改进erase,返回新的迭代器,以供更新}else //为了避免第二种可能,所以改进遍历时it自增的机制{it2++;}}//针对erase迭代器失效可以改进erase函数,对迭代器进行更新for (auto e : v3){std::cout << e << ' ';}std::cout << std::endl;
总结:迭代器失效发生在插入和删除数据(erase)时,要求在使用过insert、push_back、erase之后,原来的迭代器就不可以再使用,如果需要使用则需要先更新。