什么是vector
vector
(在C++中通常指std::vector
)是一个动态数组类模板,它属于C++标准模板库(STL)的一部分。
vector
提供了一种可以动态调整大小的数组,它能够根据需要自动分配和释放内存,从而允许我们在运行时添加或删除元素。
标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。
因为vector“容纳着”其他对象,所以它也常被称作容器(container)。
要想使用vector,必须包含适当的头文件。在后续的例子中,都将假定做了如下using声明:
#include <vector>
using std::vector;
注意:vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector<int>。
vector能容纳的元素类型
vector 能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。
除此之外,其他大多数(非引用)内置类型和类类型都可以构成vector对象,甚至组成vector的元素也可以是vector。
需要指出的是,在早期版本的C++标准中如果vector的元素还是vector(或者其他模板类型),则其定义的形式与现在的C++11新标准略有不同。
过去,必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,如应该写成vector<vector<int> >而非vector<vector<int>>。
某些编译器可能仍需以老式的声明语句来处理元素为vector的 vector对象,如vector<vector<int> >.
定义和初始化vector对象
和任何一种类类型一样,vector 模板控制着定义和初始化向量的方法。
下面列出了定义vector对象的常用方法。
vector<T> vl
//v1 是一个空vector,它潜在的元素是T类型的,执行默认初始化
vector<T> v2(v1)
//v2中包含有v1所有元素的副本(注意是副本)
vector<T> v2 = v1
//等价于v2(v1),v2中包含有v1所有元素的副本(注意是副本)
vector<T> v3(n,val)
//v3包含了n个重复的元素,每个元素的值都是val
vector<T> v4(n)
// v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a,b,c...}
//v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5={a,b,c...}
//等价于v5{a,b,c...}
vector<T> v6(v5.begin(),v5.end());
//将[v5.begin(),v5.end())区间的元素拷贝给v6
我们可以举个例子
// 声明一个空的vector<int>,v1中当前没有存储任何元素
std::vector<int> v1;
// 使用v1作为模板(复制构造函数),声明并初始化v2
// 因为v1是空的,所以v2也是一个空的vector,但它们是两个不同的对象
std::vector<int> v2(v1);
// 使用赋值操作符(或称为复制赋值),将v1的内容复制到v3中
// 注意:这里v3是在声明的同时进行赋值,所以和v2一样,v3也是空的
std::vector<int> v3 = v1;
// 声明并初始化v4,包含5个元素,每个元素的值都是3
// 实际上,v4现在包含5个元素,每个都是3
std::vector<int> v4(5, 3);
// 声明并初始化v5,包含4个元素,但没有指定初始值
// 这些元素通常会被初始化为int类型的默认值,即0
std::vector<int> v5(4);
// 使用初始化列表语法声明并初始化v6
// v6包含4个元素,分别是1, 2, 3, 4
std::vector<int> v6{1, 2, 3, 4};
// 这行和v6的初始化方式是一样的,也是使用初始化列表语法
// v7同样包含4个元素,分别是1, 2, 3, 4
// 注意:在C++11及以后的标准中,这种初始化方式是推荐的方式
std::vector<int> v7 = {1, 2, 3, 4};
std::vector<T> v6(v5.begin(),v5.end());
//将[v5.begin(),v5.end())区间的元素拷贝给v6
可以默认初始化vector对象,从而创建一个指定类型的空vector:
vector<string> svec;//默认初始化,svec不含任何元素
看起来空vector 好像没什么用,但是很快我们就会知道程序在运行时可以很高效地往vector 对象中添加元素。事实上,最常见的方式就是先定义一个空vector,然后当运行时获取到元素的值后再逐一添加。
当然也可以在定义vector对象时指定元素的初始值。
例如,允许把一个vector对象的元素拷贝给另外一个vector 对象。此时,新vector 对象的元素就是原 vector对象对应元素的副本。注意两个vector对象的类型必须相同:
vector<int> ivec;//初始状态为空
//在此处给ivec添加一些值
vector<int> ivec2 (ivec);//把ivec的元素拷贝给ivec2
vector<int> ivec3 = ivec;//把ivec的元素拷贝给ivec3
vector<string> svec(ivec2);//错误:svec的元素是string对象,不是int
列表初始化vector 对象
C++11 新标准还提供了另外一种为vector对象的元素赋初值的方法,即列表初始化。
此时,用花括号括起来的0个或多个初始元素值被赋给vector对象:
vector<string> articles ={"a","an", "the"};
上述vector 对象包含三个元素:第一个是字符串“a",第二个是字符串“an",最后一个是字符串"the"。
C++语言提供了几种不同的初始化方式。
在大多数情况下这些初始化方式可以相互等价地使用,不过也并非一直如此。
有三种例外情况是:
- 其一,使用拷贝初始化时(即使用=时),只能提供一个初始值;
- 其二,如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化。
- 第三种特殊的要求是,如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号
vector<string> vl{"a","an","the"};//列表初始化 vector<string> v2("a","an","the");// 错误
创建指定数量的元素
还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:
vector<int> ivec(10,-1);
// 10个int 类型的元素,每个都被初始化为-1
vector<string> svec(10, "hi!");
//10个string类型的元素,每个都被初始化为"hi"
值初始化
通常情况下,可以只提供vector对象容纳的元素数量而略去初始值。
此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中元素的类型决定。
- 如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0。
- 如果元素是某种类类型,比如string,则元素由类默认初始化:
vector<int> ivec(10);//10个元素,每个都初始化为0
vector<string> svec(10);//10个元素,每个都是空string对象
对这种初始化的方式有两个特殊限制:
- 其一,有些类要求必须明确地提供初始值,如果vector对象中元素的类型不支持默认初始化,我们就必须提供初始的元素值。对这种类型的对象来说,只提供元素的数量而不设定初始值无法完成初始化工作。
- 其二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化:
vector<int>vi=10;//错误:必须使用直接初始化的形式指定向量大小
这里的10是用来说明如何初始化vector对象的,我们用它的本意是想创建含有10个值初始化了的元素的vector对象,而非把数字10“拷贝”到vector中。因此,此时不宜使用拷贝初始化
花括号和圆括号的区别
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。
例如,用一个整数来初始化vector<int>时,整数的含义可能是vector对象的容量也可能是元素的值。
类似的,用两个整数来初始化vector<int>时,这两个整数可能一个是vector对象的容量,另一个是元素的初值,也可能它们是容量为2的vector对象中两个元素的初值。
通过使用花括号或圆括号可以区分上述这些含义:
vector<int> vl(10);//v1有10个元素,每个的值都是0,因为值初始化
vector<int> v2{10};//v2有1个元素,它的值是10
vector<int> v3(10,1);//v3有10个元素,每个的值都是1
vector<int> v4{10, 1};//v4有2个元素,值分别是10和1
如果用的是圆括号,可以说提供的值是用来构造vector对象的。
例如,v1的初始值说明了vector 对象的容量;v3的两个初始值则分别说明了vector对象的容量和元素的初值。
如果用的是花括号,可以表述成我们想列表初始化该vector对象。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时才会考虑其他初始化方式。
在上例中,给v2和v4提供的初始值都能作为元素的值,所以它们都会执行列表初始化,vector 对象v2包含一个元素而vector 对象v4包含两个元素。
另一方面,如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化。就要考虑用这样的值来构造vector 对象了。
例如,要想列表初始化一个含有string对象的vector对象,应该提供能赋给string对象的初值。此时不难区分到底是要列表初始化vector对象的元素还是用给定的容量值来构造vector对象:
vector<string> v5{"hi"};//列表初始化;v5有一个元素
vector<string> v6("hi");//错误:不能使用字符串字面值构建vector对象
vector<string> v7{10};//v7有10个默认初始化的元素
vector<string> v8{10, "hi"};// v8有10个值为"hi"的元素
尽管在上面的例子中除了第二条语句之外都用了花括号,但其实只有v5 是列表初始化。要想列表初始化vector对象,花括号里的值必须与元素类型相同。
显然不能用int初始化string对象,所以v7和v8提供的值不能作为元素的初始值。确认无法执行列表初始化后,编译器会尝试用默认值初始化vector对象。
向vector对象中添加元素
对vector对象来说,直接初始化的方式适用于三种情况:
- 初始值己知且数量较少
- 初始值是另一个vector对象的副本、
- 所有元素的初始值都一样。
然而更常见的情况是:创建一个vector对象时并不清楚实际所需的元素个数,元素的值也经常无法确定。还有些时候即使元素的初值已知,但如果这些值总量较大而各不相同,那么在创建vector对象的时候执行初始化操作也会显得过于烦琐。
举个例子,如果想创建一个vector 对象令其包含从0到9共10个元素,使用列表初始化的方法很容易做到这一点;但如果vector 对象包含的元素是从0到99或者从0到999呢?
这时通过列表初始化把所有元素都一一罗列出来就不太合适了。对于此例来说,更好的处理方法是先创建一个空vector,然后在运行时再利用 vector的成员函数pushback向其中添加元素。
push_back负责把一个值当成vector对象的尾元素“压到(push)”vector 对象的“尾端(back)”。例如:
vector<int> v2;
//空vector对象
for (int i= 0;i!= 100;++i)
{v2.push_back(i);//依次把整数值放到v2尾端}
//循环结束后v2有100个元素,值从0到99
在上例中,尽管知道vector对象最后会包含100个元素,但在一开始还是把它声明成空
vector,在每次迭代时才顺序地把下一个整数作为v2的新元素添加给它。
同样的,如果直到运行时才能知道vector对象中元素的确切个数,也应该使用刚刚这种方法创建vector对象并为其赋值。
例如,有时需要实时读入数据然后将其赋予vector对象:
//从标准输入中读取单词,将其作为vector对象的元素存储
string word;
vector<string> text; //空vector对象
while (cin >> word) {
text.push_back(word);
// 把 word添加到text后面
}
和之前的例子一样,本例也是先创建一个空vector,之后依次读入未知数量的值并保存到text中。
vector的其他常用操作
我们以vector<int>为例
#include<vector>
vector<int> a,b;
//b为向量,将b的0-2个元素赋值给向量a
a.assign(b.begin(),b.begin()+3);
//a含有4个值为2的元素
a.assign(4,2);
//返回a的最后一个元素
a.back();
//返回a的第一个元素
a.front();
//返回a的第i元素,当且仅当a存在
a[i];
//清空a中的元素
a.clear();
//判断a是否为空,空则返回true,非空则返回false
a.empty();
//删除a向量的最后一个元素
a.pop_back();
//删除a中第一个(从第0个算起)到第二个元素,也就是说删除的元素从a.begin()+1算起(包括它)一直到a.begin()+3(不包括它)结束
a.erase(a.begin()+1,a.begin()+3);
//在a的最后一个向量后插入一个元素,其值为5
a.push_back(5);
//在a的第一个元素(从第0个算起)位置插入数值5,
a.insert(a.begin()+1,5);
//在a的第一个元素(从第0个算起)位置插入3个数,其值都为5
a.insert(a.begin()+1,3,5);
//b为数组,在a的第一个元素(从第0个元素算起)的位置插入b的第三个元素到第5个元素(不包括b+6)
a.insert(a.begin()+1,b+3,b+6);
//返回a中元素的个数
a.size();
//返回a在内存中总共可以容纳的元素个数
a.capacity();
//将a的现有元素个数调整至10个,多则删,少则补,其值随机
a.resize(10);
//将a的现有元素个数调整至10个,多则删,少则补,其值为2
a.resize(10,2);
//将a的容量扩充至100,
a.reserve(100);
//b为向量,将a中的元素和b中的元素整体交换
a.swap(b);
//b为向量,向量的比较操作还有 != >= > <= <
a==b;
常用算法
#include<algorithm>
//对a中的从a.begin()(包括它)到a.end()(不包括它)的元素进行从小到大排列
sort(a.begin(),a.end());
//对a中的从a.begin()(包括它)到a.end()(不包括它)的元素倒置,但不排列,如a中元素为1,3,2,4,倒置后为4,2,3,1
reverse(a.begin(),a.end());
//把a中的从a.begin()(包括它)到a.end()(不包括它)的元素复制到b中,从b.begin()+1的位置(包括它)开始复制,覆盖掉原有元素
copy(a.begin(),a.end(),b.begin()+1);
//在a中的从a.begin()(包括它)到a.end()(不包括它)的元素中查找10,若存在返回其在向量中的位置
find(a.begin(),a.end(),10);
vector和下标
使用下标运算符能获取到指定的元素。
和string一样,vector 对象的下标也是从0开始计起,下标的类型是相应的 size_type 类型。
只要vector 对象不是一个常量,就能向下标运算符返回的元素赋值。
此外,也能通过计算得到vector 内对象的索引,然后直接获取索引位置上的元素。
不能用下标形式添加元素
刚接触C++语言的程序员也许会认为可以通过vector 对象的下标形式来添加元素,事实并非如此。下面的代码试图为vector对象ivec添加10个元素:
vector<int> ivec;//空 vector对象
for (decltype(ivec.size()) ix = 0; ix != 10; ++ix)
{
ivec[ix]=ix;//严重错误:ivec不包含任何元素
}
然而,这段代码是错误的:ivec是一个空vector,根本不包含任何元素,当然也就不能通过下标去访问任何元素!
如前所述,正确的方法是使用push_back:
for (decltype (ivec.size()) ix= 0; ix != 10; ++ix)
{ivec.push back(ix);}//正确:添加一个新元素,该元素的值是ix
vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。
提示:只能对确知已存在的元素执行下标操作!
关于下标必须明确的一点是:只能对确知已存在的元素执行下标操作。例如,vector<int> ivec;//空vector对象 cout << ivec[0];//错误:ivec不包含任何元素 vector<int> ivec2(10); //含有10个元素的vector对象 cout << ivec2[10]; //错误:ivec2元素的合法索引是从0到9
试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发
现,而是在运行时产生一个不可预知的值。不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误,这也是导致PC及其他设备上应用程序出现安全问题的一个重要原因。
确保下标合法的一种有效手段就是尽可能使用范围 for 语句。