C++ STL::vector底层剖析
目录
一、vector概述
二、vector常用接口
1、构造
2、迭代器
3、范围for
4、reserve
5、resize
6、push_back
7、insert
8、clear
三、vector嵌套
(1)含义
(2)遍历
(3)杨辉三角
四、vector实现
1、文件
2、成员变量
3、size、capacity、empty
4、begin、end
5、operator[ ]
6、析构函数
7、clear
8、pop_back
9、erase
(1)实现
(2)迭代器失效
10、reserve
11、push_back
12、insert
(1)实现
(2)迭代器失效(insert)
13、print_container
14、resize
15、拷贝构造
(1)直接构造
(2)迭代器版本
(3)多个相同元素构造
16、operator=
(1)直接构造赋值
(2)swap间接构造赋值
五、结语
一、vector概述
C++STL标准模块库中集成了一系列的容器,每个容器都有着较丰富的接口和功能,本文将围绕vector展开介绍,vector底层以动态顺序表为基础,其接口则是在动态顺序表的基础上进行增删改查等一系列操作,vector相比STL的其他容器,如string,其接口和实现更加简洁,实现起来相比string没有那么繁琐,vector作为STL的一个重要容器,在C++很多领域都有广泛的应用,vector实现了自动内存管理,能够根据需要灵活改变空间大小,此外,vector与算法也紧密关联,能与算法实现无缝协作,具有强大的灵活性和功能性。
二、vector常用接口
调用vector相关接口,需包含头文件<vector>,即#include<vector>
1、构造
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1;vector<int> v2(10, 1);for (size_t i = 0;i < v2.size();i++){cout << v2[i] << " ";}cout << endl;return 0;
}
vector底层为类模块类型,使用时需进行实例化,如vector<int> v1,使用默认构造对v1进行初始化,vector<int> v2(10,1),对v2进行构造并将v2初始化为10个int元素1的一维数组,size()为vector的一个接口,表示vector的元素个数,for循环对v2进行遍历,[ ]重载,对v2特定下标元素进行访问,结果如(1)所示。

(1)
2、迭代器
#include<iostream>#include<vector>using namespace std;int main(){vector<int> v2(10, 1);vector<int>::iterator it = v2.begin();while (it != v2.end()){cout << *it << " ";it++;}cout << endl;}
STL容器的迭代器用法相似,vector也不例外,需要注意的是vector为类模板,使用时需进行实例化,即vector<int>::iterator it=v2.begin(),begin()指向vector的首元素,end()指向最后一个元素的下一个位置,通过while循环即可实现对v2的遍历,结果如(2)所示。

(2)
vector也支持迭代器区间构造:
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v2(10, 1);vector<int> v3(++v2.begin(), --v2.end());for (size_t i = 0;i < v3.size();i++){cout << v3[i] << " ";}cout << endl;return 0;
}
vector<int> v3(++v2.begin(),--v2.end()),即去掉v2的首元素和最后一个元素,取v2剩余元素对v3进行构造,结果如(3)所示。

(3)
3、范围for
范围for底层原理为迭代器,范围for本质也是通过迭代器来访问容器vector
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);for (auto e : v1){cout << e << " ";}cout << endl;return 0;
}
auto会自动识别数据类型,将v1的每个数据拷贝给e,通过e来遍历v1的内容,结果如(4)所示。

(4)
4、reserve
reserve接口用于预先给vector容器开辟特定大小的空间,可以避免频繁扩容,capacity表示容器的空间大小。
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);v1.reserve(20);cout << v1.size() << endl;cout << v1.capacity() << endl;v1.reserve(15);cout << v1.size() << endl;cout << v1.capacity() << endl;v1.reserve(5);cout << v1.size() << endl;cout << v1.capacity() << endl;return 0;
}
v1.reserve(20),预先开20个空间给v1,v1.size()表示v1的元素个数,不受reserve的影响,均为10,v1.capacity()表示v1的空间大小,故v1.capacity()为20,后面reserve进行了缩容,VS环境下采取空间换时间,不进行缩容,还是保留原来空间的大小,故v1.capacity()为20,不同环境会有所不同,linux环境下会进行缩容,VS环境下结果如(5)所示。

