代码改变世界

《Effective STL 读书笔记》 第七章 在程序中使用STL

2011-09-12 01:57  咆哮的马甲  阅读(471)  评论(0编辑  收藏  举报
作者:咆哮的马甲
出处:http://www.cnblogs.com/arthurliu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。

第四十三条:算法调用优先于手写的循环
 

算法往往作用于一对迭代器所指定的区间中的每一个元素上,所以算法的内部实现是基于循环的。虽然说类似于find和find_if的算法可能不会遍历所有的元素就返回了结果,但是在极端情况下,还是需要遍历全部的元素。

从以下几点分析,算法调用是优于手写的循环的

  • 效率
  • 正确性
  • 可维护性



第四十四条:容器的成员函数优于同名的算法
 

  • 成员函数速度优于同名算法
  • 成员函数与容器的联系更加紧密

对于关联容器请看下面的例子:

1 set<int> s;
2
3 set<int>::iterator i1 = s.find(727);
4
5 set<int>::iterator i2 = find(s.begin(), s.end(), 727);

对于set而言,它的find成员函数的时间复杂度是log(n),而算法find的时间复杂度是线性的n。明显,成员函数的效率要远高于算法。

另外,算法是基于相等性而关联容器基于等价性,在这种情况下,调用成员函数和调用算法可能会得到不同的结果。(参见第19条)

对于map以及multimap,成员函数之针基于key进行操作,而算法基于key-value pair进行操作。

对于list而言,成员函数相对于算法的优势更加明显。算法是基于元素的拷贝的,而list成员函数可能只需要修改指针的指向。

还有之前所提到的list的remove成员函数,同时起到了remove和erase的作用。

有些算法,例如sort并不能应用在list上,因为sort是基于随机访问迭代器的。还有merge算法,它要求不能修改源区间,而merge成员函数总是在修改源链表的元素的指针指向。



第四十五条:
正确区分count、find、binary_search、lower_bound、upper_bound和equal_range
 

  • count: 区间内是否存在某个特定的值,如果存在的话,这个值有多少个拷贝。
  • find:  区间内时候存在某个特定的值,如果存在的话,第一个符合条件的值在哪里。
  • binary_search:一个排序的区间内是否存在一个特定的值。
  • lower_bound:返回一个迭代器,或者指向第一个满足条件的元素,或者指向适合于该值插入的位置。切记lower_bound是基于等价性的,用相等性来比较lower_bound的返回值和目标元素是存在潜在风险的。
  • upper_bound:返回一个迭代器,指向最后一个满足条件元素的后面一个元素。
  • equal_range:返回一对迭代器,第一个指向lower_bound的返回值,第二个指向upper_bound的返回值。如果两个返回值指向同一位置,则说明没有符合条件的元素。Lower_bound与upper_bound的distance可以求得符合条件的元素的个数。

下表总结了在什么情况下使用什么样的算法或成员函数

clip_image002


对于multi容器来说,find并不能保证找出的元素是第一个具有此值的元素。如果希望找到第一个元素,必须通过lower_bound,然后在通过等价性的验证。Equal_range是另外一种方式,而且可以避免等价性测试,只是equal_range的开销要大于lower_bound。



第四十六条:
考虑使用函数对象而不是函数作为STL算法的参数


函数对象优于函数的第一个原因在于函数对象的operator方法可以被优化为内联函数,从而使的函数调用的开销在编译器被消化。而编译器并没有将函数指针的间接调用在编译器进行优化,也就是说,函数作为STL算法的参数相对于函数对象而言,具有函数调用的开销。

第二个理由是某些编译器对于函数作为STL的参数支持的并不好。

第三个理由是有助于避免一些微妙的、语言本身的缺陷。比如说实例化一个函数模板,可能会与其他已经预定义的函数产生冲突。



第四十七条:
避免产生“直写型”(write-only)的代码


根据以往的经验,代码被阅读的次数要远远多于被编写的次数,所以要有意识的写出具备可读性的代码。对于STL而言,则是尽量避免“直写型”的代码。

直写型的代码是这样的,对于程序的编写者而言,它显得非常的直接,并且每一步都符合当初设计的逻辑。但是对于程序的阅读者来说,在没有全面了解程序编写者动机的前提下,这样的代码往往让人一头雾水。

1 v.erase(remove_if(find_if(v.rbegin(),v.rend(),bind2nd(greater_equaql<int>(),y)).base()),v.end(),bind2nd(less<int>(),x));

  

比较易读的写法最好是这样的

// 初始化range_begin,使它指向v中大于等于y的最后一个元素之后的那个元素
// 如果不存在这样的元素,则rangeBegin被初始化为v.begin()
// 如果这个元素恰好是v的最后一个元素,则range_begin将被初始化为v.end()
VecIt rangeBegin = find_if(v.rbegin(),v.rend(),bind2nd(greater_equal<int>(),y)).base();

// 从rangeBegin到v.end()的区间中,删除所有小于x的值
v.erase(remove_if(rangeBegin,v.end(),bind2nd(less<int>(),x)),v.end());

  


第四十八条 总是include正确的头文件


与STL头文件相关的一些总结

  • 几乎所有的STL容器都被声明在与之同名的头文件之中
  • 除了accumulate、inner_product、adjacent_difference和partial_sum被声明在<numeric>中之外,其他都所有算法都声明在<algorithm>中
  • 特殊类型的迭代器,例如isteam_iterator和istreambuf_iterator,都被声明在<iterator>中
  • 标准的函数子,比如less<T>,和函数子配接器,比如not1、bind2nd都被声明在<functional>中。



第四十九条
学会分析与STL相关的编译器诊断信息


STL的编译错误信息往往冗长而且难以阅读,通过文本替换将复杂的容器名称替换为简单的代号,可以使得错误信息得到简化。

例如,将std::basic_string<char, std::char_traits<char>, std::allocator<char>>替换为可读性更强的string。


下面列举一些常见的STL错误,以及可能的出错原因

  • Vector和string的迭代器通常就是指针,当错误的使用iterator的时候,编译器的错误信息中可能会包含指针类型的错误。
  • 如果诊断信息提到了back_insert_iterator, front_insert_iterator和insert_iterator,则几乎意味着程序中直接或间接地调用了back_inserter, front_inserter或者是inserter。
  • 输出迭代器以及inserter函数返回的迭代器在赋值操作符内部完成输入或者插入操作,如果有赋值操作符有关的错误信息,可以关注这些迭代器。
  • 如果错误信息来自于算法的内部实现,往往意味着传递给算法的对象使用了错误的类型。
  • 如果在使用一个常见的STL组件,但编译器却不认知,可能是没有包含合适的头文件。



第五十条
熟悉与STL相关的Web站点


SGI STL http://www.sgi.com/tech/stl

STLport http://www.stlport.org

Boost http://www.boost.org

另外个人推荐一个中文站点http://stlchina.huhoo.net/