0


【STL】 模拟实现简易 vector

1. 读源码

想要自己实现一个 vector,读源码来理解他的实现是必不可少的一个步骤,

但是,当我们拿到 vector 的源码之后,一堆代码,我们应该从何看起呢?

我们当然是从一个类的核心读起,也就是从他的成员变量开始读:

这里我们找到了他的成员变量,他的类型是 iterator,这又是个啥,

我们来溯源一下:

我们可以看到,实际上 iterator 就是一个T* 的指针类型,

而 iterator 是迭代器,这里我们也可以大致猜到,vector 的迭代器其实就是原生指针。

回归正题,那他的成员函数有什么作用呢?

这个时候我们就可通过去看他的:构造函数 + 插入接口,来进一步的了解:

先来看构造函数,其他的就是一些重载,而且具体的实现也封装起来了,

但是我们看他的默认构造也看不出什么名堂来,只是都初始化成0了,

那我们还是得去看看他的插入接口:

源码的 push_back 说,如果 finish != end_of_storage 就调 construct 然后 finish++

这里我们可以先猜一下源码的意思,他给了 start,finish,end_of_storage,

那其实我们可以菜 start 是数组开始的位置,finish 是结束的位置,

end_of_storage 是数组容量的最后一个位置,那这个 if 语句的判断就是如果数组没满,

就插入一个数据,让 finish++,这里我们暂时不知道 construct 究竟是什么,

但是看源码千万不要陷进一些细节,我们先把大的框架给看好先,那这个时候,

我们就可以大概猜到,else 里面的就是需要扩容的逻辑,他调用了 insert_aux,

那我们就再去看一看这个函数:

这个函数很大,我就一点一点分析啊,

一开始是又进行一次判断,这里的 insert 不一定是只被 push_back 使用的,

所以可能其他地方调用的时候需要这一个判断:

然后我们来看 else 里面的逻辑,首先这里是扩容的策略,

如果第一次就扩容成 1,如果是其他情况就双倍扩容,

然后这里调用的是 allocate 也就是STL自己的空间配置器来要内存,

应为STL比较嫌弃 malloc 开内存的速度啊,就自己内部实现了一个内存池。

然后这一段逻辑就是拷贝数据到新的空间,

然后又调用了 construct 把数据插入进去,这里我还是先不看他的底层实现啊:

然后最后这里就是把旧的空间释放掉,然后更新成员变量:

然后我这里再补充一个小的点:

这里使用的就是 try catch 来捕获异常的操作,为了防止内存泄漏 catch 这里有销毁内存的操作,

这个时候不得不吐槽一下老 C++ 程序员的爱好,使用宏,总是喜欢搞一堆宏,让人很难受啊。

回归正题啊,这里我们是想搞懂成员变量的含义啊,但是他的 push_back 封装的比较复杂,

所以我们再去看一个扩容的逻辑(reserve)验证我们刚刚的猜想:

其他的我们不在意啊,就来看着几个成员变量的操作,

start = tmp,这里就差不多能证实 start 指向的是数组的开头位置了,

finish = tmp + old_size,这里的 old_size 不就是以前的数据大小吗,那 finish 也没错,

end_of_storage = start + n,reserve 函数传来的 n 就是要扩容到的容量大小,

那我们就大致了解了他的成员变量的含义了。

刚刚说好的,来看看 construct 的实现是怎么样的:

发现没有,construct 其实就是一个定位 new,如果我们需要给一个自定义类型开空间,

那我们就不能直接调用 malloc 了,得调用该自定义类型的构造函数,

而这个 destroy 为什么也把它放出来呢,因为清理资源的时候,他调用的destroy,

其实就是在调用自定义类型的析构函数来清理资源。

2. 框架搭建

那我们话不多说,直接开始写我们自己的 vector。

先来快速打个架子,让代码跑起来:

  1. #pragma once
  2. #include <iostream>
  3. #include <vector>
  4. using namespace std;
  5. namespace xl {
  6. template<class T>
  7. class vector {
  8. public:
  9. typedef T* iterator;
  10. private:
  11. iterator _start;
  12. iterator _finish;
  13. iterator _end_of_storage;
  14. public:
  15. vector()
  16. : _start(nullptr)
  17. , _finish(nullptr)
  18. , _end_of_storage(nullptr)
  19. {}
  20. public:
  21. iterator begin() {
  22. return _start;
  23. }
  24. iterator end() {
  25. return _finish;
  26. }
  27. public:
  28. void reserve(size_t n) {
  29. if (n > capacity()) {
  30. size_t old_size = size();
  31. T* tmp = new T[n];
  32. if (_start) {
  33. for (size_t i = 0; i < size(); i++) {
  34. tmp[i] = _start[i];
  35. }
  36. delete[] _start;
  37. }
  38. _start = tmp;
  39. _finish = _start + old_size;
  40. _end_of_storage = _start + n;
  41. }
  42. }
  43. void push_back(const T& x) {
  44. if (_finish == _end_of_storage) {
  45. size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
  46. reserve(new_capacity);
  47. }
  48. *_finish = x;
  49. _finish++;
  50. }
  51. public:
  52. int size() const {
  53. return _finish - _start;
  54. }
  55. int capacity() const {
  56. return _end_of_storage - _start;
  57. }
  58. };
  59. }