(5)
5、resize
resize用于改变vector的元素个数,即改变size大小,resize(n),若n大于当前的size,reserve会通过构造函数对size位置后的元素进行初始化,若n<size,则vector将减少元素个数,使得size==n。
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);v1.reserve(20);cout << v1.size() << endl;cout << v1.capacity() << endl;v1.resize(15, 2);cout << v1.size() << endl;cout << v1.capacity() << endl;v1.resize(25, 3);cout << v1.size() << endl;cout << v1.capacity() << endl;v1.resize(5);cout << v1.size() << endl;cout << v1.capacity() << endl;return 0;
}
v1.resize(15,2),表示将v1的元素增加到15个,用2填充,v1.resize(25,3)也是同样的道理,v1.resize(5)将v1的元素减少到5个,此时size==5,resize并不直接改变capacity的大小,capacity会根据需要进行扩容,结果如(6)所示。

(6)
6、push_back
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);v1.push_back(2);for (auto e : v1){cout << e << " ";}cout << endl;return 0;
}
push_back接口实现在vector后尾插数据,v1.push_back(2),表示在v1后尾插数据2,结果如(7)所示。

(7)
7、insert
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);v1.insert(v1.begin() + 3, 10);for (auto e : v1){cout << e << " ";}cout << endl;return 0;
}
insert实现在vector指定位置插入元素,v1.insert(v1.begin()+3,10)表示在v1第3个元素后插入数据10,结果如(8)所示。

(8)
8、clear
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(10, 1);for (auto e : v1){cout << e << " ";}cout << endl;cout << v1.size() << endl;cout << v1.capacity() << endl;v1.clear();for (auto e : v1){cout << e << " ";}cout << endl;cout << v1.size() << endl;cout << v1.capacity() << endl;return 0;
}
clear用于清空vector的数据,但不改变其空间大小,即clear使size==0,但不改变capacity,v1.clear(),则v1.size()==0,v1.capacity不会改变,结果如(9)所示。

(9)
三、vector嵌套
vector作为一个类模板,可嵌套其他容器,这是vector灵活性的一个体现。
1、vector<string>
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<string> v1;string st1("hello");v1.push_back(st1);v1.push_back("world");for (const auto& e : v1){cout << e << " ";}cout << endl;return 0;
}
vector<string> v1,定义了一个存储string对象的vector,string st1("hello"),v1.push_back(st1),将st1尾插到v1中,同理v1.push_back("world"),通过范围for遍历v1,引用可减少拷贝,可知结果为"hello world",如(10)所示。

(10)
2、vector<vector<int>>
(1)含义
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(5, 1);vector<vector<int>> v2(10, v1);v2[2][1] = 2;return 0;
}
vector<vector<int>>,表示vector的元素为vector<int>,vector<int>为一维数组,一维数组的数组为二维数组,即vector<vector<int>>表示二维数组,如(11)所示。

(11)
vector<int> v1(5,1),构造并初始化v1,vector<vector<int>> v2(10,v1),构造并初始化10个元素为v1的二维数组v2,v2[2][1]=2,[ ]为重载操作符,v2[2][1]可拆分两部分来看,v2[2]表示访问v2第3个元素,即vector<int> v1,再访问v1[1],即访问v1的第2个元素1,将其修改为2,因此v2[2][1]=2表示将二维数组v2第3行第2列的元素修改为2,如(12)所示,具体表示了vector<int>、vector<vector<int>>中operator[ ]的实现原理。

(12)
对于vector<vector<int>>[ ],返回类型为vector<int>,即v2[2]的返回类型,v2[2]为vector<int>类型,则vector<int>v2[2] [ ]的返回类型为int,即v2[2] [1]为int类型,表示二维数组v2第3行第2列的元素,v2[2][1]=2表示将其修改为2,也可表示为v2.operator[ ](2).operator[ ](1)=2,这种写法比较少用。
(2)遍历
#include<iostream>
#include<vector>
using namespace std;
int main()
{vector<int> v1(5, 1);vector<vector<int>> v2(10, v1);v2[2][1] = 2;for (size_t i = 0;i < v2.size();i++){for (size_t j = 0;j < v2[i].size();j++){cout << v2[i][j] << " ";}cout << endl;}return 0;
}
遍历vector<vector<int>>,就可以采取类似遍历二维数组的方式来遍历,即通过v2[i][j]来遍历,通过两层for循环,外层for循环v2.size()为二维数组的行数,内层for循环v2[i].size()为二维数组的列数,cout<<v2[i][j]<<" ",即可实现对二维数组的遍历,如(13)所示。

(13)
(3)杨辉三角

