️作者:@malloc不出对象
⛺专栏:C++的学习之路
个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐
本篇文章我们要来模拟实现一下SGI版本的vector类以及讲解迭代器失效的问题。
对于类来说最重要的是知道它的成员变量,只有知道它的成员变量了你才能知道它需要利用成员变量干什么事,SGI版本下vector类的成员变量是由三个迭代器组成的,而这里的迭代器就充当原生指针的作用,这三个迭代器(指针)_start 指向第一个位置,_finish 指向最后一个元素的下一个位置,_end_of_storage 指向最大容量的下一个位置。
namespace curry
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
对三个成员变量进行初始化操作。
vector类元素的个数以及容量大小,不需要修改的成员函数最好加上const防止被修改。
bool empty() const
{
return _start == _finish; // 当_start与_finish指向同一位置时,_finish - _start = size = 0
}
size_t size() const
{
return _finish - _start; //指针相减为两个指针之间的元素个数
}
size_t capacity() const
{
return _end_of_storage - _start; // 容量大小
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
reserve与resize都起到扩容的作用,而关于这两者的区别我们在讲解string类的时候就已经对比过了,对于vector类来说在尾部或者某一位置处插入元素前都是需要检查容量的,一旦size等于capacity我们就需要进行扩容操作,在SGI版本中vector类的扩容机制是每次以2倍容量进行扩容。
void reserve(size_t n)
{
if (capacity() < n)
{
T* tmp = new T[n];
size_t sz = size();
if (_start) //_start为nullptr时,不需要拷贝数据
{
memcpy(tmp, _start, sizeof(T) * size()); // 浅拷贝
delete[] _start;
}
_start = tmp;
_finish = tmp + sz;
_end_of_storage = tmp + n;
}
}
void resize(size_t n, T val = T()) // 使用缺省值,模板调用无参构造函数进行初始化,T()是一个匿名对象
{
if (n < size())
{
_finish = _start + n;
}
else if (n > size())
{
if (n > capacity())
{
reserve(n);
}
iterator end = _start + n;
while (_finish != end) // 对后续空间进行初始化操作
{
*_finish = val;
_finish++;
}
}
}
注意事项1:reserve
函数扩容后_start
、_finish
以及_end_of_storage
的位置发生了变化。
非常容易写成下面的错误代码:
_start = tmp;
_finish = tmp + size();
_end_of_storage = tmp + n;
tmp + size() = _start + _finish - _start == _finish
,注意此时的_start
扩容后位置已经发生变化,要想得到_finish
的位置必须知道变化之前_start
与它的相对位置,即在变化前得到sz = _finish - _start
。
注意事项2: 在拷贝
_start
指向空间的数据时采用的是memcpy
浅拷贝,它是一个字节一个字节拷贝数据的,但是在涉及对象空间资源的释放时就会析构两次,此时就产生了问题!!这个问题后续我们再来探究,现阶段使用memcpy
对内置类型进行数据拷贝是安全的。
下面我们进行vector常用函数接口尾插和尾删的实现,还是非常简单的。
// 尾插
void push_back(const T& x)
{
if (_finish == _end_of_storage) // 当_finish与_end_of_storage指向的位置相等时说明此时容量已满需要进行扩容操作
{
reserve(capacity() == 0 ? 4 : capacity() * 2); // 这里当容量为0时,我给了4个大小的容量,这里可以大家可以随意去控制
}
*_finish = x;
_finish++;
}
// 尾删
void pop_back()
{
assert(!empty()); // 断言检查是否为空
--_finish;
}
[ ]运算符重载,返回对应下标的值,有两个版本。
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
下面我们来模拟实现一下在某一合法位置进行插入和删除,下面的代码是最原始的insert与erase版本,这其中涉及迭代器失效的问题,大家也可以先看看这段代码有什么问题,后续我们会结合迭代器失效问题来给出最终的版本。
void insert(iterator pos, const T& val)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t sz = pos - _start;
reserve(2 * capacity()); // 这里如果直接扩容的话,那么_start就改变了,pos位置没有了参照物,所以为了找到pos位置,我们可以利用扩容前的与_start的相对位置
pos = _start + sz; // 扩容之后再根据_start与pos的相对位置求出此时pos的位置
}
// 版本一:
/*iterator end = _finish - 1;
while (end != pos + 1)
{
*(end + 1) = *end;
end--;
}*/
// 版本二:
iterator end = _finish;
while (end != pos)
{
*end = *(end - 1);
end--;
}
*pos = val;
_finish++;
return pos;
}
void erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
/*iterator begin = pos;
while (begin + 1 != _finish)
{
*begin = *(begin + 1);
begin++;
}*/
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
}
我们先来测试一下原始版本的正确性:
从上图我们发现此时并没有什么问题,真的没问题吗?下面我们再插入一个数据:
我们发现这段代码已经崩了??这是为何?看到代码出现崩溃其实我们第一个问题就要想到是不是指针指向的空间位置是不是改变了、指向的空间是否已经释放了…为何插入4个元素时不会崩溃,插入5个元素就崩溃了呢?
我们想到我们的capacity容量大小起始给的是4,此时_start指向的位置不变,而一旦插入第五个元素了此时就需要进行扩容,扩容之后_start指向的位置发生了变化,所以此时pos的位置不正确造成了程序崩溃,虽然扩容之后_start指向的位置发生了变化,但是pos与_start之前的相对位置是不变的,所以我们可以在扩容前记录下pos与_start的相对位置,扩容后就知道了pos的位置。
修改后的代码如下:
void insert(iterator pos, const T& val)
{
assert(pos >= _start && pos < _finish);
if (_finish == _end_of_storage) // 需要扩容
{
size_t sz = pos - _start; // 求出pos与_start的相对位置
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + sz; // 扩容后pos的位置
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
通过上述代码我们解决了在扩容后pos迭代器失效的问题,那么还有没有其他的问题呢?我们看下下面这段代码:
为何上述输出的结果一致?我们的目的不是让pos位置处++吗?得到的答案应该是1 2 11 3 4
啊,为什么这里没有任何变化?
虽然在insert内我们成功的控制了pos,但是形参改变不影响实参,所以此时的pos还是扩容前的位置,该位置已失效是野指针,所以此时其实是会造成非法访问的,至于这里为什么没报错跟编译器的处理有关,可能在另外版本的编译器上就崩溃了。
如何解决这个问题?
返回插入位置的迭代器pos,能用传引用吗?不能,iterator返回的是临时对象,临时对象具有常属性,要想使用需要加const修饰,可是我们的pos经过扩容后修改了!!所以肯定是不支持这种写法的。
修改后的最终代码:
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage)
{
size_t sz = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + sz;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
return pos;
}
我们来回答一个问题:insert之后会使插入位置的迭代器失效吗(原始版本)?
在SGI版本中有可能能用有可能不能用,但它也失效了,因为我们的_finish不是++到pos位置处,而是++到挪动后的数据;第二种失效是因为pos位置在扩容之后就失效了。
对于插入数据如果未接收返回插入后的位置,P.J.版本VC++的态度是坚决不允许这种行为的出现,不管你此时的pos还是不是原来的pos都视为失效,此时(*pos)++进行访问直接就是非法访问导致程序崩溃!!
同样的我们先来检查一下正确性:
通过上图我们发现原始版本的删除函数看起来是没有任何问题的,那么接下来我们看看在G++下SGI版本对这两段代码的态度:
删除中间位置:
删除最后一个位置:
删除中间位置时,我们的数据进行了挪动,其实pos位置是已经失效了的,此时它指向的是pos位置的下一个位置即4号位置,再对4号位置进行++,这也似乎说的过去;但是对于删除最后一个位置,此时我们的数据未进行挪动,但此时pos与_finish指向同一个位置,即最后一个元素的下一个位置,这个位置是的内容是未知的,此时我们进行(*pos)++不就非法访问了吗!!这肯定是不太合理的,而P.J.版本是更为合理的,一旦使用未接收删除后的返回值,P.J.版本认为erase以后pos位置失效了,不能进行访问并且强制进行检查,此时导致程序崩溃。
P.J.版本VC++下进行erase
我们发现在erase之后一旦使用未接收删除后的返回值,P.J.版本认为pos位置已经失效,此时再进行(*pos)++是在进行非法访问!!
下面我们继续来看一个问题:删除vector容器中所有的偶数,请你用迭代器实现?
上述代码好像已经实现了删除vector容器中的所有偶数?下面我们来看看这段代码:
这段代码似乎出现了错误,我们继续看下一种情况:
我们不是要删除所有的偶数吗?那这个4怎么没删掉呢??下面我们通过图来模拟一下这三种情况:
这里本质上的问题就是it不管是在遇到偶数还是未遇到时都跳到下一个位置上去了,这就导致出现越界或者跳过了某些偶数,我们的解决办法是在删除元素时不往后跳,我们的代码这样一改就好了:
还有一种情况大家自己下来去检测吧。
那么如何解决erase后迭代器失效的问题呢?返回pos的下一个位置。
erase的最终版本
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
/*iterator begin = pos;
while (begin + 1 != _finish)
{
*begin = *(begin + 1);
begin++;
}*/
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
return pos;
}
总结:对于insert以及erase我们从上述例子进行分析之后,其实P.J.版本的设计其实是更为合理的,一旦我们未接收插入/删除后的位置,无论此时插入/删除的位置有没有问题都认为该位置已经失效,如果我们再对此位置进行访问就是非法访问了!!我们最好不要对插入/删除后的位置进行访问!!!
另外,我们使用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);
}
void clear()
{
_finish = _start;
}
_finish = _start,size大小为0,但并不会进行缩容操作!!
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
vector(size_t n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (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)
{
while (first != last)
{
push_back(*first);
first++;
}
}
我们来看一段代码:
这里为什么会出现非法间接寻址?我不是已经实现了n个val的构造函数吗???为什么调用n个val的构造函数会出现这种报错信息呢??
从编译器提示来看这是因为它的实参类型为int, int,而我们的n个val的构造函数参数类型为size_t, int;所以它最为匹配的函数是上述实例化出来的函数模板,但是*int是非法的行为,所以这里出现了非法寻址的报错信息!!
要想解决这个问题我们可以改变一下实参的类型,我们可以传v1(10u, 5),u代表无符号整型,这样就可以匹配到我们的n个val的构造函数了;又重新重载一份int,int参数类型的n个val的构造函数:
vector(int n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(n);
for (int i = 0; i < n; ++i)
{
push_back(val);
}
}
vector<T>& operator=(vector<T>& v)
{
vector<T> tmp(v.begin(), v.end()); // 迭代器构造
swap(tmp);
return *this;
}
传统写法
// 拷贝构造01
vector(const vector<T>& v)
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
// 拷贝构造02 -- 浅拷贝
vector(const vector<T>& v)
{
reserve(v.capacity());
memcpy(_start, v._start, sizeof(T) * v.size()); // 浅拷贝
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
现代写法
vector(const vector<T>& v)
{
vector<T> tmp(v.begin(), v.end()); // 迭代器构造
swap(tmp);
}
我们以传统写法拷贝构造–02为例,看看浅拷贝遇到自定义类型会出现什么情况:
我们知道string类是自定义类型,当我们进行浅拷贝时会出现下图情形:
一旦我们出了函数作用域,v3和v4对象都会去调用析构函数,而此时它们的元素_str指向同一块空间,由此会被析构两次,于是就造成了程序崩溃。所以我们在此处不应该使用memcpy浅拷贝,我们应该使用深拷贝使_str指向不同的位置。
我们的代码应该这样来写:
vector(const vector<T>& v)
{
reserve(v.capacity());
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i]; // this->_start._str = v->_start._str, 调用string类中的赋值重载函数
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
我们继续来看向这段代码:
v1尾插4次此时是没有问题的,下面我们再插入一次看看会出现什么情况:
这是由于push_back第五次时发生扩容,而扩容是浅拷贝,我们再次回头看看第一次我们写的reserve函数,当我们的需要扩容时先将_start指向的原始数据拷贝到新空间tmp中,此时进行的是memcpy浅拷贝,它们的元素指向的是同一块空间,当_start进行delete时将这块空间进行释放了,所以tmp指向的这块空间是无效的,所以最终_start指向的就是无效的空间。
最终我们的reserve函数代码应该如下:
void reserve(size_t n)
{
if (capacity() < n)
{
T* tmp = new T[n];
size_t sz = size();
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i]; // 自定义类型深拷贝
}
delete[] _start;
}
_start = tmp;
_finish = tmp + sz;
_end_of_storage = tmp + n;
}
}
以上就是一些重要的vector类的模拟实现代码了。
namespace curry
{
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;
}
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
// n个val的构造函数
vector(size_t n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
// 拷贝构造函数
vector(const vector<T>& v)
{
reserve(v.capacity());
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i]; // 自定义类型赋值重载 -- 深拷贝
}
_finish = _start + v.size();
_end_of_storage = _start + v.capacity();
}
// 现代写法
/*vector(const vector& v)
{
vector tmp(v.begin(), v.end());
swap(tmp);
}*/
// 赋值重载函数
vector<T>& operator=(vector<T>& v)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
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);
}
// 函数模板,任意类型的迭代器构造
template <class InputIterator>
vector(InputIterator first, InputIterator last)
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
// 扩容
void reserve(size_t n)
{
if (capacity() < n)
{
T* tmp = new T[n];
size_t sz = size();
if (_start)
{
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i]; // 自定义类型深拷贝
}
delete[] _start;
}
_start = tmp;
_finish = tmp + sz;
_end_of_storage = tmp + n;
}
}
void resize(size_t n, T val = T()) // 使用缺省值,模板调用无参构造函数进行初始化,T()是一个匿名对象
{
if (n < size())
{
_finish = _start + n;
}
else if (n > size())
{
if (n > capacity())
{
reserve(n);
}
iterator end = _start + n;
while (_finish != end) // 进行初始化
{
*_finish = val;
_finish++;
}
}
}
// 某一位置插入元素
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start && pos <= _finish);
if (_finish == _end_of_storage) // 需要扩容
{
size_t sz = pos - _start; // 求出pos与_start的相对位置
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + sz; // 扩容后pos的位置
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
return pos;
}
// 某一位置删除元素
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
/*iterator begin = pos;
while (begin + 1 != _finish)
{
*begin = *(begin + 1);
begin++;
}*/
iterator begin = pos + 1;
while (begin != _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
return pos;
}
// 尾插
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = x;
_finish++;
}
// 尾删
void pop_back()
{
assert(!empty());
--_finish;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _end_of_storage - _start;
}
bool empty()
{
return _start == _finish;
}
// 析构函数
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
void clear()
{
_finish = _start;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
以上就是本文的所有内容了,如有错处或者疑问欢迎大家在评论区相互交流orz~