template <class T>
class vector {
public:
T _start;//(1)
T _finsih;//(2)
T _endofstorge;//(3)
};
( 1 ) (1) (1)指向当前空间的第一个元素。
( 2 ) (2) (2)指向当前空间存储的最后一个元素的下一个位置。
( 3 ) (3) (3)指向当前空间的末尾。
vector()//默认构造函数
:_start(nullptr),//(1)
_finish(nullptr),//(2)
_endofstorge(nullptr)//(3)
{
}
( 1 ) ( 2 ) ( 3 ) (1)(2)(3) (1)(2)(3)因为一开始没有分配空间,所以三个指针都置为空。
template<class InputIterator>
vector(InputIterator first, InputIterator last)//(1)
:_start(nullptr),
_finish(nullptr),
_endofstorge(nullptr)
{
while (first != last)
{
push_back(*first);//(2)
first++;
}
}
( 1 ) (1) (1)使用一个迭代器区间进行初始化。
( 2 ) (2) (2)复用push_back
。
为什么这里要将这个这个函数设置为函数模板呢?
并且将模板参数的名字取为InputIterator
。
迭代器其实也有其分类:
1.Input_iterator 2.Output_iterator 这两个没有实际对应的类型
3.forward_iterator (单向迭代器)如: map,unordered_map 迭代器只能++
4.Bidirectional_iterator (双向迭代器) 能够++和-- 如list
5.Randoaccess_iterator (随机迭代器) 能够访问任意位置 如:vector
上面是父类,下面是子类,子类包含了父类的功能。除此之外迭代器都具有解引用这类功能
迭代器的分类有什么作用呢?
我们查看下算法库中的sort
,
sort
要求传入随机迭代器,这时传入双向迭代器,单向迭代器就不满足要求了,如list就不支持sort
算法。
又如reverse
其迭代器的类型为双向迭代器,但是也可以传入随机迭代器,因为随机迭代器具有双向迭代器的全部功能。
总结:迭代器的分类其实是一种引导的功能,告诉你该算法可以使用哪些迭代器,哪些不能用。
int size() const
{
return _finish - _start;//(1)
}
( 1 ) (1) (1)_finish
指向当前空间存储元素的最后一个元素的下一个位置,所以直接减去_start
就能得到当前空间存储了多少个数据。
int capacity() const
{
return _endofstorge - _start;
}
在C++中自定义类型具有构造函数,为了兼容,内置类型也具有构造函数,如int
的构造函数int()
。
匿名对象的生命周期为当前行,出了该行以后即被回收。
但是如果const
修饰以后呢?
结论:const
修饰的匿名对象生命周期得到延长,直至函数执行完毕
证明:通过调试模式, 当执行到23
行时,仍然没有执行person
的析构函数,main
函数运行完才执行析构。
void reserve(int newcapacity)//这里capacity是存储的个数,不是字节数
{
T *tmp = new T[newcapacity];
int sz = size();//(1)
if (_start)
{
//memcpy(tmp, _start, sizeof(T)*newcapacity);//(2)
for (int i = 0; i < sz; i++)
tmp[i] = _start[i];//(3)
}
delete[]_start;//(4)
_start = tmp;//(5)
_finish = _start + sz;//(6)
_endofstorge = _start + newcapacity;//(7)
}
( 1 ) (1) (1)需要提前记录下旧空间存储数据的个数,用于后面更新_finish
。关于为什么要提前记录。这里我们画图理解。
因为后面更新_finish
需要_finish
距离_start
的距离,但是因为先更新的_start
,导致后面通过size()
函数获得的数据个数是错误的,就无法得到_finish
正确的位置
( 2 ) (2) (2)这里不能使用memcpy
,如果单纯的对两个string
进行拷贝,会导致两个string
类的_str
指针指向的空间一致,这会导致浅拷贝。在释放空间的时候会造成错误。
( 3 ) (3) (3)浅拷贝引发的问题使用深拷贝进行解决,对vector
的每个数据都进行赋值拷贝。
( 4 ) (4) (4)释放原空间。
( 5 ) ( 6 ) ( 7 ) (5)(6)(7) (5)(6)(7)分别更新_start
,_finish
,_endofstorage
。
void resize(int newcapacity, const T &x = T())//(1)
{
if (newcapacity > capacity())//(2)
{
reserve(newcapacity);
while (_finish!=_endofstorge)
{
*_finish = x;
_finish++;
}
}
else if (capacity() >= newcapacity)//(3)
{
_finish = _start + newcapacity;
}
}
( 1 ) (1) (1)因为resize
会对扩容的部门进行初始化,而vector
存储的数据类型有很多种,因此使用默认的构造函数作为初始化的默认值。
这里的T& x=T()
;牵扯到自定义类型有点不好理解,我们进行画图理解。
假设要创建一个三行三列的二维vector
( 2 ) (2) (2)扩容的空间大于原空间,将多余的空间进行初始化。
( 3 ) (3) (3)扩容的空间小于原空间,更新_finish
就好
typedef T* iterator;
typedef const T* const_iterator;
( 1 ) (1) (1)普通版本
iterator begin()
{
return _start;//(1)
}
iterator end()
{
return _finish;//(2)
}
( 1 ) (1) (1)返回第一个元素。
( 2 ) (2) (2)返回最后一个元素的下一个位置。
( 1 ) (1) (1)const版本
const_iterator begin() const
{
return _start;
}
const_iterator end()const
{
return _finish;
}
:
vector(const vector<T> &v)
{
_start = new T[v.capacity()];//(1)
_finish = _start + v.size();//(2)
_endofstorge = _start + v.capacity();//(3)
//memcpy(_start, v._start, sizeof(T)*v.size());碰到string这种自定义类型的时候浅拷贝导致同一块空间被析构两次。//(4)
for (int i = 0; i < v.size(); i++)//(5)
_start[i] = v._start[i];
}
( 1 ) (1) (1)申请新空间。
( 2 ) ( 3 ) (2)(3) (2)(3)更新_finish
和_endofstorage
。
( 4 ) (4) (4)这里不能使用memcpy
,和reserver
一样,当vector
存储的数据类型是自定义类型string
的时候,这里就是一个浅拷贝,而每个对象出来作用域就会被析构,就会导致堆区内存重复释放。
( 5 ) (5) (5)浅拷贝的问题需要通过深拷贝进行解决。
void swap(vector<T>&v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorge, v._endofstorge);
}
vector(const vector<T>&v)
:_start(nullptr),//(1)
_finish(nullptr),//(2)
_endofstorge(nullptr)//(3)
{
vector<T> tmp(v.begin(),v.end());//(4)
swap(tmp);//(5)
}
( 1 ) ( 2 ) ( 3 ) (1)(2)(3) (1)(2)(3)因为一开始指向的空间是随机值,这样在释放空间的时候会导致非法访问,所以需要置空。
( 4 ) (4) (4)复用有参构造函数
( 5 ) (5) (5)直接进行空间交换就好,tmp
是个临时对象,出了函数作用域会被析构,空间会被释放。
iterator find(iterator first,iterator last,T val )
{
while (first!=last)
{
if (*first == val)//(1)
return first;
first++;
}
return last;//(2)
}
( 1 ) (1) (1)找到了就返回该元素的迭代器位置
( 2 ) (2) (2)没找到返回末尾迭代器
在了解什么是迭代器失效之前,我们先看一下vector
中insert
的模拟实现。
我们知道,插入数据的时候需要扩容。并且需要从插入位置开始挪动数据。
原因正是出在这,扩容以后_start
,_finish
,_endofstorage
都指向了新空间的位置。但是pos
指向的还是原空间,但是原空间已经被释放,这时pos
就是一个野指针。挪动数据的时候就会失败。
void insert(iterator pos,T val)
{
if (_finish == _endofstorge)//这里扩容会导致迭代器失效
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
iterator right = _finish;
iterator left = pos;//因为增容,pos原来指向的空间会被释放,所以需要计算出新的pos的位置
while (left < right)
{
(*right) = *(right - 1);
right--;
}
*pos = val;
_finish++;
}
解决代码如下:
void insert(iterator pos,T val)
{
if (_finish == _endofstorge)//这里扩容会导致迭代器失效
{
int sz = pos - _start;//(1)
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + sz;//(2)
}
iterator right = _finish;
iterator left = pos;//因为增容,pos原来指向的空间会被释放,所以需要计算出新的pos的位置
while (left < right)
{
(*right) = *(right - 1);
right--;
}
*pos = val;
_finish++;
}
加上(1)和(2)代码以后就可以正常插入数据了。
( 1 ) (1) (1)提前计算pos
的位置
( 2 ) (2) (2)扩容完成后更新pos
这时候insert
在插入数据的时候迭代器不会失效了,但是还有一种场景迭代器失效的场景存在:
如果我想打印插入的数据呢?这时候实参传递的it
迭代器会不会失效呢?
答案是会的,在前面我们只是对pos
迭代器进行了矫正,但是传参过程是一个值传参,形参的改变不会影响实参。也就是说it
还是指向旧空间。
解决办法:返回值进行接收,it
接收insert
函数返回pos
更新以后的位置。
void test10()//测试insert
{
vector<int> v1;
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
v1.push_back(40);
auto it=v1.find(v1.begin(), v1.end(), 30);
if (it != v1.end())
{
v1.insert(it, 50);//(1)
cout << *it << endl;
}
}
( 1 ) (1) (1)外部迭代器仍然是失效的
正确代码:
iterator insert(iterator pos,T val)
{
if (_finish == _endofstorge)//这里扩容会导致迭代器失效
{
int sz = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + sz;
}
iterator right = _finish;
iterator left = pos;//因为增容,pos原来指向的空间会被释放,所以需要计算出新的pos的位置
while (left < right)
{
(*right) = *(right - 1);
right--;
}
*pos = val;
_finish++;
return pos;
}
void test10()//测试insert
{
vector<int> v1;
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
v1.push_back(40);
auto it=v1.find(v1.begin(), v1.end(), 30);
if (it != v1.end())
{
it=v1.insert(it, 50);//(1)
cout << *it << endl;
}
}
( 1 ) (1) (1)接收pos
更新以后位置的迭代器
iterator erase(iterator pos)
{
//删除pos位置的数据
iterator left = pos;
iterator right = _finish;
while (left<right)//(1)
{
(*left) = *(left + 1);
left++;
}
_finish--;
return pos;
}
( 1 ) (1) (1)挪动数据。
( 1 ) (1) (1)别忘了 更新_finish
的位置。
总结:
1.拷贝构造传统写法和reseve一定要注意使用,避开浅拷贝带来的问题。
2.insert带来的迭代器失效也需要认真画图思考一下。
3.在模拟实现的时候,迭代器,模板参数T,typdef以后很容易让人搞乱,大家可以多想一想,并可以根据下图进行思考。