我们实现了一个构造,一个 push_back,一个最基本的迭代器,

现在我们可以把代码跑起来了:

  1. void test() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. v.push_back(5);
  8. for (auto e : v) cout << e << " ";
  9. cout << endl;
  10. }

这里我们是直接用范围for,因为范围for 没问题,迭代器肯定没问题。

来看结果:

这里我们再把重要的析构函数给加上:

  1. ~vector()
  2. {
  3. if (_start) {
  4. delete[] _start;
  5. _start = _finish = _end_of_storage = nullptr;
  6. }
  7. }

3. vector 的迭代器

我们来看这样一个场景:

  1. void Print(const vector<int>& v) {
  2. for (auto e : v) cout << e << " ";
  3. cout << endl;
  4. }
  5. void test2() {
  6. xl::vector<int> v;
  7. v.push_back(1);
  8. v.push_back(2);
  9. v.push_back(3);
  10. v.push_back(4);
  11. v.push_back(5);
  12. for (auto e : v) cout << e << " ";
  13. cout << endl;
  14. Print(v);
  15. }

实际上,编译器报错了:

这是为什么呢?

我们搭建框架的时候,只实现了普通迭代器,

这里我们传参的时候加了 const 导致出现了权限放大的情况,

所以我们需要重载一份 const 迭代器:

  1. public:
  2. typedef T* iterator;
  3. typedef const T* const_iterator;
  4. public:
  5. iterator begin() {
  6. return _start;
  7. }
  8. iterator end() {
  9. return _finish;
  10. }
  11. const_iterator begin() const {
  12. return _start;
  13. }
  14. const_iterator end() const {
  15. return _finish;
  16. }

我们赶紧来测试一手:

  1. void Print(const xl::vector<int>& v) {
  2. for (auto e : v) cout << e << " ";
  3. cout << endl;
  4. }
  5. void test2() {
  6. xl::vector<int> v;
  7. v.push_back(1);
  8. v.push_back(2);
  9. v.push_back(3);
  10. v.push_back(4);
  11. v.push_back(5);
  12. for (auto e : v) cout << e << " ";
  13. cout << endl;
  14. Print(v);
  15. }

输出:

4. vector 的拷贝构造与赋值

拷贝构造

这里就实现一下传统写法:

  1. // 传统写法
  2. vector(const vector<T>& v)
  3. : _start(nullptr)
  4. , _finish(nullptr)
  5. , _end_of_storage(nullptr)
  6. {
  7. _start = new T[v.capacity()];
  8. for (size_t i = 0; i < v.size(); i++) {
  9. _start[i] = v._start[i];
  10. }
  11. _finish = _start + v.size();
  12. _end_of_storage = _start + v.capacity();
  13. }

当然啦,实现方法有很多,怎么舒服怎么来就好~

赋值

这里我就直接用现代写法啦,因为实现起来真的和方便:

  1. void swap(vector<T>& v) {
  2. std::swap(_start, v._start);
  3. std::swap(_finish, v._finish);
  4. std::swap(_end_of_storage, v._end_of_storage);
  5. }
  6. // 现代写法
  7. vector<T>& operator=(vector<T> v) {
  8. swap(v);
  9. return *this;
  10. }

5. vector 的常见重要接口实现

operator[ ] 的实现

  1. T operator[](size_t pos) {
  2. assert(pos < size());
  3. return _start[pos];
  4. }
  5. const T operator[](size_t pos) const {
  6. assert(pos < size());
  7. return _start[pos];
  8. }

来测试一下:

  1. void test1() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. v.push_back(5);
  8. for (int i = 0; i < 5; i++) {
  9. cout << v[i] << " ";
  10. }
  11. cout << endl;
  12. }

输出:

insert 接口的实现

  1. void insert(iterator pos, const T& x) {
  2. assert(pos >= _start && pos <= _finish);
  3. if (_finish == _end_of_storage) {
  4. size_t len = pos - _start; // 防止迭代器失效的问题(扩容之后pos仍指向旧空间)
  5. size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
  6. reserve(new_capacity);
  7. pos = _start + len;
  8. }
  9. iterator end = _finish - 1;
  10. while (end >= pos) {
  11. *(end + 1) = *end;
  12. end--;
  13. }
  14. *pos = x;
  15. _finish++;
  16. }

实现了 insert 之后,其实我们已经不需要自己实现 push_back 了,

直接复用 insert 就行了:

  1. void push_back(const T& x) {
  2. //if (_finish == _end_of_storage) {
  3. // size_t new_capacity = capacity() == 0 ? 4 : capacity() * 2;
  4. // reserve(new_capacity);
  5. //}
  6. //*_finish = x;
  7. //_finish++;
  8. insert(end(), x);
  9. }