(14)
vector<vector<int>>的一个经典应用就是杨辉三角,杨辉三角中每行的首元素和最后一个元素都为1,其余数的大小为其左上方和右上方的数之和,可以考虑用vector<vector<int>>实现杨辉三角。
class Solution
{
public:vector<vector<int>> YanghuiTriangle(int num){vector<vector<int>> vv(num);for (size_t i = 0;i < num;i++){vv[i].resize(i + 1, 1);}for (int i = 2;i < vv.size();i++){for (int j = 1;j < vv[i].size() - 1;j++){vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];}}return vv;}
};
杨辉三角本质可看成一个二维数组,因此先构造num行的二维数组vv,即vector<vector<int>> vv(num),杨辉三角每行的元素个数为当前所在的行数+1,先通过for循环将每行的元素都置为1,即vv[i].resize(i+1,1),这样每行的头尾元素都为1了,再处理剩余不为1的元素,由于第1行和第2行元素都为1,因此外层for循环只需从第3行开始遍历,内层for循环处理每行元素,每行的首尾元素为1,无需处理,因此每行从第2个元素开始遍历,到倒数第2个元素结束,元素的大小等于左上方和右上方的数据之和,即vv[i][j]=vv[i-1][j-1]+vv[i-1][j],最后返回二维数组vv,这样就实现了杨辉三角。
四、vector实现
1、文件
vector实现所需文件:vector.h头文件,负责vector接口声明与实现,test.cpp负责vector接口测试,如(15)所示,vector实现为模板类型,可以根据需求灵活使用。

(15)
2、成员变量
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace YZK
{template<class K>class vector{public:typedef K* iterator;typedef const K* const_iterator;private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
vector实现为模板类型,可以通过typedef指针实现迭代器,即typedef K* iterator,typedef const K* const_iterator,3个迭代器指针成员变量即可实现vector结构,_start指向vector首元素,_finish指向vector最后一个元素的下一个位置,_end_of_storage指向vector空间的结束位置。
3、size、capacity、empty
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace YZK
{template<class K>class vector{public:typedef K* iterator;typedef const K* const_iterator;size_t size() const{return _finish - _start;}size_t capacity() const{return _end_of_storage - _start;}bool empty(){return _start == _finish;}private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}
size表示vector的元素个数,可用_finish-_start表示,capacity为vector的空间大小,可用_end_of_storage-_start表示,empty用于判断vector有无数据,可通过_start==_finish判断,这样就实现了size、capacity、empty 3个接口。
4、begin、end
iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}
begin、end为迭代器所需接口,begin指向vector首元素位置,即返回_start,end指向vector最后一个元素的下一个位置,即返回_finish,const修饰begin、end也是同样的道理,同样指向_start、_finish。
5、operator[ ]
K& operator[](size_t i){assert(i < size());return _start[i];}const K& operator[](size_t i)const{assert(i < size());return _start[i];}
operator[ ]重载用于访问vector特定下标位置的元素,assert断言确保下标不越界,return返回_start[i]即可,同理const修饰也是返回_start[i]。
6、析构函数
~vector(){if (_start){delete[] _start;_start = _finish = _end_of_storage = nullptr;}}
vector析构:先判断_start是否为空,若不为空,则通过delete[ ] _start释放空间,再将_start、_finish、_end_of_storage置为nullptr,就完成了vector的析构。
7、clear
void clear(){_finish = _start;}
clear用于清空vector的数据,但不改变vector空间大小,因此只需让_finish=_start即可,此时size==0。
8、pop_back
void pop_back(){assert(!empty());_finish--;}
pop_back用于删除vector的数据,assert断言确保vector数据不为空,_finish--就实现了vector数据的删除。
9、erase
(1)实现
iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);iterator it = pos + 1;while (it != end()){*(it - 1) = *it;it++;}_finish--;return pos;}
erase用于vector指定位置元素的删除,删除的原理是用后一个数据覆盖前一个数据实现删除,通过while循环即可实现删除,_finish--,最后返回pos位置即可。
(2)迭代器失效
这里需要注意的是erase会导致迭代器失效,erase删除数据后,这时迭代器所指向的数据会发生变化,不再指向原先要删除的数据,指向了删除元素的下一个元素,这种迭代器指向数据的变化称为迭代器的失效,下面以一个例子说明迭代器的失效,在v1中删除偶数,可以通过迭代器、while循环遍历v1,erase来删除偶数,这里需要注意的就是erase删除偶数后,迭代器指向的数据就发生了变化,指向了删除元素的下一个元素,因此需要更新迭代器的值,不需要++,否则会跳过该元素,若元素不为偶数,则it++,继续遍历下一个元素,这样就实现了在v1中删除偶数,代码如下:
void test_vector3(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);auto it = v1.begin();while (it != v1.end()){if (*it % 2 == 0){it=v1.erase(it);}else{it++;}}print_Container(v1);}
结果如(16)所示:

