内容专栏: C/C++编程
本文概括:vector的介绍与使用、深度剖析及模拟实现。
本文作者: 阿四啊
发布时间:2023.10.8
像string的学习一样,我们依旧得学会在cplusplus网站中学会查看文档。
vector在实际中非常的重要,在实际中我们熟悉常见的接口就可以。我们一一介绍学习常见的vector接口。
constructor构造函数声明 | 接口说明 |
---|---|
vector()(重点) | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x);(重点) | 拷贝构造 |
vector (InputIterator first, InputIterator last) | 使用迭代器进行初始化构造 |
//vector的构造
void test_vector1()
{
vector<int> v1; // 无参初始化
vector<int> v2(10, 1); //带参初始化
//利用迭代区间进行初始化
vector<int> v3(v1.begin(), v1.end());
vector<int> v4(v2); //拷贝构造
//也利用string的迭代区间也可以进行初始化
string s1("hello world");
vector<int> v5(s1.begin(), s1.end());
vector<int>::iterator it = v5.begin();
while (it != v5.end())
{
//打印字符所对应的ascii码值
cout << *it << " ";
it++;
}
}
iterator的使用 | 接口说明 |
---|---|
begin/cbegin + end/cend | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator |
void PrintVector(const vector<int>& v)
{
// const对象使用const迭代器进行遍历打印
vector<int>::const_iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
void test_vector2()
{
vector<int> vec(10,0);
vector<int>::iterator it = vec.begin();
while (it != vec.end())
{
cout << *it << " ";
it++;
}
cout << endl;
//使用迭代器进行修改
it = vec.begin();
int i = 1;
while (it != vec.end())
{
*it += i;
it++;
i++;
}
//反向迭代器
auto rit = vec.rbegin();
while (rit != vec.rend())
{
cout << *rit << " ";
rit++;
}
cout << endl;
//迭代器遍历打印vec
PrintVector(vec);
}
容量空间 | 接口说明 |
---|---|
size | 获取数据个数 |
capacity | 获取容量大小 |
empty | 判断是否为空 |
resize | 改变vector的size |
reserve | 改变vector的capacity |
// 测试vector的默认扩容机制
void TestVectorExpand()
{
size_t sz;
vector<int> v;
sz = v.capacity();
cout << "making v grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
vs:运行结果:vs下使用的STL基本是按照1.5倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 3
capacity changed: 4
capacity changed: 6
capacity changed: 9
capacity changed: 13
capacity changed: 19
capacity changed: 28
capacity changed: 42
capacity changed: 63
capacity changed: 94
capacity changed: 141
g++运行结果:linux下使用的STL基本是按照2倍方式扩容
making foo grow:
capacity changed: 1
capacity changed: 2
capacity changed: 4
capacity changed: 8
capacity changed: 16
capacity changed: 32
capacity changed: 64
capacity changed: 128
// 如果已经确定vector中要存储元素大概个数,可以提前将空间设置足够
// 就可以避免边插入边扩容导致效率低下的问题了
void TestVectorExpandOP()
{
vector<int> v;
size_t sz = v.capacity();
v.reserve(100); // 提前将容量设置好,可以避免一遍插入一遍扩容
cout << "making bar grow:\n";
for (int i = 0; i < 100; ++i)
{
v.push_back(i);
if (sz != v.capacity())
{
sz = v.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
vector的增删查改 | 接口说明 |
---|---|
push_back | 尾插 |
pop_back | 尾删 |
insert | 在position位置之前插入val值 |
erase | 删除position位置的元素 |
swap | 交换两个vector的数据空间 |
operator[ ] | 像数组一样访问 |
clear | 删除容器的所有元素,将size置为0,但并不改变capacity的大小 |
// 尾插和尾删:push_back/pop_back
void test_vector3()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
v.pop_back();
v.pop_back();
it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
// 任意位置插入:insert和erase,以及查找find
// 注意find不是vector自身提供的方法,是STL提供的算法
void test_vector4()
{
// 使用列表方式初始化,C++11新语法
vector<int> v{ 1, 2, 3, 4 };
// 在指定位置前插入值为val的元素,比如:3之前插入30,如果没有则不插入
// 1. 先使用find查找3所在位置
// 注意:vector没有提供find方法,如果要查找只能使用STL提供的全局find
auto pos = find(v.begin(), v.end(), 3);
if (pos != v.end())
{
// 2. 在pos位置之前插入30
v.insert(pos, 30);
}
vector<int>::iterator it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据
v.erase(pos);
it = v.begin();
while (it != v.end()) {
cout << *it << " ";
++it;
}
cout << endl;
v.clear();
//调用clear后,vector的size将变成0,但是它的容量capacity并未发生改变
}
创建一个vector的源文件,写一个vector的类模板,放入一个自己的MyVector
的命名空间里面,以免与库里面的vector发生冲突。
在模拟vector时,我们并没有和string一样使用动态分配的指针_Ptr、_size、_capacity,在类中我们使用了三个iterator
,贴近stl库里面的实现方式,其实本质就是原生指针。
_start
: _start
指向动态数组或容器的第一个元素的位置。它用于表示容器的起始位置。
_finish
: _finish
也是一个指针,指向容器中当前元素的下一个位置。它表示容器中元素的结束位置。通常,_finish 处于有效元素的末尾,但它之后的内存可能已经分配,但未被使用。
_end_of_storage
: _end_of_storage
指向容器内存分配的末尾位置。这个位置之后的内存是容器为将来添加更多元素而预留的。当容器的大小接近容量时,它可能需要重新分配内存,并将新的_end_of_storage
更新为新的内存末尾。
namespace MyVector
{
template<class T>
class vector
{
public:
typedef T* iterator;
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
};
首选我们提前需要写好构造函数与析构函数,构造函数在初始化列表将三个iterator
置为nullptr
即可,析构函数进行释放资源与指针置空操作。
size()接口函数
:表示vector的有效数据个数,即:_finish
- _start
capacity()接口函数
:表示vector的容量大小,即:_end_of_storage
- _start
在push_back尾插之前,我们还需要进行判断是否要进行扩容,扩容机制我们在数据结构学习了很多,不作细致讲解,下面直接放代码:
namespace MyVector
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
size_t capacity()
{
return _end_of_storage - _start;
}
size_t size()
{
return _finish - _start;
}
void push_back(const T& val)
{
//扩容
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
T* tmp = new T[newcapacity];
if (_start)
{
memcpy(tmp, _start, sizeof(T)* size());
delete[] _start;
}
_start = tmp;
_finish = _start + size();
_end_of_storage = _start + newcapacity;
}
*_finish = val;
_finish++;
}
//通过[]进行访问vector
T& operator[] (size_t n)
{
assert(n < size());
return *(_start + n);
}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
void test_vector1()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
}
};
在main函数中我们调用MyVector::test_vector1()
,将程序运行起来之后程序就出问题了:
报错说_finish是nullptr
,什么原因呢?
我们将程序调试起来,观察发现vector发生了扩容,其_start与_end_of_storage均发生了改变,我们讲他俩相减等于16
字节,1个int占4个字节,说明确实刚开始扩容了4个元素,但是我们细心观察发现_finish的值还是为空指针,原因其实就在于_finish = _start + size(),这里更新了_start,而size()里面的_finish还是指向原来的空间,也就是0x00000000,属于迭代器失效问题,解决办法就是在扩容之前,提前用sz变量记录好偏移量。
解决方案:
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
size_t sz = size();
T* tmp = new T[newcapacity];
if (_start)
{
memcpy(tmp, _start, sizeof(T)* sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + newcapacity;
}
*_finish = val;
_finish++;
}
第一种:[]下标访问遍历
第二种:将迭代器_start与_finish用begin与end方法进行封装为成员函数,利用iterator进行遍历。
第三种:一旦有了迭代器,就可以支持范围for语句,因为其底层就是迭代器。
namespace MyVector
{
template<class T>
class vector
{
public:
typedef T* iterator;
vector()
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
size_t capacity()
{
return _end_of_storage - _start;
}
size_t size()
{
return _finish - _start;
}
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
size_t sz = size();
T* tmp = new T[newcapacity];
if (_start)
{
memcpy(tmp, _start, sizeof(T)* sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + newcapacity;
}
*_finish = val;
_finish++;
}
T& operator[] (size_t n)
{
assert(n < size());
return *(_start + n);
}
~vector()
{
delete[] _start;
_start = _finish = _end_of_storage = nullptr;
}
private:
iterator _start;
iterator _finish;
iterator _end_of_storage;
};
void test_vector1()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
vector<int> v2;
v2.push_back(10);
v2.push_back(20);
v2.push_back(30);
v2.push_back(40);
v2.push_back(50);
vector<int>::iterator it = v2.begin();
while (it != v2.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for(auto e:v2)
{
cout << e << " ";
}
cout << endl;
}
};
resize接口函数
:改变vector的size,可以增加或减少容器中的元素数量。
当使用 resize 减少size大小时,多余的元素会被移除,当使用 resize 增加size大小时,会发生扩容。
reserve接口函数
:改变vector的容量大小,它能够预留足够的空间,使用 reserve
可以减少因为频繁扩容而带来的性能开销。
ps:reserve本身有检查扩容机制的意思,我们直接使用push_back写的扩容机制代码,然后用push_back复用reserve.
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t sz = size();
if (_start)
{
memcpy(tmp, _start, sizeof(T) * sz);
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
//为什么这里的val要给匿名对象初始化,而不是0,
//因为这里写的是vector类模板,
//传进来的参数类型可能是int,double,也可能是vector,vector……
//C++泛型编程对内置类型也支持构造函数(匿名对象),不然C++模板很难用
//添加const说明匿名对象具有常属性,添加&可以延长匿名对象的生命周期
void resize(size_t n, const T& val = T())
{
//分为三种情况
//小于等于size =>缩容(多余元素被移除)
// 大于size 小于capacity
//大小capacity =>扩容
if (n <= size())
{
_finish = _start + n;
}
else
{
reserve(n);
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
}
void push_back(const T& val)
{
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*_finish = val;
_finish++;
}
测试:
void test_vector2()
{
vector<int*> v1;
v1.resize(5);
vector<string> v2;
v2.resize(10,"xxx");
for (auto e: v1)
{
cout << e << " ";
}
cout << endl;
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
}
void insert(iterator pos,const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
//判断是否需要扩容
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
测试:
void test_vector3()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
//第一次在下标为2的位置插入一个元素30
v1.insert(v1.begin() + 2, 30);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
//第二次头插一个元素8,此时会发生扩容,导致pos失效
v1.insert(v1.begin(), 8);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
}
第一次我们在下标为2的位置插入一个元素30,程序能够正确运行,结果也是对的。
但是我们再次利用insert头插一个元素,此时正好是添加新的元素,需要进行扩容,最后程序发生了崩溃。
为何呢?是因为扩容的原因导致的吗?接下来,我们探究一下
在扩容之前,我们调试观察看到pos接收的地址的确和_start的地址一模一样,都是0x0122da28
一旦经过reseve扩容,我们发生三个iterator
地址都发生了变化,唯独pos却纹丝不动,还是原来的地址空间,此时的问题貌似很眼熟,没错,就是在前面部分我们提到的_finish如出一辙,也属于迭代器失效问题,本质就是因为pos使用的是释放之前的空间,空间发生了扩容,pos在对以前已经释放的空间进行操作时,就会引起代码运行崩溃。
解决方案:在扩容之前,保存pos位置的偏移量,在扩容后更新pos位置。
void insert(iterator pos,const T& val)
{
assert(pos >= _start);
assert(pos <= _finish);
if (_finish == _end_of_storage)
{
size_t len = pos - _start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = _start + len;
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = val;
_finish++;
}
void erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
}
测试:
void test_vector4()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
for (size_t i = 0; i < v1.size(); i++)
{
cout << v1[i] << " ";
}
cout << endl;
auto it = v1.begin();
v1.erase(it);
for (auto e:v1)
{
cout << e << " ";
}
cout << endl;
v1.erase(it + 2);
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
以上我们对头部和下标为2的元素进行了删除操作,代码的结果也能顺畅地跑出来,结果也是正确的,但是这里的it
迭代器不会失效吗?答案并非如此,我们来看下面的场景:
我们给出三组样例数据,分别计算给出的样例中用erase
删除偶数元素。
第一组测试数据:1 2 3 4 5 6 7
//第一种情况
void test_vector5()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
cout << "begin:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
it++;
}
cout << "after:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
运行结果:结果正确
begin:1 2 3 4 5 6 7
after:1 3 5 7
第二种测试数据:1 2 3 4 5 6 7 8
//第二种情况
void test_vector6()
{
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
v1.push_back(8);
cout << "begin:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
it++;
}
cout << "after:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
运行结果:程序崩溃
begin:1 2 3 4 5 6 7 8
error运行崩溃(触发断言)
第三种测试数据:2 2 3 4 5 6 7
//第三种情况
void test_vector7()
{
vector<int> v1;
v1.push_back(2);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
cout << "begin:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
v1.erase(it);
}
it++;
}
cout << "after:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
打印结果:结果错误
begin:2 2 3 4 5 6 7
after:2 3 5 7
分析:
注⚠️:
以上也是Linux下g++的编译器对迭代器的检测并不严格,处理没有vs编译器果断极端。
以上代码在vs下程序会出现崩溃,vs一些编译器会进行强制检查,认为erase之后it就失效了,访问就会报错。但是在Linux下,虽然可能可以运行,但是输出的结果是不对的。
那么对于以上it等迭代器失效问题,该如何解决呢?
其实erase
有具体的返回值,返回的是一个iterator
,指向被删除元素的下一个元素的位置。
修正erase的代码:
iterator erase(iterator pos)
{
assert(pos >= _start);
assert(pos < _finish);
iterator begin = pos + 1;
while (begin < _finish)
{
*(begin - 1) = *begin;
begin++;
}
_finish--;
return pos;
}
//迭代器失效的解决方案
//在使用前,对迭代器进行重新赋值
void test_vector8()
{
vector<int> v1;
v1.push_back(2);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
v1.push_back(5);
v1.push_back(6);
v1.push_back(7);
cout << "begin:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
auto it = v1.begin();
while (it != v1.end())
{
if (*it % 2 == 0)
{
//在下次操作it之前,对迭代器进行重新赋值
it = v1.erase(it);
}
else
{
//不删除++即可
it++;
}
}
cout << "after:";
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
结论:在使用了 insert
和 erase
之后,迭代器失效了,不能再访问,需要谨慎处理。
关于剖析迭代器失效问题 博客==>关于迭代器失效问题
使用memcpy拷贝的是内置类型,那么通常是高效又安全的,但是如果对于自定义类型,涉及动态资源管理,会导致浅拷贝问题,造成内存泄露等不可预知的结果!
//测试对于自定义类型,扩容时使用memcpy会导致浅拷贝问题
void test_vector9()
{
vector<string> v1;
v1.push_back("11111111111");
v1.push_back("11111111111");
v1.push_back("11111111111");
v1.push_back("11111111111");
v1.push_back("11111111111");
for (auto e : v1)
{
cout << e << " ";
}
cout << endl;
}
解决方案:
很简单,使用一个for循环,对每个字节进行赋值操作,对于内置类型是赋值,对于自定义类型就会调用自身的赋值重载函数!
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
size_t sz = size();
if (_start)
{
//memcpy(tmp, _start, sizeof(T) * sz);
for (size_t i = 0; i < sz; i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
_finish = _start + sz;
_end_of_storage = _start + n;
}
}
//拷贝构造
//v2(v1)
vector(const vector<T>& x)
: _start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
//开辟和x一样大的空间
reserve(x.capacity());
for (size_t i = 0; i < x.size(); i++)
{
push_back(x[i]);
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v.finish);
std::swap(_end_of_storage, v._end_of_storage);
}
//赋值重载
//v2 = v1
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
测试赋值重载:
//测试赋值重载
void test_vector10()
{
vector<int> v1;
v1.push_back(10);
v1.push_back(20);
v1.push_back(30);
v1.push_back(40);
vector<int> v2;
v2 = v1;
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
}
类模板里面可以嵌套函数模板,可以传入任意类型的迭代区间初始化,在形参部分用InputIterator
进行接收,具体细节可下面的测试用例。
对于n个val初始化,不能直接写成vector(size_t n, const T& val = T())
,在编译器认为会优先去调用最匹配的,就会调用迭代区间的初始化,此时编译就会出错,那么我们就需要写一个更匹配的vector(int n, const T& val = T())
,此时就能正确编译并执行了。
//利用迭代器区间进行初始化
//函数模板
template <class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _end_of_storage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
vector(size_t n, const T& val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
vector(int n, const T& val = T())
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
测试迭代器区间初始化与n个val初始化:
//测试迭代器区间初始化与n个val初始化
void test_vector11()
{
//n个val初始化
vector<int> v1(10, 0);
//利用迭代器区间初始化
string str("hello world");
vector<int> v2(str.begin(), str.end());
for (auto e : v2)
{
cout << e << " ";
}
cout << endl;
}
vector的深度剖析及模拟实现