赞
踩
容器就是特定类型对象的集合。
顺序容器提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素值,而是与元素加入容器时的位置相对应。有序和无序关联容器则根据关键字的值来存储元素。
所有顺序容器都提供了快速顺序访问元素的能力。但这些容器在以下两方面有不同的性能折中:
表1 顺序容器类型 | |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能会很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置插入/删除速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在任何位置插入/删除速度都很快 |
array | 固定大小数组。支持快速随机访问。不能增加/删除元素 |
string | 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
除了固定大小的array,其他容器都提供高效、灵活的内存管理。可以添加、删除元素,扩张、收缩容器大小。容器保存元素的策略对容器操作的效率有重大影响。某些情况下,存储策略还会影响特定容器是否支持特定操作。
list和forward_list的设计目的是令容器可以在任何位置快速添加和删除元素,代价是不支持随机访问——为了访问一个元素,需要遍历整个容器。和vector、deque、array相比,这两个容器的额外内存开销也很大。
deque支持快速随机访问。在deque的中间位置添加或删除元素的代价可能很高,但在deque两端添加或删除元素非常快,与list和forward_list相当。
与内置数组相比,array更安全、更容易使用。array对象的大小是固定的,不支持添加或删除元素以及改变容器大小的操作。
forward_list没有size操作,因为保存或计算其大小会增加额外开销。对其他容器而言,size是一个快速的常量时间操作。
选择容器的基本原则如下:
如果程序既需要随机访问元素,又需要在容器中间位置插入元素,就要在list或forward_list中访问元素与vector或deque插入、删除元素的性能之间做取舍。
容器通常定义在和类型同名的头文件中,且容器均定义为模板类。对大多数容器(不是所有)来说,需要额外提供元素类型信息。
顺序容器几乎可以保存任意类型的元素,其元素甚至可以是另一个容器。
vector<vector<int>> ivec;
vector<vector<int> > ivec2; // 较旧的编译器需要在最后两个>中加个空格
尽管可以在容器中保存几乎所有类型,但某些容器操作对元素类型有特殊要求。我们可以用不支持特定要求的类型定义容器,但此时就不能使用依赖这些特定要求的容器操作了。譬如,顺序容器有一个仅接收容器大小参数的构造函数,它会使用元素类型的默认构造函数。如果元素类型没有定义默认构造函数,我们可以定义该类型的顺序容器,但不能使用这种构造函数。
表2 容器操作 | |
---|---|
类型别名 | |
iterator | 此容器类型的迭代器类型 |
const_iterator | 只读迭代器类型 |
size_type | 无符号整数类型,足够保存此容器类型最大大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,与value_type&含义相同 |
const_reference | 元素的const左值类型,与const value_type&含义相同 |
构造函数 | |
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造c2的拷贝c1 |
C c(b, e); | 构造c,将迭代器b和e指定范围内的元素拷贝到c(array不支持) |
C c{a, b, c, ...}; | 列表初始化c |
赋值与swap | |
c1 = c2; | 将c1中的元素替换为c2中的元素 |
c1 = {a, b, c, ...}; | 将c1中的元素替换为列表中的元素(不适用于array) |
a.swap(b); | 交换a和b的元素 |
swap(a, b); | 与a.swap(b)等价 |
大小 | |
c.size() | c中元素的数目(不支持forward_list) |
c.max_size() | c可保存的最大元素数目 |
c.empty() | c为空则返回true |
添加/删除元素(不适用于array,且在不同容器中这些操作的接口不同) | |
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中的所有元素,返回void |
关系运算符 | |
==, != | 所有容器都支持相等/不相等运算符 |
<, <=, >, >= | 关系运算符(无序容器不支持) |
获取迭代器 | |
c.begin()、c.end() | 返回指向c的首元素和尾后元素位置的迭代器 |
c.cbegin()、c.cend() | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin()、c.rend() | 返回指向c的尾元素和首前元素位置的迭代器 |
c.crbegin()、c.crend() | 返回const_reverse_iterator |
迭代器有公共接口——如果一个迭代器提供某个操作,则所有提供相同操作的迭代器对这个操作的实现方式都是相同的。
表3列出了容器迭代器支持的所有操作,其中只有一个例外,不符合公共接口特点——forward_list迭代器不支持递减运算符(–)。
表3 标准容器迭代器的运算符 | |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素名为mem的成员,等价于(*iter).mem |
++iter | 令iter指向容器中的下一个元素 |
--iter | 令iter指向容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等 |
iter1 != iter2 | 判断两个迭代器是否不等 |
表4列出了迭代器支持的算术运算,这些运算只能应用于string、vector、deque和array的迭代器,不能应用于其他任何容器类型的迭代器。
表4 vector与string迭代器支持的运算 | |
---|---|
iter + n | 迭代器加上一个整数值,即将迭代器向前移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置 |
iter - n | 迭代器减去一个整数值,即将迭代器向后移动对应个数。结果仍是迭代器,指向容器中的某个元素,或指向容器尾元素的下一位置 |
iter1 += n | 迭代器加法的复合赋值语句 |
iter1 -= n | 迭代器减法的复合赋值语句 |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离,类型为diffrence_type(带符号) |
>、>=、<、<= | 如果某迭代器指向的容器位置在另一迭代器所指位置之前,则称前者小于后者 |
迭代器范围由一对迭代器表示,这两个迭代器分别指向同一个容器中的元素或尾后元素。这两个迭代器通常被称为begin和end。end可以和begin指向相同的位置,但不能指向begin之前的位置。元素范围的数学表示为左闭合区间[begin, end)。
标准库使用左闭合区间有三个性质:
以上性质意味着可以用循环来处理元素范围:
while (begin != end) {
*begin = val;
++begin;
}
每个容器都定义了多个类型,我们已经见过其中三种:size_type、iterator、const_iterator。
除了已经使用过的迭代器,大多数容器还提供反向迭代器。反向迭代器是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义都发生颠倒。譬如对反向迭代器执行++操作,会得到上一个元素。
剩下的就是类型别名。通过类型别名,可以在不了解容器中元素类型的情况下使用它。如果需要元素类型,可以使用容器的value_type。如果需要元素类型的引用,可以使用reference或const_reference。这些元素相关的类型别名在泛型编程中非常有用。
为了使用这些类型,必须显式使用其类名:
list<string>::iterator iter;
begin和end操作生成指向容器中第一个元素和尾后元素的迭代器。
begin和end有多个版本:
begin()、end()、rbegin()、rend()这四个接口,在非常量对象上调用时,得到的是iterator。在const对象上调用时,得到的是const iterator。所以当auto和这四个接口结合使用时,获得的迭代器类型依赖于容器类型。
iterator可以转换为对应的const_iterator,反之则不行。
不需要写访问时,应使用cbegin和cend。
每个容器类型都定义了默认构造函数。除array外,其他容器的默认构造函数会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
表5 容器定义和初始化 | |
---|---|
C c; | 默认构造函数。如果C是一个array,则c中元素按默认方式初始化。否则c为空 |
C c1(c2); | c1初始化为c2的拷贝。c1和c2必须是相同的容器类型,且保存相同元素类型。对于array类型来说,两者还必须具有相同大小。 |
C c1 = c2; | |
C c{a, b, c, ...}; | c初始化为初始化列表中元素的拷贝。列表中元素的类型必须与C的元素类型相容。对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化 |
C c = {a, b, c, ...}; | |
C c(b, e); | c初始化为迭代器b和e指定范围中元素的拷贝。范围中元素的类型必须和C的元素类型相容(array不适用) |
只有顺序容器(不包括array)的构造函数才能接受大小参数 | |
C seq(n); | seq包含n个元素,这些元素进行了值初始化。此构造函数是explicit的 |
C seq(n, t); | seq包含n个初始化为值t的元素 |
将一个新容器创建为另一个容器的拷贝的方法有两种:
在这里插入代码片
1. 对于下面的程序任务,vector、deque和list哪个容器最为适合?请解释理由。如果没有哪一种容器优于其他容器,也请解释理由。
(a)读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章看到,关联容器更适合这个问题。
(b)读取未知数量的单词,总是将新单词插入到末尾。删除操作在头部进行。
(c)从一个文件读取未知数量的整数。将这些数排序,然后打印到标准输出
(a)按字典序插入,意味着需要频繁插入操作,list更为适合。如果不是必须边读取边插入,也可以先插入到vector的尾部,读取完成后再排序。
(b)由于需要在容器两端增删元素,deque更为适合。
(c)快速排序算法需要频繁随机存取数据,使用vector更合适。
2. 定义一个list对象,其元素类型是int的deque。
list<deque<int>> a;
3. 构成迭代器范围的迭代器有何限制?
4. 编写函数,接受一对指向vector的迭代器和一个int值。在两个迭代器指定的范围中查找给定的值,返回布尔型结果。
#include <iostream> #include <vector> using namespace std; bool search_vector(vector<int>::iterator begin, vector<int>::iterator end, int val) { for (; begin != end; ++begin) { if (*begin == val) return true; } return false; } int main(int argc, char **argv) { vector<int> vec = {1, 2, 3, 4, 5, 6, 7}; cout << search_vector(vec.begin(), vec.end(), 3) << endl; cout << search_vector(vec.begin(), vec.end(), 8) << endl; return 0; }
5. 重写上一题的函数,返回一个迭代器指向找到的元素。注意,必须处理未找到给定值的情况。
#include <iostream> #include <vector> using namespace std; vector<int>::iterator search_vector(vector<int>::iterator begin, vector<int>::iterator end, int val) { for (; begin != end; ++begin) { if (*begin == val) return begin; } return end; } int main(int argc, char **argv) { vector<int> vec = {1, 2, 3, 4, 5, 6, 7}; cout << search_vector(vec.begin(), vec.end(), 3) - vec.begin()<< endl; cout << search_vector(vec.begin(), vec.end(), 8) - vec.begin()<< endl; return 0; }
6. 下面程序有何错误?应如何修改?
list lst1;
list::iterator iter1 = lst1.begin(),
iter2 = lst1.end();
while (iter1 < iter2) {}
list是链式存储结构,不能使用<。可以改用!=来判断。
7. 为了索引int的vector中的元素,应该使用什么类型?
应使用vector::iterator。
8. 为了读取string的list中的元素,应该使用什么类型?如果写入list,又该使用什么类型?
读取时使用list<string>::value_type。
list<string> slist = {"aaa", "bbb", "ccc"};
list<string>::value_type tmp = *slist.begin();
写入时使用list<string>::reference。
list<string> slist = {"aaa", "bbb", "ccc"};
list<string>::reference tmp = *slist.begin();
tmp = "albert";
9. begin和cbegin两个函数有何不同?
begin有两个重载版本,其一返回普通迭代器,其二返回const迭代器。取决于发起调用的对象是否为const。
cbegin返回const迭代器。当程序没有写入需求时,应使用cbegin。
10. 下面六个对象分别是什么类型?
vector<int> v1;
const vector<int> v2;
auto it1 = v1.begin(), it2 = v2.begin();
auto it3 = v1.cbegin(), it4 = v2.cbegin();
v1是vector<int>类型。
v2是const vector类型。
it1是普通迭代器。
it2是const迭代器。
it3是const迭代器。
it4是const迭代器。
11. 对6种创建和初始化vector对象的方法,每种都给出一个实例。解释每个vector包含什么值。
vector<int> vec1;
vector<int> vec2(vec1); // 等价于vector<int> vec2 = vec1;
vector<int> vec3 = {1, 2, 3}; // 等价于vector<int> vec3{1, 2, 3};
vector<int> vec4(vec3.begin() + 1, vec3.end() - 1);
vector<int> vec5(7);
vector<int> vec6(7, 5);
12. 对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。
接受已有容器的构造函数会拷贝此容器中的所有元素。
接受两个迭代器的构造函数可以拷贝容器中指定范围的元素。
13. 如何从一个list<int>初始化一个vector<double>?从一个vector<int>又该如何创建?
由于list<int>和vector<double>是不同的容器类型,因此无法采用容器拷贝的方式来初始化。但int和double是相容的,所以可以用范围初始化来构造。对vector<int>来说也是这种思路。
list<int> ilist = {1, 2, 3, 4, 5};
vector<int> ivec = {1, 2, 3, 4, 5};
vector<double> dvec1(ilist.begin(), ilist.end());
vector<double> dvec2(ivec.begin(), ivec.end());
14. 编写程序,将一个list中的char *指针元素付给一个vector中的string
list<char *>和vector<string>是不同类型的容器,所以不能直接采用赋值运算符来赋值。但char *可以转换为string,所以可以用范围赋值方式来实现。
list<char *> slist = {"aaa", "bbb", "ccc"};
vector<string> svec;
svec.assign(slist.begin(), slist.end());
15. 编写程序,判断两个vector<int>是否相等。
vector<int> ivec1 = {1, 2, 3};
vector<int> ivec2 = {1, 2, 3};
cout << (ivec1 == ivec2) << endl;
16. 重写上一题程序,比较一个list<int>中的元素和一个vector<int>中的元素
bool list_vector_equal(const list<int> &ilist, const vector<int> &ivec) { if (ilist.size() != ivec.size()) return false; auto lbegin = ilist.begin(); auto lend = ilist.end(); auto vbegin = ivec.begin(); while (lbegin != lend) { if (*lbegin != *vbegin) return false; lbegin++; vbegin++; } return true; }
17. 假定c1和c2是两个容器,比较操作if (c1 < c2)有何限制?
18. 编写程序,从标准输入读取string序列,存入一个deque中。编写一个循环,用迭代器打印deque中的元素。
int main(int argc, char **argv)
{
deque<string> sdeq;
string word;
while (cin >> word) {
sdeq.push_back(word);
}
for (auto begin = sdeq.begin(); begin != sdeq.end(); begin++) {
cout << *begin << endl;
}
return 0;
}
19. 重写上题的程序,用list代替deque。列出程序需要做出哪些改变。
并无太大差异。
int main(int argc, char **argv)
{
list<string> slist;
string word;
while (cin >> word) {
slist.push_back(word);
}
for (auto begin = slist.begin(); begin != slist.end(); begin++) {
cout << *begin << endl;
}
return 0;
}
20. 编写程序,从list<int>拷贝元素到两个deque中。值为偶数的所有元素都拷贝到一个deque中,值为奇数的元素都拷贝到另一个deque中。
void print_vector(vector<int> &vec) { for (auto beg = vec.begin(); beg != vec.end(); beg++) { cout << *beg << " "; } cout << endl; } int main(int argc, char **argv) { list<int> ilist = {1, 2, 3, 4, 5}; vector<int> odd_vec; vector<int> even_vec; for (auto beg = ilist.begin(); beg != ilist.end(); beg++) { auto &vec = (*beg % 2) ? odd_vec : even_vec; vec.push_back(*beg); } print_vector(odd_vec); print_vector(even_vec); return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。