Chapter2:vector概述和模拟实现
1.vector概述
vector的底层实际上就是一个顺序表,它能够自动的进行容量管理,使得我们在使用时不需要关注扩容问题。但是STL很奇怪,因为vector的翻译是向量。我们可以打开这个网站阅读有关vector的信息:vector的相关接口信息。
我们可以发现,vector的接口相较于string类来说是比较少的(因为string类要向STL靠齐,所以增加了很多重复的接口),并且在STL容器当中,所有容器的接口都大差不差,这就是STL的优越之处:

vector是一个类模板,它的原型是这样的:

它有两个模板参数,第一个名为T的模板参数就是我们要存储的元素类型,第二个名为Alloc的模板参数使用了缺省值(是的!模板参数也可以用缺省值!),缺省值为allocator<T>类型,它表示空间配置器。但是我们在使用的时候,往往只需要给定前面一个模板参数类型即可,即我们不关心什么是空间配置器。那么为什么vector需要空间配置器,或者说空间配置器有什么用?这就不得不提到内存池化技术:
STL的容器要频繁的申请内存扩容,那么每次如果按需申请堆上的空间是非常消耗操作系统资源和浪费时间的,因为申请内存会调用操作系统提供的系统调用,调用系统的调用会使CPU陷入内核,然后再申请内存最后回到用户态。所以STL使用了内存池化技术,即预先地开辟一大块空间,那么容器每次需要扩容时,就不需要直接向操作系统申请内存,而是直接从已经申请好的空间拿内存。这就类似于家里有水井一样,如果家里有一口水井,每次需要水的时候直接从水井打水就行了,就不需要辛辛苦苦地跑去河里挑水了。空间配置器的逻辑图是这样的:

其实我们身边就有空间配置器,那就是水龙头,我们直接在家打开水龙头就可以直接拿水,我们就不需要去水库了。
空间配置器实际上是一个可替换的组件,所以STL将空间配置器作为了模板的缺省参数,目的就是告诉使用STL的人,如果发现了性能、效率比STL当中的空间配置器更高的空间配置器,就可以直接通过模板参数替换这个组件。事实上99.99%的程序员都不关心这件事,因为都做不到写出一个更好的空间配置器。
那么vector同样也提供了一个名为resize()的接口,它的作用也是扩容,并且也分成三种情况(具体内容已经在string类的那篇博客中介绍了),但是它的参数非常奇怪:

首先明确一点,STL非常喜欢使用typedef,所以浏览容器档案可以发现这么一张表:

看不懂没关系,这里明确说明一下,size_type就是一个无符号整数,value_type就是容器当中存储的元素类型(实际上就是对模板参数的typedef)。
对于resize()这个接口来说,如果我们不传递第二个参数,需要填充元素时,填充的元素就是该元素类型通过构造函数实例化出来的对象。这里就存在一个问题,自定义类型确实可以通过构造函数来实例化对象,但是如果容器当中保存的元素是内置类型呢?所以这里就是一个C++的特性,即在模板当中,赋予了内置类型构造函数和析构函数的概念。我们以一幅图来理解我所要表达的意思:

因为vector容器的扩容机制是自动的,所以我们非常有必要去了解一下它是如何扩容的。以一段相同的代码,分别在Windows的Visual Studio 2013、Linux的g++中测设:
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<char> v;
size_t curCap = v.capacity();
cout << "当前容量: " << curCap << endl;
for (int i = 0; i < 1000; i++)
{
v.push_back('c');
if (curCap != v.capacity())
{
cout << "发生扩容,当前容量: " << v.capacity() << endl;
curCap = v.capacity();
}
}
return 0;
}

那么在使用STL容器的过程中,如果我们能够预先知道将使用多少空间,我们可以调用reserve()接口来预先扩容,减少频繁扩容带来的消耗。
同时我们需要注意一个非常重要的点,那就是vector<char>不能代替string。即使vector<char>在底层本质也是字符数组,但是它与string最大的区别就在于'\0',因为vector属于类模板,它就是一种泛型,它不可能针对用char实例化出的vector对象在其数组末尾添加一个'\0'。并且由于一些接口上的设计使得vector<char>无法代替string,例如vector没有流插入运算符重载、没有比较大小运算符重载、没有流提取运算符重载、没有+=运算符重载等等。
vector容器当中是少了很多成员函数的,例如find(),vector当中没有这个成员函数。但是STL提供一个算法库,名为<algorithm>,STL将find()作为了算法库的内容。原因在于STL当中的每个容器都要支持find()算法,那么每个容器都有一个find()成员函数的话,在设计上来说非常low的,所以STL将这些容器共用的算法作为了全局函数,当然,他们肯定也是泛型的。我们可以浏览该文档:

但是vector依然保留了swap()成员函数,因为我们在上一篇的string类当中说过,算法库当中的swap()会发生三次深拷贝。所以STL希望我们是这样调用swap()的:
int main()
{
vector<int> v1;
vector<int> v2;
v1.swap(v2); /*STL期望的swap()用法*/
return 0;
}
但事实上很难保证每个程序员都这样使用,难免有些人会这样使用:
int main()
{
vector<int> v1;
vector<int> v2;
swap(v1, v2);
return 0;
}
所以STL干脆在算法库当中重载了一个专门针对vector的swap()函数,它的底层也是调用成员函数:

vector的构造函数,相较于string来说是比较少的。因为string的出现比STL早,也就是出现的vector早,所以现在来看string类的许多接口都是冗余的(string类的接口好像有100多个):

我们需要重点关注的是上图红色方框所示的构造函数,我称它为范围构造。我们以一段代码来体会这个构造函数的作用:
int main()
{
string s = "hello world";/*这种写法会发生隐式类型转换*/
vector<char> v(s.begin(),s.end());
return 0;
}

