我们知道:
在STL中,我们上次学了string
但其实准确来说string并不算是STL中的成员。
因为string出现的比STL要早
所以string的设计相比于STL中其他的容器要麻烦的多。
但是今天带来的vector可以说是是完完全全的STL中的容器。
这里可能还有人不知道容器是什么东西。
容器准确来说是用来处理数据的
就比如我们之前学习进程的时候,在管理文件的时候
文件:文件内容+文件属性
本质都是数据,所以我们能看到很多的链表和顺序表的成分在。
而之后我们要学习的算法
作用则是:处理数据
存储数据+处理数据可以说是组成了程序
为什么这里要讲string和vector的区别?
因为string和vector一样实际上就是一个顺序表
但是还是有很大的区别:
而vector通过模板的功能来实现,能存储多种类型的数据
string相当于将char类型的数据拎出来,为它拓宽了很多接口的同时,为了兼容C语言,保留了很多字符串的特殊点。
所以:
i. string只能存储char类型
ii. string保留了末尾是’\0’的成分
而vector则是全新的对象,不需要兼容C语言。
i. 所以vector能存储各种类型
ii. 同时末尾不用添加’\0’
我们模拟实现vector的目的:
是为了更好的了解vector来帮助我们更好的使用和学习。
所以这里就挑几个比较重要的接口进行实现
做到一个比较基础的vector。
因为这个和string的实现实在是太像了,所以就选几个比较重点的讲
其他未讲到的会后面会放在整体代码中。
这里不同于string的capacity和data实现方法
这里vector用的是三个指针成员变量的实现方法,但是实际上没有相差多少,用string的同样可以实现。
class vector
{
public:
private:
iterator _start;
iterator _finish;
iterator _eofstorage;
};
这里的:
_start:就是开辟数组的开始指针
_finishi:就是开辟数组最后一个元素的指针
_eofstorage:数组开辟空间的空间的最后位置的地址
这个实现方法区别其实不重要,用string的实现方法依旧可以实现。
erase
这个函数的功能是:
删除指定位置的元素。
int main()
{
std::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
std::vector<int>::iterator begin = v1.begin();
v1.erase(begin);
for (auto i : v1)
{
std::cout << i;
}
}
接下来就来演示erase的一个非常经典的错误用法。
这里用的是STL中的vector,因为官方的使用中,这个问题也是十分常见的。
#include
#include
#include"vector.h"
int main()
{
std::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
std::vector<int>::iterator begin = v1.begin();
v1.erase(begin);
while (begin != v1.end())
{
std::cout << *begin;
begin++;
}
}
这是我们发现
这里我们把begin迭代器指定的内容删除后
然后继续使用了begin迭代器。
但是按照我们的道理来讲
这个begin的指向的方向应该还是老方向。
erase之后,各类元素会向前面补位,begin应该指向新补位的2了。
按道理来说这个begin应该是可以继续使用的。
因为在vs中,这个问题算是迭代器失效
在使用迭代器对象进行insert与erase之后。
vs会默认该迭代器无法继续使用
我们知道STL不同编译器下是不同的,因为官方给了一个STL的最基本的设定,具体的修改和实现方法是取决于公司自己
所以在linux中,可能就不会判断这个迭代器失效的效果。
这里我们来试一下。
这里能发现在linux中能进行访问。
但是这样的话为什么vs要禁止用户使用呢?
这里就要讲下允许使用这个的坏处了。
这里我们在linux中用这个测试一道题:
删除数组中的所有偶数
这里结果我们发现少删除了一个2。
这里其实仔细观察一下我们就能发现问题所在。
这里就能发现it删除完,还会进行一次++,会略过连续的偶数。
这里掠过算是比较ok一点的错误了,但是如果想象一下最后一位是偶数,这样的话会直接掠过数组的最后一位,直接向后面遍历,非常容易导致越界访问。
这里其实要解决也是很简单。
但是这样虽然能解决问题,但是大大限制了它的平台可移植性。
所以这个情况也是开发者所在意的。
这里能发现erase有一个iterator 的返回值。
这个iterator是用来返回下一个位置的值的。
所以这里需要去接受一下返回值,才能继续用这个begin变量。
void resize(int n, const T& val = T())
{
if (n < size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish != _start + n)
{
*_finish = val;
_finish++;
}
}
}
这个是resize的函数实现。
我们知道:
reserve:开辟空间,插入数据时不扩容
resize:开空间+初始化为指定值
但是resize初始化为指定值的时候,指定值有多种不同的类型。
但是有时候如果用户不输入初始化的值该怎么办?
void resize(int n, const T& val = T())
这个第二个参数,const T& val=T()
;
这个可以说是一个新写法,这个前面是类型
val=T();
是缺省值。
那这个T();
其实就是调用T类型的构造函数的的意思。
那这样我们大致就能明白是什么意思了
如果用户没有给初始化值,那就去调用vector类型T的构造函数
这里的operator和string的的一样,要注意深浅拷贝的问题。
所以同样可以用到swap深拷贝函数。
void check_capacity()
{
if (_finish == _eofstorage)
{
if (_start == NULL)
reserve(4);
else
reserve(capacity() * 2);
}
}
void reserve(int n)
{
if (n > capacity())
{
int oldsize = capacity();
T* new_vec = new T[n];
if (_start != nullptr)
{
for(int i=0;i<oldsize;i++)
{
new_vec[i]=_start[i];
}
delete _start;
}
_start = new_vec;
_finish = _start + oldsize;
_eofstorage = _start + n;
}
}
这个是完成版的reserve
void reserve(int n)
{
if (n > capacity())
{
int oldsize = capacity();
T* new_vec = new T[n];
if (_start != nullptr)
{
memcpy(new_vec, _start, oldsize*sizeof(T));
delete _start;
}
_start = new_vec;
_finish = _start + oldsize;
_eofstorage = _start + n;
}
}
这个是改版前的reserve函数
这里发现只有一个地方发生了了改变。
if (_start != nullptr)
{
memcpy(new_vec, _start, oldsize*sizeof(T));
delete _start;
}
这里乍一看好像没有什么问题。
但是我们仔细想想,如果vector的类型是一个自定义类型,具有地址成员变量。
那memcpy对new_vec与_start进行改变时,就会这个是浅拷贝,无法进行深拷贝,就会发生严重的问题
所以就改成了
for(int i=0;i<oldsize;i++)
{
new_vec[i]=_start[i];
}
这里使用赋值重载来进行深度拷贝。
就没有这样的问题了
#include
#include
#include
namespace my_vector
{
template<typename T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
vector()
:_start(nullptr)
,_finish(nullptr)
,_eofstorage(nullptr)
{}
vector(const vector<T>& v)
{
_eofstorage=_finish=_start=nullptr;
for(int i=0;i<v.size();i++)
{
push_back(v[i]);
}
}
vector(int n,const T& data)
{
_start=nullptr;
_finish=nullptr;
_eofstorage=nullptr;
resize(n,data);
}
template<class alliterator>
vector(alliterator first,alliterator end)
{
_finish=nullptr;
_start=nullptr;
_eofstorage=nullptr;
while(first!=end)
{
push_back(*first);
first++;
}
}
~vector()
{
delete _start;
_start = _finish =_eofstorage = nullptr;
}
void check_capacity()
{
if (_finish == _eofstorage)
{
if (_start == NULL)
reserve(4);
else
reserve(capacity() * 2);
}
}
vector<T>& operator= (const vector<T>& v)
{
if(this!=v)
{
std::swap(_start,v._start);
std::swap(_finish,v.finish);
std::swap(_eofstorage,v._eofstorage);
}
return *this;
}
void push_back(const T& data)
{
check_capacity();
insert(size(), data);
}
void reserve(int n)
{
if (n > capacity())
{
int oldsize = capacity();
T* new_vec = new T[n];
if (_start != nullptr)
{
for(int i=0;i<oldsize;i++)
{
new_vec[i]=_start[i];
}
delete _start;
}
_start = new_vec;
_finish = _start + oldsize;
_eofstorage = _start + n;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
size_t size() const
{
return _finish - _start;
}
void insert(int pos,const T& data)
{
assert(pos >= 0 && pos <= size());
check_capacity();
T* end = _finish;
while (end > _start+pos)
{
*end = *(end - 1);
end--;
}
_start[pos] = data;
_finish++;
}
void erase(int pos)
{
assert(pos >=0 && pos <= size());
T* end = _start+pos;
while (end <_finish)
{
*end = *(end + 1);
end++;
}
_finish--;
}
size_t capacity() const
{
return _eofstorage - _start;
}
T& operator[](int pos)
{
assert(pos < size());
return _start[pos];
}
const T& operator[](int pos) const
{
assert(pos < size());
return _start[pos];
}
void pop_back()
{
erase(_finish - _start);
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
void resize(int n,const T& val=T())
{
if (n < size())
{
_finish=_start + n;
}
else
{
reserve(n);
while(_finish!=_start+n)
{
*_finish=val;
_finish++;
}
}
}
private:
iterator _start;
iterator _finish;
iterator _eofstorage;
};
}