4、 迭代器
标准STL容器提供了四种不同的迭代器:iterator、const_iterator、reverse_iterator和const_reverse_iterator
为什么有四种迭代器?它们之间的关系是什么?它们可以互相转化吗?在调用算法和STL实用函数时不同类型可以混合使用吗?这些类型是怎么关联到容器和它们的成员函数的?本章回答了这些问题,同时介绍了一个比通常更值得注意的迭代器类型:istreambuf_iterator。
条款26:尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator
每个标准容器类都提供四种迭代器类型。对于container<T>而言,iterator的作用相当于T*,而const_iterator则相当于const T*;增加一个iterator或者const_iterator可以在一个从容器开头趋向尾部的遍历中让你移动到容器的下一个元素。reverse_iterator与const_reverse_iterator同样相当于对应的T*和const T*,所不同的是,增加reverse_iterator或者const_reverse_iterator会在从尾到头的遍历中让你移动到容器的下一个元素。
迭代器使用的一个重要指导方针是:尽量使用iterator代替其他三种迭代器,原因有:
(1) insert和erase的一些版本要求iterator。如果你需要调用这些函数,你就必须产生iterator,而不能用const或reverse iterators。
(2)不可能把const_iterator隐式转换成iterator。
条款27:用distance和advance把const_iterator转化成iterator
如果你只有一个const_iterator,而你要在它所指向的容器位置上插入新元素呢?也就是如何把const_iterator转化为iterator呢?前面提到:并不存在从const_iterator到iterator之间的隐式转换,也就是说下面操作是不可以的:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
typedef deque< int > IntDeque; // 方便的typedef
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
ConstIter ci; // ci是const_iterator
...
Iter i(ci); // 错误!没有从const_iterator 到iterator隐式转换的途径
Iter i( const_cast <Iter>(ci)); // 仍是个错误!不能从const_iterator映射为iterator!
|
正确方法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
typedef deque< int > IntDeque; // 和以前一样
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
... // 让ci指向d
Iter i(d.begin()); // 初始化i为d.begin()
advance(i, distance <ConstIter> (i, ci)); // 把i移到指向ci位置
|
注:distance返回两个指向同一个容器的iterator之间的距离;advance则用于将一个iterator移动指定的距离。如果i和ci指向同一个容器,那么表达式advance(i, distance(i, ci))会将i移动到与ci相同的位置上。
条款28:了解如何通过reverse_iterator的base得到iterator
条款29:XXXXXXXXXXXXXXXXXXXX(不常见)
5、算法
条款30:确保目标区间足够大
“把transform的结果放入叫做results容器的结尾”的正确方法是调用back_inserter来产生指定目标区间起点的迭代器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
vector< int > results;
transform(values.begin(), values.end(), //把transmogrify应用于values中的每个对象,
back_inserter(results), // 在results的结尾
transmogrify); // 插入返回的values
同样,将结果插到results容器的前面采用的方法是:
... // 同上
list< int > results;
transform(values.begin(), values.end(), // results现在是list在results前端
front_inserter(results), // 以反序
transmogrify); // 插入transform的结果
|
更高效的方法是:
1
2
3
4
5
6
7
8
9
10
11
|
vector< int > values; // 同上
vector< int > results;
results.reserve(results.size() + values.size()); // 同上
transform(values.begin(), values.end(), // 把transmogrify的结果
back_inserter(results), // 写入results的结尾,
transmogrify); // 处理时避免了重新分配
|
条款31:了解你的排序选择
(1) 如果你需要在vector、string、deque或数组上进行完全排序,你可以使用sort或stable_sort。
(2) 如果你有一个vector、string、deque或数组,你只需要排序前n个元素,应该用partial_sort。
(3) 如果你有一个vector、string、deque或数组,你需要鉴别出第n个元素或你需要鉴别出最前的n个元素,而不用知道它们的顺序,nth_element是你应该注意和调用的。
(4) 如果你需要把标准序列容器的元素或数组分隔为满足和不满足某个标准,你大概就要找partition或stable_partition。
(5) 如果你的数据是在list中,你可以直接使用partition和stable_partition,你可以使用list的sort来代替sort和stable_sort。如果你需要partial_sort或nth_element提供的效果,你就必须间接完成这个任务,主要有三种方法:
[1] 把元素拷贝到一个支持随机访问迭代器的容器中,然后对它应用需要的算法。
[2] 另一个方法是建立一个list::iterator的容器,对那个容器使用算法,然后通过迭代器访问list元素。
[3]使用有序的迭代器容器的信息来迭代地把list的元素接合到你想让它们所处的位置
条款32:如果你真的想删除东西的话就在类似remove的算法后接上erase
先看一个错误的实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
vector< int > v; // 建立一个vector<int> 用1-10填充它
v.reserve(10);
for ( int i = 1; i <= 10; ++i) {
v.push_back(i);
}
cout << v.size(); // 打印10
v[3] = v[5] = v[9] = 99; // 设置3个元素为99
remove (v.begin(), v.end(), 99); // 删除所有等于99的元素
cout << v.size(); // 仍然是10!
|
正确的删除元素的方法是:
1
2
3
4
5
|
vector< int > v; // 正如从前
v.erase( remove (v.begin(), v.end(), 99), v.end()); // 真的删除所有等于99的元素
cout << v.size(); // 现在返回7
|
需要注意的是remove和erase是亲密联盟,这两个整合到list成员函数remove中。这是STL中唯一名叫remove又能从容器中除去元素的函数:
1
2
3
4
5
6
7
|
list< int > li; // 建立一个list
// 放一些值进去
li. remove (99); // 除去所有等于99的元素:真的删除元素,所以它的大小可能改变了
remove_if ,unique和 remove 类似,都需要跟erase连用才可以真正删除数据,同样,对于list是个特殊。
|
条款33:提防在指针的容器上使用类似remove的算法
该条款是对条款32的补充,采用remove/erase方式删除指针容器中的数据会造成内存泄漏,如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Widget{
public :
...
bool isCertified() const ; // 这个Widget是否通过检验
...
};
vector<Widget*> v; // 建立一个vector然后用动态分配的Widget
v.push_back( new Widget); // 的指针填充
v.erase(remove_if(v.begin(), v.end(), // 删除未通过检验的
not1(mem_fun(&Widget::isCertified))), // Widget指针
v.end());
|
Widget C和Widget B的内存不会被释放,造成内存泄漏,正确的方法有两种:
(1)在应用erase-remove惯用法之前先删除指针并设置它们为空,然后除去容器中的所有空指针:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
void delAndNullifyUncertified(Widget*& pWidget) // 如果*pWidget是一个
{ // 未通过检验Widget,
if (!pWidget->isCertified()) { // 删除指针
delete pWidget; // 并且设置它为空
pWidget = 0;
}
}
for_each(v.begin(), v.end(), // 把所有指向未通过检验Widget的
delAndNullifyUncertified); // 指针删除并且设置为空
v.erase( remove (v.begin(), v.end(), // 从v中除去空指针
static_cast <Widget*>(0)), // 0必须映射到一个指针,
v.end()); // 让C++可以正确地推出remove的第三个参数的类型
|
(2)采用智能指针,如boost库中的shared_ptr和scoped_ptr
条款34:注意哪个算法需要有序区间
STL中只能操作有序数据(升序)的算法有:
(1) binary_search:二分查找
(2) lower_bound:下界
(3) upper_bound:上街
(4) equal_range:所有等于某个值的元素
(5) set_union:集合并集
(6) set_intersection:集合交集
(7) set_difference :集合差集
(8) set_symmetric_difference:包含在第一个集合但是不包含在第二个集合中的元素,包含在第2个集合但是不包含在第1个集合中的元素
(9) merge:合并两个有序表
(10) inplace_merge:合并两个有序表
(11) includes:检测一个区间的所有对象是否在另一个区间中
另外,下面的算法一般用于有序区间,虽然它们不要求:
(12) unique:去重,相同的元素必须紧挨着,排序是个特例
(13) unique_copy:同上
条款35:通过mismatch或lexicographical比较实现简单的忽略大小写字符串比较
实现忽略大小写字符串比较有两种方法,一种是使用mismatch函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
int ciStringCompareImpl( const string& s1, const string& s2);
int ciStringCompare( const string& s1, const string& s2)
{
if (s1.size() <= s2.size()) return ciStringCompareImpl(s1, s2);
else return -ciStringCompareImpl(s2, s1);
}
int ciStringCompareImpl( const string& si, const strings s2)
{
typedef pair<string::const_iterator, // PSCI = “pair of
string::const_iterator> PSCI; // string::const_iterator”
PSCI p = mismatch( // 下文解释了
s1.begin(), s1.end(), // 为什么我们
s2.begin(), // 需要not2;参见
not2(ptr_fun(ciCharCompare))); // 条款41解释了为什么
// 我们需要ptr_fun
if (p.first== s1.end()) { // 如果为真,s1等于
if (p.second == s2.end()) return 0; // s2或s1比s2短
else return -1;
}
return ciCharCompare(*p.first, *p.second); // 两个字符串的关系
} // 和不匹配的字符一样
|
第二种时使用lexicographical函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
bool ciCharLess( char c1, char c2) // 返回在忽略大小写
{ // 的情况下c1是否
// 在c2前面;
tolower ( static_cast <unsigned char >(c1)) < // 条款46解释了为什么
tolower ( static_cast <unsigned char >(c2)); // 一个函数对象可能
} // 比函数好
bool ciStringCompare( const string& s1, const string& s2)
{
return lexicographical_compare(s1.begin(), s1.end(),
s2.begin(), s2.end(),
ciCharLess);
}
|
条款36:了解copy_if的正确实现
STL有很多有趣的地方,其中一个是虽然有11个名字带“copy”的算法:
(1) copy
(2) copy_backward
(3) replace_copy
(4) reverse_copy
(5) replace_copy_if
(6) unique_copy
(7) remove_copy
(8) rotate_copy
(9) remove_copy_if
(10) partial_sort_copy
(11) unintialized_copy
但没有一个是copy_if,这需要自己实现,非常经典的一个实现是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
template < typename InputIterator, // 一个copy_if的
typename OutputIterator, // 正确实现
typename Predicate>
OutputIterator copy_if(InputIterator begin,
InputIterator end,
OutputIterator destBegin,
Predicate p) {
while (begin != end) {
if (p(*begin))*destBegin++ = *begin;
++begin;
}
return destBegin;
}
|
条款37:用accumulate或for_each来统计区间
本条款总结了用自定义的方式统计(summarize)区间的方法,主要有两种(在头文件<numeric>中):
(1) accumulate存在两种形式:第一种是: 带有一对迭代器和初始值的形式,它可以返回初始值加由迭代器划分出的区间中值的和:
1
2
3
4
5
|
list< double > ld; // 建立一个list
... // 放一些double进去
double sum = accumulate(ld.begin(), Id.end(), 0.0); // 计算它们的和,从0.0开始
|
注意初始值指定为0.0,0.0的类型是double,所以accumulate内部使用了一个double类型的变量来存储计算的和。如果这么写这个调用:
1
|
double sum = accumulate(ld.begin(), Id.end(), 0); // 计算它们的和,从0开始;
|
初始值是int 0,所以accumulate内部就会使用一个int来保存它计算的值,这是不正确的,因为每次加法后会将结果转换为一个int,这造成小数点后面的数值丢失。
另一种形式带有一个初始和值与一个任意的统计函数,实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
string::size_type // string::size_type的内容
stringLengthSum(string::size_type sumSoFar, // 请看下文
const string& s)
{
return sumSoFar + s.size();
}
set<string> ss; // 建立字符串的容器,
... // 进行一些操作
string::size_type lengthSum = // 把lengthSum设为对
accumulate(ss.begin(), ss.end(), // ss中的每个元素调用
0, stringLengthSum); // stringLengthSum的结果,使用0作为初始统计值
|
(2) for_each,带有一个区间和一个函数(一般是一个函数对象)来调用区间中的每个元
素,但传给for_each的函数只接收一个实参(当前的区间元素),而且当完成时for_each返回它的函数。