【C++基础】第十五课:标准库类型vector

标准库vector,定义和初始化vector对象,push_back,vector内对象的索引

Posted by x-jeff on March 9, 2020

【C++基础】系列博客为参考《C++ Primer中文版(第5版)》C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。

1.前言

标准库类型vector表示对象的集合,其中所有对象的类型都相同。

集合中的每个对象都有一个与之对应的索引,索引用于访问对象。

vector也常被称作容器(container)

要想使用vector,必须包含适当的头文件。在后续的例子中,都将假定做了如下using声明:

1
2
#include <vector>
using std::vector;

C++语言既有类模版,也有函数模版,其中vector是一个类模版。编译器根据模版创建类或函数的过程称为实例化,当使用模版时,需要指出编译器应把类或函数实例化成何种类型。

对于类模版来说,我们通过提供一些额外信息来指定模版到底实例化成什么样的类,需要提供哪些信息由模版决定。提供信息的方式总是这样:即在模版名字后面跟一对尖括号,在括号内放上信息。

vector为例,提供的额外信息是vector内所存放对象的类型:

1
2
3
vector<int> ivec;
vector<Sales_item> Sales_vec;
vector<vector<string>> file;

⚠️vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector

在早期版本的C++标准中如果vector的元素还是vector(或着其他模版类型),必须在外层vector对象的右尖括号和其元素类型之间添加一个空格,如应该写成vector<vector<int> >。但是在C++11新标准中,可直接写成vector<vector<int>>

2.定义和初始化vector对象

以下为定义vector对象的常用方法:

可以默认初始化vector对象,从而创建一个指定类型的空vector

1
vector<string> svec;//默认初始化,svec不含任何元素

此外,还允许把一个vector对象的元素拷贝给另外一个vector对象,注意两个vector对象的类型必须相同:

1
2
3
4
vector<int> ivec;//初始状态为空
vector<int> ivec2(ivec);//把ivec的元素拷贝给ivec2
vector<int> ivec3=ivec;//把ivec的元素拷贝给ivec3
vector<string> svec(ivec2);//错误:svec的元素是string对象,不是int

2.1.列表初始化vector对象

在第2部分一开始,我们介绍了很多初始化方式,都是C++中常用的初始化方式。在大多数情况下这些初始化方式可以相互等价地使用,但是在某些情况下会有限制:

  1. 使用拷贝初始化(即使用=时),只能提供一个初始值。
  2. 如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化。
  3. 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
1
2
vector<string> v1{"a","an","the"};//列表初始化
vector<string> v2("a","an","the");//错误

2.2.创建指定数量的元素

还可以用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:

1
2
vector<int> ivec(10,-1);//10个int类型的元素,每个都被初始化为-1
vector<string> svec(10,"hi !");//10个string类型的元素,每个都被初始化为"hi !"

2.3.值初始化

通常情况下,可以只提供vector对象容纳的元素数量而不提供初始值。此时库会创建一个值初始化的元素初值,并把它赋给容器中的所有元素。这个初值由vector对象中元素的类型决定。

1
2
vector<int> ivec(10);//10个元素,每个都初始化为0
vector<string> svec(10);//10个元素,每个都是空string对象

⚠️特殊情况:如果初始化时使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造vector对象了:

1
2
vector<string> v7{10};//v7有10个默认初始化的元素
vector<string> v8{10,"hi"};//v8有10个值为"hi"的元素

❗️要想使用列表初始化vector对象,花括号里的值必须与元素类型相同。

3.向vector对象中添加元素

可以利用vector的成员函数push_back向其中添加元素。push_back负责把一个值当成vector对象的尾元素“压到(push)”vector对象的“尾端(back)”。例如:

1
2
3
4
vector<int> v2;
for(int i=0;i!=100;++i)
	v2.push_back(i);//依次把整数值放到v2尾端
//循环结束后v2有100个元素,值从0到99

⚠️如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。范围for语句体内不应改变其所遍历序列的大小。

4.其他vector操作

除了push_back之外,vector还提供了几种其他操作,大多数都和string的相关操作类似,下表中列出了其中比较重要的一些:

举个例子:

1
2
3
4
5
6
vector<int> v{1,2,3,4,5,6,7,8,9};
for(auto &i : v)
	i*=i;
for(auto i : v)
	cout<<i<<" ";
cout<<endl;

⚠️第一个循环把控制变量i定义成引用类型,这样就能通过iv的元素赋值。输出结果见下:

vectoremptysize两个成员与string的同名成员功能完全一致:empty检查vector对象是否包含元素然后返回一个布尔值;size则返回vector对象中元素的个数,返回值的类型是由vector定义的size_type类型。

⚠️要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:

1
2
vector<int>::size_type //正确
vector::size_type //错误

4.1.计算vector内对象的索引

举个简单的例子,假设有一组成绩的集合,其中成绩的取值是从0到100。以10分为一个分数段,要求统计各个分数段各有多少个成绩。

1
2
3
4
5
6
7
8
//以10分为一个分数段统计成绩的数量:0~9、10~19、...、90~99、100
vector<unsigned> scores(11,0);
unsigned grade;
while(cin>>grade)
{
	if(grade<=100)
		++scores[grade/10]
}

⚠️两个整数相除,结果还是整数,余数部分被自动忽略掉了。例如,42/10=4、65/10=6、100/10=10等。

‼️不能用下标形式添加元素。vector对象(以及string对象)的下标运算符可用于访问已存在的元素,而不能用于添加元素。例如下面这个例子:

1
2
3
vector<int> ivec;//空vector对象
for(decltype(ivec.size()) ix=0;ix!=10;++ix)
	ivec[ix]=ix;//严重错误:ivec不包含任何元素

正确做法应该是使用push_back添加新元素:

1
2
for(decltype(ivec.size()) ix=0;ix!=10;++ix)
	ivec.push_back(ix);

此外,试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。例如:

1
2
vector<int> ivec(10);
cout<<ivec[10];//错误:ivec元素的合法索引是从0到9

不幸的是,这种通过下标访问不存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出(buffer overflow)指的就是这类错误。

确保下标合法的一种有效手段就是尽可能使用范围for语句。