🍅可以先去这个网站看一下个个函数的功能 本文不再详细介绍,vector的底层还是顺序表,我讲的很详细,建议没学过顺序表的先预习一下(主页搜索顺序表,还有配套习题)
C++网站关于vector的接口函数信息
目录
☃️1.简单框架搭建
☃️2.稍难函数和迭代器失效问题
☃️3.深浅拷贝的深度理解
☃️4.容易看不懂的函数的类型复盘和总结
☃️1.简单框架搭建
为了和库里面的vector不冲突,我们自定义一个命名空间
通过看vector的源码,我们发现他的成员变量有
最后一个就是指容量位置的迭代器
很多接口函数可以去文章首的网站里看,本文基本全部实现最常用最经典的 有分析价值的函数
最基本的构造函数(不止一个 后面还有更复杂的) 析构函数
~vector()
{
delete[] _start;
_start = nullptr;
_finish = nullptr;
_endstorage= nullptr;
}
还有很短的begin() end()
运算符重载[ ]
T& operator[](size_t pos)
{
assert(pos < size());
return _start[pos];
}
很基础的功能 尾插
void push_back(T val)
{
if (capacity() == size())
{
//需要扩容
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = val;
_finish++;
}
resize()
void resize(size_t n, T val = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())
{
while ((n - size() + 1)--)
{
*_finish = val;
_finish++;
}
}
else
{
_finish = _start + n;
}
}
reserve() 这个函数很坑 这个代码目前是有问题的,但是在现在讲解的阶段实现成这样完全没问题
注意:扩容的时候最好
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
memcpy(tmp, _start, sizeof(int) * oldsize);
delete[] _start;
}
_start = tmp;
_finish = _start + oldsize;
_endstorage= _start + n;
}
//如果n更小不需要扩容
}
取capacity() empty()
size_t capacity()
{
return _endstorage- _start;
}
bool empty()
{
return _finish == _start;
}
size_t size() const
{
return _finish - _start;
}
尾删
void pop_back()
{
assert(empty());
_finish--;
}
☃️2.稍难函数和迭代器失效问题
现在是稍微有点挑战难度的函数
这里面的allocator是内存管理器,目前我们还不需要关心这是什么 以后会更新一篇博客专门分析这个问题
其实我们发现 构造函数有很多形式 现在实现稍微复杂一点的
第二个形式,n个val
val的类型是value_type,在文档里也可以查
这里的成员类型都标注的很清楚
首先要初始化肯定要开空间,直接reserve(n),然后每一个都赋值成val
vector(int n, const T& val = T())
:_start(nullptr)
, _finish(nullptr)
, _endstorage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
看起来很美妙是不是,但是这个以后都是坑,因为之前说的reserve有问题
插入insert()
在pos迭代器位置插入val 首先判断pos是否合法
如果容量不够(_finish==_endstorage)就扩容,这里涉及到迭代器失效的问题
请问这个时候你还敢用扩容之后的pos?
因为一般扩容都是异地,但是这个pos的位置就应该改变但是没变,很尴尬,这个问题不一定出现(如果原地扩容就不会有问题)但是谁能一定保证呢,最好还是更新一下pos的位置
容量没问题之后就开始挪动数据 把前面的顺延到后面,最后把空出来的pos位置填上,把_finish 的位置更新
iterator insert(iterator pos, const T& val)
{
assert(pos >= _start);
assert(pos < _finish);
if (_finish == _endstorage)
{
size_t len = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
// 扩容会导致pos迭代器失效,需要更新处理一下
pos = _start + len;
}
// 挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = val;
++_finish;
return pos;
}
删除pos位置,还是先判断位置合法,然后向前覆盖,更新_finish
为什么文档里的设计要有返回值?
其实可以试一下 实现:删除某个位置的元素,然后再去修改迭代器指针
此时程序会崩溃
为什么?
其实和刚才的pos问题一样,erase的时候pos还是不安全,此时的it是野指针,不能再对it++之类的操作
或者实现一下删除所有偶数
其实一开始这个vector里面是1234应该是段错误,第二次是122345 但是结果却是1235
显然我们这样没有返回值的erase是不可以的
首先解释第一个段错误(一般Linux这样报错就是越界或者野指针问题)
当走完4 删除之后 _finish ++ 但是啊it也++ ,一直向后走,永远没有和end()相等的时候,就会一直走下去
这样就会少删除数据
但是怎么解决呢,千万不能根据某个具体情况去更改,不得不说还是大佬厉害,用一个返回值,返回删除元素的下一个位置的迭代器
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *(begin);
++begin;
}
--_finish;
return pos;
}
clear() swap() 函数就是小case啦
void clear()
{
_finish = _start;
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endstorage, v._endstorage);
}
拷贝构造的几种写法
其实很秀的这里,我们最传统的写法就是自己开空间 然后拷贝
一个一个拷贝!不要memcpy(剧透:reserve的坑就是memcpy,思考一下为什么C++不继续沿用melloc?free?就是告诉你别再C了,用C++解决问题)
拷贝构造的几种写法
v1(v2)
vector(vector<T>& v) //传统写法
{
_start = new T[v.capacity()]; //按照v的大小开空间
for (size_t i = 0; i < v.size(); i++)
{
_start[i] = v._start[i]; //一个一个拷贝
}
_finish = _start+v.size();
_endstorage =_start+v.capacity();
}
还有一个稍微创新的传统写法,很狡猾嘛,但是可惜了reserve有问题
另一种传统方法
vector(vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endstorage(nullptr)
{
reserve(v.capacity());
for (auto& e : v) //一定要加上&,不知道v里面元素类型,如果是自定义类型需要深拷贝
{
push_back(e);
}
}
来看看创新的写法
刚才看构造函数不还有用迭代器构造的嘛(第三种),我们就实现一下
然后我直接实例化一个tmp对象,再tmp和this交换一下
//很新的方法
template <typename Inputiterator>
vector(Inputiterator first, Inputiterator last)
:_start(nullptr)
, _finish(nullptr)
, _endstorage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector(vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endstorage(nullptr)
{
vector<T> tmp(v.begin(), v.end());
swap(tmp); //this 和 tmp 交换
}
那么=运算符就很好写了,直接把v赋给this,但是很少数的情况会v=v,所以是否判断都可以
vector<T>& operator=(vector <T>v)
{
swap(v);
return *this;
}
☃️3.深浅拷贝的深度理解
我们都知道浅拷贝就是以bit为单位,一个一个拷贝,但是一些指针问题会导致对同一块空间的两次释放,对于自定义类型 我们来看一下会出现什么神奇的事情
测试这段代码
void test()
{
vector<vector<int>> vv;
vector<int > v(5, 1);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
vv.push_back(v);
for (size_t i = 0; i < vv.size(); i++)
{
for (size_t j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
}
结果是
分析,最开始是这样的
然后开始push_back ,最开始vv的容量是0,所以需要扩容,但是扩一次是4个还不行,还得扩二倍就是八个, 扩容 调用reserve
tmp前五个_start还是指向5个v,然后memcpy拷贝,然后delet[ ] _start
是不是问题就来了,我vector<int>都mb了啊,我的所有元素都变成野指针了.....
谁的锅!reserve()里面的memcpy,因为他是浅拷贝,如果拷贝的时候可以开一份空间就好了
那么就用赋值好了,_tmp[i]=_start[i] 不管是传统写法还是现代写法都会开空间的(v1=v2,是会开空间再拷贝的)
所以代码应该写成这样
void reserve(size_t n)
{
if (n > capacity())
{
size_t oldsize = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(int) * oldsize); //这个地方有问题,自定义类型的浅拷贝
for (size_t i = 0; i < oldsize; i++)
{
tmp[i]=_start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + oldsize;
_endstorage = _start + n;
}
//如果n更小不需要扩容
}
☃️4.容易看不懂的函数的类型复盘和总结
我们这里定义了模板T,这个T在vector <int> 的时候就是int 在vector<vector<int>> 的时候就是vector<int>
迭代器的类型就是和模板类型有关的
[ ]的赋值重载就应该是T类型的,至于&就是赋值拷贝的基本操作了,我们之前说过可以不加&的,但是一般来讲我们都更喜欢用筷子吃饭(虽然直接用手也可以)
他还有一个const成员函数的函数重载,当然返回值也要是const啦
这个拷贝构造的意思就是用n个val初始化val的值 类型一定是模板类型,因为这个容器里元素类型就是模板T,加上const的意思就是val的值是不能在vector这个构造内部改变的,当然如果你不写那么我们默认是T(),这是一个匿名对象,对于int来说就是0,还有这里有一个引用,意思就是万一你传过来的是指针之类的,我也可以对你直接操作 无需考虑几级指针,形参的改变影不影响实参之类的
push_back里面这个x同样的,形参的改变会影响实参
这个函数的参数是一个对象v,他的类型和this的类型一样都是,&也是为了形参的改变影响实参
其实也就是这个vector 的实现 有上面几个类型不好理解,其他的都很简单,C++的细节就是很多,希望大家不厌其烦,我们一起学好!