
🦌云深麋鹿
专栏 :C++ | 用C语言学数据结构 | Java

回顾:上两篇我们结束了容器string,接下来这篇文章让我们进入到新的容器vector,体会新的设计思路吧~
放个目录
- [一 vector的使用](#一 vector的使用)
-
- [1.1 插入删除](#1.1 插入删除)
-
- [1.1.1 尾插](#1.1.1 尾插)
- [1.1.2 尾删](#1.1.2 尾删)
- [1.1.3 头插](#1.1.3 头插)
- [1.1.4 头删](#1.1.4 头删)
- [1.2 构造函数](#1.2 构造函数)
-
- [1.2.1 initializer_list](#1.2.1 initializer_list)
- [1.2.2 n个val](#1.2.2 n个val)
- [1.2.3 默认构造](#1.2.3 默认构造)
- [1.3 vector空间接口](#1.3 vector空间接口)
-
- [1.3.1 扩容](#1.3.1 扩容)
- [1.3.2 resize](#1.3.2 resize)
- [1.4 sort](#1.4 sort)
-
- [1.4.1 升序](#1.4.1 升序)
- [1.4.2 降序](#1.4.2 降序)
- [二 题目](#二 题目)
-
- [2.1 只出现一次的数字](#2.1 只出现一次的数字)
- [2.2 杨辉三角](#2.2 杨辉三角)
- [三 模拟实现](#三 模拟实现)
-
- [3.1 成员变量](#3.1 成员变量)
-
- [3.1.1 iterator](#3.1.1 iterator)
- [3.1.2 T](#3.1.2 T)
- [3.2 构造函数](#3.2 构造函数)
-
- [3.2.1 无参](#3.2.1 无参)
- [3.2.2 n个value](#3.2.2 n个value)
- [3.2.3 花括号初始化](#3.2.3 花括号初始化)
- [3.2.4 迭代器区间](#3.2.4 迭代器区间)
- [3.2.5 拷贝构造](#3.2.5 拷贝构造)
- [3.2.6 赋值](#3.2.6 赋值)
- [3.3 析构函数](#3.3 析构函数)
- [3.4 容量相关](#3.4 容量相关)
-
- [3.4.1 size](#3.4.1 size)
- [3.4.2 capacity](#3.4.2 capacity)
- [3.4.3 reserve](#3.4.3 reserve)
- [3.4.4 resize](#3.4.4 resize)
- [3.5 改变vector](#3.5 改变vector)
-
- [3.5.1 push_back](#3.5.1 push_back)
- [3.5.2 pop_back](#3.5.2 pop_back)
- [3.5.3 insert](#3.5.3 insert)
- [3.5.4 erase](#3.5.4 erase)
- [3.4.5 swap](#3.4.5 swap)
- [3.6 访问数据](#3.6 访问数据)
-
- [3.6.1 operator[ ]](#3.6.1 operator[ ])
- [3.6.2 迭代器](#3.6.2 迭代器)
一 vector的使用
1.1 插入删除
1.1.1 尾插
cpp
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
调试:

1.1.2 尾删
cpp
v.pop_back();
调试:

1.1.3 头插
cpp
v.insert(v.begin(), 0);
调试:

1.1.4 头删
cpp
v.erase(v.begin());
调试:

1.2 构造函数
1.2.1 initializer_list
花括号初始化。
cpp
vector<int> v1 = { 1,2,3,4,5 };
调试:

1.2.2 n个val
cpp
vector<int> v2(5,1);
调试:

1.2.3 默认构造
cpp
vector<int> v3;
调试:

1.3 vector空间接口
1.3.1 扩容
vs按照1.5倍方式扩容。
cpp
vector<int> v;
size_t capacity = v.capacity();
for (size_t i = 0;i < 100;++i) {
v.push_back(i);
if (capacity != v.capacity()) {
capacity = v.capacity();
cout << "capacity:" << capacity << endl;
}
}
运行:

不同平台实现不同,linux下使用的STL基本是按照2倍方式扩容。
1.3.2 resize
在开空间的同时还会进行初始化。
cpp
vector<int> v1;
v1.resize(5);
vector<int> v2;
v2.resize(5,1);
调试:

1.4 sort
1.4.1 升序
cpp
vector<int> v = {1,2,5,1,43,1,7,1};
sort(v.begin(), v.end());
调试:

1.4.2 降序
cpp
sort(v.begin(), v.end(), greater<int>());
调试:

二 题目
2.1 只出现一次的数字
思路:异或,最后剩下的就是只出现一次的数字。
cpp
int singleNumber(vector<int>& nums) {
int num = 0;
for(int n:nums){
num ^= n;
}
return num;
}
1.num即最后剩下的数字。
2.遍历nums,分别跟num异或。
3.一个数字跟0异或不变。
2.2 杨辉三角
cpp
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for(size_t i = 0;i < numRows;++i){
vv[i].resize(i + 1,1);
}
for(size_t i = 2;i < numRows;++i){
for(size_t j = 1;j < i;++j){
vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
}
}
return vv;
}
1.第一个for循环给各行开辟不同大小的空间,都初始化为1。
2.第二个for循环给 值不是1的位置 根据公式算出它的值。
三 模拟实现
1.注意模板声明和定义不能分离到俩文件。
2.该用引用的时候用引用,深拷贝的代价很大。
3.1 成员变量
cpp
iterator _first = nullptr;
iterator _last = nullptr;
iterator _end = nullptr;
先在这里给个默认值,以便无参构造。
3.1.1 iterator
这里的iterator,我们typedef一下,本质上是指针。
cpp
typedef T* iterator;
typedef const T* const_iterator;
3.1.2 T
这里的T是我们的模板参数。
cpp
template<class T>
class vector {
//...
};
3.2 构造函数
3.2.1 无参
cpp
vector() {}
因为我们已经在声明时给出缺省值,所以这个无参构造函数可以写空。
测试代码:
cpp
vector<int> v;
调试:

3.2.2 n个value
cpp
vector(size_t n,const T& val = T()) {
if(n == 0){
return;
}
reserve(n);
for (size_t i = 0;i < n;++i) {
_first[i] = val;
}
_last = _first + n;
}
1.若参数n为0,判定为无效构造,直接返回。
2.复用reserve,申请需要的空间。
3.依次赋值我们需要的n个数。
4.插入数据后,及时更新成员变量_last。
测试代码:
cpp
vector<int> v(2,1);
调试:

查看内存空间:

3.2.3 花括号初始化
借助initializer_list初始化
cpp
vector(std::initializer_list<T> il) {
size_t n = il.size();
reserve(n);
for (auto e:il) {
push_back(e);
}
_last = _first + n;
}
1.先存储il的元素个数。
2.拿这个n申请空间。
3.e遍历il,依次把e插入当前vector。
4.插入数据后,及时更新成员变量_last。
测试代码:
cpp
vector<int> v = {1,2,3,5,6};
调试:

3.2.4 迭代器区间
cpp
template<class InputIterator>
vector(InputIterator begin, InputIterator end) {
if(begin == end){
return;
}
size_t n = end - begin;
reserve(n);
while (begin != end) {
push_back(*begin);
++begin;
}
}
1.为了适配各种参数的类型,我们写一个模板函数。
2.排除无效迭代区间的情况。
3.存储元素个数n。
4.复用reserve申请空间。
5.参数begin依次往后。
6.复用push_back,把begin指向的值依次插入vector。
测试代码:
cpp
vector<int> v1 = {1,2,3,5,6};
vector<int> v2(v1.begin(),v1.end());
调试:

问题:非法的间接寻址
编译报错:

实际上这里会有问题,不过不是当前我们写的测试代码有问题。
cpp
vector<int> v(2,1);
是上面写的这句代码。
分析:

解决方案,写一个更匹配的构造:
cpp
vector(int n, const T& val = T()) {
if (n == 0) {
return;
}
reserve(n);
for (size_t i = 0;i < n;++i) {
_first[i] = val;
}
_last = _first + n;
}
把size_t改成int,再写一个版本。
3.2.5 拷贝构造
问题:默认是浅拷贝,所以需要自己写。
cpp
vector(const vector& v) {
reserve(v.capacity());
for(auto e:v){
push_back(e);
}
}
1.先申请空间。
2.再插入数据。
测试代码:
cpp
vector<int> v1 = {1,2,3,5,6};
vector<int> v2(v1);
调试:

3.2.6 赋值
复用swap(这个函数放到3.4.5了)。
cpp
void operator=(vector v) {
swap(v);
}
1.这里的参数v是临时对象,出了这个函数就会被销毁。
2.我们和当前对象swap,销毁的就是swap后的旧对象。
3.3 析构函数
cpp
~vector() {
delete[] _first;
_first = nullptr;
_last = nullptr;
_end = nullptr;
}
3.4 容量相关
3.4.1 size
返回元素个数。
cpp
size_t size() const {
return _last - _first;
}
3.4.2 capacity
返回容量大小。
cpp
size_t capacity() const {
return _end - _first;
}
3.4.3 reserve
错误代码:
cpp
void reserve(size_t newCapacity) {
if (newCapacity > capacity()) {
T* tmp = new T[newCapacity];
if (_first) {
memcpy(tmp, _first, size() * sizeof(T));
delete[] _first;
}
_first = tmp;
_last = _first + size();
_end = _first + newCapacity;
}
}
1.这里的size函数内部通过计算_last - _first来得到返回值。
2.但是我们的_first已经更新过了,这个计算结果会失效。
所以我们提前存储size:
cpp
void reserve(size_t newCapacity) {
if (newCapacity > capacity()) {
T* tmp = new T[newCapacity];
size_t size = size();
if (_first) {
memcpy(tmp, _first, size * sizeof(T));
delete[] _first;
}
_first = tmp;
_last = _first + size;
_end = _first + newCapacity;
}
}
1.排除参数不合法情况
2.申请新空间。
3.在_first还没更新之前存储size。
4.把原数据拷贝到新空间,后释放旧空间。
5.空间改变,更新各成员变量。
3.4.4 resize
cpp
void resize(size_t n = 0,T& val = T()) {
if (n > size()) {
reserve(n);
while (_last != _first + n) {
push_back(val);
}
}
else {
_last = _first + n;
}
}
1.如果参数n比当前size大,则插入参数val,直到当前vector的size为n。
2.否则直接更新_last,表示后面的数据不要了。
测试代码:
cpp
vector<int> v1 = { 1,2,3,5,6 };
v1.resize(10,1);
调试:

3.5 改变vector
3.5.1 push_back
cpp
void push_back(cosnt T& val) {
if (_last == _end) {
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
*_last = val;
++_last;
}
1.若vector满了(即_last == _end),按照2倍方式扩容。
2.尾插新数据。
3.更新_last。
问题:vector扩容后非法访问
cpp
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
调试:

我们原本存的"11111111111111111"可以看到乱码了,这恰巧发生在扩容之后,我们检查reserve。
cpp
void reserve(size_t newCapacity) {
if (newCapacity > capacity()) {
T* tmp = new T[newCapacity];
size_t mySize = size();
if (_first) {
memcpy(tmp, _first, mySize * sizeof(T));
delete[] _first;
}
_first = tmp;
_last = _first + mySize;
_end = _first + newCapacity;
}
}
解决方案:这里是浅拷贝问题,可以依次赋值。
cpp
if (_first) {
for (size_t i = 0;i < mySize;++i) {
tmp[i] = _first[i];
}
delete[] _first;
}
把拷贝原数据的代码改巴改巴。
调试:

3.5.2 pop_back
cpp
void pop_back() {
if (_first != _last) {
--_last;
}
}
直接更新_last。
测试代码:
cpp
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.pop_back();
for (auto e:v) {
cout << e << endl;
}
运行:

3.5.3 insert
会返回新的pos。
cpp
void insert(iterator pos,const T& val) {
if (pos <= _last) {
if (_last == _end) {
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
iterator it = _last;
while (it != pos) {
*(it) = *(it - 1);
--it;
}
*(pos) = val;
++_last;
}
}
测试一下:
cpp
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.insert(v.begin() + 2, "222222222");
运行,依旧会在扩容的部分报错:

这里涉及到迭代器失效问题。
①迭代器失效问题
迭代器(这里指原生指针)指向的空间被销毁,类似于野指针。
我们调试一下刚才的测试代码:
pos:

扩容后:

可以看到pos位置已经被释放了。
解决方案:记录pos相对位置
cpp
void insert(iterator pos,const T& val) {
if (pos <= _last) {
size_t myPos = pos - _first;
if (_last == _end) {
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
iterator it = _last;
while (it != _first + myPos) {
*(it) = *(it - 1);
--it;
}
_first[myPos] = val;
++_last;
}
}
提前存了个pos的相对位置myPos,再次调试就没问题啦:

②iterator传参失效
cpp
vector<int> v = {1,4,2,5,2,6,6};
int input = 4;
auto p = std::find(v.begin(),v.end(),input);
if(p) {
v.insert(p,input);
cout << *p << endl;
}
1.使用find函数找到位置p。
2.如果找到了,则插入数据。
3.输出位置p上的数据。
调试:

观察到迭代器p失效,因为insert内部执行了扩容逻辑。
解决方案:给一个扩容后的pos作为返回值
cpp
iterator insert(iterator pos,const T& val) {
if (pos <= _last) {
size_t len = pos - _first;
if (_last == _end) {
size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newCapacity);
}
pos = _first + len;
iterator it = _last;
while (it != pos) {
*(it) = *(it - 1);
--it;
}
*(pos) = val;
++_last;
return pos;
}
return nullptr;
}
需要改改测试代码:
cpp
vector<int> v = {1,4,2,5};
int input = 4;
auto p = std::find(v.begin(),v.end(),input);
if(p) {
p = v.insert(p,input);
cout << *p << endl;
}
调试:

3.5.4 erase
待完善代码:
cpp
void erase(iterator pos) {
assert(pos && pos >= _first && pos < _last);
iterator it = pos;
while (it != _last - 1) {
*(it) = *(it + 1);
++it;
}
--_last;
}
1.检查参数。
2.pos后的数据往前覆盖。
3.更新_last。
测试代码:
cpp
vector<int> v = {1,2,1,1,5,1};
auto it = v.begin();
while (it != v.end()) {
if (*it == 1) {
v.erase(it);
}
++it;
}
it遍历v,依次erase掉我们不要的数据。
①迭代器失效问题

vs系统判断迭代器已失效,linux上则不会,但我们代码本身确实有问题,具体如图所示:

erase中后面的数据往前覆盖我们不要的值后,it再自增,明显这个2被我们漏下了。
解决方案:执行erase后就不需要执行++it了
cpp
while (it != v.end()) {
if (*it == input) {
v.erase(it);
}
else {
++it;
}
}
②缩容后迭代器失效问题
不排除其他平台erase中可能会执行缩容逻辑,这样就会造成迭代器失效。
解决方案:用erase返回值替代原迭代器。
cpp
iterator erase(iterator pos) {
assert(pos && pos >= _first && pos < _last);
size_t len = pos - _first;
if(capacity() == size() * 2){
// shrink...
}
pos = _first + len;
iterator it = pos;
while (it != _last - 1) {
*(it) = *(it + 1);
++it;
}
--_last;
return pos;
}
中间加了缩容逻辑。
总结:迭代器失效了需要重新赋值才能访问。
cpp
it = v.erase(it);
3.4.5 swap
不能直接调用算法库的swap,需要自己写。
cpp
void swap(vector& v) {
std::swap(_first, v._first);
std::swap(_last, v._last);
std::swap(_end, v._end);
}
但是函数内部可以直接调用算法库的swap实现交换,因为三个成员变量都是内置类型。
3.6 访问数据
3.6.1 operator[ ]
cpp
T& operator[](size_t pos) {
assert(pos < size());
return _first[pos];
}
assert确保参数合法。
测试代码:
cpp
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
for (size_t i = 0;i < v.size();++i) {
cout << v[i] << endl;
}
运行:

3.6.2 迭代器
cpp
iterator begin() {
return _first;
}
iterator end() {
return _last;
}
const_iterator begin() const {
return _first;
}
const_iterator end() const {
return _last;
}
俩版本,一个非const,一个const。
容器vector的学习就到这里啦,下一篇 容器list 不久后就会更出来啦~

