C++ 标准库中的 vector
标准库类型 vector 表示对象的集合,其中所有对象的类型都相同。
集合中的每个对象,都有一个与之对应的索引,索引用于访问对象。
vector 是一个类模板,编译器根据模板创建类或者函数的过程称为 实例化 (instantiation)
vector 是模板而非类型,由 vector 生成的类型必须包含 vector 中元素的类型,例如 vector
vector 能够容纳绝大多数类型的对象作为其元素,但是因为引用不是对象。所以不存在包含引用的 vector
需要指出的是,在早期版本的 C++ 标准中,如果 vector 的元素还是 vector (或者其他模板类型),其定义的形式与现在的 C++ 11 新标准略有不同。过去,必须在外层 vector 对象的右尖括号和其他元素类型之间添加一个空格,如应该写成 vector<vector<int> >
而非 vector<vector<int>>
一些老的编译器可能还存在这方面的要求
定义和初始化 vector
初始化 vector 方法 | |
---|---|
vector |
v1 是一个空 vector,它潜在的元素类型是 T 类型,执行默认初始化 |
vector |
v2 中有 v1 所有元素的副本 |
vector |
等价于 v2(v1),v2 中有 v1 所有元素的副本 |
vector |
v3 包含了 n 个重复的元素,每个元素的值都是 val |
vector |
v4 包含了 n 个重复地执行了值初始化的对象 |
vector |
v5 包含了初始值个数的元素,每个元素被赋予相应的初始值 |
vector |
等价 v5 |
列表初始化 vector 对象
C++ 11 新标准还提供了另外一种为 vector 对象的元素赋初值的方法,即列表初始化。此时,用花括号括起来的 0 个或者多个初始值被赋给 vector 对象。vector<string> articles = {"a", "an", "the"};
上述 vector 对象包含三个元素:第一个是字符串 "a" 第二个是字符串 "an" 第三个是字符串 "the"
C++ 提供了几种不同的初始化的方式,大多数情况下这些初始化的方式可以互相等价的使用,不过也并非一直如此。其一,使用拷贝初始化时(即使用 = 时)只能提供一个初始值;其二,如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化。第三种特殊的要求是,如果提供的是初始元素的列表,则只能把初始值都放在花括号里进行列表初始化,而不能放在圆括号里:
vector<string> v1{"a", "an", "the"}; // 列表初始化
vector<string> v2("a", "an", "the"); // 错误
创建指定数量的元素
可以用 vector 对象容纳的元素数量和所有元素的统一初始值来初始化 vector 对象
vector<int> ivec(10, -1); // 包含 10 个 -1 的 vector
vector<string> svec(10, "hi"); // 包含 10 个 hi 的 vector
值初始化
通常情况下,可以只提供 vector 对象容纳的元素数量而略去初始值,此时库会创建一个 值初始化 (value-initialized) 的元素初值,并把它赋值给容器中的所有元素。
vector<int> ivec(10);
vector<string> svec(10);
对于这种初始化的方式有两个特殊的限制:其一,有些类要求必须明确地提供初始值,如果 vector 对象中元素的类型不支持默认初始化,我们就必须提供初始化的元素值。对这种类型的对象来说,只提供元素的数量而没有设定初始值无法完成初始化的工作。
其二,如果只提供了元素的数量而没有设定初始值,只能使用直接初始化:vector<int> vi = 10;
// 错误:必须使用直接初始化的形式指定向量大小
列表初始化值还是元素数量?
在某些情况下,初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。例如
vector<int> v1(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 对象的。如果使用的是花括号,可以表述成我们想列表初始化 vector 对象。
另一方面,如果初始化时,使用了花括号的形式但是提供的值又不能用来列表初始化,就要考虑用这样的值来构造 vector 对象了。例如
vector<string> v5{"hi"}; // v5 包含一个元素 "hi"
vector<string> v6("hi"); // 错误 无法通过这种方式初始化 vector
vector<string> v7{10}; // v7 拥有 10 个空字符串
vector<string> v8{10, "hi"};// v8 拥有 10 个 "hi"
想要列表初始化 vector 对象,花括号里的值必须与元素类型相同。
确认无法执行列表初始化后,编译器会尝试用默认值初始化 vector 对象。
向 vector 对象中添加元素
push_back 往 vector 尾部添加元素。示例
vector<int> v2;
for(int i = 0; i != 100; i++)
{
v2.push_back(i);
}
关键概念: vector 对象能高效增长
C++ 标准要求 vector 应该能在运行时高效快速地添加元素。因此既然 vector 对象能够高效地增长,那么在定义 vector 对象的时候设定其大小也就没什么必要了,事实上这么做性能可能更差。
这一点与其他一些语言可能相反,通常情况下 vector 不需要指定初始长度。
向 vector 对象添加元素蕴含的编程假定
如果循环体内部包含有向 vector 对象添加元素的语句,则不能使用范围 for 循环。
范围 for 语句体内不应改变其所遍历序列的大小。
其他 vector 操作
vector 支持的操作 | |
---|---|
v.empty() | 如果 v 不含有任何元素,返回真,否则返回假 |
v.size() | 返回 v 中元素的个数 |
v.push_back(t) | 向 v 的尾端添加一个值为 t 的元素 |
v[n] | 返回 v 中第 n 个位置上元素的引用 |
v1 = v2 | 用 v2 中元素的拷贝替换 v1 的元素 |
v1 = | 用列表中元素的拷贝替换 v1 中的元素 |
v1 == v2 | v1 和 v2 相等当且仅当它们的元素数量相同且对应位置的元素值都相同 |
v1 != v2 <, <=, >, >= |
顾名思义,以字典顺序进行比较 |
vector 的 empty 和 size 两个成员与 string 的同名成员功能完全一致: empty 检查 vector 对象是否包含元素然后返回一个布尔值;size 则返回 vector 对象中元素的个数,返回值的类型是由 vector 定义的 size_type 类型。
要使用 size_type 需要首先指定它是由哪种类型定义的。 vector 对象的类型总是包含着元素的类型
vector<int>::size_type // ok
vector::size_type // wrong
各个相等性运算符和关系运算符也与 string 的相应运算符功能一致。若元素的值有区别,则 vector 对象的大小关系由第一对相异的元素值的大小关系决定。同时,只有当元素的值可以比较时,vector 对象才能被比较。
vector 与索引
同时值得注意的是,不能通过 v[n] 这种下标读取的方式去添加元素。正确的方式是使用 push_back
无论是 vector 对象还是 string 对象,下标运算符可用于访问已存在的元素,而不能用于添加元素。
同时值得注意的是,只能对已存在的元素执行下标操作!
关于下标必须明确的一点是:只能对确知已存在的元素执行下标操作。例如,
vector<int> ivec; // 空 vector 对象
cout << ivec[0]; // 错误 ivec 不包含任何元素
vector<int> ivec2(10); // 包含 10 个 0 的 vector 对象
cout << ivec2[10]; // 错误 ivec2 元素的合法索引从 0 到 9
试图用下标的形式去访问一个不存在的元素将引发错误,不过这种错误不会被编译器发现,而是在运行时产生一个不可预知的值。
不幸的是,这种通过下标访问的存在的元素的行为非常常见,而且会产生很严重的后果。所谓的缓冲区溢出 (buffer overflow) 指的就是这类错误,这也是导致 PC 及其他设备上应用程序出现安全问题的一个重要原因。
确保下标合法的一种有效手段就是尽可能使用范围 for 语句。for(auto& i : vec){ ... }