主要讲述vector的模拟实现,重点在于理解迭代器失效问题。
同string类模拟实现一致,此处为了解决命名冲突问题,我们使用添加命名空间myvector
的方式来处理。
#pragma once
namespace myvector
{
template<class T>
class vector
{
typedef T* iterator;//将模板T*命名为迭代器iterator
public:
private:
iterator _start;//起始
iterator _finish;//结束
iterator _end_of_storage;//容量空间
};
}
由于后续涉及到迭代器问题,若将typedef T* iterator;
定义成私有,则无法在类外很好的使用。此处修改如下:
#pragma once
namespace myvector
{
template<class T>
class vector
{
public:
typedef T* iterator;//将模板T*命名为迭代器iterator
typedef const T* const_iterator;
private:
iterator _start;//起始
iterator _finish;//结束
iterator _end_of_storage;//容量空间
};
}
1)、构造函数
对构造函数,我们之前学习时看到其中有内存池的相关内容,此处由于我们暂时没学习它,故对vector的模拟实现中我们不使用它。
vector() //构造函数
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
2)、析构函数
~vector()//析构函数
{
delete[] _start;//null出来的空间是连续一块的,以_start为起始点。注意delete的使用方式
_start = _finish = _end_of_storage = nullptr;
}
1)、库函数中声明回顾
//C++98
void push_back (const value_type& val);
2)、push_back模拟实现
void push_back(const T& x)
{
//涉及扩容检查
if (_finish == _end_of_storage)
{
reserve(capacity() == 0?4:capacity() * 2);
}
//尾插数据
*_finish = x;
_finish++;
}
为什么需要使用引用和const修饰?
因为这里使用的是T模板参数,我们传入的值可能是内置类型,也可能是自定义类型,如果是后者,则传值传参代价很大。
3)、pop_back模拟实现
void pop_back()
{
assert(_finish > _start);
_finish--;
}
_finish > _start
:①尾删需要注意元素为空的情况;②需要注意这里使用的是>
,与_finish指向尾元素下一个位置有关。
1)、库函数中声明回顾
2)、模拟实现
size_t size()const
{
return _finish - _start;
}
size_t capacity()const
{
return _end_of_storage - _start;
}
void reserve(size_t n)
{
if (n > capacity())//满足该条件才进行扩容
{
size_t sz = size();//因为后续重新确定指向关系时需要知道size值
T* tmp = new T[n];
if (_start)//说明原先空间有数据,
{
memcpy(tmp, _start, sizeof(T) * sz);//需要挪动
delete[] _start;//释放旧空间
}
//重新确定指向关系
_start = tmp;
_finish = _start + sz;
//_finish = _start + size();//如果是在这里获取size值,则在原先空间有数据的情况下,_start已经被delete
_end_of_storage = _start + n;
}
}
1)、库函数中声明回顾
2)、模拟实现
T& operator[](size_t pos)//加&是为了支持可读可写
{
assert(pos < size());//检查下标是否非法
return _start[pos];
}
const T& operator[](size_t pos)const
{
assert(pos < size());
return _start[pos];
}
1)、普通迭代器
//vector的迭代器就是原生指针
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
2)、const修饰的迭代器
为什么需要?
存在如下的情况:const vector
,所创建的vector对象被const修饰,如果直接使用vector
,则属于权限放大。
typedef const T* const_iterator;
const const_iterator begin()const
{
return _start;
}
const const_iterator end()const
{
return _finish;
}
void Func(const vector<int>& v)
{
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
//*it = 10;//error
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)//此处若使用范围for,const对象会调用对应的const迭代器
{
cout << e << " ";
}
cout << endl;
}
3)、为什么说范围for是傻瓜式替换?
只要我们仿照库中使用对应字符begin、end,则访问for就能起效。相应的,如果我们使用了Begin、End等其它字母,则在我们模拟的vector中范围for失效。
2)、insert模拟实现1.0
void insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
//扩容检查
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//数据挪动
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
//插入val值
*pos = val;
++_finish;
}
3)、insert涉及扩容时迭代器失效问题
问题:
原因解释: reserve
扩容时,若此时n>capacity
,我们采取的是从新开辟一块空间,并将原空间数值拷贝过去的做法。因此,_start
、_finish
、_end_of_storage
因reserve
的实现会更新其指向关系,而pos
仍旧指向原先位置,且此时属于野指针,存在非法访问的问题。
事实上,迭代器失效其中一个典型问题就是野指针。
4)、insert模拟实现2.0
解决方案: 在扩容时顺带更新pos指向位置
//保存二者指针差距
size_t len = pos - _start;
//扩容后更新pos指向
pos = len + _start;
如下:
void insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
//扩容检查
if (_finish == _end_of_storage)
{
//保存二者指针差距
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//扩容后更新pos指向
pos = len + _start;
}
//数据挪动
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
//插入val值
*pos = val;
++_finish;
}
1)、问题引入与现象分析
接上一阶段代码,迭代器失效并不仅仅会出现在这一种情况中,如下述情形:
我们使用std::find
找到p
位置,然后在p
位置前连续插入多次。问:是否能成功?
//使用算法中的find
auto p = find(v.begin(), v.end(), 3);
if (p != v.end())//找到了
{
v.insert(p, 30);//1、插入一个30
cout << *p << endl;//2、再次来到p的位置
v.insert(p, 40);//3、我们p位置前插入一个40,问:是否能成功?
}
现象如下:
可能出现疑问如下:我们在3.5.1中解决了insert中pos位置更新的问题,为什么此处p仍旧不起效?
这里我们需要思考上述写的insert函数中,形参pos和实参p之间的关系。可知晓的是,在insert函数中,它们是值传递,故形参改变不影响实参。
void insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
//扩容检查
if (_finish == _end_of_storage)
{
//保存二者指针差距
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//扩容后更新pos指向
pos = len + _start;
}
//数据挪动
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
//插入val值
*pos = val;
++_finish;
}
2)、解决方案
①一个相对比较适合的方法是,使用insert
这类函数时,最好别再p
位置失效后,再去访问p
。
②有人可能提出,我们可以在insert
中为pos
加上一个引用,即使用传引用返回,这样不就解决了?void insert(iterator& pos, const T& val)
void insert(iterator& pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
//扩容检查
if (_finish == _end_of_storage)
{
//保存二者指针差距
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//扩容后更新pos指向
pos = len + _start;
}
//数据挪动
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
//插入val值
*pos = val;
++_finish;
}
事实上这样做,针对insert
而言确实可以解决问题,但同样会面临新的问题。
第一,这样的模拟实现与库中不匹配,库中直接使用iterator pos
;
第二,这样做会带来新的问题,如下:
v.insert(v.begin(), 1);
此段代码无法编译通过。因为begin
模拟实现时,我们使用的是iterator
传值返回,中间会生成一份临时变量,具有常性,后续insert
处权限放大。
Ⅰ、若在insert
中pos
为加上const
,即const_iterator& pos
那么又无法解决pos
修改问题。
Ⅱ、而如果将begin
也写为传引用返回,iterator& begin()
,这样会使得begin
具有修改能力,反而增添麻烦。
iterator begin()
{
return _start;
}
3)、思考:上述值传递中,p
一定存在迭代器失效问题吗?
回答:同3.5.2中所讲,在涉及扩容问题时,p
才存在失效。
演示:如下述,假如我们一开始插入5个数值,容量空间足够的情况下,此处不存在p失效问题。
4)、insert为push_back提供复用
void push_back(const T& x)
{
insert(end(), x);
}
5)、insert模拟实现3.0:案例演示
案例要求: 在所有的偶数前插入该偶数的二倍值。
代码演示+现象分析:
我们以上述insert2.0版本为例进行演示,顺带再来回顾一下迭代器失效问题。
void test_vector5()
{
std::vector<int> v;//以库里面的vector来演示
v.reserve(10);//直接把需要的空间扩好,这里可排除由于扩容问题带来的迭代器失效
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
//在所有偶数前插入该偶数的2倍
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.insert(it, *it * 2);
}
++it;
}
}
上述代码我们直接运行则程序崩溃无输出结果,调试后发现如下:it始终指向2的位置。这就是迭代器失效的另一种模式:因为数据挪动,导致外部指针指向错乱。
为了解决上述问题,也一并解决insert中扩容后外部迭代器失效问题,一个方案如下:
Ⅰ、对insert
函数,模仿库中带上iterator
返回值;
//C++98
iterator insert (iterator position, const value_type& val);
iterator insert(iterator pos, const T& val)
{
assert(pos <= _finish && pos >= _start);
//扩容检查
if (_finish == _end_of_storage)
{
//保存二者指针差距
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
//扩容后更新pos指向
pos = len + _start;
}
//数据挪动
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
//插入val值
*pos = val;
++_finish;
return pos;
}
Ⅱ、对原先的while (it != v.end())
循环中,需要更新it
指向的新位置,旨在解决扩容后外部迭代器失效问题。
void test_vector5()
{
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
//在所有偶数前插入该偶数的2倍
auto it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it=v.insert(it, *it * 2);//接受返回值
++it;//连跳两次
}
++it;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
1)、erase模拟实现1.0
void erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
iterator end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
}
2)、erase是否会导致迭代器失效?
①主要看编译器如何实现erase函数,不排除有些编译器以时间换空间进行缩容:
if (size() < capacity()/2)
{
// 缩容 -- 以时间换空间
}
②其它案例演示:删除vector中的偶数
使用代码如下:
void erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
iterator end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
}
void test_vector4()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//删除所有偶数
auto it = v.begin();
while (it != v.end())
{
if(*it % 2 == 0)
{
v.erase(it);
}
++it;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
现象如下:
3)、erase模拟实现2.0
为了解决上述迭代器失效问题,仍旧按照库里实现的方法,给它一个返回值。
iterator erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
iterator end = pos + 1;
while (end < _finish)
{
*(end - 1) = *end;
++end;
}
--_finish;
//if (size() < capacity()/2)
//{
// // 缩容 -- 以时间换空间
//}
return pos;
}
基于2.0版
本的erase
我们再来修改上述 2) 中的题目:
void test_vector4()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//v.push_back(5);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//删除所有偶数
auto it = v.begin();
while (it != v.end())
{
if(*it % 2 == 0)
{
it=v.erase(it);
}
else {
++it;
}
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
需要注意的是,上述我们对迭代器失效的问题演示,其结果是未定义的,因为针对不同平台其STL底层实现并不一致。
即,STL只是一个规范,其细节如何实现不做要求。
VS:PJ版。
g++:SGI版。
2)、模拟实现
根据上述可知,resize
面临三种情况:
Ⅰ、当n>capacity
:扩容+使用val初始化;
Ⅱ、当size
Ⅲ、当n
模拟实现如下:
void resize(size_t n, const T& val=T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())//两种情况:n>capacity、size
{
//只需要初始化即可
while (_finish < _start + n)
{
push_back(val);
++_finish;
}
}
else
{
_finish = _start + n;
}
}
演示验证一:
void test_vector11()
{
//resize常用场景:在生成对象后开辟空间
vector<int>v;
v.resize(5);//验证默认缺省值
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.resize(10, 2);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
void test_vector11()
{
vector<int> v1;
v1.reserve(10);
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.resize(8,8);//由大到小,而值只有5个,会添3个值
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
2)、模拟实现
T& front()
{
assert(size() > 0);
return *_start;
}
T& back()
{
assert(size() > 0);
return *(_finish - 1);
}
1)、知识回顾
在类和对象章节,我们曾说明:拷贝构造对于内置类型完成浅拷贝/值拷贝,对于自定义类型则会调用它对应的拷贝构造。
2)、vector拷贝构造分析
问题:vector中拷贝能否使用编译器默认的拷贝构造函数?
回答:vector
的成员变量是内置类型T*
,故编译器默认生成的拷贝构造函数完成的是浅拷贝。
PS:typedef T* iterator;
、此处尽管iterator
是类模板,且T*
会存在自定义类型的指针,但其仍旧是内置类型。
private:
iterator _start;//起始
iterator _finish;//结束
iterator _end_of_storage;//容量空间
以如下代码进行拷贝构造:
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
vector<int> v2(v);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
浅拷贝带来的两个问题:
Ⅰ、析构两次
Ⅱ、一个对象的修改,会影响另一个对象
与string中拷贝构造类似思路。
vector(const vector<T>& v)
{
_start = new T[v.size()];//此处也可以开辟v.capacity()大小的空间,各有各的优缺点
memcpy(_start, v._start, sizeof(T)*v.size());//照搬数据
_finish = _start + v.size();
_end_of_storage = _start + v.size();//此处this._finish的大小根据上述我们开辟空间时的选择而变动
}
上述这种写法在细节考虑上存在一定问题, 将在后续 “二维数组深浅拷贝” 中讲到。
代码如下:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(v.size());//感觉v的值为*this重新扩容
for (const auto& e : v)//之后,再根据v中元素值复制一份到*this中
{
push_back(e);//this.push_back()
}
}
1)、vector构造函数:基本说明
观察库中,我们可以看到此处除了无参,n个val外,还有一个使用迭代器的构造函数:
template <class InputIterator>
vector (InputIterator first, InputIterator last,const allocator_type& alloc = allocator_type());
除却后面的配置器,这里first、last我们使用了新模板。
涉及问题:
Ⅰ、能否双模板嵌套式使用?
回答:可以,这是模板运用的一个知识,只是在之前的模板初阶中我们没有讲到。
Ⅱ、为什么需要新定义一个模板InputIterator
,而不使用原先的Iterator
?
用于定义迭代器的类型不一定是当前模板类型,这样做避免局限性。
2)、vector构造函数:使用迭代器区间构造1.0
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
void test_vector7()
{
string str("hello world");
vector<int> v(str.begin(), str.end());//使用迭代器区间构造
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
上述代码是否会存在什么问题?
回答:_start
、_finish
、_end_of_storage
没有初始化,在有些编译器下其是随机值,而push_back
涉及扩容问题,若需要扩容,那么reserve
开辟空间时,此处非空就会拷贝数据、释放空间、存在越界问题。
3)、vector构造函数:使用迭代器区间构造2.0
解决方案:对_start
、_finish
、_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;
}
}
根据stirng中现代写法(复用构造函数),vector中也可以进行类似操作,但需要注意我们需要借助的是哪个构造函数。(vector中即上述的迭代器构造)
相关实现如下:
vector(const vector<T>& v)
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
vector<T> tmp(v.begin(), v.end());//构造一个vector
swap(tmp);//交换
}
正巧,vector中也有一个成员函数swap,这里我们可以一并实现:
实现如下:
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v.__end_of_storage);
}
问题:已经存在std::swap
,为什么还需要vector::swap
?
根据string中的讲解,std::swap是连同地址也一并交换,属于一种深拷贝,而对于自定义类型,这相对来说很耗时,故而我们在各自类中实现一个swap,让其在内部借助std::swap来完成内置类型的拷贝。相对来说代价较小。
1)、模拟实现及细节解析
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);
}
}
对const T& val = T()
,实际上这个函数使用了半缺省参数,其缺省值是T()
,一个T类型的匿名对象。
Ⅰ、假若T()
是自定义类型,则调用自定义类型的默认构造(事实上内置类型也有模板参数)
Ⅱ、假若T()
是内置类型,因C++中模板的出现,也拥有对应的默认构造函数,此处以int举例。
void test_vector9()
{
int i=11;
int j = int();
int k(10);
cout << "i:" << i << endl;
cout << "j:" << j << endl;
cout << "k:" << k << endl;
}
2)、演示
void test_vector10()
{
vector<int>v1(10);//验证默认缺省值
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
vector<int*>v2(5);
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
}
3)、一个错误说明
vector<int>v1(10,1);//验证默认缺省值
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
为什么在该函数中输入两个int类型的变量会显示如下错误,且报错还是报在我们之前写的迭代器区间构造上?
原因解释:类型匹配。相比于(size_t
、T
)类型,(InputIterator
、InputIterator
)更匹配(int、int)
。
解决方法:
①强制类型转换:vector
;
②修改函数形参类型:vector(int n, const T& val = T())
③使用函数重载:
vector(int 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);
}
}
传统模式如下:
借助打工人模式如下:
//赋值v1=v2
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
1)、问题引入
在vector(一)中,我们曾写过杨辉三角:其涉及到了vector
嵌套使用。
class Solution {
public:
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv;//定义一个vector>类型的数据
vv.resize(numRows);//第一次开辟空间:numRows,表示总行数(整体大小)
for(size_t i=0;i<numRows;i++)//对每行预处理:空间大小、边界数值
{
vv[i].resize(i+1,0);//第二次开辟空间,表示初始化杨辉三角的每行大小
vv[i].front()=vv[i].back()=1;//杨辉三vv.size()角每行首尾数据为1
//vv[i].resize(i+1,1);//上述代码也可以合并为一行实现
}
for(size_t i=2;i<vv.size();i++)//对每行的中间数据做处理:第i行第j个元素=第i-1行第j=1个元素+第i-1行第j个元素
{
for(size_t j=1;j<i;j++)
{
vv[i][j]=vv[i-1][j-1]+vv[i-1][j];
}
}
return vv;
}
};
vector<vector<int>> ret = Solution().generate(5);
在使用std::vector
时,这段代码成功运行。而将其放入我们自己实现的vector中,则会发现运行失败。
为什么我们自己的模拟实现的vector会失败呢?
Ⅰ、对vector
,如果我们不传值返回ector
则运行成功,而传值返回运行失败。需要注意的是,此处传值返回涉及自定义类型,存在数据拷贝的问题。
Ⅱ、基于此我们调试发现:外层的vector深拷贝成功(值一致、地址空间不一致),而内存的vector居然是浅拷贝。
如上下两幅图,对vector
,我们可以发现,紫色框中的第一次拷贝vector
地址不同是深拷贝,而绿色框中,即内部的第二次拷贝vector
地址相同。
以上是现象,现在来分析情况:
①自定义类型传值返回,中间生成一个临时变量,涉及深浅拷贝问题。
②拷贝构造我们模拟实现了两类,一类是传统写法,使用了memcpy
,由上述图二可知,memcpy
是浅拷贝,那么就涉及到同一空间析构两次的问题。修改方法如下:借助赋值,我们自己来手动拷贝
vector(const vector<T>& v)//拷贝构造传统写法
{
_start = new T[v.size()];//此处也可以开辟v.capacity()大小的空间,各有各的优缺点
//memcpy(_start, v._start, sizeof(T)*v.size());//照搬数据
for (size_t i = 0; i < sz; i++)//照搬数据
{
_start[i] = v._start[i];//此处假若是自定义类型,则是赋值运算符
}
_finish = _start + v.size();
_end_of_storage = _start + v.size();//此处this._finish的大小根据上述我们开辟空间时的选择而变动
}
③假若使用的是现代写法,在我们写的两个以swap为交换的拷贝构造,都没问题。此处出现问题的是扩容。原先我们写的扩容函数reserve
中用了memcpy
,同样是浅拷贝导致。修改方法如下:
void reserve(size_t n)
{
if (n > capacity())//满足该条件才进行扩容
{
size_t sz = size();//因为后续重新确定指向关系时需要知道size值
T* tmp = new T[n];
if (_start)//说明原先空间有数据,
{
//memcpy(tmp, _start, sizeof(T) * sz);//需要挪动
for (size_t i = 0; i < sz; i++)//需要挪动
{
tmp[i] = _start[i];
//*(tmp + i) = *(_start + i);
}
delete[] _start;//释放旧空间
}
//重新确定指向关系
_start = tmp;
_finish = _start + sz;
//_finish = _start + size();//如果是在这里获取size值,则在原先空间有数据的情况下,_start已经被delete
_end_of_storage = _start + n;
}
}