vector 一.vector的介绍及使用 1.1vector的介绍 1.vector时可变大小数组的序列容器。 2.类似于数组,vector也采用连续储存空间来储存元素。vector支持下标访问元素。 3.vector使用动态分配数组来储存他的元素。当新的元素插入的时候,这个数组需要被重新分配大小。 4.vector分配空间策略:vector会分配一些额外的空间来适应可能的增长,因为储存空间比实际需要的存储空间更大。重新分配都应该是对数增长的间隔的大小,以至于在末尾插入一个新的元素的时间复杂度往往为O(1). 5.vector占用了更多的储存空间,为了获得管理储存空间的能力,以一种有效的方式动态增长。 6.与其他动态序列容器相比,vector在访问元素的时候更加高效,在末尾添加或删除元素相对高效,但在中间位置进行相关操作,成本稍高。 1.2vector使用 1.2.1vector定义
构造函数声明 | 接口声明 |
---|
vector() | 无参构造 | vector(size_type n,const value_type& val = value_type()) | 构造并初始化n个val | vector(const vector& x) | 拷贝构造 | vector(inputIterator first,inputIterator last); | 使用迭代器进行初始化构造 |
示例: ? 1.2.2vector iterator 的使用
iterator | 接口说明 |
---|
begin+ end | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator | rbegin+ rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
示例: ? 1.2.3vector空间增长问题
容量空间 | 接口说明 |
---|
size | 获取数据个数 | capacity | 获取容量大小 | empty | 判断是否为空 | resize | 改变wector的size | reserve | 改变vector的capacity |
capacity再vs下是1.5倍增长,g++下是2倍增长。 ? ? reserve只负责开辟空间。 resize支持开辟空间的同时也会进行初始化,影响size。 可以看得到,前期扩容比较频繁,可以使用reserve进行提前扩容,以提升性能。 ? ? 1.2.3vector增删查改
增删擦改 | 接口说明 |
---|
push_back | 尾插 | pop_back | 尾删 | find | 查找(属于算法模块) | insert | 在pos位置插入数据 | erase | 删除postion位置的数据 | swap | 交换两个vector的数据空间 | opertaor[] | 像数组一样访问 | ? ? 1.2.4vector迭代器失效问题 迭代器的主要作用是让算法能够不关心底层数据结构,其底层实际上是一个指针,或者是针对指针进行了封装。 vector的迭代器就是原生指针T*,迭代器失效就是迭代器底层对应指针所指向的空间被销毁了,使用了一块已经释放的空间。造成的结果就是程序崩溃。 对于vector可能导致迭代器失效的操作有: 1.引起空间底层改变的操作,可能引起迭代器的失效。比如:resize,reserve,insert,assign,push_back。 2. 指定位置元素的删除操作--erase 1.erase的失效都是意义变了,或者不在有效访问数据范围。 2.一般不会使用缩容方案,那么erase的失效,一般不存在野指针的失效。 一般vector删除数据,都不会考虑缩容方案。 缩容方案:size()<capacity()/2时,可以考虑开一个size()大小的空间,拷贝数据,释放旧空间。 缩容的本质是时间换空间。一般设计都不考虑缩容,因为实际关注的大都是时间效率,不关注空间效率,因为现在的硬件设备都比较大,时间储存也比较便宜。 erase(pos)以后pos就失效了,pos的意义变了,但是不同平台下面对于访问pos的反应是不一样的。我们用的时候要小心,统一以失效角度去看待 3. 注意:Linux下,g++编译器对迭代器失效的检测并不是非常严格,处理也没有vs下极端。 g++对于erase迭代器失效检查时就非常的佛系,但在实际场景中,迭代器的意义变了,也会出现各种问题 *补充:vector迭代器失效有2种: 1.扩容,缩容,导致野指针,失效。 2.迭代器指向位置意义变了。 * 系统的越界机制检查。 ---- 不一定能查到。 编译实现机制检查。 ---- 相对靠谱。 二.模拟实现vector部分接口# 2.1关于底层的问题## vector本身就是容器,意味着vector本身就必须满足各种类型的变量,即vector要使用模板。 准备阶段: namespace bit
{
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
*******
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
}
} 2.2 构造函数 无参的默认构造函数 只需要将private中的参数设置成nullptr即可 //实现默认构造函数
//无参
vector()
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{} 迭代器的方法初始化一段空间 采用模板,使得其能够泛型编程 //构造函数,拷贝构造一段空间
template<class InputIterator>
vector(InputIterator first, InputIterator last)
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
} 构造并初始化一段空间 这个接口并不常用,但是是给构造函数铺路用的 //构造并初始化一段空间
vector(size_t n, T& val = T())//给予一个默认构造函数
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//预开n个大小的空间
reserve(n);
//插入val
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
} 还需要重载一个(int, T)类型的,因为无法完成类似于vector<int> v(10,2)的初始化。 //构造并初始化一段空间
vector(int n, T& val = T())//给予一个默认构造函数
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//预开n个大小的空间
reserve(n);
//插入val
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
} 交换函数 采用c++库中的swap接口即可,本质上是交换指针 //交换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);
} 拷贝构造 采取的是现代化写法,编译器自己生成vector后,通过swap函数,交换指针即可 //拷贝构造
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
} 重载= 现代化写法 //重载=
vector<T> operator=(vector<T> v)//这里已经进行了一次拷贝构造了
{
swap(v);
return *this;
} 2.3实现size 和 capacity接口 值得注意的是size指的是有效数据的大小 capacity指的是可以储存的有效数据的大小 因为类型没有规定,所以要单独使用接口来获取 //大小
size_t size()const
{
return _finish - _start;
}
//容量
size_t capacity()
{
return _end_of_storage - _start;
} 2.4迭代器部分接口实现 主要实现迭代器的begin 和 end 接口,保证正向迭代器能够正常使用,至于++不需要重载,因为 本身底层就是使用的“迭代器”,指针++可以跳过一个类型的长度。 <u>迭代器要满足const && 非const类型的指针都能够使用。<u> //迭代器返回begin end
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;
} 2.5模拟实现reserve reserve就是进行扩容的函数接口 //实现扩容接口reverse
//上面提及扩容会导致迭代器失效,这里就是因为扩容销毁了更改了原有的指针指向
//导致pos指针成了野指针,所以要更新pos指针。
/***更新方法:记录pos和_start之间的相对距离,以便更新pos***/
void reserve(size_t n)
{
size_t sz = size();
//避免无端的申请扩容
if (n > capacity())
{
T* tmp = new T[n];
//判断数据是否需要拷贝
if (_start)
{
//memcpy(tmp, _start, size() * sizeof(T));
//memcpy是浅拷贝,会导致内存泄漏的风险。
? ? ? ? ? ? ? ? ? ?for(size_t i = 0;i<size(),i++)
? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? tmp[i] = _start[i];
? ? ? ? ? ? ? ? ? }
delete[] _start;
}
//更新private数据
_start = tmp;
}
_finish = _start + sz;
_end_of_storage = _start + n;
} 2.5模拟实现resize接口 string提及过resize可以更改capacity和size,并且还能进行相应的初始化,vector也一样。
-
n>capacity 进行扩容即可 -
n<=capacity 1.n>size 那么将size至capacity之间的空间赋值val。 2.n<size 截断即可。
void resize(size_t n, T val = T())//单参数支持隐式类型的转换
{
//n > capacity
if (n > capacity)
{
reserve(n);
}
else
{
//n>size() && n<capacity
if (n > size())
{
while (_finish < _end_of_storage)
{
*_finish = val;
_finish++;
}
}
else
{
_finish = _start + n;
}
}
} 2.6模拟实现push_back && pop_back## push_back是尾插,尾插就需要考虑扩容和越界访问的问题 pop_back是尾删,要考虑越界访问的问题。 //插入push_back
void push_back(const T& val)
{
//判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 :capacity()* 2;
reserve(newcapacity);
}
//插入数据
*_finish = val;
_finish++;
}
?
//pop_back
void pop_back()
{
assert(_finish > _start);
--_finish;
} 2.7重载[] vector是在堆上开辟的一段连续的空间,用于储存数据的容器,所以它可以支持类似于数组的下标直接访问的优势,重载[]是必不可少的。 //重载[]
//为了减少拷贝构造,可以引用返回。
T& operator[](const size_t pos)
{
assert(pos < size());
?
return *(_start + pos);
}
//const版本
const T& operator[](const size_t pos)const
{
assert(pos < size());
?
return _start[pos];
} 2.8模拟实现insert && ersae 对于insert要考虑: 1.给定的pos是否合法? 2.插入是否会需要扩容? 3.pos指针是否会失效,导致迭代器失效? 4.挪动数据是否会出现问题? 5.是否需要更新基底? 对于earse要考虑: *1.给定的pos是否合法? 2.是否存在越界删除? 3.是否需要更新基地?* //模拟insert
iterator insert(iterator pos, const T& val)
{
//检查pos是否合法
assert(pos>=_start && pos<=_finish);
//判断是否需要扩容
if (_finish == _end_of_storage)
{
size_t n = pos - _start;
?
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
?
pos = _start + n;
}
//开始挪动数据
//扩容完成后,pos的位置会指向不明确,会使得迭代器失效。需要单独处理pos.
auto end = _finish -1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
return pos;
}
iterator erase(iterator pos)
{
//判断pos是否合法
assert(pos >= _start && pos <= _finish);
auto it = pos;
while (it < _finish)
{
*it = *(it + 1);
it++;
}
--_finish;
return pos;
} 2.9清屏函数 清屏:将有效数据删除 void clear()
{
_start = _finish;
} 3.0资源管理 //资源管理
~vector()
{
if (_start)
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
} vector完结。 |