(16)
erase删除过程迭代器的变化可参考(17)所示,假设在vector删除数据2,迭代器p指向2,erase删除2之后,这时p指向的数据就发生了变化,不再指向2,指向2的下一个元素3,这时迭代器的位置意义就发生了变化,称为迭代器的失效,因此erase删除数据后需要更新迭代器的值,再进行访问。

(17)
10、reserve
void reserve(size_t n){if (n > capacity()){size_t old_size = size();K* tmp = new K[n];for (size_t i = 0;i < old_size;i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;}}
reserve用于预先给vector开辟特定大小的空间,当n大于capacity时,需要扩容,这时需要注意的是扩容会导致_start改变,则size=_finish-_start也会发生改变,因此需要先保存size的值,通过new实现扩容,for循环将_start的内容拷贝到tmp中,再释放_start,将tmp赋值给_start,_finish=tmp+old_size,_end_of_storage=tmp+n,这样就实现了reserve扩容,当reserve的空间小于size时,这时vector不会进行缩容,不进行任何操作。
11、push_back
void push_back(const K& x){if (_finish == _end_of_storage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;_finish++;}
push_back实现vector数据的尾插,先判断是否需要进行扩容,若_finish==_end_of_storage,则需要扩容,通常reserve采取2倍扩容,先判断capacity是否为0,若为0,则初始化空间为4,否则就采取2倍扩容,再尾插数据,*_finish=x,_finish++,就实现了push_back的功能。
测试(test.cpp)
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>::iterator it = v1.begin();while (it != v1.end()){cout << *it << " ";it++;}cout << endl;}
对push_back、operator[ ]、iterator迭代器等进行测试,结果如(18)所示,结果正确,测试通过。

(18)
12、insert
(1)实现
iterator insert(iterator pos, const K& x){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 = x;_finish++;return pos;}
(2)迭代器失效(insert)
insert实现在vector指定位置插入数据,同样需先判断是否需要扩容,若_finish==_end_of_storage,则需要扩容,这里同样需要注意的是扩容会导致迭代器失效,会改变_start,因此需先保存pos和_start的相对位置,通过reserve进行扩容,扩容完成后,再将_start+len赋值给pos,扩容过程的具体变化可参考(19)所示,假设vector数据为1、2、3、4,在1位置后插入数据20,需要异地扩容,这时原先的空间将被释放,pos就变为野指针了,因此扩容会导致迭代器失效,原本的迭代器会变成野指针,需要更新迭代器的值,需要注意的是更新后迭代器的值也发生了变化,不再指向原来的位置,插入数据后,pos指向新插入数据的位置,这也属于迭代器失效的一种,插入数据后,pos指向的数据发生改变,这时不建议访问pos,若要访问,则需更新迭代器的值,再进行访问。

(19)
更新完迭代器的值后,通过while循环将数据往后挪动,再插入pos位置的数据x,++_finish,返回更新后迭代器的值,这样就实现了insert。
13、print_container
template<class Container>void print_Container(const Container& v){auto it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;}
print_container用于输出容器的数据,为函数模板类型,可以利用迭代器实现输出,通过while循环遍历容器的数据。
测试(test.cpp)
void test_vector2(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);v1.insert(v1.begin() + 2, 30);print_Container(v1);}
v1.insert(v1.begin()+2,30),表示在v1第3个元素位置插入数据30,结果为1 2 30 3 4 5,如(20)所示,结果正确,测试通过。

(20)
14、resize
void resize(size_t n, K val = K()){if (n < size()){_finish = _start + n;}else{reserve(n);while (_start + n > _finish){*_finish = val;_finish++;}}}
resize可以改变vector的元素个数,当resize(n),n<size时,这时resize会减少元素个数,使得size==n,即_finish=_start+n,当n>size时,先调用reserve确保n个空间,再通过while循环对size位置后的元素进行初始化,若没有传参,则通过默认构造对其进行初始化,表示为K val=K(),再逐个插入数据,即*_finish=val,_finish++,就实现了resize。
测试(test.cpp)
void test_vector4(){vector<int> v1;v1.resize(10, 1);print_Container(v1);v1.resize(15, 2);print_Container(v1);v1.resize(25, 3);print_Container(v1);v1.resize(5);print_Container(v1);}
resize常用于vector的初始化,v1.resize(10,1),初始化v1为10个元素均为1,v1.resize(15,2),则在v1.resize(10,1)的基础上对11~15位置的元素初始化,初始化为2,v1.resize(25,3)则在两者的基础上对16~25的元素初始化,初始化为3,v1.resize(5),将减少v1的元素个数到5个,即v1的5个元素都为1,结果如(21)所示,结果正确,测试通过。

