【C++STL: vector(下)】手搓vector的常见坑+详细代码
前言
上篇文章带大家认识了vector的一些基本接口,了解了这些接口时如何使用的,本篇博主带大家手搓一个vector类,从底层的角度认识、理解和掌握vector,让我们对内存管理、深浅拷贝有更深的理解!
目录
(一)vector源码怎么看?

注意:阅读源码对新手来说是个巨大的挑战,我们不求把每一行代码都搞懂,大概了解每个函数是用来做什么的就行。
成员变量都有哪些?

这三个成员变量都是迭代器,看到命名就大致能猜到每个迭代器指向顺序表的哪个位置了,为验证猜想,我们先在源码里找到vector的begin()/ end()是怎么定义的:

证明我们猜想正确:start指向顺序表的首元素地址,finish指向顺序表最后一个元素的后一个地址。

通过capacity()容易得到:end_of_storage指向开辟好的空间的下一个位置。
push_back源码

如何理解函数体里的construct?
vector在类中自己实现了一个construct.h,它的作用就是:在已经分配好但尚未初始化的原始内存上,调用类型T的构造函数,构造出一个对象。意义是:使用定位new,为一个未初始化的空间进行初始化。(因为vector自己实现的申请空间是从内存池申请的,不像我们直接new出来的对象,会直接初始化)
template <class _T1, class _T2>
inline void _Construct(_T1* p, const _T2& value) {
new (p) _T1(value); // placement new:在 p 指向的内存上构造对象
}
定位new在前文介绍过,这里就不再过多赘述了!详细内容见下面链接:
【C++初阶】内存管理:从 malloc 到 placement-new 的全解析-CSDN博客
(二)手搓myvector可能出现的问题
注意:
1.vector是一个模板类,模板不支持声明和定义分离,所以博主只实现了一个myvector.h,而不像string那样又搞一个.cpp文件。
2.为延续以前类的代码风格,myvector中的成员变量前也加一个下划线做区分,三个成员变量分别为_start , _finish , _end_of_storage。
3.myvector不使用内存池,直接用new开空间,不用像源码一样考虑construct的问题。
模板在前文也已经介绍过了,这里就不再过多赘述!模板内容见前文:
【C++初阶】一文讲透:如何使用模板实现泛型编程-CSDN博客
vector接口的实现步骤和string类似,博主在这里就不一一带大家实现了,文章结尾会贴出完整代码,手搓过程中出现问题可以看博主的代码,希望可以帮到你!
1.迭代器失效
迭代器失效是所有容器都存在的问题,string类之所以不介绍迭代器失效的问题,是因为我们一般直接使用指针,而不用迭代器封装,vector中迭代器失效分为两类:insert迭代器失效和erase迭代器失效。
什么是迭代器失效?
迭代器本质是一个指向容器内部元素的“指针”(或封装)。当容器内部存储结构发生变化时,原有的迭代器可能不再指向预期的元素,甚至变成野指针,这就是迭代器失效。


容易看出,vector的insert和erase的返回值不像string一样是void,而是返回了一个迭代器。
下面,博主会详细解释为什么这样设计:
insert时迭代器失效
案例:
错误的insert定义:

void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
Print(v);
v.insert(v.begin(), 0);
Print(v);
auto it = v.begin() + 3;
// insert以后,it是否失效?
// it失效了,也就意味着,insert以后,it失效了,it就不能使用了
v.insert(it, 30);
Print(v);
}
我们给的空间大小首次给4,先push_back四个元素,在insert一个元素就会触发扩容机制,reserve会开辟一个新的空间,把_start/_finish/_end_of_storage都给拷贝走,只有pos在原来的空间,原空间被释放后,pos就变成野指针了。

所以要更新pos,也就有了下面的写法:

它解决了什么?
扩容(reserve)会重新分配内存,原来传入的 pos 迭代器会失效。代码通过记录偏移量 len,在扩容后用 _start + len 重新定位 pos,使得后续挪动数据时 pos 仍然有效。
它没解决什么?
MSVC编译器默认使用过的迭代器就会失效:扩容后,所有指向原 vector 的迭代器都失效了,这是无法避免的。
erase时迭代器失效(删除顺序表的偶数)
从一个翻车场景切入
先用一段代码引出问题——看起来很合理的代码,运行却崩溃了。
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.erase(it);
}
else
{
++it;
}
}
前面已经说明,MSVC编译器下,一个迭代器一旦被使用则会失效,不能再次调用,上述代码里while循环中频繁调用迭代器v,直接运行崩溃。
解决办法:重置迭代器
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.erase(it);
}
else
{
++it;
}
}
2.Gcc编译器下如何判定迭代器失效
删除顺序表的偶数:

