文章目录
📖前言:
本章我们将学习STL中另一个重要的类模板vector…
- vector是表示可变大小数组的序列容器。
- 就像数组一样,vector也采用的连续存储空间来存储元素。
- 本质讲,vector使用动态分配数组来存储它的元素。
- vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。
vector是向量的意思。
能不能用vector来替代string呢?
- 可以是可以,但是有区别的,string的部分功能vector实现不了
- string比较大小是按照assic码比较,但是vector的规则就不一样了
C++为什么推荐用引用传参?
- 这个问题在模板阶段尤其明显
- 若T是个int直接拷贝,要是string或者vector的话要拷贝构造 – 深拷贝的话代价更大
- 所以能用引用传参就用引用传参,用引用传参要是不改变尽量加上const
加上const普通对象和const对象都可以调用。
vector参考学习文档:👉 传送门
1. vector的使用
1.1 vector的初始化:
在我们使用vector之前我们需要先包一下头文件#include< vector >。
直接见代码:
void test_vector1()
{
//vector可以存储任意类型的数据
vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
v1.push_back(4);
vector<double> v2;
v2.push_back(1.1);
v2.push_back(2.2);
v2.push_back(3.3);
vector<string> v3;
v3.push_back("李白");
v3.push_back("杜甫");
v3.push_back("苏轼");
v3.push_back("白居易");
//单参数的构造函数支持隐式类型的转换
//本质是构造一个临时对象再去拷贝构造,然后优化成了直接构造
vector<int> v4(10, 5);
//迭代器区间的内容初始化 -- 可以是任意类型的迭代器
vector<string> v5(++v3.begin(), --v3.end());
string s = "hello world";
vector<char> v6(s.begin(), s.end());
}
- vector< int >::push_back的使用和string中的push_back一样的使用方式:
- vector v3; 的意思是,这个顺序表中的每个元素是string类型的对象:
- vector v4(10, 5); 的意思是用10个5初始化vector中的元素:
- vector v5(++v3.begin(), --v3.end()); vector v6(s.begin(), s.end());
- 迭代器区间的内容初始化 – 可以是任意类型的迭代器:
1.2 vector的迭代器:
直接见代码:
void test_vector2()
{
//遍历
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//1、下标 + []
for (size_t i = 0; i < v.size(); i++)
{
v[i] += 1;
cout << v[i] << " ";
}
cout << endl;
//2、迭代器
vector<int>::iterator it = v.begin();
while (it != v.end())
{
*it -= 1;
cout << *it << " ";
it++;
}
cout << endl;
//3、范围for
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
用法和string中的迭代器一样:
-
- 参考博文string的用法:👉传送门
1.3 vector的内存管理:
STL中vector每次扩容的规律:
void test_vector3()
{
//vector<int> v;
//cout << v.max_size() << endl;
//容量测试 -- VS是PJ版本 大概是1.5倍增容,Linux是SGI版本 是2倍增容
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';
}
}
//这里用reserve更合适
//增容增多少的问题分析:
//单次增容越多,插入N个值,增容次数越少,效率就越高
//单次增容越多,可能浪费空间越多
}
由图可见:
- 容量测试 – VS是PJ版本 大概是1.5倍增容,Linux是SGI版本 是2倍增容
- 单次增容增少了,会导致频繁增容,效率低下
- 1.5倍和二倍是个中间值,是相对合适的
reserve / resize / clear的使用:
void tese_vector4()
{
vector<int> countV;
//开空间 + 初始化
countV.resize(100, 1);
countV.resize(10);
countV.reserve(1000);
//sting 和 vector等都有一个特点,删除数据,一般不会主动缩容的
countV.shrink_to_fit();
cout << countV.size() << endl;
cout << countV.capacity() << endl;
cout << endl << endl;
//clear的使用:
countV.clear();
cout << countV.size() << endl;
cout << countV.capacity() << endl;
//操作系统的空间不允许一部分一部分还
//缩容是开了一块新的小空间
//vector没有头插头删,效率比较低
}
insert / erase 的使用:
void test_vector5()
{
//遍历
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.insert(v.begin(), -1);
v.insert(v.begin(), -2);
v.insert(v.begin(), -3);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
//可以在尾最后一个位置插入,越界了是不行的
v.insert(v.begin() + 7, 3000);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
v.erase(v.begin());
v.erase(v.begin());
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
上述可见vector和string内存管理的使用并无二异。
1.4 迭代器可以访问容器:
查找vector中的指定元素:
直接见代码:
void test_vector6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//auto pos = find(v.begin(), v.end(), 3);
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
if (pos != v.end())
{
cout << "找到了" << endl;
v.erase(pos);
}
else
{
cout << "没有找到" << endl;
}
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
v.push_back(0);
v.push_back(9);
v.push_back(3);
v.push_back(1);
//默认是升序
//sort(v.begin(), v.end()); // <
//排降序,仿函数
//关于仿函数,先记住这个用法,具体后面学习队列细学
sort(v.begin(), v.end(), greater<int>()); // > ,greater<int>()匿名对象
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
}
- 仿函数我们后期会学,先包一下头文件:#include< functional> – 仿函数
- vector和list没有find,没有查找函数,我们需要包一个算法的头文件
#include< functional >
2. vector的模拟实现
template<class T>
class vector
{
public:
typedef T* iterator;
typedef const T* const_iterator;
//构造函数
vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{}
//给一个迭代器的区间去构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
:_start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{
while (first != last)
{
push_back(*first);
first++;
}
}
//用n个val初始化
vector(size_t n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
//重载一个
vector(int n, const T& val = T())
: _start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{
reserve(n);
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
void swap(vector<T>& v)
{
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstoage, v._endofstoage);
}
//拷贝构造 -- 现代写法
//正常情况下要加模板参数,但是这里特殊可以不加模板参数
//虽然可以这样但是不推荐,因为不规范
//拷贝构造
vector(const vector<T>& v)
//vector(const vector& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstoage(nullptr)
{
//拷贝构造中调用一个构造函数,构造一个tmp出来
vector<T> tmp(v.begin(), v.end());
//通过this指针调用该对象的成员函数
this->swap(tmp);
}
//vector<T>& operator=(vector<T> v)
vector<T>& operator=(vector<T> v)
{
this->swap(v);
return *this;
}
(1)C++中内置类型也可以认为有构造函数和析构函数:
- 这样才能更好支持模板
- void resize(size_t n, T val = T())
与类和对象使用方法一样:
int i = 0;
int j = int();
int k = int(1);
int m(1);
(2)这里的拷贝构造还是用了现代的写法,具体方法和string类模拟实现时用到了同样的方法:
- 思路一样,思路可以参考上一篇博客:👉 传送门
- 这里我们还模仿标准库中,用迭代器区间的内容初始化
//资源原理
~vector()
{
if (_start != nullptr)
{
delete[] _start;
_start = _finish = _endofstoage = nullptr;
}
}
iterator begin()
{
return _start;
}
iterator end()
{
return _finish;
}
const_iterator begin() const
{
return _start;
}
const_iterator end() const
{
return _finish;
}
size_t size() const
{
return _finish - _start;
}
size_t capacity() const
{
return _endofstoage - _start;
}
void reserve(size_t n)
{
//这里存在start更新之后就算不准的现象
size_t sz = size();
if (n > capacity())
{
T* tmp = new T[n];
if (_start != nullptr)
{
//memcpy(tmp, _start, size() * sizeof(T));
for (size_t i = 0; i < size(); i++)
{
tmp[i] = _start[i];
}
delete[] _start;
}
_start = tmp;
}
_finish = _start + sz;
_endofstoage = _start + n;
}
注意:
- 一开始的时候 _finish 和 _start 都是0空指针nullptr
- _start被更新了,要是这样:_finish = _start + size();
- 不能用size(),因为_finish = _start + _finsih - _start
- 即 _finish = _start + 0 - _start = 0
- 在下面push_back函数中对(_finsih)的解引用就会崩,因为不能解引用空指针
- 程序可能会崩溃
//void resize(size_t n, T val = T())
//生成T类型的匿名对象,C++内置类型也有默认构造函数
void resize(size_t n, const T& val = T())
{
if (n > capacity())
{
reserve(n);
}
if (n > size())
{
while (_finish < _start + n)
{
*_finish = val;
_finish++;
}
}
else
{
_finish = _start + n;
}
}
void push_back(const T& x)
{
/*if (_finish == _endofstoage)
{
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_finish = x;
_finish++;*/
insert(end(), x);
}
void pop_back()
{
/*if (_finish > _start)
{
_finish--;
}*/
//返回的临时对象不能改变,不能++(自增)和--(自减)
erase(end() - 1);
}
T& operator[](size_t pos)
{
assert(pos <= size());
return _start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return _start[pos];
}
1.1 迭代器失效问题:
两种迭代器失效问题:
- 1、野指针
- 2、意义变了
演示代码1:
void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.insert(v.begin(), 0);
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
- insert函数如果pos不更新的话…
运行结果:
- 随机值 1 2 3 4
迭代器失效问题,是因为pos是不更新的
- 当reserve扩容之后,如果是新空间,_start和_finish更新了,pos(就是v.begin())还指向旧空间
- 但是旧空间被释放了,这个问题就是迭代器失效,pos为野指针
- 所以就插入失败了
- 迭代器发生了野指针的问题
虽然insert函数中的pos指针更新了,但是形参的改变不会影响实参,pos指针是一个传值传参。
如图所示:
- 当insert函数中pos更新了之后…
要求:在所有的偶数的前面插入一个20:
演示代码2:
void test_vector3()
{
vector<int> v;
v.reserve(10);
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
//迭代器同样失效
//如果扩容的话,迭代器中it指针传参给insert函数中的pos
//因为是传值传参,如果insert函数发生了扩容,该函数内部pos更新了
//但是实参it并没有更新还是指向原来的空间,原来的空间却已经被释放了
//当再次进入insert的时候直接assert断言报错了
/*vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
v.insert(it, 20);
}
it++;
}*/
//insert以后虽然没有扩容,it没有成为野指针,但是it指向位置的意义变了
//it++之后一直指向2,导致了我们这个程序重复插入20
//不是it是野指针失效的,而是it指向的位置意义变了
//也叫作迭代器失效
vector<int>::iterator it = v.begin();
while (it != v.end())
{
if (*it % 2 == 0)
{
it = v.insert(it, 20);
it++;
}
it++;
}
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
分两种情况讨论:
(1) 扩容的情况:(野指针)
- 如果扩容的话,迭代器将it指针传参给insert函数中的pos
- 因为是传值传参,如果insert函数发生了扩容,该函数内部pos更新了
- 但是实参it并没有更新还是指向原来的空间,原来的空间却已经被释放了
- 当再次进入insert的时候,it再传参,直接assert断言报错了(it已经不在断言的范围内了)
(2) 不扩容的情况:(意义变了)
- insert以后虽然没有扩容,it没有成为野指针,但是it指向位置的意义变了
- it++之后一直指向20,导致了我们这个程序重复插入20
- 不是it是野指针失效的,而是it指向的位置意义变了
- 也叫作迭代器失效
为什么采取返回指针的方式:
- 采取返回指针的话可以完美避开上述所有问题
- 详情见下述代码的实现
总结:
erase之后pos到底失效还是没失效,怎么认定这个问题
VS百分百崩(失效),无论pos访问哪个位置都报错
Linux是没失效的,Linux其实也失效了,它的意义变了
erase(pos)以后pos失效了,pos的意义变了
但是不同平台下面对于访问pos的访问是不一样的
我们统一以失效的角度去看
整体总结:
对于insert和erase造成的迭代器失效问题,linux平台检查很佛系
基本依靠操作系统自身的野指针越界检查机制
windows下VS系列检查更严格,使用一些强制检查机制,意义变了可能也会检查出来
//pos前一个位置插入
iterator insert(iterator pos, const T& x)
{
//检查参数
assert(pos >= _start && pos <= _finish);
//扩容
//扩容以后pos就失效了,需要更新一下
if (_finish == _endofstoage)
{
size_t n = pos - _start;
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
pos = _start + n;
}
//挪动数据
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
end--;
}
*pos = x;
_finish++;
return pos;
}
//一般vector删除数据,都不考虑缩容的方案
//缩容方案:size() < capacity() / 2 的时候
//可以考虑开一个size() 大小的空间,拷贝数据,释放旧空间
//缩容方案本质是时间换空间,一般的设计都不会考虑缩容
//因为实际比较关注时间效率,不关注空间效率,因为现在硬件设备空间都比较大
//空间存储也比较便宜
//有返回值是防止有缩容的情况
iterator erase(iterator pos)
{
assert(pos >= _start && pos < _finish);
iterator it = pos + 1;
while (it != _finish)
{
*(it - 1) = *it;
it++;
}
_finish--;
return pos;
}
void clear()
{
_finish = _start;
}
private:
iterator _start;
iterator _finish;
iterator _endofstoage;
};
//类外定义:
//template<typename T>
//vector<T>::vector(const vector<T>& v)
// //vector(const vector& v)
// :_start(nullptr)
// , _finish(nullptr)
// , _endofstoage(nullptr)
//{
// vector<T> tmp(v.begin(), v.end());
// this->swap(tmp);
//}
3. 动态开辟二维数组vector<vector>
3.1 以杨辉三角为例:
void test()
{
vector<vector<int>> vv;
//vv.size()二维数组的行数,可能规则或者可能不规则二维数组
for (size_t i = 0; i < vv.size(); i++)
{
//vv[i] -- int数组,每一行的那个管理int的vector
//vv[i].size() -- 取每一行的个数
for (size_t j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
}
}
int main()
{
test_vector5();
//test();
return 0;
}
vv.operator [ ] ( i ) – 返回值是vector< int > – 这个对象继续结合第二个 [ j ]
vv[i].operator [ ] ( j ) 和二维数组很像,但是原理不一样。
-
整体展开来是(vv.operator [ ] ( i ) ) . operator [ ] ( j )
-
通过两次operator [ ] 的调用,像访问二维数组一样访问数据
-
访问第i行的,第j个数据
-
本质是先访问第i个vector对象,再访问对这个对象的第j个int数据
-
- vector<vectot< int >>表示里面的每个数据存的都是vector的对象
-
- vector< int >表示vector里面每个数据存的都是int
-
- 相当于一个对象数组和每个对象又是int类型的数组
C++遍历二位数组最好是用下标,其他方式都不是很舒服
杨辉三角:
3.2 更深层次的深拷贝:
将杨辉三角这个代码直接拷贝到我们的编译器中调用我们刚刚实现的vector时,如果我们用的是浅拷贝,还是会出现打印不来的问题。
class Solution
{
public:
vector<vector<int>> generate(int numRows)
{
vector<vector<int>> vv;
vv.resize(numRows);
for (size_t i = 0; i < vv.size(); i++)
{
vv[i].resize(i + 1, 1);
for (size_t j = 1; j < i; j++)
{
vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
}
}
//先打印一下
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;
}
cout << endl;
return vv;
}
};
void test_vector7()
{
//匿名对象
vector<vector<int>> ret = Solution().generate(5);
for (size_t i = 0; i < ret.size(); i++)
{
for (size_t j = 0; j < ret[i].size(); j++)
{
cout << ret[i][j] << " ";
}
cout << endl;
}
}
如图所见,只有最后一行打印出来了,前面的杨辉三角都是随机值,这是什么原因呢?
原因就出在最后函数返回的是传值返回,要去调用拷贝构造,拷贝构造要去调用扩容,而扩容是memcpy,就会出事。
memcpy把一个int的数组浅拷贝没问题,将vector的数组浅拷贝就会出问题!!
- 因为是传值返回,要调用拷贝构造,拷贝构造要调用区间构造,区间构造调用push_back,再调用insert,在不扩容的情况下,一行一行插入一个vector的对象。
- 因为一开始只开了4个空间,只能装四行(四个vector的对象),再次增加(尾插)的时候就要扩容,而我们扩容是直接将旧空间的数据直接按字节拷贝(memcpy)所以原来空间中每一行的vector的指针被拷贝过来了。
- 当源空间delete [ ] 的时候,delete [ ] 会挨个释放每一行的vector< int > (调用析构函数),原来的空间被释放了
- 但是,拷贝过来的之前对应行的vector< int > 指针被直接拷贝过来了,导致扩容之前拷贝的数据并没有真正的拷贝过来,当再去访问扩容之前拷贝的数据时,非法访问,为随机值。
解决办法:
- 我们只需要,将reserve函数中扩容时的memcpy换成一个循环,挨个赋值
- 如果是内置类型,直接浅拷贝
- 如果是自定义类型,直接调用赋值重载