(21)
15、拷贝构造
vector相比其他容器,有多种构造写法:
(1)直接构造
vector(const vector<K>& v){reserve(v.size());for (auto& e : v){push_back(e);}}
第1种是直接构造写法,先调用reserve开好跟v一样大小的空间,再通过范围for遍历v,并通过push_back将数据逐个尾插,就实现了直接拷贝构造。
(2)迭代器版本
template<class inputiterator>vector(inputiterator first, inputiterator last){while (first != last){push_back(*first);first++;}}
迭代器版本实现为模板类型,可兼容不同类型的容器,灵活性更高,只需传一段迭代器区间,通过while循环遍历该区间,调用push_back将数据逐个尾插,即可实现迭代器区间构造。
(3)多个相同元素构造
vector(size_t n, const K& val = K()){reserve(n);for (size_t i = 0;i < n;i++){push_back(val);}}
n个相同元素构造,先通过reserve开好n个空间,再通过for循环逐个将相同数据尾插,若没有显式传参,则传默认构造,表示为const K& val=K(),即可实现多个相同元素的构造。
测试(test.cpp)
void test_vector5(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);vector<int> v2 = v1;print_Container(v2);vector<int> v3;v3.push_back(5);v3.push_back(6); v3.push_back(7);v3.push_back(8);v3.push_back(9);v3.push_back(10);vector<int> v4(v3.begin(), v3.begin() + 3);print_Container(v4);vector<string> v5(10, "abc");print_Container(v5);}
对vector的3种构造方式进行测试,第1种使用v1对v2进行直接拷贝构造,第2种v4采取迭代器区间构造,取v3的前3个元素进行构造,第3种对v5进行多个相同元素的构造,即vector<string> v5(10,"abc"),结果如(22)所示,结果正确,测试通过。

(22)
16、operator=
vector的operator=重载也有两种写法:
(1)直接构造赋值
vector<K>& operator=(const vector<K>& v){if (this != &v){clear();reserve(v.size());for (auto& e : v){push_back(e);}}return *this;}
第1种是直接构造,先分两种情况,若是自身赋值,则无需构造,直接返回*this即可,若非自身赋值,则先clear清空vector的数据,再调用reserve开好与v一样大的空间,通过范围for遍历v,push_back将v的数据逐个尾插,就实现了直接构造进行赋值。
(2)swap间接构造赋值
void swap(vector<K>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}vector<K>& operator=(vector<K> v){swap(v);return *this;}
另一种方式是通过swap来间接进行构造赋值,先实现swap函数,完成两个vector的交换,交换两个vector,只需交换二者的_start、_finish、_end_of_storage即可,可以调用std库的swap函数完成二者的交换,这样就实现了vector的swap函数,那么实现operator=,就只需传赋值对象的拷贝,再调用swap函数交换二者,就通过swap间接实现了赋值。
测试(test.cpp)
void test_vector6(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);vector<int> v2;v2.push_back(10);v2.push_back(20);v2.push_back(30);v1 = v2;print_Container(v1);}
operator=重载测试:注意赋值是两个已存在对象的赋值,先构造并初始化v1、v2,再将v2赋值给v1,则v1的结果为:10 20 30,如(23)所示,结果正确,测试通过。

(23)
五、结语
本文主要围绕C++中STL容器的vector展开介绍,从vector接口的用法出发,着重分析了vector的常用接口,可以看出,vector接口相比其他容器,如string要更加简洁,没有那么繁琐冗余,且vector相比其他容器也更加灵活,vector结构上也支持嵌套,可嵌套string等其他容器,也可嵌套自身,即二维数组,利用这个结构特点,可以借助vector实现杨辉三角。vector接口功能的实现逻辑上也较为简单,需要注意的是insert、erase会导致迭代器的失效,需要更新迭代器的值再进行访问,此外,vector的构造也有多种方式,较为灵活,可以采取直接构造、迭代器区间构造、以及多个相同元素构造3种构造方式,operator=赋值重载除了直接构造赋值,也可通过swap交换间接实现构造赋值。vector强大的灵活性与功能性无疑是C++ STL中的一把瑞士军刀,很多情况下,vector可以解决90%的问题,了解并熟练使用vector的接口,以及掌握vector底层原理,是C++ STL的核心,当你不知道该用什么容器时,优先考虑使用vector!
