C++(初阶)(十)——vector模拟实现
vector
- vector
- 构造
- 尾插(删)和扩容
- inert(插入)
- 迭代器失效
- erase(删除)
- resize(调整空间)
- 深浅拷贝
- 迭代器
- 拷贝和赋值(v2(v1)和v1 = v3)
- 多个数据插入
- 迭代器区间初始化
(constructor)构造函数声明 | 接口说明 |
---|---|
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val =value_type()) | 构造并初始化n个val |
vector (const vector& x); (重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last); | 使用迭代器进行初始化构 |
构造
namespace A
{
//存储在vector的值可能是int或者string 不同类型,写为模版
template<class T>
class vector
{
public:
//若为私有,访问时需要突破类域,因此重命名为公有
typedef T* iterator;
//构造
vector()
{ }
//析构
~vector()
{
if (_start)
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
private:
//给缺省值,缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的,不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表则用这个缺省值初始化
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
先实现一些简单的功能
//求空间大小
size_t capacity()
{
//指针差值
return _end_of_storage - _start;
}
//有效元素个数
size_t size()
{
return _finish - _start;
}
//重载[],下标访问
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
尾插(删)和扩容
实现尾部插入之前,需要判断先判断空间的大小,如果大小不够,需要扩容。
而关于扩容,并非是只在尾插时使用,所以需要在内部再次判断。
步骤;1,直接new与所需空间大小相同的空间即n。
2,即拷贝原空间的数据到新空间,并delete原空间。
但是我们需要注意的一点是在拷贝原空间的数据时,使用memcpy对内置类型的拷贝时满足我们的需求,但是在对自定义类型的拷贝时是值拷贝,也就是浅拷贝,不符合需求,此时解决办法是遍历原空间依次赋给新空间。
3,更新首尾指针和空间大小。
//扩容
void reserve(size_t n)
{
if (n > capacity())
{
//记录旧的size,因为_start更新后,size的值会改变
size_t old_size = size();
T* tmp = new T[n];
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * size());
for (size_t i = 0; i < old_size; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + old_size;
_end_of_storage = _start + n;
}
}
void push_back(const T& x)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
}
*_finish = x;
++_finish;
}
//尾删
void pop_back()
{
assert(_start < _finish);
--_finish;
}
顺序表
resize 开空间+初始化
reservve 单纯开空间
emplace和insert功能类似,插入,细节有差别
vector< char >不能替代string,string兼容c语言。
1,string 数组后面有’ \0 ',vector< char >没有。
2,string += 字符或者字符串,即插入字符,字符串,string的find可以查找子串
3,vector< char >可以调用算法库的查找,未找到return last;
如何看源码
1,了解功能
2,抓核心枝干
时间换空间:顺序表缩容逻辑(不划算)
空间换时间:(比较划算)
inert(插入)
//pos是一个指针,是iterator即T*类型的
void insert(iterator pos, const T& x)
{
//位置不能越界
assert(pos >= _start);
assert(pos <= _finish);
//1,判断空间,不够扩容
//2,拷贝原数据
if (_finish == _end_of_storage)
{
size_t newcapacity = _end_of_storage == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
}
iterator it = _finish - 1;
while (pos >= it)
{
*(it + 1) = *it;
--it;
}
*it = x;
++_finish;
}
迭代器失效
代码看似没有问题,其实在扩容后会形成迭代器失效,pos的位置已经丢失,
所以需要想办法得到扩容后新的pos的位置。
迭代器失效在vs下会强制检查,迭代器失效后不能访问,访问会崩溃报错。
失效原因:可能是底层成为野指针,即缩容逻辑,也可能是删除后,位置更改。
g++下没有强制检查。
失效后不要访问,重新赋值后再访问。
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
size_t newcapacity = _end_of_storage == 0 ? 4 : 2 * capacity();
reserve(newcapacity);
pos = len + _start;
}
erase(删除)
关于erase也存在迭代器失效的问题,解决办法是返回一个迭代器,在删除后接收。
//.c
//不返回值
//迭代器失效在vs下会强制检查,迭代器失效后不能访问,访问会崩溃报错。
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
//更新pos,否则迭代器会失效
return pos;
}
//返回迭代器
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
//更新pos,否则迭代器会失效
return pos;
}
//------------------------------------------
//test
A::vector<int> v1 = { 1,2,2,2,3,4,5,6,6,6 };
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
A::vector<int>::iterator it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
else
{
it++;
}
}
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
resize(调整空间)
比原来空间大
比如说,原来数据为:vector< int > v = { 1,2,3,4,5 };
使用resize(10)
,如果不给值,会默认赋值为0
会将数据置为1,2,3,4,5,0,0,0,0,0
再如resize(10,2)
会变为1,2,3,4,5,2,2,2,2,2
比原来空间小
resize(2)
变为1,2
void resize(size_t n, T val = T())
{
if (n > size())
{
//插入数据,先扩容
reserve(n);
while (_start + n != _finish)
{
*_finish = val;
++_finish;
}
}
else
{
_finish = _start + n;
}
}
深浅拷贝
memcpy是一种值拷贝。对深拷贝的自定义类型会进行浅拷贝,然后就会发生问题,
对于内置类型是没有问题的。
解决办法是,一个一个赋值解决。
迭代器
一般情况下我们会实现两种迭代器,一种是普通版本的迭代器,可以修改其本身,也可以修改指向的对象;另外一种是const修饰的迭代器,其本身可以修改,指向对象不可以修改。
需要注意的一点是迭代器的end()是指向结尾数据的下一个位置,我们此处的_finish正好满足条件,所以直接放回即可。
typedef T* iterator;
typedef const T* const_iterator;
//迭代器
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
//const迭代器
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
拷贝和赋值(v2(v1)和v1 = v3)
//v2(v1)
vector(vector<T>& v)
{
//申请和v1相同的空间大小,使用范围for(支持迭代器就支持范围for)尾插数据到新空间即可。
reserve(v.capacity());
for (auto& e : v)
{
push_back(e);
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_end_of_storage, v._end_of_storage);
}
//v1 = v3
vector<T>& operator=(vector<T> v)
{
//直接交换即可,因为拷贝构造已经形成
swap(v);
return *this;
}
多个数据插入
//对val的类型不确定,给缺省值,是匿名对象
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
但是如果仅仅只写这一种的话,在我们实现对迭代器区间初始化时,会发生错误匹配的情况,是模板实例化时的问题,
可以想到的是把size_t类型修改为int类型,这样就可以解决,但是这样有引发新的问题,对于无符号整型类型没办法处理,所以直接两个都留下,构成了函数重载。
//为了解决模版实例化的问题
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
//c++11
vector(initializer_list<T> il)
{
reserve(il.size());
//此处用引用,因为T的类型不确定
for (auto& e : il)
{
push_back(e);
}
}
迭代器区间初始化
//迭代器区间初始化
//写为模版,可以初始化非iterator的类型
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos <= _finish);
iterator it = pos + 1;
while (it < _finish)
{
*(it - 1) = *it;
++it;
}
--_finish;
//更新pos,否则迭代器会失效
return pos;
}