前言
STL 容器是 C++ 开发中绕不开的 “神兵利器”,而vector作为最常用的动态数组容器,更是新手入门 STL 的核心内容。但多数时候,我们只是 “会用”vector,却对它的底层逻辑一知半解 —— 比如它如何动态扩容?push_back的内存管理是怎样的?构造函数的匹配规则为何如此复杂?
一、vector的介绍
简单说,vector是 C++ 里可以动态变大小的数组 —— 它既保留了普通数组 “连续存储、下标访问高效” 的优点,又解决了普通数组 “大小固定、不能自动扩容” 的痛点。
它的底层逻辑是动态分配的数组:
当你往vector里加新元素时,它会自动管理内存 —— 如果当前数组存满了,就会重新分配一块更大的空间,把旧元素全搬过去,再继续存新元素(不过这个 “搬家” 操作开销不低,所以vector会提前多分配一些额外空间,避免频繁搬家)。
vector是我们学习的第一个真正的 STL 容器,它接口的使用方式和string有一点点的不同,但大部分都是一样的,所以这里我们就只演示其中一些接口的使用,大家如果有疑惑的地方直接在cplusplus上面查看对应的文档即可。
二、构造函数
vector提供了四种构造方式 – 无参构造、n个val构造、迭代器区间构造以及拷贝构造:
其中构造函数的最后一个参数alloc是空间配置器,它和内存池有关,作用是提高空间分配的效率;我们日常使用时不用管这个参数,使用它的缺省值即可,但是可能有极少数的人想要用自己实现的空间配置器来代替 STL 库提供的,所以留出了这一个参数的位置。
vector<int>v1;// 默认构造:空向量vector<int>v2(5,1);// 5 个元素,每个初始化为 1vector<int>v3(v2.begin(),v2.end());// 用迭代器区间拷贝构造vector<int>v4(v3);// 直接拷贝构造// 遍历输出 v1:当前为空,循环不会执行for(size_t i=0;i<v1.size();++i){cout<<v1[i];}cout<<endl;// 仅换行// 遍历输出 v2:5 个 1for(size_t i=0;i<v2.size();++i){cout<<v2[i];}cout<<endl;// 再次遍历输出 v3(与上一段完全相同)for(size_t i=0;i<v3.size();++i){cout<<v3[i];}cout<<endl;// 再次遍历输出 v4(与上一段完全相同)for(size_t i=0;i<v4.size();++i){cout<<v4[i];}cout<<endl;需要注意的是,迭代器区间构造是一个函数模板,即我们可以用其他类来构造vector对象:
string s="hello";vector<char>v(s.begin(),s.end());// 用迭代器区间拷贝构造for(size_t i=0;i<v.size();++i){cout<<v[i];}cout<<endl;同时,上面还有一个非常重要的细节:
在
n个val的构造中,val的缺省值是T的匿名对象就像是T(),该对象使用T的默认构造来初始化,而不是以 0 作为缺省值,这是因为T不仅仅可能是内置类型,也可能是自定义类型,比如string、list、vector;
当T为自定义类型时,0 就不一定能够对val进行初始化,所以我们需要使用T的匿名对象来调用默认构造完成初始化工作;当T为内置类型时,我们仍然可以以这种方式进行初始化,因为内置类型也具有构造函数,你没听错,内置类型也是有构造函数的,大家可以理解为,为了解决上面这种情况,编译器对内置类型进行了特殊处理,像是把自定义类型也包装了一下;
利用匿名对象调用构造函数来作为缺省值的方法在下面模拟实现中也有体现。
三、扩容机制
vector的扩容机制和string的扩容机制是一样的,因为它们都是动态增长的数组:VS 下大概是 1.5 倍扩容,Linux g++ 下是标准的二倍扩容,多出来的也是一样因为内存对齐的原因,测试用例如下:
voidTestVectorExpand(){size_t sz;vector<int>v;sz=v.capacity();cout<<"making v grow:\n";for(inti=0;i<1000;++i){v.push_back(i);if(sz!=v.capacity()){sz=v.capacity();cout<<"capacity changed: "<<sz<<'\n';}}}四、三种遍历方式
和string一样,vector也支持三种遍历方式 – 下标加[]遍历、迭代器遍历、范围 for 遍历:
vector<int>v(10,5);// 10 个元素,每个值为 5// 1. 下标 + []for(inti=0;i<v.size();++i)cout<<v[i]<<" ";cout<<endl;// 2. 迭代器vector<int>::iterator it=v.begin();while(it!=v.end()){cout<<*it<<" ";++it;}cout<<endl;// 3. 范围 forfor(auto&e:v)cout<<e<<" ";cout<<endl;需要注意的是,vector和string之所以支持 下标 +[]的方式遍历,是因为它们底层都是数组,而数组支持随机访问,但是像我们后面要学习的list、set、map等容器,它们的底层不是数组,不支持随机访问,就只能通过迭代器和范围 for 的方式进行遍历了;不过,范围 for 只是一个外壳,它在使用时也是被替换成迭代器,所以其实迭代器遍历才是最通用的遍历方式。
五、容量操作
vector有如下容量相关的接口:
| 成员函数 | 功能 |
|---|---|
size() | 返回当前容器中实际存储的元素个数。 |
max_size() | 返回容器理论上能存储的最大元素个数。(很少用) |
resize(n) | 把容器大小改为n,多删少补(补的默认用 0 值或指定值)。 |
capacity() | 返回当前已分配的存储空间(容量),单位是元素个数。 |
empty() | 容器为空时返回true,否则返回false。 |
reserve(n) | 提前一次性分配至少能存n个元素的内存,避免后续频繁扩容。 |
其中,最重要的两个函数是reserve和resize,reserve只用于扩容,它不改变size的大小;而resize是扩容加初始化,既会改变capacity,也会改变size;
注意:reserve和resize,包括后面的clear函数都不会缩容,因为缩容需要开辟新空间、拷贝数据、释放旧空间,而对于自定义类型又有可能存在深拷贝问题,时间开销极大;vector中唯一可能缩容的函数就只有shrink_to_fit,对于它来说,如果capacity大于size,它会进行缩容,让二者相等。
六、元素访问
| 成员函数 | 功能 |
|---|---|
v[pos] | 下标访问:返回第pos个元素(不越界检查)。 |
v.at(pos) | 安全下标访问:返回第pos个元素,越界时抛出std::out_of_range。 |
v.front() | 返回首元素 |
v.back() | 返回尾元素 |
其中,operator[]和at都是返回pos下标位置元素的引用,且它们内部都会对pos的合法性进行检查;不同的是,operator[]中如果检查到pos非法,那么它会直接终止程序,报断言错误,而at则是抛异常;
七、修改 – 迭代器失效
vector提供了如下接口来进行修改操作:
| 成员函数 | 功能 |
|---|---|
v.assign(n, val) | 清空原内容并重新赋值为n个val。 |
v.assign(beg, end) | 清空原内容并用迭代器区间赋值。 |
assign&&push_back&&pop_backassign函数用来替换vector对象中的数据,支持n个val替换,以及迭代器区间替换,push_back尾插、pop_back尾删,这些接口的使用和string一模一样,这里就不再过多阐释;insert&&erase和
string不同,为了提高规范性,STL 中的容器都统一使用iterator作为pos的类型,并且插入/删除后会返回pos:所以,以后我们如果要在中间插入或删除元素的话,必须配合算法库里面的
find函数来使用:代码演示
vector<int>v;for(size_t i=0;i<20;i++){v.push_back(i);}//找到6删除6vector<int>::iterator tmp=find(v.begin(),v.end(),6);v.erase(tmp);for(auto&e:v)cout<<e<<" ";这里其实有一个问题我们来解引用一下
*tmp程序报了一个这个错误,这个就是迭代器失效了,为什么了,我来画个图理解一下
如何解决了,我们让
tmp指向删除元素的下一个位置就好了那我们继续思考一下是不是
insert也会有问题,先分析再验证一下但是为了代码的可移植性,我们统一认为
insert和erase之后迭代器会失效,所以,如果要再次使用迭代器,我们必须对其进行更新;我们以移除vector中的所有偶数为例:vector<int>v;vector<int>::iterator it=v.begin();for(size_t i=0;i<20;i++){it=v.insert(it,i);}while(it!=v.end()){if(*it%2==0)it=v.erase(it);elseit++;}for(auto&e:v)cout<<e<<" ";cout<<endl;swap和
string一样,由于算法库swap函数存在深拷贝的问题,vector自己提供了一个不需要深拷贝的swap函数,用来交换两个vector对象:同时,为了避免我们不使用成员函数的
swap,vector还将算法库中的swap进行了重载,然后该重载函数的内部又去调用成员函数swap: