本文核心内容为vector的介绍及使用和对vector的模拟实现。本文包含很多易错点,都是我在学习vector过程中踩过的坑。因为自己淋过雨,希望可以为你们撑一把伞!共勉。
vector使用了模板,以满足任意类型数据的适用。 另外还使用到了内存池,以提高效率。
这里主要以介绍vector常用接口的方式进行讲解,由于vector的接口与过去所学的string的接口雷同,许多接口直接平移使用即可。
使用vector需包头文件:#include<vector>
使用格式:vector<T> + [对象名]
构造函数声明 | 接口说明 |
---|---|
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x)(重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
vector& operator=(vector v) | 赋值运算符重载 |
调用演示:
//无参构造
vector<int> v;
//构造并初始化n个val
vector<int> v1(10, 6);
vector<string> v2(10, "1111111");
//使用迭代器进行初始化构造
string str("qwertyuiop");
vector<char> v3(str.begin(), str.end())
//拷贝构造
vector<int> v4(v1);
//赋值运算符重载
v = v1;
疑点:
这里你可能看不懂构造并初始化n个val的构造函数的第二个参数:const value_type& val = value_type()
。
缺省值是一个类型后面跟个()
,这是啥?为什么不用0?
这里是为了解决val是自定义类型时的情况。
当val是string类型是,val还能为0吗?当然不能!
其实,这里这样用在语法上看是不合理的,但在C++引入模板的时候,包容了这个写法,以兼容模板的功能。
vector的迭代器是一个原生指针
iterator的使用 | 接口说明 |
---|---|
begin + end(重点) | 获取第一个数据位置的iterator/const_iterator; 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator;获取第一个数据前一个位置的reverse_iterator |
容量空间 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize(重点) | 改变vector的size |
reserve (重点) | 改变vector的capacity |
vector增删查改 | 接口说明 |
---|---|
push_back(重点) | 尾插 |
pop_back (重点) | 尾删 |
find | 查找。(注意这个是算法模块实现,不是vector的成员接口) |
insert | 在pos之前插入val |
erase | 删除pos位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] | (重点) 像数组一样访问 |
这里在使用insert和erase时,可能会发生迭代器失效的问题,导致出错。在后文模拟实现这两个接口时会进行讲解。
当我们在模拟实现vector的过程中会出现很多问题,这些问题都十分重要!都是重点! 罗列一下:
所有接口我就不一一实现了,我会讲解实现:构造、拷贝构造、赋值重载、reserve、resize、insert、erase。
对标stl30学习,成员变量包括:
typedef T* iterator;
typedef const T* const_iterator;
iterator _start = nullptr; // 指向数据块的开始
iterator _finish = nullptr; // 指向有效数据的尾
iterator _endOfStorage = nullptr; // 指向存储容量的尾
功能:当n>capacity()时,进行扩容;反之,不作处理。
STL中的做法是:分配一个容量更大的新的数组,然后将全部元素迁移到这个数组中,再清理原数组的空间,完成扩容。
实现(bug):
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
//创建一个容量更大的新的数组
T* tmp = new T[n];
if (_start)
{
//元素迁移
memcpy(tmp, _start, sizeof(T) * size());
//清理原数组
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endOfStorage = _start + n;
}
}
在进行元素迁移时,T为内置类型的时候,使用memcpy是没有问题的。 但T为自定义类型时,就不行了。
以vector<string>
为例:
memcpy会将原数组的元素一一拷贝到新的扩容数组中,并没有调用拷贝构造,因此发生了浅拷贝。
所以在接下来调用delete时,原数组和新数组的元素都会被清理,后序再进行扩容操作时,会导致一个对象被析构两次,最终程序崩溃
总结问题: vector需要的是深拷贝,但是当存储的数据为自定义类型时,使用memcpy会导致元素对象浅拷贝。
解决方案:
调用元素对象的赋值重载来进行对象的深拷贝。
for (int i = 0; i < size(); ++i)
{
tmp[i] = _start[i];
}
reserve正确代码:
void reserve(size_t n)
{
if (n > capacity())
{
size_t sz = size();
T* tmp = new T[n];
if (_start)
{
//自定义类型会发生浅拷贝
//memcpy(tmp, _start, sizeof(T) * size());
//用赋值重载来避免自定义类型发生浅拷贝问题
for (int i = 0; i < size(); ++i)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_endOfStorage = _start + n;
}
}
功能:在pos位置插入一个元素,并返回pos位置(pos是一个迭代器) 实现思路:
实现(bug):
iterator insert(iterator pos, const T& x)
{
//检查pos是否合法
assert(pos <= _finish && pos >= _start);
//检查容量,不足就扩
if (_finish == _endOfStorage)
{
int newcapacity = capacity() == 0 ? 1 : capacity() * 2;
reserve(newcapacity);
}
//挪数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
// 插入数据
*pos = x;
++_finish;
return pos;
}
我们仔细走读一下代码(包括reserve),可以发现:pos成野指针了!
我们每次在扩容时,会使得数据更换空间,此时pos迭代器指向的空间地址是无效地址(pos成了野指针(pos迭代器失效))。一旦我们再使用这个迭代器,就会出错。
在这里:扩容使得迭代器失效,后移使用了失效迭代器
解决问题:
更新迭代器:每次扩容时,将pos也同时指向新数组的对应位置。
正确代码:
iterator insert(iterator pos, const T& x)
{
assert(pos <= _finish && pos >= _start);
if (_finish == _endOfStorage)
{
int len = pos - _start;
int newcapacity = capacity() == 0 ? 1 : capacity() * 2;
reserve(newcapacity);
//解决迭代器失效问题
pos = _start + len;
//如果发生扩容,会使得数据更换空间,此时迭代器指向的空间地址是无效地址(pos变为野指针)
//因此,要更新迭代器pos。
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
功能:删除pos位置的元素,并返回pos(pos是迭代器)
实现思路:
代码实现:
iterator erase(iterator pos)
{
assert(pos < _finish && pos >= _start);
assert(size() > 0);
iterator end = pos;
while (end < _finish - 1)
{
*end = *(end + 1);
++end;
}
--_finish;
return pos;
}
都没有发生扩容呀?难道pos也能失效???
在我们删除pos位置的元素后,pos位会由其他元素代替。 如果pos位及其之后的所有元素都被删除了,pos就会指向一个无效的位置,导致迭代器失效。
例如:2,3,5,7,9
,pos指向第三个位置,我们要对第3个位置删除3次:
这里我们可以发现:pos迭代器是有失效的风险的,也不是一定会失效。
但是:在vs上,编译器认为vector的insert和erase后的迭代器就是失效的,不能再去访问这个迭代器,否则报错
然而,g++就有所不同,它就是我们刚刚的结论:不失效不报错。但是我们也不能这样去做,否则代码你的代码没有可移植性。
void resize(size_t n, const T& value = T());
功能:改变数组大小 实现思路:
n <= size()
:将数组大小缩小至nn > size()
: n < capacity
:将数组大小扩大至n,增加的元素设置为valuen > capacity
:扩容,并将数组大小扩大至n,增加的元素设置为value代码实现:
void resize(size_t n, const T& value = T())
{
if (n > size())
{
reserve(n);
while (_finish != _start + n)
{
*_finish = value;
++_finish;
}
}
else
{
_finish = _start + n;
}
}
我们来实现常用的构造:
实现(bug):
//构造并初始化n个val
vector(size_t n, const T& val = T())
{
//直接复用resize的功能可以完美解决
resize(n, val);
}
// 迭代器构造
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
//无参构造
vector()
{}
注意:要在定义成员变量处设置缺省值,否则就需要用初始化列表来进行初始化。
private:
iterator _start = nullptr; // 指向数据块的开始
iterator _finish = nullptr; // 指向有效数据的尾
iterator _endOfStorage = nullptr; // 指向存储容量的尾
我们试试这样实例化对象:vector<int> v(10, 10)
运行代码,发现报错了,为什么呢?
调试一下,可以发现并没有去调用vector(size_t n, const T& val = T())
,而是去调用了迭代器构造函数。
原来,我们的两个参数都是int型,恰好符合迭代器的两个相同参数,而vector(size_t n, const T& val = T())
第一个参数类型是size_t
,符合度没有迭代器构造函数高才导致这个结果的。
解决问题:
再重载一个vector(int n, const T& val = T())
构造函数,以防止调用迭代器构造。
//构造1
vector(size_t n, const T& val = T())
{
resize(n, val);
}
//构造2
//避免vector<int> v(10, 1) 调用3
//亦或者用vector<int> v(10u, 1),使其完美匹配1
vector(int n, const T& val = T())
{
resize(n, val);
}
// 构造3
// 迭代器构造
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
vector()
{}
有两个实现思路:
代码实现:
//拷贝构造v1
vector(const vector<T>& v)
{
_start = new T[v.capacity()];
int sz = v.size();
//自定义类型会发生浅拷贝
//memcpy(_start, v, sizeof(T) * sz);
//用赋值重载来避免自定义类型发生浅拷贝问题
for (int i = 0; i < sz; ++i)
{
_start[i] = v[i];
}
_finish = _start + sz;
_endOfStorage = _start + v.capacity();
}
//拷贝构造v2
vector(const vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
reserve(v.capacity());
for (auto e : v)
{
push_back(e);
}
}
实现思路:利用传值会进行拷贝构造的特性,再用swap完成实现。
代码实现:
//赋值重载
vector<T>& operator=(vector<T> v)
{
swap(v);
return *this;
}
这是我模拟实现vector的源码,仅供参考。
vector嵌套一个vector就可以实现一个二维数组了。
vector<vector<int>> vv(n);
构造一个vv动态二维数组,vv中总共有n个元素,每个元素都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:
vv中元素填充完成之后,如下图所示:
在使用时和普通二维数组差不多,也是vv[][]
。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有