前言
本文将模拟实现vector的常用功能,目的在于更深入理解vector。
一、前置知识
- 在模拟之前先对vector的结构和常用接口学习,有一个大致了解。
- 看源码,本文参考的源码是SGI版本的stl3.0。
- 技巧:
- 看源码不要一行一行的看,要先看框架,了解整体框架
- 看源码要学会猜,根据单词的意思去猜它想表达什么。规范的代码,每一个名字都有它的含义。
- 总结:一看框架;二去猜(带着猜想,去验证)。
- 看框架的步骤:
- 先看成员变量
- 再看成员函数
- 技巧:
- 参考vector的源码:
- vector的成员变量:是三个原生指针变量(设为原生指针类型有什么好处,在模拟时讲解)
- vector的成员函数:vector的常用接口讲解链接
- STL中的容器因为需要频繁的申请和释放空间,所以STL中提供了内存池(allocator类模板),内存池的本质是先在堆区中申请一定的空间留作备用,当有新的内存需求时,就从内存池中分出一块内存块,若内存块不够再继续申请新的内存,这样可以提高内存分配的效率。现阶段我们只是简单模拟vector,所以我们这没有使用内存池,而是直接在堆区申请空间,后期会讲解内存池的。
二、vector常用接口的模拟
1、vector的成员变量
vector的成员变量是三个原生指针T*:
- _start:开始位置,即指向第一个元素的位置
- _finish:结束位置,即指向最后一个元素的下一个位置
- _end_of_storage:存储结束位置
虽然vector使用的是三个原生指针,但是可以通过指针运算得到size和capacity。
代码示例:
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public:typedef T* iterator;//获取容器中的元素个数size_t size()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用{//指针-指针=两者之间的元素个数return _finish - _start;}//获取为当前容器分配的存储空间size_t capacity()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用{//指针-指针=两者之间的元素个数return _end_of_storage - _start;}private:iterator _start;//开始位置,指向第一个元素iterator _finish;//结束位置,指向最后一个元素的下一个位置iterator _end_of_storage;//指向存储结束位置};
}
tip:
- 使用命名空间将我们模拟实现的vector封装,避免命名冲突。
- 类模板的定义与实现不分离,后续在模板进阶讲解。
- size和capacity可以通过指针-指针得到,注意指针-指针运算有一个前提是:物理存储空间是连续的。
- const成员:
- const修饰的是*this,即const成员函数的内部不能修改成员变量
- 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用
2、vector的默认成员函数
构造函数
:
- 构造函数:创建类对象时,编译器自动调用,给成员变量赋初值
- 类的成员变量建议在初始化列表初始化
- 成员变量为内置类型需要我们手动初始化,不然为随机值;成员变量为自定义类型不初始化,会去调用它的默认构造(建议每个类,都要有一个默认构造)
- vector的常用构造函数:
- 默认构造函数:一般使用最多,构造一个空的vector,即将每个成员初始化为空
- 构造并初始化n个val:先初始化成员变量,再复用reserve开n的空间,最后再通过尾插将val插入容器
- 使用迭代器初始化构造:先初始化成员变量,再将迭代器区间的数据尾插入容器
析构函数
:
- 析构函数:对象销毁时,编译器自动调用,完成对象中资源的清理
- 编译器生成的析构函数,对内置类型不做处理,自定义类型会去调用它的析构函数
- 当类涉及动态资源的申请时,就需要显式实现析构释放资源。
赋值重载函数
:
- 赋值重载函数:已经存在的两个对象复制拷贝
- 当类涉及资源管理时,就需要自己显示实现完成深拷贝,编译器默认生成的赋值重载函数只能完成浅拷贝
- 赋值重载深拷贝的现代写法:让形参去调用拷贝构造,去帮我们开空间拷贝数据,之后与形参交换(函数结束后形参销毁,也顺便帮我们把旧空间释放了)
- 现代写法无法避免自己给自己赋值的情况,当现实中也很少会出现
拷贝构造函数
:
- 拷贝构造函数:用一个已经存在的对象初始化另一个对象
- 注意:拷贝构造只有一个参数,并且必须是本类型的引用(使用传值会引发无限递归)
- 编译器默认生成的拷贝构造也是只能完成浅拷贝,所以当类涉及资源管理时,就需要自己显式实现完成深拷贝
- 拷贝构造深拷贝的现代写法:自己开空间,自己拷贝数据
总结:当类涉及资源管理时,拷贝构造、赋值重载、析构都需要显式实现。
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://默认构造函数vector()//初始化列表:成员变量定义的地方,建议在初始化列表初始化成员变量//成员变量为内置类型不初始化,为随机值:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}//构造并初始化n个valvector(size_t n, const T& val = T())//T()调用构造函数//初始化成员变量:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){//复用reserve,开空间reserve(n);//复用push_back,尾插n个valfor (size_t i = 0; i < n; ++i){push_back(val);}}//使用迭代器区间初始化//类模板的成员函数也可以是函数模板template<class InputIterator>vector(InputIterator first, InputIterator last)//初始化成员变量:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){//复用push_back,将迭代器区间[first,last)的数据尾插进容器while (first != last){push_back(*first);++first;}}//当wjs::vector<int> v(10, 1);报错——》 error C2100: 非法的间接寻址//函数重载,调用时会走最匹配的,wjs::vector<int> v(10, 1)两个参数类型都是int,所以他走使用迭代器构造//重载一个vector(int n, const T& val = T()),他就会走构造n个val//构造并初始化n个valvector(int n, const T& val = T())//T()调用构造函数//初始化成员变量:_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){//复用reserve,开空间reserve(n);//复用push_back,尾插n个valfor (int i = 0; i < n; ++i){push_back(val);}}//交换两个vector对象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=(vector<T> v)//形参v直接就去调用拷贝构造,帮我们开空间可拷贝数据了{//与v交换swap(v);return *this;}//拷贝构造函数vector(const vector<T>& v){//传统写法:自己开空间自己拷贝_start = new T[v.capacity()];//注意不能使用memcpy,它只能完成浅拷贝for (size_t i = 0; i < v.size(); ++i){//当T为自定义类型时,会去调用的它的赋值重载,完成深拷贝_start[i] = v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}//析构函数~vector(){if (_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}}};
}
tip:
- 重载函数,在调用时,会走匹配的。
- T():T是一个模板参数,所以T()是一个任意类型的匿名对象。如果T是定义类型会去调用它的默认构造(从这点建议每个类都需要有一个默认构造),如果是内置类型也去调用内置类型的默认构造。
- 理论上内置类型是没有构造函数的,但是有了模板之后,C++对此做了特殊处理,对内置类型做了升级,也提供了构造。
- 结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,可能会引起内存泄漏甚至程序崩溃。
3、vector的遍历
迭代器
:
- begin():返回指向容器的第一个元素的位置的迭代器
- end():返回指向容器最后一个元素的下一个位置的迭代器
- begin和end一般会实现两个版本:普通版本和const版本
- 有了迭代器,就可以使用范围for,因为范围for的底层就是替换为begin和end
operator[]
:
- operator []越界是断言处理(断言只在debug下会生效,release下不生效)
- operator[]一般也会实现两个版本:一个返回普通引用,一个返回常引用
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;//普通版本迭代器——迭代器可读可写iterator begin(){//返回指向第一个元素的位置的迭代器return _start;}iterator end(){//返回指向最后一个元素的下一位置的迭代器return _finish;}//const版本迭代器——迭代器只可读const_iterator begin()const{//返回指向第一个元素的位置的const迭代器return _start;}const_iterator end()const{//返回指向最后一个元素的下一位置的const迭代器return _finish;}//operator[]//普通版本——返回普通引用,可读可写T& operator[](size_t pos){//operator[]越界断言assert(pos >= 0 && pos < size());//返回pos位置元素的引用return _start[pos];}//cosnt版本——返回const引用,只可读const T& operator[](size_t pos)const{//operator[]越界断言assert(pos >= 0 && pos < size());//返回pos位置元素的常引用return _start[pos];}};
}
4、vector的reserve和resize
resize
:
- resize将容器大小调整为n
- 如果n小于当前容器大小,则内容将减少到其前n个元素,删除超出的部分
- 如果n大于当前容器大小,则通过在末尾插入所需数量的元素来扩展内容,以达到n的大小。如果指定了val,则新元素将初始化为val的副本,否则,它们将进行值初始化
- 注意:如果n也大于当前容器容量,则会自动重新分配存储空间
reserve
:
- reserve请求改变容器的capacity,只有当n>当前容量时,reserve才会重新分配空间,将其容量增加到n(或更大)
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://调整容器的sizevoid resize(size_t n, const T& val = T()){//如果n>sizeif (n > size()){//如果n>capacity,一次扩容,避免多次扩容reserve(n);//尾插val,使size=nwhile (_finish < _start + n){push_back(val);//++_finish;尾插之后finish会+1,所以这里不能再重复+1了}}else{//n<size,使size=n,调整_finish的位置即可_finish = _start + n;}}//调整容器容量void reserve(const size_t n){//只有n>capacity时,才会重新开空间,将其capacity增加到nif (n > capacity()){size_t old_size = size();iterator tmp = new T[n];//判断if (_start){//memcpy是浅拷贝,所以当拷贝的自定义类型且涉及资源管理时,就会报错!//memcpy(tmp, _start, sizeof(T) * old_size);for (size_t i = 0; i < old_size; ++i){//当T为自定义类型时会去调用它的赋值重载,完成深拷贝tmp[i] = _start[i];}delete[] _start;}_start = tmp;//_start的改变会影响size,所以需要在前面将旧size保存_finish = _start + old_size;_end_of_storage = _start + n;}}};
}
tip:
- memcpy是内存的二进制格式拷贝,将一段内存空间中的内容原封不动的拷贝到另一段内存空间中,即memcpy是浅拷贝
- 当memcpy拷贝的内容不涉及资源管理时,memcpy即高效又不会出错,但当memcpy拷贝的是自定义类型且涉及资源管理时,就会出错,因为memcpy不能完成深拷贝
- 结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,可能会引起内存泄漏甚至程序崩溃。
5、vector的插入
push_back
:
- 尾插,在vector的末尾插入x
- 尾插需要注意:①尾插之前需要检查是否扩容;②尾插之后size+1,即++_finish
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://尾插void push_back(const T& x){//插入之前判断是否需要扩容if (_finish == _end_of_storage){//扩容size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();reserve(new_capacity);}//尾插*_finish = x;++_finish;}};
}
insert
:
- 在pos位置的元素之前插入x
- insert需要注意:①断言pos位置是否合理;②插入之前检查是否需要扩容;③插入之后需要更新size
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://pos位置的元素之前插入void insert(iterator pos, const T& x){//断言pos位置是否合理assert(pos >= _start && pos <= _finish);//插入之前判断是否需要扩容if (_finish == _end_of_storage){//扩容size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();reserve(new_capacity);}//[pos, _finish - 1]的数据向后挪动,将pos位置空出iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}//pos位置插入x*pos = x;++_finish;}};
}
tip:
- insert在pos位置的元素之前插入x,需要向后挪动数据,在模拟实现string的时候,我们使用的是下标,当是头插的时候,结束条件我们不好控制,因为size_t不会小于0,我们当时使用了npos,现在vector使用iterator就不会出现这种情况了。
测试代码:
//测试insert
void test_vector04()
{wjs::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);v.push_back(7);v.push_back(8);for (auto& e : v){cout << e << " ";}cout << endl;
}
运行结果:
分析:
修改1:
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://pos位置的元素之前插入void insert(iterator pos, const T& x){//断言pos位置是否合理assert(pos >= _start && pos <= _finish);//插入之前判断是否需要扩容//注意:扩容之后需要更新pos,否则pos指向释放的旧空间,会造成迭代器失效if (_finish == _end_of_storage){//计算pos与_start的相对距离size_t len = pos - _start;//扩容size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();reserve(new_capacity);//更新pospos = _start + len;}//[pos, _finish - 1]的数据向后挪动,将pos位置空出iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}//pos位置插入x*pos = x;++_finish;}};
}
测试代码:
//测试insert
void test_vector04()
{wjs::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);v.push_back(7);v.push_back(8);for (auto& e : v){cout << e << " ";}cout << endl;//头插100auto pos = v.begin();v.insert(pos, 100);//插入之后修改pos位置的元素*pos += 10;cout << *pos << endl;for (auto& e : v){cout << e << " ";}cout << endl;
}
运行结果:
分析:
- 扩容之后,在insert中更新pos只解决了内部迭代器失效的问题,外面的pos并没有解决,它仍指向一块已经释放的空间。
- 思考:那我们可以将pos参数设为引用,内部的改变也影响外面吗?
答案是:不可以,因为引用的权限可以平移或缩小,但是不可以放大。当外面传的pos迭代器为一个const迭代器时,引用权限被放大,这是错误的,那将参数也设为常引用呢,这也不可以,因为常引用就不可以修改pos了。 - insert解决外部pos迭代器失效的方法是插入之后,返回修改的pos迭代器(即指向新插入的第一个元素的迭代器)。
修改2:
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://pos位置的元素之前插入iterator insert(iterator pos, const T& x){//断言pos位置是否合理assert(pos >= _start && pos <= _finish);//插入之前判断是否需要扩容//注意:扩容之后需要更新pos,否则pos指向释放的旧空间,会造成迭代器失效if (_finish == _end_of_storage){//计算pos与_start的相对距离size_t len = pos - _start;//扩容size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();reserve(new_capacity);//扩容之后更新pos,解决内部pos失效问题pos = _start + len;}//[pos, _finish - 1]的数据向后挪动,将pos位置空出iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}//pos位置插入x*pos = x;++_finish;//返回形参pos,解决外部pos失效问题return pos;}};
}
测试代码:
void test_vector04()
{wjs::vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);v.push_back(5);v.push_back(6);v.push_back(7);v.push_back(8);for (auto& e : v){cout << e << " ";}cout << endl;//头插100auto pos = v.begin();//insert之后,pos迭代器可能会失效(扩容)//记住,insert之后就不要使用这个pos迭代器,因为它可能失效了//使用这个pos迭代器是一个高危行为//如果非要使用这个pos这个位置的迭代器,可以接收insert的返回值//insert的返回值就是指向pos这个位置的迭代器auto ret = v.insert(pos, 100);*ret += 10;cout << *ret << endl;for (auto& e : v){cout << e << " ";}cout << endl;
}
tip:
- insert之后,pos迭代器可能会失效(扩容)
- 记住,insert之后就不要使用这个pos迭代器,因为它可能失效了
- 使用这个pos迭代器是一个高危行为
push_back可以复用insert:
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://尾插void push_back(const T& x){插入之前判断是否需要扩容//if (_finish == _end_of_storage)//{// //扩容// size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();// reserve(new_capacity);//}尾插//*_finish = x;//++_finish;//复用insertinsert(_finish, x);}};
}
6、vector的删除
erase
:
- 删除pos位置的元素
- erase需要注意:①断言pos位置是否合理(即有没有数据);②删除之后更新size,即_finish。
思考
: erase存在迭代器失效吗?
- erase删除pos位置元素后,迭代器的意义变了(指向删除的最后一个元素之后的元素的新位置,理论上迭代器并没有失效,因为删除并没有改变底层空间)
- 注意:如果pos是最后一个元素,删除之后pos刚好是_finish的位置,而_finish位置是没有元素的,所以pos迭代器失效
- VS系列下检测比较严格,删除vector任意位置上的元素,都认为该位置迭代器失效了
- Linux下,g++编译器对迭代器失效的检测就相对佛系,处理没有VS下极端,只有删除vector最后一个元素,才认为迭代器失效了(在实际场景中,迭代器的意义变了,也容易出现各种问题)
- 总结:vector 迭代器对象在erase或insert后,不能再访问这个迭代器,我们都认为它失效了,访问结果是未定义的。
- erase通过返回一个指向删除的最后一个元素之后的元素的新位置迭代器解决迭代器失效问题。
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public://删除pos位置的元素iterator erase(iterator pos){//断言pos是否合理assert(pos >= _start && pos < _finish);//删除pos位置元素,即将[pos+1, _finish - 1]的元素向前挪动iterator begin = pos + 1;while (begin < _finish){*(begin - 1) = *begin;++begin;}--_finish;//erase通过返回一个指向删除的最后一个元素之后的元素的新位置迭代器解决迭代器失效问题,即pos迭代器return pos;}};
}
测试代码:
void test_vector05()
{wjs::vector<int> v2;v2.push_back(1);v2.push_back(2);v2.push_back(2);v2.push_back(3);v2.push_back(4);v2.push_back(5);v2.push_back(6);for (auto& e : v2){cout << e << " ";}cout << endl;//删除所有偶数auto it2 = v2.begin();while (it2 != v2.end()){if (*it2 % 2 == 0){//erase之后迭代器失效//解决方案:it2需要接收erase的返回值it2 = v2.erase(it2);}else{++it2;}}for (auto& e : v2){cout << e << " ";}cout << endl;
}
tip:
- insert和erase之后的迭代器失效,是通过接收返回值解决的
pop_back
:
- 尾删,删除vector中的最后一个元素,尾删之后size-1
- 这里直接复用erase即可
namespace wjs
{//类模板的实现和定义不分离,后续学到模板进阶会讲解!template<class T>class vector{public:void pop_back(){//复用eraseerase(_finish - 1);}};
}