在上一篇中我们介绍了 string 类的常用函数模拟,接下来我们将开始讲解 vector 类的常用函数的讲解以及模拟实现,相较于 string 来说,vector 的函数不那么冗余,用法也没有那么多,但是在 vector 中的函数使用和模拟中,也存在很多的细节,我们将在以下一一讲解。
我们将按照迭代器、容量、获取元素、修改、默认成员函数的顺序讲解和实现函数,在实现函数的时候,详细了给出实现该函数需要注意的事项。最后谈了谈迭代器失效的情况,给出了使用迭代器的使用注意事项。
讲解的顺序按照网站:vector - C++ Reference (cplusplus.com):
目录如下:
目录
1. Iterator —— 迭代器与成员变量
2. Capacity —— 容量
3. Element access —— 获取元素
4. Modify —— 修改内容函数
5. 默认成员函数
6. 迭代器注意事项 —— 迭代器失效
1. Iterator —— 迭代器与成员变量
首先我们要介绍的便是 vector 的迭代器,迭代器的主要作用可以帮助我们遍历成员,还有有些函数的需要传入的参数也是需要传入迭代器,迭代器的种类有以下几种,我们只介绍其中的 begin 和 end:
但是在介绍迭代之前,我们给出 vector 的成员变量。我们的成员变量根据 Linux 下的 g++ 编译器下的 vector ,如下:
在 g++ 下的 vector 存在内存配置器,本篇就不实现该内存配置器,因为在大多数情况下都用不到,所以我们的成员变量为 start finish end_of_storage。同时需要注意的是 vector 和 string 不同,vector 需要使用适用于多种变量,所以我们需要适用模板实现。
iterator 的实现同时还需要注意要适用于被 const 修饰的变量,iterator 的实现如下:
namespace MyVector { template <class T> class vector { public: typedef T* iterator; typedef const T* const_iterator; iterator begin() { return _start; } iterator end() { return _finish; } const_iterator begin() const { return _start; } const_iterator end() const { return _finish; } private: // 成员初始化为 nullptr,就不用走初始化列表了 iterator _start = nullptr; iterator _finish = nullptr; iterator _endofstorage = nullptr; }; }
2. Capacity —— 容量
接着我们实现和容量相关的函数,我们需要实现和掌握的函数如下:
其中的重点为 resize 和 reserve 函数,对于其他函数的实现都较为轻松,我们先实现 size、capacity、empty 函数,如下:
size_t size() const { // 指针减指针,返回指针间的元素个数 return _finish - _start; } size_t capacity() const { return _endofstorage - _start; } bool empty() { return _start == _finish; }
接下来我们实现 reserve 函数,reserve 函数的作用为对 vector 进行扩容,传入的参数为需要扩容的容量。
对于reserve 函数的实现,我们需要注意的是只有在传入的长度大于当前容量的时候,我们才对其接下扩容。进行扩容的时候,我们需要重新开辟一段空间,然后将原始空间的内容拷贝回去,然后释放原始空间,将 成员变量指向新的空间。实现 reserve 需要注意的细节:
1. 在将 _finish 指向新的空间的时候,我们不能使用 _start + size() 实现,因为对于 size() 的返回值为 _finish - _start ,将其展开之后,_finish = _start + _finish - _start ,这就导致,_finish 的值根本没有改变。所以我们需要使用一个值记录原始 _start _finish 之间的间距。
2. 在将原始空间拷贝到新空间的时候,不能使用 memcpy 将其拷贝到新空间。因为:假若我们 vector 为 vector<int> 那么使用 memcpy 拷贝过去的时候,确实可以每个值都拷贝过去,但是当我们将 vector 实现为 vector<string> 的时候,在 vector 开辟的空间中存在的 string 成员也会指向一块属于他的空间,存在于 vector 中的只是 string 的成员变量,假若我们将原空间拷贝到新空间之后,将原空间释放的时候,就会调用到 string 的析构函数,然后新空间中的 string 指向的就是一系列被释放掉的空间,当我们访问这段空间的时候,就是非法访问,析构的时候也是释放已经被释放的空间。解决这个问题的方法:将原空间的值一个一个拷贝到新空间,然后在释放原空间,因为一个一个拷贝到新空间,也会调用对应的拷贝构造函数。实现如下:
void reserve(size_t num) { if (num > capacity()) { // 记录 _finish 与 _start 的相对位置 size_t len = size(); T* tmp = new T[num]; //不能使用memcpy(tmp, _start, size() * sizeof(T)); for (int i = 0; i < len; i++) { tmp[i] = _start[i]; } delete[] _start; _start = tmp; _finish = _start + len; _endofstorage = _start + num; } }
接着是对 resize 函数的使用,resize 函数的作用为对 vector 的 size 进行控制,当传入的参数大于当前的 size 的时候,我们将使用默认缺省值将其多出来的 size 进行初始化,传入的参数小于当前的 size 时,就i将 _finish 向前进行移动即可,具体实现如下:
void resize(size_t num, const T& val = T()) { if (num > size()) { reserve(num); while (_finish < _start + num) { *_finish = val; _finish++; } } else { _finish = _start + num; } }
由上的代码我们可以发现,我们可以发现对于默认缺省给出的是一个匿名对象,对于自定义类型来说,匿名对象可以给出,但是对于 int、char 等内置类型的对象,也存在匿名对象吗?答案是存在的,在 Cpp 中,为了使缺省值匿名对象更加使用,对内置类型也给出了匿名对象,对于浮点型给出的为 0.0,对整型给出的为 0,对指针给出的为 nullptr。
3. Element access —— 获取元素
这段便介绍获取元素的函数,对于获取元素的函数如下:
其中很常用的就是对 [ ] 的运算符重载,其次为 front 和 back 函数,front 和 back 分别为获取第一个元素和最后一个元素。对于 [ ] 的运算符重载实现,我们只需要先检查需要获取的位置是否合法,然后将对应位置的元素传引用传回去即可。该函数还存在一个由 const 修饰的重载函数,用于被 const 修饰的变量。实现如下:
T& operator[](iterator pos) { assert(pos < _finish && pos >= _start); return *pos; } const T& operator[](iterator pos) const { assert(pos < _finish && pos >= _start); return *pos; }
4. Modify —— 修改内容函数
关于 modify 的函数便是 vector 中常用的函数,也是需要实现的重头戏,需要掌握以及会在本篇实现的如下:
我们首先实现较为简单的函数,clear ,该函数的作用为将 vector 的 size 清零,所以实现该函数只需要将 _finish 指针指向 _start 指向的地址即可。实现如下:
void clear() { _finish = _start; }
接下来我们实现,push_back 和 pop_back 函数,前者的作用为:尾插一个元素,后者的作用为:尾删一个元素。实现 push_back 函数只需要判断是否需要扩容,然后加入一个元素,pop_back 函数只需判断当前 size 是否为 0,然后在删除一个元素,实现如下:
void pop_back() { assert(!empty()); _finish--; } void push_back(const T& val) { if (_finish == _endofstorage) reserve(capacity() == 0 ? 4 : 2 * capacity()); *_finish = val; _finish++; // 使用尾插也可以 //insert(end(), val); }
接着实现 swap 函数,该函数作用为与另一个 vector 变量交换。实现的原理也较为简单,我们只需要调用 std 中的 swap 函数,将两个变量的 _start _finish _endofstorage,给交换即可。实现如下:
void swap(vector& v) { std::swap(_finish, v._finish); std::swap(_start, v._start); std::swap(_endofstorage, v._endofstorage); }
最后我们实现 insert 函数和 erase 函数,前者的作用为在指定位置插入元素,后置的作用为在指定位置删除元素,对于这两个函数传入的参数都为迭代器,不能传入数字。
先实现 erase 函数,在实现该函数的时候,我们只需要先判断传入的位置是否合法,然后再通过循环将 pos 位置后的元素往前移一位,最后的返回值为新删除元素的这个位置。实现如下:
iterator earse(iterator pos) { assert(pos >= _start && pos < _finish); iterator begin = pos; while (pos != _finish - 1) { *pos = *(pos + 1); pos++; } _finish--; return pos; }
然后 insert 函数,对于该函数的实现,首先还是判断需要插入的位置是否合法,然后判断是否需要扩容,若要扩容,直接调用 reserve 函数即可,然后将 pos 位置及以后的元素都向后移动一位,最后的返回值为新插入元素的这个位置,实现如下:
iterator insert(iterator pos, const T& val) { // 对数据进行插入,需要判断插入的位置 assert(pos <= _finish && pos >= _start); // 进行扩容的同时,需要将 pos 也给更改 if (_finish == _endofstorage) { size_t len = pos - _start; reserve(capacity() == 0 ? 4 : 2 * capacity()); } // 对数据进行挪动 iterator end = _finish; while (end != pos) { *end = *(end - 1); end--; } *pos = val; _finish++; return pos; }
5. 默认成员函数
最后需要讲解的便是 vector 的默认成员函数,我们通常只实现四种默认成员函数:构造函数、拷贝构造函数、赋值运算符重载、析构函数。但是对于这些成员的函数的实现,也存在很多的细节。
我们首先先介绍易于实现的成员函数:析构函数,析构函数只需要将空间释放,然后将指针置 nullptr 即可,如下:
~vector() { if (_start != nullptr) { // 当存入的是一个 string 的时候,需要先分别释放空间,然后 delete[] _start; _start = _finish = _endofstorage = nullptr; } }
接着实现赋值运算符重载函数,对于赋值运算符重载函数的实现,按照普通的写法,我们只需要开辟一段一个与被拷贝成员一样大的空间,然后将值拷贝过去即可。但是我们可以直接将赋值运算符重载函数的形参设置为一个临时变量,非引用非 const 修饰,然后在函数体内调用 swap 函数,就可以成功达到赋值运算符重载的功能,原因为:当我们使用一个形参作为参数,当实参传入进来的时候,将形参拷贝构造给了形参,然后此时与形参交换值,函数退出时候,析构的也是原地址的空间,实现如下:
vector& operator=(vector v) { swap(v); return *this; } // 普通写法 vector& operator=(const vector& v) { reserve(v.capacity()); //memcpy(_start, v._start, sizeof(T) * v.size()); //_finish = _start + v.size(); for (auto& e : v) push_back(e); return *this; }
接下来我们实现拷贝构造函数,关于拷贝构造的实现的不能像赋值运算符重载在形参阶段调用拷贝构造函数,因为这样会导致无限循环调用的情况。我们只能老实的开辟空间然后一个一个的赋值即可,如下:
// 拷贝构造 vector(const vector& v) { reserve(v.capacity()); // 对于范围for的使用,最好使用引用,因为v中的变量空间可能也很大 for (auto& e : v) push_back(e); }
最后实现我们的构造函数,对于构造函数的实现存在多种构造函数,我们先实现默认拷贝构造函数,对于默认拷贝构造函数,可以传入两个值,一个为需要开辟元素个数的空间,一个为开辟空间的后元素初始化为什么。这个默认构造函数的功能其实和 resize 非常相似,所以我们直接在函数中调用 resize 函数即可,如下:
vector(size_t num = 0, const T& val = T()) { // 进行扩容,然后将所有的值变成 val resize(num, val); }
接下来需要实现的迭代器区间构造函数,迭代器区间构造函数需要传入的参数为两个迭代器,然后从第一个参数迭代拷贝到第二个参数,实现如下:
template<class InputIterator> vector(InputIterator first, InputIterator last) { reserve(last - first); while (first != last) { push_back(*first); first++; } }
当实现以上迭代器区间构造函数之后,我们会发现当我们再一次调用构造函数的时候,会报错,这是因为迭代器区间构造函数与默认构造函数发生了冲突,当我们想调用默认构造函数的时候,比如:MyVector::vector<int> v1(10, 6); 反而会调用到迭代器区间构造函数,这是因为传入 v1 中的 10 和 6 为 int 类型,而默认构造函数中的第一个参数类型为 size_t,与 int 类型不对应,但是这个时候我们写出了迭代器区间构造函数,这个函数为一个模板函数,所以想要调用默认构造函数反而调用到了迭代器区间构造函数,在函数内存在解引用,所以会报错,所以想要解决这个问题,我们只需要写出一个非默认构造函数,如下:
// 创建一个重载构造函数,防止vector<int>类与模板迭代器区间构造冲突 vector(int num, const T& val = T()) { // 进行扩容,然后将所有的值变成 val resize(num, val); }
初始化列表构造函数,在 C++11 标准中,引入了初始化列表构造函数,如下:
以上两种调用初始化列表函数的方式分别为:第一种直接调用构造函数,先对构造函数中的参数初始化,第二种,隐式类型转换,先调用构造函数生成一个 vector 然后调用拷贝构造函数,但编译器一般会优化,直接调用构造函数。
实现初始化列表函数如下:
vector(initializer_list<T> il) { // 初始化列表初始化 auto it = il.begin(); while (it != il.end()) { push_back(*it); it++; } }
6. 迭代器注意事项 —— 迭代器失效
在使用迭代器的时候,存在一个严重的问题,那就是迭代器失效的问题。如下这种情况
对于以上的这种情况,我们发现第一次打印的时候,可以正常的打印,但是增加一个元素之后在使用迭代器打印却出错了。这是因为增加一个元素之后,原 vector 变量进行了扩容,然后就到了新开辟的空间处,但是此时的 it 还是指向原来的地方,所以会出错。
以上这种情况为增容时会出错,那么是不是只要不增容就不会出错,只能说不一定,因为对于 vector 中的函数值规定了功能,并没有规定该怎么实现,每个平台都有每个平台实现的风格,万一某些版本的 vector,会对 size 小于 capacity 一般的变量进行缩容呢?
所以对于迭代器的使用:
1. 使用之后最好就不再使用,可以重新定义一个;
2. 当再次使用的时候,我们可以将其更新。