👊👊你间歇性的努力和蒙混过日子,都是对之前努力的清零!
目录
3.9.3 赋值重载(现代写法)(再提前面reserve拷贝数据写法)
①拷贝数据不能用memcpy()函数,对于某些存储类的vector(比如存储string),会发生浅拷贝,后面会详细说浅拷贝导致的结果!
4.4 insert() 指定位置插入(迭代器失效问题引入)
5.2.1 空间异地扩容导致迭代器变为野指针(insert)
5.2.2 空间意义变化(迭代器所指空间数据变化)erase举例
一、vector介绍
1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
4. vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。
5. 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
6. 与其它动态序列容器相比(deque, list and forward_list), vector在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。
二、基本框架
namespace wyz//与标准库vector区别,自己定义一个命名空间,在里面写!
{
template<class T>
class vector
{
public:
//构造函数
vector()
vector(const vector<T>& v)
void swap(vector<T>& v)
vector<T>& operator=(vector<T> tmp)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
vector(int n, const T& val = T())
//析构函数
~vector()
//返回空间数据个数
size_t size()const
//返回空间大小
size_t capacity()const
//预开辟空间
void reserve(size_t n)
//预指定数据个数
void resize(size_t n, T val = T())
//迭代器
typedef T* iterator;
typedef const T* const_iterator;
iterator begin()
iterator end()
const_iterator begin()const
const_iterator end()const
//返回数组指定位置内容
T& operator[](size_t pos)
const T& operator[](size_t pos)const
//判断是否为空
bool empty()
//尾插
void push_back(const T& x)
//尾删
void pop_back()
//指定位置插入
iterator insert(iterator pos, T x)
//指定位置删除
iterator erase(iterator pos)
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
}
vector私有成员变量讲解:
三、成员函数自我实现
3.1 size() 返回数据个数
size_t size()const
{
return _finish - _start;
}
3.2 capacity()返回空间容量
size_t capacity()const
{
return _end_of_storage - _start;
}
3.3 empty() 判断是否空间没有数据
bool empty()
{
return _start == _finish;
}
3.4 clear() 清理空间数据
void clear()
{
_finish=_start;
}
3.5 operator[] 获取指定下标数据
//可读可写
T& operator[](size_t pos)
{
assert(pos<size());
return _start[pos];
}
//只读
const T& operator[](size_t pos)const
{
assert(pos<size());
return _start[pos];
}
3.6 reserve() 预开辟空间
先申明一下:reserve不会缩容!当n<=capacity() 该函数什么也不做!
这里有两点细节需要注意:
①拷贝数据不能用memcpy()函数,对于某些存储类的vector(比如存储string),会发生浅拷贝,后面会详细说浅拷贝导致的结果!
②更新_finish的时候 不能写成_finish=_start+size();因为size() 返回的是_finish-_start,这里_start已经更新,然而_finish没有更新!所以我们要先记录原来数据个数!
void reserve(size_t n)
{
if (n > capacity())
{
//记录数组有效个数
size_t sz = size();
//开辟新空间
T* tmp = new T[n];
//拷贝
if (_start)//原空间不为空,拷贝到新vector
{
for (int i = 0;i < sz;i++)
{
tmp[i] = _start[i];
}
delete[]_start;
}
//更新指针
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
3.7 resize() 预指定数据个数和内容
申明:当n<capacity(),resize不会缩容!
细节:这里传参默认缺省值T() 这样做是考虑到如果传的参数是类对象!需要匿名构造对象!
void resize(size_t n, T val = T())
{
if (n > capacity())
{
//预开辟空间
reserve(n);
//尾插
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
//n<capacity() 直接修改_finish位置,不需要挪动数据
else
{
_finish = _start + n;
}
}
3.8 迭代器实现
3.8.1 迭代器实现函数
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;
}
3.8.2 迭代器遍历
//只读遍历
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << (*it);
it++;
}
3.9 构造函数
3.9.1 默认构造函数
vector()
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
3.9.2 拷贝构造(深拷贝+现代写法)
这里注意要初始化指针!不让它们成为野指针!
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//预开辟空间后,让尾插不需要额外开辟空间,更加效率!
reserve(v.size());
for (auto& e : v)
{
push_back(e);
}
}
3.9.3 赋值重载(现代写法)(再提前面reserve拷贝数据写法)
void swap(vector<T>& v)
{
//这里用的swap交换对象是内置成员,需要调用std标准库swap!
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
//先调用拷贝构造函数初始化tmp,再交换!交换完后局部变量tmp出作用域,
//自动调用析构函数释放所☞空间,最后就可以简写成一下代码
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
①拷贝数据不能用memcpy()函数,对于某些存储类的vector(比如存储string),会发生浅拷贝,后面会详细说浅拷贝导致的结果!
if (_start)//原空间不为空,拷贝到新vector { for (int i = 0;i < sz;i++) { tmp[i] = _start[i]; } delete[]_start; }
memcpy()底层是逐字节拷贝,也就是浅拷贝!
如果vector存储数据是类对象,如果类是Date这样的类对象,memcpy浅拷贝可以满足需求,不会出现问题!如果vector存储的是string、vector这样有指针的内置成员类,如果浅拷贝,会使得程序崩溃!原因我在另一篇博客C++类与对象中篇详细说明。
所以这里我们需要用到自己写的赋值重载拷贝数据!
3.9.4 迭代器区间构造
//这里为了更加直观化,不用T* 而是重新定义一个模板Inputiterator
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(last - first);
while (first != last)
{
push_back(*first);
first++;
}
}
3.9.5 指定数据个数并初始化构造函数
💡💡下面有两种写法,有一种出现的问题很特别!这里很重要,涉及很多知识!
1.会报错的写法,代表vector<int> v(5,1) 调用函数的时候
vector(size_t n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
while (n)
{
push_back(val);
n--;
}
}
!!!为什么会出现非法寻址?
这里就要结合上一个构造函数迭代器区间构造函数!我们知道5,1是默认是int类型,这样两者类型一样,我们理所当然认为一定调用现在这个构造函数!只不过int会隐式类型转换成size_t ,然后模板推演成int(注意推演是你是什么就是什么类型,不会类型转化!)也就说两者类型不同!然而真的会调用这个函数吗?
我们知道地址本身是十六进制表示的,也就是说5,1也是一个地址,这样就麻烦了!因为我们之前写的那个迭代器区间两个参数是地址!而且这两个参数类型一样!也就是说5,1也可以调用这个模板构造函数!这样同样是模板,这个5,1调用选取模板优先级当然就是先选最匹配自己的也就是参数类型相同的迭代器区间构造!但是5,1是什么?是野指针!这样解引用就是非法访问!所以会报错!
这里问题关键就是优先级!如果我们是这样调用函数:vector<char> v(5,'a') 这样问题就不会出现了!因为两者类型不同,这样自然就会调用现在这个构造函数了!
2.正确写法(不让int 隐式类型转化)这里根据需要如果传的数据是long 或者其他,需要我们自己写重载
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
while (n)
{
push_back(val);
n--;
}
}
四、数据的挪动
4.1 push_back() 尾插
void push_back(const T& x)
{
//判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*(_finish) = x;
_finish++;
}
4.2 pop_back() 尾删
void pop_back()
{
assert(!empty());
_finish--;
}
4.3 erase() 指定位置删除
//为什么返回iterator,后面迭代器失效讲
iterator erase(iterator pos)
{
assert(!empty());
iterator begin = pos;
while (begin != _finish - 1)
{
*begin = *(begin + 1);
begin++;
}
_finish--;
return pos;
}
4.4 insert() 指定位置插入(迭代器失效问题引入)
void insert(iterator pos, T x)
{
//记录相对位置
size_t n = pos - _start;
//判断是否需要扩容
if (_finish == _end_of_storage)//异地扩容,迭代器失效
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
pos = _start + n;
}
iterator end = _finish;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*(end + 1) = x;
_finish++;
}
这里需要记录pos相对_start的相对位置! 因为如果数据插入导致需要异地扩容,原来迭代器指针所指区域被释放,迭代器失效!后面详谈什么情况迭代器失效!
五、迭代器失效
5.1 迭代器失效概念
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装。因此 迭代器失效,实际就是迭代器底层对应指针所指向的 空间被销毁了,而使用一块已经被释放的空间 ,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。
5.2 什么情况迭代器失效
5.2.1 空间异地扩容导致迭代器变为野指针(insert)
错误写法:没有记录相对位置
这里已经很明显了pos指向位置值变为随机值了!
5.2.2 空间意义变化(迭代器所指空间数据变化)erase举例
我们来看看标准库如果调用如下代码会是什么结果!
void vector_test_2()
{
std::vector<int>v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
std::vector<int>::iterator pos = find(v.begin(), v.end(), 1);
v.erase(pos);
cout << *pos;
}
这是VS PJ 编译器结果!这里发生了迭代器失效,可以理解为因为空间数据的变动,编译器底层使迭代器指向位置发生变化!
如何解决呢?当然就是让迭代器继续指向该指向的位置!所以我们需要返回原先所传的位置!所以erase函数返回iterator,也就是返回传进来时候迭代器的位置!
5.3 如何解决迭代器失效
一句话:对迭代器重新赋值即可!