• C++ —— 模拟实现vector和迭代器失效


    目录

    1.成员变量的设计

     2.迭代器以及接口设计

    3.容量接口

    4.reserve的设计

    5.resize的设计

    6.尾插尾删的设计

    7.构造、析构函数

    8.运算符重载

    9.insert接口设计与迭代器失效

    10.erase的设计和迭代器失效

    11.双层深拷贝问题

    12.完整代码

    1.成员变量的设计

    成员变量设计为三个迭代器,分别是:_start、_finish、_endofstorage。此设计原理参考STL源码。_start与_finish之间是存储有效元素的;_start与_endofstorage之间表容器的容量。

    1. iterator _start;
    2. iterator _finish;
    3. iterator _endofstorage;

     2.迭代器以及接口设计

    使用指针的方式实现迭代器,同时需要兼顾 const 对象和 const 迭代器。

    1. typedef T* iterator;
    2. typedef const T* const_iterator; //使用指针实现迭代器
    3. iterator begin()
    4. {
    5. return _start;
    6. }
    7. iterator end()
    8. {
    9. return _finish;
    10. }
    11. const_iterator begin() const
    12. {
    13. return _start;
    14. }
    15. const_iterator end() const
    16. {
    17. return _finish;
    18. }

    3.容量接口

    1. size_t size()
    2. {
    3. return _finish - _start;
    4. }
    5. size_t capacity()
    6. {
    7. return _endofstorage - _start;
    8. }
    9. void clear()
    10. {
    11. _finish = _start;
    12. }
    13. bool empty()
    14. {
    15. return _finish == _start;
    16. }

    4.reserve的设计

    reserve如果涉及到扩容,统一采用异地扩容的方式。当参数小于当前容量时不作处理。

    1. void reserve(const size_t& n)
    2. {
    3. if (n > capacity())
    4. {
    5. T* tmp = new T[n];
    6. assert(tmp); //检查是否成功开辟
    7. int oldsize = _finish - _start;
    8. if (_start) //如果_start不为空需要拷贝数据
    9. {
    10. memcpy(tmp, _start, sizeof(T) * oldsize);
    11. delete[] _start; //释放旧空间
    12. }
    13. //异地扩容,更新迭代器
    14. _start = tmp;
    15. _finish = _start + oldsize;
    16. _endofstorage = _start + n;
    17. }
    18. }

    5.resize的设计

    resize需要分情况讨论,如果接收的参数小于当前容量时,空间不进行变动;如果小于当前有效元素个数时,需要更新_finish迭代器达到删除的效果。同时需要注意resize有第二个参数(官方手册)。

    1. void resize(const size_t n, T val = T())
    2. {
    3. if (n > capacity) reserve(n); //大于当前容量,扩容
    4. if (n > size())
    5. {
    6. while (_finish < _start + n)
    7. {
    8. *_finish = val;
    9. ++_finish;
    10. }
    11. }
    12. else _finish = _start + n;
    13. }

    6.尾插尾删的设计

    1. void push_back(const T& val)
    2. {
    3. if (_finish == _endofstorage) //是否需要扩容
    4. {
    5. size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    6. reserve(newcapacity);
    7. }
    8. *_finish = val;
    9. ++_finish;
    10. }
    11. void pop_back()
    12. {
    13. assert(!empty()); //尾删容器不能为空
    14. --_finish;
    15. }

    7.构造、析构函数

    这里模拟实现:默认构造函数、范围拷贝、指定容器大小、拷贝构造函数(现代写法)、析构函数。

    1. vector() //默认构造函数
    2. :_start(nullptr)
    3. ,_finish(nullptr)
    4. ,_endofstorage(nullptr)
    5. {}
    6. template <class it>
    7. vector(it first, it last) //范围拷贝构造
    8. : _start(nullptr)
    9. , _finish(nullptr)
    10. , _endofstorage(nullptr)
    11. {
    12. while (first != last)
    13. {
    14. push_back(*first);
    15. ++first;
    16. }
    17. }
    18. //拷贝构造现代写法
    19. void swap(vector& v)
    20. {
    21. std::swap(_start, v._start);
    22. std::swap(_finish, v._finish);
    23. std::swap(_endofstorage, v._endofstorage);
    24. }
    25. vector(const vector& v)
    26. :_start(nullptr)
    27. , _finish(nullptr)
    28. , _endofstorage(nullptr)
    29. {
    30. vector tmp(v.begin(), v.end());
    31. swap(tmp);
    32. }
    33. vector(size_t n, const T& val = T()) //指定容器大小构造
    34. :_start(nullptr)
    35. , _finish(nullptr)
    36. , _endofstorage(nullptr)
    37. {
    38. reserve(n);
    39. for (size_t i = 0; i < n; ++i)
    40. {
    41. push_back(val);
    42. }
    43. }
    44. ~vector() //析构函数
    45. {
    46. delete[] _start;
    47. _start = _finish = _endofstorage = nullptr;
    48. }

    上面的代码会出一个错误:

    其原因在于:10 在编译器的视角中默认是一个 int 类型的数,而我们的接口设计是一个 size_t 类型的数,所以会存在一些类型提升。而我们又设计了一个模板,在编译器的视角看来,调用模板函数是开销更小的选择。

    解决方案是添加一个重载:

    1. vector(int n, const T& val = T()) //指定容器大小构造
    2. :_start(nullptr)
    3. , _finish(nullptr)
    4. , _endofstorage(nullptr)
    5. {
    6. reserve(n);
    7. for (int i = 0; i < n; ++i)
    8. {
    9. push_back(val);
    10. }
    11. }

    8.运算符重载

    需不需要使用 const 函数是看我们需求来的,这里给出设计原则:

    • 只读接口函数,设计为const函数
    • 只写接口函数,设计为非const函数
    • 读、写接口函数,const函数、非const函数都要设计

    上面的代码可能考虑的不够周全,读者可自行改进。下标运算符对于我们来说是可读可写的。

    1. T& operator[](const size_t n)
    2. {
    3. assert(n >= 0 && n < size()); //防止越界
    4. return _start[n];
    5. }
    6. const T& operator[](const size_t n) const
    7. {
    8. assert(n >= 0 && n < size()); //防止越界
    9. return _start[n];
    10. }
    11. vector& operator=(vector v) //赋值运算符重载
    12. {
    13. swap(v);
    14. return *this;
    15. }

    9.insert接口设计与迭代器失效

    1. void insert(iterator pos,const T& val)
    2. {
    3. assert(pos >= _start && pos < _finish);
    4. if (_finish == _endofstorage) //检查是否需要扩容
    5. {
    6. size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    7. reserve(newcapacity);
    8. }
    9. auto end = _finish - 1;
    10. while (end >= pos)
    11. {
    12. *(end + 1) = *end;
    13. --end;
    14. }
    15. *pos = val;
    16. ++_finish;
    17. }

    这样设计的接口发生扩容时会产生迭代器失效,因为扩容是异地扩容,就会导致pos指向的空间已经被释放了,需要更新pos。因为是传值调用,我们还必须保证外部的迭代器能够及时更新。

    1. iterator insert(iterator pos,const T& val)
    2. {
    3. assert(pos >= _start && pos < _finish);
    4. if (_finish == _endofstorage) //检查是否需要扩容
    5. {
    6. size_t oldsize = _finish - _start;
    7. size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    8. reserve(newcapacity);
    9. pos = _start + oldsize; //扩容后,_start的指向已经更改,需要更新pos
    10. }
    11. auto end = _finish - 1;
    12. while (end >= pos)
    13. {
    14. *(end + 1) = *end;
    15. --end;
    16. }
    17. *pos = val;
    18. ++_finish;
    19. return pos; //添加一个返回值供外部迭代器更新
    20. }

    外部需要继续使用迭代器的正确方法为:

    在VS环境中使用标准库的insert接口,不按上面的方式使用迭代器,就会发生迭代器失效问题。

    10.erase的设计和迭代器失效

    1. void erase(iterator pos)
    2. {
    3. assert(pos >= _start);
    4. assert(pos < _finish);
    5. auto del = pos + 1;
    6. while (del < _finish)
    7. {
    8. *(del - 1) = *del;
    9. ++del;
    10. }
    11. --_finish;
    12. }

    这样做会导致迭代器失效。其原因在于:删除最后一个元素,del的位置会指向一个不合法的位置,如果后续要对这个指针指向的内容进行读、写是非常危险的。所以与insert一样,VS的编译环境是使用insert和erase后迭代器失效。解决方案是返回指向删除元素位置的迭代器。

    1. iterator erase(iterator pos)
    2. {
    3. assert(pos >= _start);
    4. assert(pos < _finish);
    5. auto del = pos + 1;
    6. while (del < _finish)
    7. {
    8. *(del - 1) = *del;
    9. ++del;
    10. }
    11. --_finish;
    12. return pos;
    13. }

    外部调用时正确的使用方法:

    使用标准库中的vector容器,不按上述方法使用会发生错误:

    11.双层深拷贝问题

    留意刚才的reserve接口:

    1. void reserve(const size_t& n)
    2. {
    3. if (n > capacity())
    4. {
    5. T* tmp = new T[n];
    6. assert(tmp); //检查是否成功开辟
    7. int oldsize = _finish - _start;
    8. if (_start) //如果_start不为空需要拷贝数据
    9. {
    10. memcpy(tmp, _start, sizeof(T) * oldsize);
    11. delete[] _start; //释放旧空间
    12. }
    13. //异地扩容,更新迭代器
    14. _start = tmp;
    15. _finish = _start + oldsize;
    16. _endofstorage = _start + n;
    17. }
    18. }

    这么做会导致一个非常严重的野指针问题:

    其原因在于:异地扩容之后,使用memcpy进行拷贝,而这个拷贝是一次浅拷贝,新空间和旧空间的对象都指向原来的_start指向的空间,当_start指向的空间释放后,新空间的对象_start就是野指针了,所以打印的就是乱码。当函数栈帧退出时又析构一次,就会造成一次严重的野指针问题。 

    所以我们的解决方案是不使用memcpy进行浅拷贝,而是使用赋值运算符重载进行深拷贝。

    1. void reserve(const size_t n)
    2. {
    3. if (n > capacity())
    4. {
    5. int oldsize = _finish - _start;
    6. T* tmp = new T[n];
    7. assert(tmp); //检查是否成功开辟
    8. if (_start) //如果_start不为空需要拷贝数据
    9. {
    10. //memcpy(tmp, _start, sizeof(T) * oldsize);
    11. for (size_t i = 0; i < size(); i++)
    12. {
    13. tmp[i] = _start[i]; //使用赋值运算符重载
    14. }
    15. delete[] _start; //释放旧空间
    16. }
    17. //异地扩容,更新迭代器
    18. _start = tmp;
    19. _finish = _start + oldsize;
    20. _endofstorage = _start + n;
    21. }
    22. }

    12.完整代码

    1. #include
    2. #include
    3. #include
    4. namespace ly
    5. {
    6. template <class T>
    7. class vector
    8. {
    9. public:
    10. typedef T* iterator;
    11. typedef const T* const_iterator; //使用指针实现迭代器
    12. iterator begin()
    13. {
    14. return _start;
    15. }
    16. iterator end()
    17. {
    18. return _finish;
    19. }
    20. const_iterator begin() const
    21. {
    22. return _start;
    23. }
    24. const_iterator end() const
    25. {
    26. return _finish;
    27. }
    28. vector() //默认构造函数
    29. :_start(nullptr)
    30. ,_finish(nullptr)
    31. ,_endofstorage(nullptr)
    32. {}
    33. ~vector() //析构函数
    34. {
    35. delete[] _start;
    36. _start = _finish = _endofstorage = nullptr;
    37. }
    38. template <class it>
    39. vector(it first, it last) //范围拷贝构造
    40. : _start(nullptr)
    41. , _finish(nullptr)
    42. , _endofstorage(nullptr)
    43. {
    44. while (first != last)
    45. {
    46. push_back(*first);
    47. ++first;
    48. }
    49. }
    50. //拷贝构造现代写法
    51. void swap(vector& v)
    52. {
    53. std::swap(_start, v._start);
    54. std::swap(_finish, v._finish);
    55. std::swap(_endofstorage, v._endofstorage);
    56. }
    57. vector(const vector& v)
    58. :_start(nullptr)
    59. , _finish(nullptr)
    60. , _endofstorage(nullptr)
    61. {
    62. vector tmp(v.begin(), v.end());
    63. swap(tmp);
    64. }
    65. vector(size_t n, const T& val = T()) //指定容器大小构造
    66. :_start(nullptr)
    67. , _finish(nullptr)
    68. , _endofstorage(nullptr)
    69. {
    70. reserve(n);
    71. for (size_t i = 0; i < n; ++i)
    72. {
    73. push_back(val);
    74. }
    75. }
    76. vector(int n, const T& val = T()) //指定容器大小构造
    77. :_start(nullptr)
    78. , _finish(nullptr)
    79. , _endofstorage(nullptr)
    80. {
    81. reserve(n);
    82. for (int i = 0; i < n; ++i)
    83. {
    84. push_back(val);
    85. }
    86. }
    87. T& operator[](const size_t n)
    88. {
    89. assert(n < size()); //防止越界
    90. return _start[n];
    91. }
    92. const T& operator[](const size_t n) const
    93. {
    94. assert( n < size()); //防止越界
    95. return _start[n];
    96. }
    97. vector& operator=(vector v)
    98. {
    99. swap(v);
    100. return *this;
    101. }
    102. void reserve(const size_t n)
    103. {
    104. if (n > capacity())
    105. {
    106. int oldsize = _finish - _start;
    107. T* tmp = new T[n];
    108. assert(tmp); //检查是否成功开辟
    109. if (_start) //如果_start不为空需要拷贝数据
    110. {
    111. //memcpy(tmp, _start, sizeof(T) * oldsize);
    112. for (size_t i = 0; i < size(); i++)
    113. {
    114. tmp[i] = _start[i]; //使用运算符重载
    115. }
    116. delete[] _start; //释放旧空间
    117. }
    118. //异地扩容,更新迭代器
    119. _start = tmp;
    120. _finish = _start + oldsize;
    121. _endofstorage = _start + n;
    122. }
    123. }
    124. void resize(const size_t n, T val = T())
    125. {
    126. if (n > capacity) reserve(n); //大于当前容量,扩容
    127. if (n > size())
    128. {
    129. while (_finish < _start + n)
    130. {
    131. *_finish = val;
    132. ++_finish;
    133. }
    134. }
    135. else _finish = _start + n;
    136. }
    137. size_t size()
    138. {
    139. return _finish - _start;
    140. }
    141. size_t capacity()
    142. {
    143. return _endofstorage - _start;
    144. }
    145. void clear()
    146. {
    147. _finish = _start;
    148. }
    149. bool empty()
    150. {
    151. return _finish == _start;
    152. }
    153. void push_back(const T& val)
    154. {
    155. if (_finish == _endofstorage) //是否需要扩容
    156. {
    157. size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    158. reserve(newcapacity);
    159. }
    160. *_finish = val;
    161. ++_finish;
    162. }
    163. void pop_back()
    164. {
    165. assert(!empty()); //尾删容器不能为空
    166. --_finish;
    167. }
    168. iterator insert(iterator pos,const T& val)
    169. {
    170. assert(pos >= _start && pos < _finish);
    171. if (_finish == _endofstorage) //检查是否需要扩容
    172. {
    173. size_t oldsize = _finish - _start;
    174. size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
    175. reserve(newcapacity);
    176. pos = _start + oldsize; //扩容后,_start的指向已经更改,需要更新pos
    177. }
    178. auto end = _finish - 1;
    179. while (end >= pos)
    180. {
    181. *(end + 1) = *end;
    182. --end;
    183. }
    184. *pos = val;
    185. ++_finish;
    186. return pos; //添加一个返回值供外部迭代器更新
    187. }
    188. iterator erase(iterator pos)
    189. {
    190. assert(pos >= _start);
    191. assert(pos < _finish);
    192. auto del = pos + 1;
    193. while (del < _finish)
    194. {
    195. *(del - 1) = *del;
    196. ++del;
    197. }
    198. --_finish;
    199. return pos;
    200. }
    201. private:
    202. iterator _start;
    203. iterator _finish;
    204. iterator _endofstorage;
    205. };
    206. }

  • 相关阅读:
    c++ stl(标准模板库)
    Strtok函数切割字符串(附代码演示)
    绿联搭建rustdesk服务器
    javascript:Array.prototype.reduce()的使用场景
    软件设计模式系列之二十二——状态模式
    从双非硕士到大厂工作,优秀
    [网站部署03]宝塔+worldPress部署Ripro主题网站
    时间序列中的6大类10种异常值处理方法(从根源上提高预测精度)
    [前端基础] 浏览器篇
    C++静态代码检查工具 - cppcheck
  • 原文地址:https://blog.csdn.net/weixin_59913110/article/details/128025392