本章我们将学习STL中另一个重要的类模板vector…
vector是向量的意思。
能不能用vector来替代string呢?
C++为什么推荐用引用传参?
加上const普通对象和const对象都可以调用。
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());
}
直接见代码:
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中的迭代器一样:
STL中vector每次扩容的规律:
void test_vector3()
{
//vector 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个值,增容次数越少,效率就越高
//单次增容越多,可能浪费空间越多
}
由图可见:
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内存管理的使用并无二异。
查找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()匿名对象
for (auto ch : v)
{
cout << ch << " ";
}
cout << endl;
}
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& operator=(vector v)
vector<T>& operator=(vector<T> v)
{
this->swap(v);
return *this;
}
(1)C++中内置类型也可以认为有构造函数和析构函数:
与类和对象使用方法一样:
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;
}
注意:
//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:
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;
}
运行结果:
迭代器失效问题,是因为pos是不更新的
虽然insert函数中的pos指针更新了,但是形参的改变不会影响实参,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::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) 扩容的情况:(野指针)
(2) 不扩容的情况:(意义变了)
为什么采取返回指针的方式:
总结:
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
//vector::vector(const vector& v)
// //vector(const vector& v)
// :_start(nullptr)
// , _finish(nullptr)
// , _endofstoage(nullptr)
//{
// vector tmp(v.begin(), v.end());
// this->swap(tmp);
//}
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数据
C++遍历二位数组最好是用下标,其他方式都不是很舒服
杨辉三角:
将杨辉三角这个代码直接拷贝到我们的编译器中调用我们刚刚实现的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的数组浅拷贝就会出问题!!
解决办法: