容器是一些相同类型对象的集合。从概念上说,瓶子是一个容器,可以用来装水。容器分为顺序容器和无序关联容器,对于顺序容器,每个元素进入容器的顺序是有意义和作用的,而无序关联容器中每个元素进入的先后顺序也无关紧要。
顺序容器分为以下几个:
容器类型 | 容器描述 |
vector | 动态数组 |
deque | 双端队列 |
list | 双向链表 |
forward_list | 单向链表 |
array | 固定数组 |
string | 字符串 |
上面这些容器都支持顺序访问元素,大部分还支持随机访问,这些容器的区别是:1.从容器添加/删除元素的效率;2.随机访问元素的效率。例如,从vector中间插入或者删除元素的速度远远低于list,而从vector中随机访问元素的速度则远远高于list。造成这些区别的原因是底层使用内存方式的不同,有的使用连续内存,有的使用链表内存。链表内存实现的容器还附带有额外的内存开销。
需要说明的是,新标准库容器比旧版和个人手写版本通常快的多,因为使用了大量C++新标准的特性和优化,比如很重要的一点是对象移动。另外,标准库容器也易于移植和维护。
表格列出的七种容器每个都有特定的应用场景,对于如何选择,有以下几个原则:
1.如果不知道选择什么,那么就使用vector;
2.如果程序要用的容器元素小且多,并且对内存空间占用敏感,那么不要使用list和forward_list;
3.如果要随机访问元素,应该使用vector和deque;
4.如果需要在头尾插入或删除元素,但不会在容器中间有这些操作,则使用deque;
5.如果既需要随机访问,又需要中间插入,那么可以考虑使用两个容器,一个用于插入,一个用于访问,最后合并容器元素。
练习9.1:对于下面的程序任务,vector、deque和list哪种容器最为适合?解释你的选择的理由。如果没有哪一种容器优于其他容器,也请解释理由。
(a) 读取固定数量的单词,将它们按字典序插入到容器中。我们将在下一章中看到,关联容器更适合这个问题。
(b) 读取未知数量的单词,总是将单词插入到末尾。删除操作在头部进行。
(c) 从一个文件读取未知数量的整数。将这些数排序,然后将它们打印到标准输出。
(a)应该使用array,因为单词的数量是固定的,也就是说元素的个数是固定的,因此符合array的特性。其次虽然要求插入是字典顺序,但是因为个数确定,所以插入次数固定,我们可以每次插入到array时直接在末尾插入,之后调用sort来排序,这样得到的array将比list等不支持随机访问的容器更易用。
(b)这里由于数量不确定,但不要求字典序,要求头部删除,毫无疑问应该用deque
(c)不需要删除则使用vector
练习9.2:定义一个list对象,其元素类型是int的deque。
list<deque<int>> lst;
练习9.3:构成迭代器范围的迭代器有何限制?
begin和end必须指向同一个容器,且end不在begin之前
练习9.4:编写函数,接受一对指向vector<int>的迭代器和一个int值。在两个迭代器指定的范围中查找给定的值,返回一个布尔值来指出是否找到。
bool fun(vector<int>::iterator b, vector<int>::iterator e, int i) { while (b != e) { if (i == *b) return true; else ++b; } return false; }
练习9.5:重写上一题的函数,返回一个迭代器指向找到的元素。注意,程序必须处理未找到给定值的情况。
vector<int>::iterator fun(vector<int>::iterator b, vector<int>::iterator e, int i) { while (b != e) { if (i == *b) return b; else ++b; } return e; }
练习9.6:下面程序有何错误?你应该如何修改它?
list<int> lst1;
list<int>::iterator iter1 = lst1.begin(),iter2 = lst1.end();
while (iter1 < iter2) /* ... */
list迭代器没有<操作,因此while循环错误,应该修改为iter1 != iter2;
练习9.7:为了索引int的vector中的元素,应该使用什么类型?
vector<int>::size_type
练习9.8:为了读取string的list中的元素,应该使用什么类型?如果写入list,又应该使用什么类型?
list<string>::const_iterator
list<string>::iterator
练习9.9:begin和cbegin两个函数有什么不同?
begin返回的迭代器视对象是否是const而定,而cbegin返回的一定是const迭代器。
练习9.10:下面4个对象分别是什么类型?
vector<int> v1;
const vector<int> v2;
auto it1 = v1.begin(), it2 = v2.begin();
auto it3 = v1.cbegin(), it4 = v2.cbegin();
auto只能推断同一类型,因此auto it1 = v1.begin(), it2 = v2.begin();是错误的
it3是vector<int>::cosnt_iterator
it4是vector<int>::const_iterator
练习9.11:对6种创建和初始化vector对象的方法,每一种都给出一个实例。解释每个vector包含什么值。
vector<int> v1; //默认初始化,空vector
vector<int> v2 = {1, 2, 3}; //列表初始化,含有3个元素,值分别为1,2,3
vector<int> v3(v2); //拷贝初始化,拷贝v2,含有3个元素,值分别为1,2,3
vector<int> v4(10); //特定构造函数定义的初始化,含有10个元素,元素值初始化
vector<int> v5(10, 5); //特定构造函数定义的初始化,含有10个元素,元素值初始化为5
vector<int> v6(vector<int>::iterator b, vector<int>::iterator e); //特定构造函数定义的初始化,含有e-b个元素,元素值对应迭代器范围的元素值
练习9.12:对于接受一个容器创建其拷贝的构造函数,和接受两个迭代器创建拷贝的构造函数,解释它们的不同。
接受一个容器创建拷贝的构造函数要求容器类型和元素类型必须一致。
接受两个迭代器创建拷贝的构造函数只要求元素类型一致或者能隐式转换得到。
练习9.13:如何从一个list初始化一个vector?从一个vector又该如何创建?编写代码验证你的答案。
#include <iostream> #include <vector> #include <list> using namespace std; int main(int argc, char const *argv[]) { list<int> l = {1, 2, 3}; vector<double> vd(l.begin(), l.end()); vector<int> vi(l.begin(), l.end()); return 0; }
练习9.14:编写程序,将一个list中的char *指针(指向C风格字符串)元素赋值给一个vector中的string。
#include <iostream> #include <vector> #include <list> using namespace std; int main(int argc, char const *argv[]) { list<char *> l = {"hello", "world"}; vector<string> v; v.assign(begin(l), end(l)); return 0; }
练习9.15:编写程序,判定两个vector<int>是否相等。
if (v1 == v2) //v1、v2是2个vector<int>
练习9.16:重写上一题的程序,比较一个list中的元素和一个vector中的元素。
不同类型容器不具有可比较性,如果不同容器类型的元素个数不一样,也没有比较意义。
如果两种不同容器类型元素个数相同,则用一种容器初始化出一个临时的另一种容器,然后再比较。
练习9.17:假定c1和c2是两个容器,下面的比较操作有何限制(如果有的话)?
if (c1 < c2)
c1和c1如果是自定义类型,则必须定义过<运算符。
练习9.18:编写程序,从标准输入读取string序列,存入一个deque中。编写一个循环,用迭代器打印deque中的元素。
#include <iostream> #include <deque> using namespace std; int main(int argc, char const *argv[]) { deque<string> d; string s; while (cin >> s) { d.push_back(s); } for (auto i : d) { cout << i << ' '; } cout << endl; return 0; }
练习9.19:重写上题的程序,用list替代deque。列出程序要做出哪些改变。
#include <iostream> #include <list> using namespace std; int main(int argc, char const *argv[]) { list<string> l; string s; while (cin >> s) { l.push_back(s); } for (auto i : l) { cout << i << ' '; } cout << endl; return 0; }
程序无需做出任何改变
练习9.20:编写程序,从一个list<int>拷贝元素到两个deque中。值为偶数的所有元素都拷贝到一个deque中,而奇数值元素都拷贝到另一个deque中。
#include <iostream> #include <deque> #include <list> using namespace std; int main(int argc, char const *argv[]) { list<int> l{1, 2, 3, 4, 5, 6, 7, 8, 9}; deque<int> d1, d2; for (auto i : l) { if (i & 1) { d1.push_back(i); cout << "d1:" << i << ' '; } else { d2.push_back(i); cout << "d2:" << i << ' '; } } cout << endl; return 0; }
练习9.21:如果我们将第308页中使用insert返回值将元素添加到list中的循环程序改写为将元素插入到vector中,分析循环将如何工作。
与使用list容器没有任何区别,仅仅可能会有效率上的差异。
练习9.22:假定iv是一个int的vector,下面的程序存在什么错误?你将如何修改?
vector<int>::iterator iter = iv.begin(), mid = iv.begin() + iv.size() / 2;
while (iter != mid)
if (*iter == some_val)
iv.insert(iter, 2 * some_val);
从书本给出的代码,大致可以推断该程序想要做的事情是判断一个迭代器的前半部分是否等于一个给定的值,如果等于,则在该值前插入一个2倍值的元素。
这里需要注意的是尾后迭代器和中间点指示迭代器mid在插入元素之后,原来的迭代器失效。
#include <iostream> #include <vector> using namespace std; int main(int argc, char const *argv[]) { int some_val; cin >> some_val; vector<int> v = {2, 2, 2, 2, 2}; auto b = v.begin(); auto mid = v.size() / 2; if (v.empty()) { cout << "void vector!" << endl; return -1; } while ((v.end() - b) > mid) //当b和v.end()之间的距离不大于mid时,说明v已经移动到了mid,此时退出循环 { if (*b == some_val) { b = v.insert(b, 2 * some_val); ++b; //移动1次,指向插入前的那个元素 } ++b; //如果有插入动作,则向后移动2次,否则移动1次 } cout << endl; return 0; }
练习9.23:在本节第一个程序(第309页)中,若c.size()为1,则val、val2、val3和val4的值会是什么?
它们的值都将是第一个元素值。
练习9.24:编写程序,分别使用at、下标运算符、front和begin提取一个vector中的第一个元素。在一个空vector上测试你的程序。
#include <iostream> #include <vector> using namespace std; int main(int argc, char const *argv[]) { vector<int> v; cout << v.at(0) << endl; cout << v[0] << endl; cout << v.front() << endl; cout << v.back() << endl; return 0; }
练习9.25:对于第312页中删除一个范围内的元素的程序,如果elem1与elem2相等会发生什么?如果elem2是尾后迭代器,或者elem1和elem2皆为尾后迭代器,又会发生什么?
如果elem1和elem2相等,则范围为0不进行任何删除。如果elem2是尾后迭代器,那么从elem1删到结尾。如果两者皆为尾后迭代器,则范围为0不进行任何删除。
练习9.26:使用下面代码定义的ia,将ia拷贝到一个vector和一个list中。使用单迭代器版本的erase从list中删除奇数元素,从vector中删除偶数元素。
int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 };
#include <iostream> #include <vector> #include <list> using namespace std; int main(int argc, char const *argv[]) { int ia[] = { 0, 1, 1, 2, 3, 5, 8, 13, 21, 55, 89 }; vector<int> v; list<int> l; for (auto i : ia) { v.push_back(i); l.push_back(i); } for (vector<int>::iterator b = v.begin(); b != v.end();) { if (*b & 1) { ++b; } else { b = v.erase(b); } } for (list<int>::iterator b = l.begin(); b != l.end();) { if (*b & 1) { b = l.erase(b); } else ++b; } return 0; }
练习9.27:编写程序,查找并删除forward_list<int>中的奇数元素。
#include <iostream> #include <forward_list> using namespace std; int main(int argc, char const *argv[]) { forward_list<int> f{1, 2, 3, 4, 5, 6, 7, 8, 9}; forward_list<int>::iterator pre = f.before_begin(); forward_list<int>::iterator cur = f.begin(); while (cur != f.end()) { if (*cur & 1) { cur = f.erase_after(pre); } else { pre = cur; ++cur; } } return 0; }
练习9.28:编写函数,接受一个forward_list<string>和两个string共三个参数。函数应在链表中查找第一个string,并将第二个string插入到紧接着第一个string之后的位置。若第一个string未在链表中,则将第二个string插入到链表末尾。
#include <iostream> #include <forward_list> using namespace std; void fun(forward_list<string>& f, const string s1, const string s2) { forward_list<string>::iterator b = f.begin(); while (b != f.end()) { if (s1 == *b) b = f.insert_after(b, s2); else ++b; } } int main(int argc, char const *argv[]) { return 0; }
如果未找到,则将第二个string插入到链表末尾,这里并没有说明清楚对第一个string的查找是一次性的还是重复的,如果是一次性的,可以使用循环是否提前return来添加。否则应当对链表进行计数来判断有无插入动作来添加。
练习9.29:假定vec包含25个元素,那么vec.resize(100)会做什么?如果接下来调用vec.resize(10)会做什么?
追加75个值初始化的元素到vec中。
删除vec靠后90个元素。
练习9.30:接受单个参数的resize版本对元素类型有什么限制(如果有的话)?
元素类型必须具有默认构造函数。
练习9.31:第316页中删除偶数值元素并复制奇数值元素的程序不能用于list或forward_list。为什么?修改程序,使之也能用于这些类型。
因为list和forward_list的迭代器不支持复合赋值运算。
对于list,iter += 2;应该修改为++iter;++iter;
forward_list<int> f = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; auto cur = f.begin(); auto pre = f.before_begin(); while (cur != f.end()) { if (*cur % 2) { f.insert_after(pre, *cur); //forward_list插入操作不会使cur迭代器失效,无需重新赋值给cur pre = cur; //将pre指向cur的位置 ++cur; //将cur移动到下一位置 } else { f.erase_after(pre); } }
练习9.32:在第316页的程序中,像下面语句这样调用insert是否合法?如果不合法,为什么?
iter = vi.insert(iter, *iter++);
不合法。因为参数的求值顺序是未指定,无法保证括号内的计算顺序。
练习9.33:在本节最后一个例子中,如果不将insert的结果赋予begin,将会发生什么?编写程序,去掉此赋值语句,验证你的答案。
不将insert的结果赋予begin,begin将会失效,程序无法正常工作或崩溃。
#include <iostream> #include <vector> using namespace std; int main(int argc, char const *argv[]) { vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; auto begin = v.begin(), end = v.end(); while (begin != v.end()) { ++begin; v.insert(begin, 42); ++begin; } return 0; } /* *** Error in `./main': munmap_chunk(): invalid pointer: 0x0000000001e8f040 *** Aborted */
练习9.34:假定vi是一个保存int的容器,其中有偶数值也有奇数值,分析下面循环的行为,然后编写程序验证你的分析是否正确。
iter = vi.begin();
while (iter != vi.end())
if (*iter % 2)
iter = vi.insert(iter, *iter);
++iter;
死循环。
练习9.35:解释一个vector的capacity和size有何区别。
capacity表示在不重新分配内存空间的情况下,容器可以容纳多少元素,size值是容器已经保存的元素的个数。
练习9.36:一个容器的capacity可能小于它的size吗?
不可能。
练习9.37:为什么list或array没有capacity成员函数?
因为list是链表,不需要事先分配连续内存,而array不允许改变容器大小。
练习9.38:编写程序,探究在你的标准实现中,vector是如何增长的。
#include <iostream> #include <vector> using namespace std; int main(int argc, char const *argv[]) { vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; v.reserve(50); cout << "v: size: " << v.size() << " capacity: " << v.capacity() << endl; while (v.size() != v.capacity()) v.push_back(0); cout << "v: size: " << v.size() << " capacity: " << v.capacity() << endl; v.push_back(42); cout << "v: size: " << v.size() << " capacity: " << v.capacity() << endl; return 0; }
练习9.39:解释下面程序片段做了什么:
vector<string> svec;
svec.reserve(1024); //为该vevtor申请1024个元素空间
string word;
while (cin >> word)
svec.push_back(word);
svec.resize(svec.size() + svec.size() / 2); //将可以容纳1024个string的vector扩大0.5倍
练习9.40:如果上一题的程序读入了256个词,在resize之后容器的capacity可能是多少?如果读入了512个、1000个、或1048个呢?
如果读入了256个或512个词,capacity 仍然是 1024
如果读入了1000或1048个词,capacity 取决于具体实现。
练习9.41:编写程序,从一个vector<char>初始化一个string。
vector<char> v{ 'h', 'e', 'l', 'l', 'o' };
string s(v.cbegin(), v.cend());
练习9.42:假定你希望每次读取一个字符存入一个string中,而且知道最少需要读取100个字符,应该如何提高程序的性能?
使用 reserve(200) 函数预先分配200个元素的空间。
练习9.43:编写一个函数,接受三个string参数是s、oldVal 和newVal。使用迭代器及insert和erase函数将s中所有oldVal替换为newVal。测试你的程序,用它替换通用的简写形式,如,将"tho"替换为"though",将"thru"替换为"through"。
#include <iostream> #include <string> using namespace std; void fun(string& s, const string& oldVal, const string& newVal) { auto b = s.begin(); while (b != s.end() - oldVal.size()) //迭代器b最多移动到s.end() - oldVal.size(),继续往后移动则删除行为未定义 { if (oldVal == string(b, b + oldVal.size())) //使用s构建临时对象,使用临时对象与oldVal进行对比 { b = s.erase(b, b + oldVal.size()); //进行删除 b = s.insert(b, newVal.begin(), newVal.end());//删除后插入新的newVal b += newVal.size(); //移动到插入前元素的下一位置 } else { ++b; } } } int main() { string s("a an tho the thru"); fun(s,"tho","though"); fun(s, "thru", "through"); cout << s; }
练习9.44:重写上一题的函数,这次使用一个下标和replace。
#include <iostream> #include <string> using namespace std; void fun(string& s, const string& oldVal, const string& newVal) { for (size_t pos = 0; pos <= s.size() - oldVal.size();) { if (s[pos] == oldVal[0] && s.substr(pos, oldVal.size()) == oldVal) { s.replace(pos, oldVal.size(), newVal); pos += newVal.size(); } else { ++pos; } } } int main() { string s("a an tho the thru"); fun(s, "tho", "though"); fun(s, "thru", "through"); cout << s; }
练习9.45:编写一个函数,接受一个表示名字的string参数和两个分别表示前缀(如"Mr."或"Ms.")和后缀(如"Jr."或"III")的字符串。使用迭代器及insert和append函数将前缀和后缀添加到给定的名字中,将生成的新string返回。
#include <iostream> #include <string> using namespace std; string& newStr(string& s, const string& prefix, const string& postfix) { if (s != "") { s.insert(s.begin(), prefix.begin(), prefix.end()); s.append(postfix); } return s; } int main() { string s = "Davi"; cout << newStr(s, "Mr. ", " Jr.") << endl; return 0; }
练习9.46:重写上一题的函数,这次使用位置和长度来管理string,并只使用insert。
#include <iostream> #include <string> using namespace std; string& newStr(string& s, const string& prefix, const string& postfix) { if (s != "") { s.insert(0, prefix); s.insert(s.size(), postfix); } return s; } int main() { string s = "Davi"; cout << newStr(s, "Mr. ", " Jr.") << endl; return 0; }
练习9.47:编写程序,首先查找string"ab2c3d7R4E6"中每个数字字符,然后查找其中每个字母字符。编写两个版本的程序,第一个要使用find_first_of,第二个要使用find_first_not_of。
#include <iostream> using namespace std; int main() { string serial = "ab2c3d7R4E6"; string x = "0123456789"; string y = "abcdER"; string::size_type pos = 0; while ((pos = serial.find_first_of(x, pos)) != string::npos) { cout << pos << " " << serial[pos] << " "; ++pos; } pos = 0; while ((pos = serial.find_first_of(y, pos)) != string::npos) { cout << pos << " " << serial[pos] << " "; ++pos; } return 0; }
练习9.48:假定name和numbers的定义如325页所示,numbers.find(name)返回什么?
返回string::npos
练习9.49:如果一个字母延伸到中线之上,如d 或 f,则称其有上出头部分(ascender)。如果一个字母延伸到中线之下,如p或g,则称其有下出头部分(descender)。编写程序,读入一个单词文件,输出最长的既不包含上出头部分,也不包含下出头部分的单词。
#include <iostream> #include <fstream> using namespace std; int main(int argc, char const *argv[]) { //string serial = "abccefg"; string not_in("bdfhkltgjpqy"); ifstream is; string word, picked; string::size_type len = word.size(); is.open("word.txt"); if (is) { cout << "open word.txt "; while (is >> word) { string::size_type pos = 0; if ((pos = word.find_first_of(not_in, pos)) == string::npos) { if (word.size() > len) { len = word.size(); picked = word; } } } cout << picked << endl; } return 0; }
练习9.50:编写程序处理一个vector<string>,其元素都表示整型值。计算vector中所有元素之和。修改程序,使之计算表示浮点值的string之和。
#include <iostream> #include <vector> using namespace std; int main() { vector<string> s = {"1", "2", "3", "4", "5"}; int sum = 0; for (auto i : s) { sum += stoi(i); } cout << sum << endl; double sum2 = 0; vector<string> s2 = {"1.1", "2.2", "3.3", "4.4", "5.5"}; for (auto i : s2) { sum2 += stod(i); } cout << sum2 << endl; return 0; }
练习9.51:设计一个类,它有三个unsigned成员,分别表示年、月和日。为其编写构造函数,接受一个表示日期的string参数。你的构造函数应该能处理不同的数据格式,如January 1,1900、1/1/1990、Jan 1 1900 等。
是个硬搞的体力活,懒得写了。
练习9.52:使用stack处理括号化的表达式。当你看到一个左括号,将其记录下来。当你在一个左括号之后看到一个右括号,从stack中pop对象,直至遇到左括号,将左括号也一起弹出栈。然后将一个值(括号内的运算结果)push到栈中,表示一个括号化的(子)表达式已经处理完毕,被其运算结果所替代。