《C++ Primer 4th》读书笔记
标准容器(the standard container)定义了很少的操作。标准库并没有为每种容器类型都定义实现这些操作的成员函数,而是定义了一组泛型算法:因为它们实现共同的操作,所以称之为“算法”;而“泛型”指的是它们可以操作在多种容器类型上——不但可作用于 vector 或 list 这些标准库类型,还可用在内置数组类型、甚至其他类型的序列上。
标准算法固有地独立于类型,与容器的类型无关:在前面的描述中,没有任何内容依赖于容器类型。这种算法只在一点上隐式地依赖元素类型:必须能够对元素做比较运算。该算法的明确要求如下:
1. 需要某种遍历集合的方式:能够从一个元素向前移到下一个元素。
2. 必须能够知道是否到达了集合的末尾。
3. 必须能够对容器中的每一个元素与被查找的元素进行比较。
4. 需要一个类型指出元素在容器中的位置,或者表示找不到该元素。
标准库提供了超过 100 种算法。与容器一样,算法有着一致的结构。
关键概念:算法永不执行容器提供的操作
泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。算法基于迭代器及其操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法从不直接添加或删除元素。
使用泛型算法必须包含 algorithm 头文件:
#include <algorithm>
标准库还定义了一组泛化的算术算法(generalized numeric algorithm),其命名习惯与泛型算法相同。使用这些算法则必须包含 numeric 头文件:
#include <numeric>
除了少数例外情况,所有算法都在一段范围内的元素上操作,我们将这段范围称为“输出范围(input range)”。
accumulate 带有三个形参。头两个形参指定要累加的元素范围。第三个形参则是累加的初值。
find_first_of 函数。这个算法带有两对迭代器参数来标记两段元素范围,在第一段范围内查找与第二段范围中任意元素匹配的元素,然后返回一个迭代器,指向第一个匹配的元素。如果找不到元素,则返回第一个范围的 end 迭代器。
写入到输入序列的一个简单算法是 fill 函数,带有一对迭代器形参,用于指定要写入的范围,而所写的值是它的第三个形参的副本。
fill_n 函数带有的参数包括:一个迭代器、一个计数器以及一个值。该函数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。fill_n 函数假定对指定数量的元素做写操作是安全的。常犯的错误的是:在没有元素的空容器上调用 fill_n 函数
确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。插入迭代器是可以给基础容器添加元素的迭代器。通常,用迭代器给容器元素赋值时,被赋值的是迭代器所指向的元素。而使用插入迭代器赋值时,则会在容器中添加一个新元素,其值等于赋值运算的右操作数的值。
back_inserter 的程序必须包含 iterator 头文件。
back_inserter 函数是迭代器适配器。与容器适配器一样,迭代器适配器使用一个对象作为实参,并生成一个适应其实参行为的新对象。
vector<int> vec; // empty vector // ok: back_inserter creates an insert iterator that adds elements to vec fill_n (back_inserter(vec), 10, 0); // appends 10 elements to vec
copy函数带有三个迭代器参数:头两个指定输入范围,第三个则指向目标序列的一个元素。传递给 copy 的目标序列必须至少要与输入范围一样大。
vector<int> ivec; // empty vector // copy elements from ilst into ivec copy (ilst.begin(), ilst.end(), back_inserter(ivec));
replace函数带有四个形参:一对指定输入范围的迭代器和两个值。
// replace any element with value of 0 by 42 replace(ilst.begin(), ilst.end(), 0, 42);
replace_copy算法接受第三个迭代器实参,指定保存调整后序列的目标位置。
// create empty vector to hold the replacement vector<int> ivec; // use back_inserter to grow destination as needed replace_copy (ilst.begin(), ilst.end(),back_inserter(ivec), 0, 42);
sort 算法带有两个迭代器实参,指出要排序的元素范围。这个算法使用小于(<)操作符比较元素。
unique 算法带有两个指定元素范围的迭代器参数。该算法删除相邻的重复元素,然后重新排列输入范围内的元素,并且返回一个迭代器,表示无重复的值范围的结束。unique 实际上并没有删除任何元素,而是将无重复的元素复制到序列的前端,从而覆盖相邻的重复元素。unique 返回的迭代器指向超出无重复的元素范围末端的下一位置。
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作。
谓词是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立。
stable_sort 算法保留相等元素的原始相对位置。
count_if 算法返回使谓词函数返回条件成立的元素个数。
插入迭代器
标准库所定义的迭代器不依赖于特定的容器。事实上,C++ 语言还提供了另外三种迭代器:
插入迭代器:这类迭代器与容器绑定在一起,实现在容器中插入元素的功能。
iostream 迭代器:这类迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。
反向迭代器:这类迭代器实现向后遍历,而不是向前遍历。所有容器类型都定义了自己的 reverse_iterator 类型,由 rbegin 和 rend 成员函数返回。
插入器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素。通过插入
迭代器赋值时,迭代器将会插入一个新的元素。C++ 语言提供了三种插入器,其差别在于插入元素的位置不同。
• back_inserter,创建使用 push_back 实现插入的迭代器。
• front_inserter,使用 push_front 实现插入。
• inserter,使用 insert 实现插入操作。除了所关联的容器外,inserter还带有第二实参:指向插入起始位置的迭代器。
list<int> ilst, ilst2, ilst3; // empty lists // after this loop ilst contains: 3 2 1 0 for (list<int>::size_type i = 0; i != 4; ++i) ilst.push_front(i); // after copy ilst2 contains: 0 1 2 3 copy (ilst.begin(), ilst.end(), front_inserter(ilst2)); // after copy, ilst3 contains: 3 2 1 0 copy (ilst.begin(), ilst.end(), inserter (ilst3, ilst3.begin()));
front_inserter 的使用将导致元素以相反的次序出现在目标对象中,这点非常重要。
iostream 迭代器
标准库提供了在 iostream 对象上使用的迭代器:istream_iterator 用于读取输入流,而 ostream_iterator 则用
于写输出流。
iostream |
迭代器的构造函数 |
istream_iterator<T> in(strm); |
创建从输入流 strm 中读取 T 类型对象的istream_iterator 对象 |
istream_iterator<T> in; |
istream_iterator 对象的超出末端迭代器 |
ostream_iterator<T> in(strm); |
创建将 T 类型的对象写到输出流 strm 的ostream_iterator 对象 |
ostream_iterator<T> in(strm, delim); |
创建将 T 类型的对象写到输出流 strm 的ostream_iterator 对象,在写入过程中使用 delim作为元素的分隔符。delim 是以空字符结束的字符数组 |
istream_iterator |
的操作 |
it1 == it2 it1 != it2 |
比较两上 istream_iterator 对象是否相等(不等)。迭代器读取的必须是相同的类型。如果两个迭代器都是 end 值,则它们相等。对于两个都不指向流结束位置的迭代器,如果它们使用同一个输入流构造,则它们也相等 |
*it |
返回从流中读取的值 |
it->mem |
是 (*it).mem 的同义诩。返回从流中读取的对象的 mem 成员 |
++it it++ |
通过使用元素类型提供的 >> 操作从输入流中读取下一个元素值,使迭代器向前移动。通常,前缀版本使用迭代器在流中向前移动,并返回对加 1 后的迭代器的引用。而后缀版本使迭代器在流中向前移动后,返回原值 |
流迭代器都是类模板:任何已定义输入操作符(>> 操作符)的类型都可以定义 istream_iterator。类似地,任何已定义输出操作符(<< 操作符)的类型也可定义 ostream_iterator。
在创建流迭代器时,必须指定迭代器所读写的对象类型:
istream_iterator<int> cin_it(cin); // reads ints1 from cin istream_iterator<int> end_of_stream; // end iterator value // writes Sales_items from the ofstream named outfile // each element is followed by a space ofstream outfile; ostream_iterator<Sales_item> output(outfile, " ");
ostream_iterator 对象必须与特定的流绑定在一起。在创建istream_iterator 时,可直接将它绑定到一个流上。
istream_iterator<int> in_iter(cin); // read ints from cin istream_iterator<int> eof; // istream "end" iterator // read until end of file, storing what was read in vec while (in_iter != eof) // increment advances the stream to the next value dereference reads next value from the istream vec.push_back(*in_iter++);
eof 迭代器定义为空的istream_iterator 对象,用作结束迭代器。绑在流上的迭代器在遇到文件结束或某个错误时,将等于结束迭代器的值。
istream_iterator<int> in_iter(cin); // read ints from cin istream_iterator<int> eof; // istream "end" iterator vector<int> vec(in_iter, eof); // construct vec from an iteratorrange
可使用 ostream_iterator 对象将一个值序列写入流中,其操作的过程与使用迭代器将一组值逐个赋给容器中的元素相同:
// write one string per line to the standard output ostream_iterator<string> out_iter(cout, "\n"); // read strings from standard input and the end iterator istream_iterator<string> in_iter(cin), eof; // read until eof and write what was read to the standard output while (in_iter != eof) // write value of in_iter to standard output and then increment the iterator to get the next value from cin *out_iter++ = *in_iter++;
这个程序读 cin,并将每个读入的值依次写到 cout 中不同的行中。
流迭代器有下面几个重要的限制:
• 不可能从 ostream_iterator 对象读入,也不可能写到istream_iterator 对象中。
• 一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator 对象中每个不同的值都只能正好输出一次。
• ostream_iterator 没有 -> 操作符。
在一些泛型算法上使用流类迭代器。
istream_iterator<int> cin_it(cin); // reads ints from cin istream_iterator<int> end_of_stream; // end iterator value // initialize vec from the standard input: vector<int> vec(cin_it, end_of_stream); sort(vec.begin(), vec.end()); // writes ints to cout using " " as the delimiter ostream_iterator<int> output(cout, " "); // write only the unique elements in vec to the standard output unique_copy(vec.begin(), vec.end(), output);
反向迭代器
反向迭代器是一种反向遍历容器的迭代器。也就是,从最后一个元素到第一个元素遍历容器。反向迭代器将自增(和自减)的含义反过来了:对于反向迭代器,++ 运算将访问前一个元素,而 -- 运算则访问下一个元素。
// reverse iterator of vector from back to front vector<int>::reverse_iterator r_iter; for (r_iter = vec.rbegin(); // binds r_iter to last element r_iter != vec.rend(); // rend refers 1 before 1st element ++r_iter) // decrements iterator one element cout << *r_iter << endl; // prints 9,8,7,...0
从一个既支持 -- 也支持 ++ 的迭代器就可以定义反向迭代器,流迭代器不能创建反向迭代器。
反向迭代器与其他迭代器之间的关系
// find first element in a comma-separated list string::iterator comma = find(line.begin(), line.end(), ','); cout << string(line.begin(), comma) << endl; // find last element in a comma-separated list string::reverse_iterator rcomma = find(line.rbegin(), line.rend(), ','); // wrong: will generate the word in reverse order cout << string(line.rbegin(), rcomma) << endl; // ok: get a forward iterator and read to end of line cout << string(rcomma.base(), line.end()) << endl;
图显示的对象直观地解释了普通迭代器与反向迭代器之间的关系。例如,正如 line_rbegin() 和 line.end() 一样,rcomma 和 rcomma.base() 也指向不同的元素。为了确保正向和反向处理元素的范围相同,这些区别必要的。从技术上来说,设计普通迭代器与反向迭代器之间的关系是为了适应左闭合范围这个性质的,所以,[line.rbegin(), rcomma) 和[rcomma.base(), line.end()) 标记的是 line 中的相同元素。
算法要求的迭代器操作分为五个类别,分别对应表列出的五种迭代器。
|
迭代器种类 |
Input iterator(输入迭代器) |
读,不能写;只支持自增运算 |
Output iterator(输出迭代器) |
写,不能读;只支持自增运算 |
Forward iterator(前向迭代器) |
读和写;只支持自增运算 |
Bidirectional iterator(双向迭代器) |
读和写;支持自增和自减运算 |
Random access iterator(随机访问迭代器) |
读和写;支持完整的迭代器算术运算 |
输入迭代器可用于读取容器中的元素,但是不保证能支持容器的写入操作。输入迭代器必须至少提供下列支持。
o 相等和不等操作符(==,!=),比较两个迭代器。
o 前置和后置的自增运算(++),使迭代器向前递进指向下一个元素。
o 用于读取元素的解引用操作符(*),此操作符只能出现在赋值运算的右操作数上。
o 箭头操作符(->),这是 (*it).member 的同义语,也就是说,对迭代器进行解引用来获取其所关联的对象的成员。
输出迭代器可视为与输入迭代器功能互补的迭代器;输出迭代器可用于向容器写入元素,但是不保证能支持读取容器内容。输出迭代器一般用作算法的第三个实参,标记起始写入的位置。输出迭代器要求:
o 前置和后置的自增运算(++),使迭代器向前递进指向下一个元素。
o 解引用操作符(*),引操作符只能出现在赋值运算的左操作数上。给解引用的输出迭代器赋值,将对该迭代器所指向的元素做写入操作。
前向迭代器 用于读写指定的容器。这类迭代器只会以一个方向遍历序列。前向迭代器支持输入迭代器和输出迭代器提供的所有操作,除此之外,还支持对同一个元素的多次读写。
双向迭代器 从两个方向读写容器。除了提供前向迭代器的全部操作之外,双向迭代器还提供前置和后置的自减运算(--)。
随机访问迭代器 提供在常量时间内访问容器任意位置的功能。这种迭代器除了支持双向迭代器的所有功能之外,还支持下面的操作:
o 关系操作符 <、<=、> 和 >=,比较两个迭代器的相对位置。
o 迭代器与整型数值 n 之间的加法和减法操作符 +、+=、- 和 -=,结果是迭代器在容器中向前(或退回)n 个元素。
o 两个迭代器之间的减法操作符(--),得到两个迭代器间的距离。
o 下标操作符 iter[n],这是 *(iter + n) 的同义词。
除了输出迭代器,其他类别的迭代器形成了一个层次结构:需要低级类别迭代器的地方,可使用任意一种更高级的迭代器。对于需要输入迭代器的算法,可传递前向、双向或随机访问迭代器调用该算法。调用需要随机访问迭代器的算法时,必须传递随机访问迭代器。
map、set 和 list 类型提供双向迭代器,而 string、vector 和 deque 容器上定义的迭代器都是随机访问迭代器都是随机访问迭代器,用作访问内置数组元素的指针也是随机访问迭代器。istream_iterator 是输入迭代器,而
ostream_iterator 则是输出迭代器。
泛型算法的结构
算法最基本的性质是需要使用的迭代器种类。所有算法都指定了它的每个迭代器形参可使用的迭代器类型。
另一种算法分类的方法,根据对元素的操作将算法分为下面几种:
• 只读算法,不改变元素的值顺序。
• 给指定元素赋新值的算法。
• 将一个元素的值移给另一个元素的算法。
算法的形参模式。大多数算法采用下面四种形式之一:
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
其中,alg 是算法的名字,beg 和 end 指定算法操作的元素范围。我们通常将该范围称为算法的“输入范围”。
带有单个目标迭代器的算法:dest 形参是一个迭代器,用于指定存储输出数据的目标对象。
带第二个输入序列的算法:有一些算法带有一个 beg2 迭代器形参,或者同时带有 beg2 和 end2 迭代器形参,来指定它的第二个输入范围。带有 beg2 而不带 end2 的算法将 beg2 视为第二个输入范围的首元素,但没有指定该范围的最后一个元素。这些算法假定以 beg2 开始的范围至少与 beg和 end 指定的范围一样大。
算法的命名规范:包括两种重要模式:第一种模式包括测试输入范围内元素的算法,第二种模式则应用于对输入范围内元素重新排序的算法。
区别带有一个值或一个谓词函数参数的算法版本
重新对容器元素排序的算法要使用 < 操作符。这些算法的第二个重载版本带有一个额外的形参,表示用于元素排序的不同运算:
sort (beg, end); // use < operator to sort the elements
sort (beg, end, comp); // use function named comp to sort the elements
检查指定值的算法默认使用 == 操作符。系统为这类算法提供另外命名的(而非重载的)版本,带有谓词函数形参。带有谓词函数形参的算法,其名字带有后缀 _if:
find(beg, end, val); // find first instance of val in the input range
find_if(beg, end, pred); // find first instance for which pred is true
区别是否实现复制的算法版本
标准库也为这些算法提供另外命名的版本,将元素写到指定的输出目标。此版本的算法在名字中添加了_copy 后缀:
reverse(beg, end);
reverse_copy(beg, end, dest);
标准库为 list 容器定义了更精细的操作集合,使它不必只依赖于泛型操作。对于 list 对象,应该优先使用 list 容器特有的成员版本,而不是泛型算法。与对应的泛型算法不同,list 容器特有的操作能添加和删除元素。
lst.merge(lst2) lst.merge(lst2, comp) |
将 lst2 的元素合并到 lst 中。这两个 list 容器对象都必须排序。lst2 中的元素将被删除。合并后,lst2 为空。返回 void 类型。第一个版本使用 < 操作符,而第二个版本则使用 comp 指定的比较运算 |
lst.remove(val) lst.remove_if(unaryPred) |
调用 lst.erase 删除所有等于指定值或使指定的谓词函数返回非零值的元素。返回 void 类型 |
lst.reverse() |
反向排列 lst 中的元素 |
lst.sort |
对 lst 中的元素排序 |
lst.splice(iter, lst2)lst.splice(iter, lst2, iter2)lst.splice(iter, beg, end) |
将 lst2 的元素移到 lst 中迭代器 iter 指向的元素前面。在 lst2 中删除移出的元素。第一个版本将 lst2 的所有元素移到 lst 中;合并后,lst2 为空。lst 和 lst2 不能是同一个 list 对象。第二个版本只移动 iter2 所指向的元素,这个元素必须是 lst2 中的元素。在这种情况中,lst 和lst2 可以是同一个 list 对象。也就是说,可在一个 list对象中使用 splice 运算移动一个元素。第三个版本移动迭代器 beg 和 end 标记的范围内的元素。beg 和 end 照例必须指定一个有效的范围。这两个迭代器可标记任意 list 对象内的范围,包括 lst。当它们指定 lst 的一段范围时,如果 iter 也指向这个范围的一个元素,则该运算未定义。 |
lst.unique() lst.unique(binaryPred) |
调用 erase 删除同一个值的团结副本。第一个版本使用 ==操作符判断元素是否相等;第二个版本则使用指定的谓词函数实现判断 |