这个范围构造的设计是非常高明的,相当泛型化。它能够将任何支持迭代器的容器的元素构造给任意一个支持迭代器的容器。
最后主要介绍一下[]重载和at()成员函数,他们两个功能、效果、作用都是一样的,它们两个的唯一区别就在于对错误的处理机制不同。同时也需要注意,[]重载和at()成员函数在某些容器当中是没有的,因为对于string类和vector来说底层都是顺序表,既然是顺序表就应该当作数组来看待,那么像其他容器,例如list,它的底层就是链表,那么对于list来说,不支持[]重载是正常的,支持了[]重载才是不正常的(map、set等容器的[]重载后面再说)。[]重载的错误处理机制非常暴力,使用了断言;而at()成员函数使用的是异常。这就意味着在release版本下,断言将不起作用:
int main()
{
vector<int> v;
v.reserve(10);/*这里虽然扩容了,但是有效数据依然为0*/
for (int i = 0; i < 10; i++)
{
v[i] = i;
}
return 0;
}

1.1习题练习

class Solution {
public:
int singleNumber(vector<int>& nums) {
/*利用异或的特性:
*相同的数字异或为0
*0异或上任何数得任何数*/
int ret = 0;
for(auto &e:nums)
{/*实际上就是在利用迭代器遍历*/
ret ^= e;
}
return ret;
}
};

class Solution {
public:
vector<vector<int>> generate(int numRows) {
/*分析可知要利用二维数组解题
*第一行:一个位置;第二行:两个位置;第三行:三个位置......
*每一行的第一个位置和最后一个位置都是1
*本体主要考验vector的扩容理解*/
vector<vector<int>> v(numRows);/*先确定有多少行*/
for(int i=0;i<v.size();i++)
{
v[i].resize(i+1,0);
v[i].front() = 1;
v[i].back() = 1;
}
for(int i=0;i<v.size();i++)
{
for(int j=0;j<v[i].size();j++)
{
if(v[i][j] == 0) v[i][j] = v[i-1][j] + v[i-1][j-1];
}
}
return v;
}
};
1.2迭代器失效问题
我们在使用insert()的时候,很有可能会发生扩容,而扩容的策略一般都是异地扩容,此时就会产生迭代器失效问题:
void test5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
cout << "size:" << v.size() << " capacity:" << v.capacity() << endl;
for (auto &e : v) cout << e << " ";
cout << endl;
/*一个迭代器指向了元素为3的位置*/
auto it = find(v.begin(), v.end(), 3);
v.insert(it, 30);
cout << "size:" << v.size() << " capacity:" << v.capacity() << endl;
for (auto &e : v) cout << e << " ";
cout << endl;
/*但是insert()之后,这个迭代器便不能使用了*/
cout << *it << endl;
}

尽管在g++中,或者是VS的release模式下程序依然能正常运行,但是我们还是对一个"野迭代器"进行了非法操作:

从这副图中不难看出发生扩容之后,it迭代器依然指向了以前的旧空间,所以在Visual Studio 2013当中,强制将it作为了已经失效的迭代器;而在g++中,因为底层使用的STL版本与VS使用的STL版本不同,所以检查不出来it迭代器是否失效。具体原因就在与VS使用的P.J版本,其vector迭代器的实现不是原生指针,而是一个具体的类型,所以执行"*it"时做的工作要比直接对指针解引用要做的工作多;而g++使用的是SGI版本,其vector迭代器的实现就是一个原生指针:

这也就意味着在g++下能运行的代码放在VS当中可能运行不了,这也意味着STL必须提出一个通用的解决迭代器失效的方案,那么这个方案就是insert()的返回值:

利用insert()的返回值可以写出一份来VS和g++中都可以正常运行的代码:
void test5()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
cout << "size:" << v.size() << " capacity:" << v.capacity() << endl;
for (auto &e : v) cout << e << " ";
cout << endl;
/*一个迭代器指向了元素为3的位置*/
auto it = find(v.begin(), v.end(), 3);
it = v.insert(it, 30);// 利用insert()的返回值更新it
cout << "size:" << v.size() << " capacity:" << v.capacity() << endl;
for (auto &e : v) cout << e << " ";
cout << endl;
/*但是insert()之后,这个迭代器便不能使用了*/
cout << *it << endl;
}
还有另外一个迭代器失效问题,是在调用erase()时发生的:
void test6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
auto it = find(v.begin(), v.end(), 3);
v.erase(it);
cout << *it << endl;
}

调用erase()造成迭代器失效的原因非常简单,分成两个方面:
1.调用erase()删除迭代器指向的元素后,该迭代器就没有意义了,即使该迭代器依然指向一个合法空间,但是这是不安全的(显然g++没有这么做,所以VS强制断言报错):

2.调用erase()删除迭代器指向的元素后,该迭代器指向了一个非法位置:

所以这两种原因都可是认为迭代器失效的原因,但是VS和g++的处理方式不同,这就意味着在g++上编译的程序放到VS当中可能不能正常运行,所以STL将erase()设计成了有返回值的接口:

那么我们可以利用erase()的返回值写出一份VS和g++都可以正常运行的代码:
void test6()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
auto it = find(v.begin(), v.end(), 3);
/*利用erase()的返回值更新it
*但是erase的返回值为it值,解引用依然失败*/
it = v.erase(it);
cout << *it << endl;
}
那么insert()和erase()的返回值意味着什么呢?对于insert()来说,它返回的是指向新插入元素的迭代器;对于erase来说,它返回的是指向删除元素的下一个元素的迭代器。


浙公网安备 33010602011771号