文章目录
基本结构
template<class T>
class vector
{
public:
typedef char* iterator;//类型重定义出迭代器
typedef const char* const_iterator;
vetor(){}
vector(const vector<T>& v);//拷贝构造
template<class InputIterator>//额外的模板
vector(InputIterator first, InputIterator last);//以迭代器区间构造初始化
vector(size_t n,const T& v=T());//以n个T类型构造初始化
vector<T>& operator=(vector<T> tmp); //赋值重载
~vector();//析构
void push_back(const T& t = T());
void pop_back();
iterator insert(iterator pos, const T& t);
iterator erase(iterator pos);
void reserve(size_t n);
void resize(size_t n,const T& t=T());
T& operator[](size_t n);
....
....
private:
T* _start;//空间起始地址
T* _finish;//有效数据结束地址 _finish-_start 就是size 有效字符个数
T* _end_of_storage;//空间结束地址 _end_of_storage-_start 就是容量
}
vector的成员变量就是上面的三个指针 这三个指针的差的组合可以表示vector的元素个数和容量 元素个数就是finish-start 容量就是end_of_storage-start
建议阅读顺序:
- 各个模块之间是有这紧密的联系的 一个函数里会调用其他的函数 要看懂一个函数就得清楚其里面复用的函数是怎样实现的 下面是几个比较重要的函数的推荐的阅读顺序 按照这个顺序慢慢阅读 就可以将本文的所有函数实现摸明白了
reserve() -> resize() -> 尾插push_back() -> insert() -> erase() -> 普通构造 -> 拷贝构造 -> 赋值重载 -> …(剩下的理解较容易不列举了)
一、默认成员函数
1.构造函数
1.1无参构造
vector()
:_start(nullptr)//初始化列表初始化 将三个标志位的指针全置为空
,_finish(nullptr)
,_end_of_storage(nullptr)
{
}
- 构造函数就需要初始化成员变量 最开始数据个数为0 容量为0 那么就是将三个指针都设为空 通过初始化列表完成初始化
2.带参构造
2.1迭代器区间构造
template<class InputIterator>//额外的模板 使得可以接收任意类型的迭代器
vector(InputIterator first, InputIterator last)
:_start(nullptr)//初始化列表初始化
,_finish(nullptr)
,_end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
++first;
}
}
创建一个新的对象还可以用一个已经存在的对象的迭代器区间进行构造 这里为了可以接收任意类型的迭代器区间 可以再定义一个模板 ,接下来就是用调用push_back()函数(下面有介绍)来向新对象中尾插传入对象的每个元素 完成拷贝构造
2.2用n个T类型的对象构造
vector(size_t n,const T& v=T())
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(v);
}
}
//重载版本 如果只有上面一种特殊情况下就可能会与迭代器区间构造冲突 使得不能正确匹配
vector(int n, const T& v)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(v);
}
}
- 例如创建一个对象并用10个3初始化 就可以调用上面的构造函数
//调用示例
vector<int> v(10,3);
原理:原理其实并不难 第一个参数是我们给定的元素个数 那么就可以先调用reserve()函数(下面有介绍)提前开辟出空间
空间开好后就剩下了数据的放置了 仍然可以调用push_back()函数实现尾插数据 完成构造 (push_back()很关键 这里不太懂,可以往下找push_back() 先将其理解,再返回此处继续阅读)
2.拷贝构造
这里的拷贝构造用的是现代写法 我们上面实现了用一段迭代器区间来构造对象的构造函数 那么就可以利用这个函数先构造出一份与实参对象数据相同的局部对象tmp(tmp可看作实参的一份临时拷贝),再将构造出来的实参对象的临时拷贝 的三个指针与当前对象交换 ,从而实现数据的交换 ,也不需要析构 ,因为交换后tmp的生命周期就结束了,就会自动调用析构函数 在此之前我们已经把我们想要的数据和tmp交换了(因为交换了指针) 恰好完成了我们的目的。
//封装一个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(const vector<T>& v)//这里使用的是已知类型的vector<T>
:_start(nullptr)
,_finish(nullptr)
,_end_of_storage(nullptr)//这里的拷贝构造的初始化为空指针不能少 否则会出现原来的空间析构两次 报错
{
vector<T> tmp(v.begin(), v.end());
swap(tmp);
}
3.赋值重载
也是一个现代写法 利用构造函数先构造出我们想要的对象(局部对象,出作用域销毁) 然后交换当前对象的数据(交换指针就可) ,就可以实现赋值重载。原来的对象的数据会被交换给构造出来的局部对象,其出了作用域会自动调用析构函数完成资源的清理空间的释放,不需要我们自己去手动释放原来的空间,这就是现代写法的好处。
//封装一个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<T>& operator=( vector<T> tmp)
{
swap(tmp);
return *this;
}
4.析构函数
首先判断该对象的起始地址_start是否为空 ,为空就不能释放(说明该对象就是空的没有数据),非空说明有数据,就需要调用delete[] 去释放我们开辟的连续的空间,完成空间的释放,最后将三个指针置空,防止出现野指针。
~vector()
{
if (_start)//判断_start 为空的时候不能释放
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
}
5.迭代器
- 该容器的迭代器有begin() 和 end() 其中begin()就是空间的起始地址,end()就是起始地址往后挪动数据个数个单位,也就是_finish这个指针了。
如下图:begin() 指向的是1的位置(vector中的迭代器就是原生指针) end()就是往后挪动4个单位指向4后面一个位置。
我们用迭代器遍历的时候就可以让begin()往后走 直到和end()相等就遍历完整个容器
//非const对象的迭代器
iterator begin()//这个就不用加const了 虽然语法上可以 但是就不能构成重载了
{
return _start;
}
iterator end()
{
return _finish;
}
//const对象的迭代器
const_iterator begin()const
{
return _start;
}
const_iterator end()const
{
return _finish;
}
二、功能函数
1.尾插、尾删
尾插
尾插是需要注意的是判断容量是否满了,满了就需要进行扩容处理。
扩容(下面有介绍扩容和代码)包括四个步骤:
1.开辟新的空间
2.将原来的数据拷贝到新空间上
3.复原start和finish的相对关系(保持扩容后未插入数据前的start和finish的距离相等)
4.将新空间给当前对象使用,释放旧空间
void push_back(const T& t = T())//引用传参 防止传参时发生拷贝构造 提高效率
{
if (_finish == _end_of_storage)//扩容
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = t;
_finish++;
//insert(end(), t);//也可以复用insert实现
}
尾删
尾删非常简单 如果里面有数据 就让_finish往前挪动一个单位 代表元素个数减少了一个
void pop_back()
{
if (size() > 0)
--_finish;
}
2.插入
传入一个合法的随机迭代器 在该位置上插入单个数据,就需要先判断是否需要扩容 然后挪动数据,再插入数据,更新_finish指针即可
iterator insert(iterator pos, const T& t)//这里的pos如果扩容后就要改变 可以想到传引用但是beginend 是传值的
//有临时拷贝有常性 那么就有权限放大 只能加const 但是加const有不能改变了 所以引用不好 且官方库也不是传的引用
{
if (_finish == _end_of_storage)
{
//int distance = _finish - _start;
int distancepos = pos - _start;
reserve(size() == 0 ? 4 : size() * 2);
//_finish = _start + distance;//防止扩容后的_finish失效 将其指向新的空间的与原来空间和_start有着相同的相对距离
pos = _start + distancepos;//更新扩容后的pos 防止其仍然指向旧空间
}//扩容判断
//挪动数据
T* end = _finish - 1;
while (end >= pos)
{
*(end+1) = *(end);
--end;
}
*pos = t;
++_finish;
return pos;
}
3.删除
删除这里就是在原空间上直接覆盖数据(不会开辟释放原空间开辟新空间后将原数据拷贝到旧空间,让空间不被浪费。而是会考虑开辟空间的时间浪费较大,效率较低,所以用空间换时间,直接在原数据上进行覆盖处理然后更新_finish即可)
iterator erase(iterator pos)
{
assert(pos >= _start && pos <= _finish);
/*T* end = _finish - 1;
while (pos < end)
{
*(pos) = *(pos + 1);
++pos;
} */
T* it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish -= 1;
return pos;
}
4.operator[] 重载 实现下标访问
因为vector是的空间是连续的,所以相邻的元素的指针也是是相邻的有关联,可以用下标访问数据。起始地址(指针)指向的是第一个元素,该地址+n个单位就可以移动到后面的元素,例如下标为1的元素就是_start往后挪动1个单位所指向的元素
T& operator[](size_t n)
{
assert(n < size());
return *(_start + n);
}
5.reserve() 、resize()实现扩容缩容
reseve
扩容包括四个步骤:
-
1.开辟新的空间
-
2.将原来的数据拷贝到新空间上
-
3.复原start和finish的相对关系(保持扩容后未插入数据前的start和finish的距离相等)
-
4.将新空间给当前对象使用,释放旧空间
void reserve(size_t n)
{
int distance = _finish - _start;
if (n > capacity())
{
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, size() * sizeof(T));//拷贝数据到新空间上 如果T的类型是指针那么就会涉及浅拷贝 出错
for (size_t i = 0; i < distance; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
}
_finish = _start + distance;
_end_of_storage = _start + n;
}
resize
void resize(size_t n,const T& t=T())
{
if (n > size())
{
//int distance = _finish - _start;//放到reserve里面去
reserve(n);//容量不够扩容
//_finish = _start + distance;
}
if (n > size())//初始化
{
while (_finish < _end_of_storage)
{
*_finish++ = t;
}
}
//1 2 3 4 5 6 7 8
else//缩容 不释放空间 释放空间的消耗时间 为追求效率 采用空间换时间的策略
{
_finish = _start + n;
}
//_size = n;
}
6.获取长度size() 和 容量 capacity() 及判空isempty()
size就是 finish 减去 start capacity(容量) 就是 end_of_storage - _start
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
当size为0时就是空
bool isempty()const
{
return size() == 0;
}