我们来测试一下:

  1. void test2() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. v.push_back(5);
  8. for (auto e : v) cout << e << " ";
  9. cout << endl;
  10. }

还是这段代码,来看输出:

现在我们解决了 insert 内部的迭代器失效的问题,

再来看看这样一个场景:

  1. void test2() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(4);
  6. v.push_back(5);
  7. xl::vector<int>::iterator it = v.begin() + 2;
  8. v.insert(it, 3);
  9. *it += 10;
  10. for (auto e : v) cout << e << " ";
  11. cout << endl;
  12. }

我们插入了一个 3 ,然后把 3 += 10 ,应该打印出 13 才对,

但是,来看输出:

为什么会还是打印 3 呢?

我们调试来看看:

目前为止还是正常的:

走到这里我们发现 it 指针变成随机值了,这是为什么?

我们虽然在 insert 实现的内部对扩容这里进行了防止迭代器失效的操作,

但是,形参的改变不影响实参,扩容之后旧空间就被释放了,导致了迭代器失效。

那我们该怎么解决呢?

我们看看源码是咋实现的:(当有细节问题的时候,就可以看看源码的实现细节了)

源码里面使用的操作,是搞了个返回值,

就是返回指向新插入位置的迭代器,如果源码看不太懂,可以去看看文档是怎么说的:

这是文档对这个返回值的描述。

erase 接口实现

  1. void erase(iterator pos) {
  2. assert(pos >= _start && pos < _finish);
  3. iterator it = pos + 1;
  4. while (it != _finish) {
  5. *(it - 1) = *it;
  6. it++;
  7. }
  8. _finish--;
  9. }

我们来测试一下:

  1. void test3() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. xl::vector<int>::iterator it = v.begin();
  8. v.erase(it);
  9. for (auto e : v) cout << e << " ";
  10. cout << endl;
  11. }

输出:

好像没什么问题,但其实并不是这样的,我们再来看一个场景:

  1. void test3() {
  2. xl::vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. v.push_back(5);
  8. xl::vector<int>::iterator it = v.begin();
  9. while (it != v.end()) {
  10. v.erase(it);
  11. it++;
  12. }
  13. for (auto e : v) cout << e << " ";
  14. cout << endl;
  15. }

输出:

怎么就崩了呢?

erase 以后,迭代器是有可能会失效的,我们试试库里的:

跑刚刚的代码:

实际上,VS的库里做了强制的检查,他不让我们访问 erase 之后的迭代器,

所以我们让 it++ 程序就报错了。

那库里是怎么处理的呢?

还得看看源码是怎么样的:

我们发现,他也是通过返回值来解决这个问题的,

我们也可以很容易的看出,返回值返回的就是原来位置的迭代器,

根据这个特性,我们测试一下:

  1. void test3() {
  2. vector<int> v;
  3. v.push_back(1);
  4. v.push_back(2);
  5. v.push_back(3);
  6. v.push_back(4);
  7. v.push_back(5);
  8. vector<int>::iterator it = v.begin();
  9. while (it != v.end()) {
  10. it = v.erase(it);
  11. }
  12. for (auto e : v) cout << e << " ";
  13. cout << endl;
  14. }

输出:

确实是都删除掉了,那我来改一改我们的代码:

  1. iterator erase(iterator pos) {
  2. assert(pos >= _start && pos < _finish);
  3. iterator it = pos + 1;
  4. while (it != _finish) {
  5. *(it - 1) = *it;
  6. it++;
  7. }
  8. _finish--;
  9. return pos;
  10. }

这样就没问题了:

pop_back 接口的实现

这个就直接复用 erase 就好了:

  1. void pop_back() {
  2. erase(end() - 1);
  3. }

resize 接口实现

这里我们先补充一个新知识:

C++ 有了模板之后,对内置类型有了升级,他们也可以使用构造函数初始化,

来看代码:

  1. void test4() {
  2. int i = 0;
  3. int j = 1;
  4. int a = int();
  5. int b = int(1);
  6. cout << i << " " << j << " " << a << " " << b << endl;
  7. }

输出:

好,我们再来看这个接口:

  1. void resize(size_t n, const T& val = T()) {
  2. if (n < size()) {
  3. _finish = _start + n;
  4. }
  5. else {
  6. reseve();
  7. while (_finish != _start + n) {
  8. *_finish = val;
  9. _finish++;
  10. }
  11. }
  12. }

这样我们给 val 缺省值的时候,就可以覆盖自定义类型和内置类型了。

vector 的接口当然不止这些,但是最核心的我们基本都实现了,其他的接口有兴趣再实现吧~

源码分享

Gitee链接:模拟实现简易STL: 模拟实现简易STL (gitee.com)

写在最后:

以上就是本篇文章的内容了,感谢你的阅读。

如果感到有所收获的话可以给博主点一个哦。

如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~

标签: STL

本文转载自: https://blog.csdn.net/Locky136/article/details/131740411
版权归原作者 戊子仲秋 所有, 如有侵权,请联系我们删除。

“【STL】 模拟实现简易 vector”的评论:

还没有评论