运行结果:
1 2 3
结果正确,说明使用过的迭代器还能被再次使用。
vs2019对迭代器会严格检查,被使用后就不能再使用了,若想使用必须重置迭代器,而Gcc没有这么严格,但也会检查。
(三)构造函数错配问题


上图是两个拷贝构造,图一:拷贝迭代器区间,图二:拷贝n 个 val
我们在常规调用第二个拷贝构造的写法是:

运行结果!

我们本意是想调用第二个拷贝构造,它却调用了第一个拷贝构造,原因就是vector(size_t n, const T& val=T()),第一个参数是size_t类型,第二个是T,我们传过去的参数两个类型是一样的,都是int,而第二个迭代器区间的拷贝构造的两个参数是相同的,所以第二个更匹配我们传入的参数,编译器就优先选择调用第二个。
解决方法
vs2011是利用函数重载修复的这个问题,vs2019及以后有特殊的处理方法:C++11 引入了 SFINAE 原则和 <type_traits> 库。核心思路是:让范围构造函数仅在 InputIt 是“真正的迭代器”时才“现身”,否则就让它“消失”。这里就不再详细介绍,后续介绍现代C++时再展开聊聊。
(四)本篇完整代码
1.myvector.h
#pragma once
#include<iostream>
#include<algorithm>
#include<assert.h>
using namespace std;
namespace myvector {
template <class T>
class vector
{
public:
void Print()
{
for (auto ch : *this)
{
cout << ch << " ";
}
cout << endl;
}
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{ }
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = 0;
}
size_t capacity()
{
return _end_of_storage - _start;
}
size_t size()
{
return _finish - _start;
}
using const_iterator = const T*;
using iterator = T*;
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
void reserve(size_t n)
{
assert(n >= capacity());
size_t sz = size();
iterator tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
//strncpy(tmp, _start, sizeof(T) * sz); err
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : 2 * capacity());
}
*_finish = x;
++_finish;
}
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
const T& operator[](size_t i) const
{
assert(i < size());
return _start[i];
}
void pop_back()
{
assert(!empty());
_finish--;
}
bool empty()
{
return _start == _finish;
}
iterator insert(iterator pos, const T& x)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : 2 * capacity());
pos = _start + len; // 更新pos
}
auto it = _finish;
while (it != pos-1)
{
*it = *(it - 1);
--it;
}
*pos = x;
++_finish;
return pos;
}
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
auto it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
return pos;
}
void resize(size_t n,T val=T())
{
if (n < size())
{
_finish = _start + n;
}
else {
reserve(n);
while (_finish < _start + n)
{
push_back(val);
_finish++;
}
}
}
//vector()
// :_start(nullptr)
// , _finish(nullptr)
// , _end_of_storage(nullptr)
//{
//}
//vector<T> v1(v2)
vector(vector<T>& v)
{
reserve(v.capacity());
for (auto& r : v)
{
push_back(r);
}
}
//拷贝迭代器区间
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
first++;
}
}
//{}初始化
//v1={1,2,3}
vector(initializer_list<T> il)
{
reserve(il.size());
for (auto& e : il)
{
push_back(e);
}
}
vector(size_t n, const T& val=T())
{
reserve(n);
while (n--)
{
push_back(val);
}
}
void clear()
{
_start = _finish;
}
//赋值运算符重载
vector<T>& operator=(const vector<T>& v)
{
clear();
reserve(v.capacity());
for (auto& r : v)
{
push_back(r);
}
return *this;
}
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(const vcetor<T>& v)
//{
// vector<T> tmp = (v.begin(), v.end());
// swap(tmp);
//}
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
2.Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Myvector.h"
namespace myvector {
void Test1()
{
vector<int> v;
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.push_back(1);
v.Print();
}
void Test2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.insert(v.begin(),5);
auto it = v.begin() + 1;
it = v.insert(it, 0);
v.Print();
v.erase(it+1);
v.Print();
}
void Test3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
//v.resize(4, 1);
//v.Print();
//v.resize(2);
//v.Print();
vector<int> v2 = v;
v2.Print();
}
void Test4()
{
vector<int> v(5, 1);
v.Print();
}
}
int main()
{
myvector::Test4();
return 0;
}
创作不易,请读者读后点赞、收藏、评论!下篇博主会向大家介绍STL:list,敬请期待~
更多推荐

所有评论(0)