Effective STL
1 容器
第1条:慎重选择容器类型
第2条:不要试图编写独立于容器类型的代码
-
不同容器是不同的,它们有着非常明显的优缺点,因此它们并不是被设计来交换使用的
-
当不可避免地要更换一种容器类型时,最好使用封装技术,最简单的方法就是对容器类型和其迭代器类型使用类型定义(typedef),比如下面是一个好的例子
class Widget{...}; typedef vector<Widget> WidgetContainer; WidgetContainer cw; Widget bestWidget; ... WidgetContainer::iterator i=find(cw.begin(),cw.end(),bestWidget);
第3条:确保容器中的对象拷贝正确而高效
- 在存在继承关系下,拷贝动作会导致剥离(slicing)。就是说如果我们创建了一个存放基类对象的容器,却向其插入派生类对象,那么派生类(通过基类的拷贝构造函数)的特有部分会丢失。剥离问题意味着向基类对象的容器插入派生类几乎总是错误的
第4条:调用empty而不是检查size()是否为0
- 理由很简单:empty对所有标准容器都是常数时间操作,而对一些list实现,size耗费线性时间
第5条:区间成员函数优于与之对应的单元素成员函数
- 通过利用插入迭代器的方式来限定目标区间的copy调用,几乎都应该替换为对区间成员函数的调用。
- 通过使用区间成员函数,通常可以少写一些代码
- 使用区间成员函数通常会得到意图清晰和更加直接的代码,它们的效率也更高
第6条:当心C++编译器最烦人的分析机制
第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
第8条:切勿创建包含auto_ptr的容器对象
- 它自身含有缺陷,对一个auto_ptr进行复制时,它自身会置为NULL;严重的是如果获得对象所有权的auto_ptr离开函数作用域后会被置NULL,造成对象的丢失
- 目前已经被C++标准给废止了
第9条:慎重选择删除元素的方法
- 要有效删除容器中的元素,除了调用erase之外还要做很多工作。解决问题的最佳方法取决于如何识别要删除的对象、储存元素的容器类型,以及当删除时你想要做点什么。
第10条:了解分配子(allocator)的约定和限制
- 你的分配子是一个模板,模板参数T代表了你为它分配内存的对象的类型
- 提供类型定义pointer和reference,但是始终让pointer为T*,reference为T&。
- 千万别让你的分配子拥有随对象而不同的状态。通常,分配子不应该拥有非静态的数据成员
- 记住,传给分配子的allocate成员函数的是那些要求内存的对象的个数,而不是所需的字节数。同时记住,这些函数返回T*指针,即使尚未有T对象构造出来
- 一定要提供嵌套的rebind模板,因为标准容器依赖该模板
第11条:理解自定义分配子的合理用法
2 vector和string
第13条:vector和string优于动态分配的数组
- 许多string实现在背后使用了引用计数技术,这种策略可以消除不必要的内存分配和不必要的字符拷贝,从而提高许多应用程序的效率
- 如果你在多线程环境中使用了引用计数的string,会发现,为了避免内存分配和字符拷贝所节省下来的时间还比不上花在背后同步控制上的时间。这时就可以选择关闭引用计数。
第14条:使用reserve来避免不必要的重新分配
- vector和string提供了所有这四个函数:size(),capacity(),resize(Container::size_type n),reserve(Container::size_type n)。
- reserve(Container::size_type n)强迫将容器的容量变为至少是n。如果当前容量大于n,则不进行任何操作,否则会进行扩展。
- 避免重新分配的关键在于,尽早地使用reserve,把容器的容量设为足够大的值,最好在容器刚被构造出来后就进行reserve。
第15条:注意string实现的多样性
- 常见的有4种STL实现,每个的特点都不太一样。
第16条:了解如何把vector和string数据传给旧的API
第17条:使用“swap技巧“除去多余的容量
-
利用swap实现将容器的容量从以前的最大值缩减为当前所需的容量(不一定刚刚好,可能会稍大一点)
string s; ... string(s).swap(s);
-
swap技巧的一种 变化形式可以用来清除一个容器,并使其容器变为该实现下的最小值。只要与一个用默认构造函数创建的vector或string做交换就可以了
vector<Contestant> v; string s; ... //使用v和s vector<Contestant>().swap(v); //清除v并把它的容量变为最小 string().swap(s); //清除s并将它的容量变为最小
第18条:避免使用vector< bool >
- vector< bool >有两点不对,首先,它不是一个STL容器;其次,它并不储存bool。
- STL容器有个特点:取容器中一个元素的地址会产生一个指向容器中元素类型的指针,但是vector< bool >不满足这一点。
- 可以使用deque< bool >和bitset代替vector< bool >
3 关联容器
第19条:理解相等(equality)和等价(equivalence)的区别
-
成员函数find对”相同“的定义是相等,是以operator==为基础的;set::insert对”相同“的定义是等价,是以operator<为基础的。
-
例子:如果下面的表达式为真,则w1和w2对于operator<有等价的值:
!(w1<w2)&&!(w2<w1);
这里的含义是:如果两个值中的任何一个(按照一定的排序规则)都不在另一个的前面,则这两个值(按照这一准则)就是等价的。
第20条:为包含指针的关联容器指定比较类型
-
每当你要创建包含指针的关联容器时,一定要记住,容器将会按照指针的值进行排序
-
set模板的三个参数都是类型,并不能简单地写一个函数然后放到set里面去。比如set< string, stringCompare > ssp;不会通过编译(stringCompare是一个函数)。常用的模板如下:
struct cmp{ template<typename ptrType> bool operator()(ptrType ptr1,ptrType pt2) const { return *ptr1<*ptr2; } }; set<string* , cmp> ssp; //这样就可以实现我们的目的,按照string指针所指对象的大小进行排序
第21条:总是让比较函数在等值的情况下返回false
- 原因:它会破坏关联容器关于等价的定义
- 任何一个定义了”严格的弱序化“的函数必须对相同值的两个拷贝返回false
第22条:切勿直接修改set或multiset中的键
-
一种可行且安全地修改set、multiset、map、multimap中地元素,可以按照下面5个简单步骤进行,它是安全且可移植的
Employee selectedID; ... EmpIDSet::iterator i=se.find(selectedID); //第1步:找到待修改的元素 if(i!=se.end()){ Employee e(*i); //第2步:拷贝该元素 e.setTitle("Corpotate Deity"); //第3步:修改拷贝 se.erase(i++); //第4步:删除该元素;递增该迭代器保持它的有效性(见第9条) se.insert(i,e); //第5步:插入新元素;提示它的位置和原来的相同 }
第23条:考虑用排序的vector替代关联容器
- 对数据结构的使用方式是:查找操作几乎从不跟插入和删除操作混在一起时,再考虑使用排序的vector而不是关联容器才是合理的
第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎做出选择
- 当效率至关重要时,你应该在map::operator[]和map::insert之间做出选择。如果更新一个已有的映射表元素时,则优先选择operator[];但如果要添加一个新的元素,那么最好还是选择insert。
第25条:熟悉非标准的散列容器
4 迭代器
第26条:iterator优先于const_iterator、reverse_iterator及const_reverse_iterator
- 当iterator和const_iterator之间做选择时,我们有充分的理由来选择iterator而非const_iterator。
- 从const_iterator到iterator之间不存在隐式转化。
第27条:使用distance和advance将容器的const_iterator转换成iterator
-
可以通过< iterator >中声明的两个函数模板来实现这一转换:distance用于取得两个迭代器(它们指向同一个容器)之间的距离;advance则用于将一个迭代器移动指定的距离。
advance(i,distance<ConstIter>(i,ci)); //将i和ci都当作const_iterator,计算出它们之间的距离,然后将i移动这段距离
第28条:正确理解由reverse_iterator的base()成员函数所产生的iterator的用法
- 通过base()函数可以得到一个与reverse_iterator”相对应的“iterator的说法并不准确。对于插入操作,这种对应关系确实存在;但是对于删除操作,情况并非如此简单。
第29条:对于逐个字符的输入请考虑使用istreambuf_iterator
- 如果你需要从一个输入流逐个读取字符,那么就不必使用格式化输入;如果你关心的是读取流的时间开销,那么使用istreambuf_iterator只是多输入了三个字符而已,却获得了很明显地性能改善。
- 同样的,对于非格式化的逐个字符输出过程,你也应该考虑使用ostreambuf_iterator。
5 算法
第30条:确保目标区间足够大
- 无论何时,如果所使用的算法需要指定一个目标区间,那么必须确保目标区间足够大,或者确保它会随着算法的运行而增长。要在算法执行的过程中增大目标区间,请使用插入型迭代器,比如ostream_iterator,或者由back_inserter、front_inserter和inserter返回的迭代器。
第31条:了解各种与排序有关的选择
- 如果需要对vector、string、deque或者数组中的元素执行一次完全排序,那么可以使用sort或者stable_sort
- 如果有一个vector、string、deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那么可以使用partial_sort
- 如果有一个vector、string、deque或者数组,并且需要找到第n个位置上的元素,或者,你需要找到等价性最前面的n个元素但又不必对这n个元素进行排序,那么nth_element正是我们所需要的函数
- 如果需要将一个标准序列容器中的元素按照是否满足某个特定的条件区分开,那么,partition和stable_partition是我们所需要的函数
- partial_sort、nth_element和sort都属于非稳定的排序算法,但是有一个名为stable_sort的算法可以提高稳定排序算法
第32条:如果确实需要删除元素,则需要在remove这一类算法之后调用erase
-
用remove从容器中删除元素,而容器中的元素数目却不会因此而减少。因为remove只是移动了区间的元素,将”不需要被删除的元素“移动到区间前面,并返回一个迭代器指向最后一个”不需要被删除的元素“之后的元素,它也就成了“新的逻辑结尾”。
-
如果我们想真的删除元素,那就必须在remove之后使用erase。
vector<int> v; //同前 ... v.erase(remove(v.begin(),v.end(),99),v.end()); //真正删除所有元素值等于99的元素
-
list的remove是唯一一个确实删除了容器中元素的函数
第33条:对包含指针的容器使用remove这一类算法时要特别小心
- 可以采用智能指针来解决资源泄露的问题
第34条:了解哪些算法要求使用排序的区间作为参数
- 要求排序的算法之所以这样要求,是为了提供更好的性能,而对于未排序的区间它们无法保证有这样的性能
第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
第36条:理解copy_if算法的正确实现
-
一个正确的copy_if实现:
template<typename InputIterator,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进行区间统计
- 用for_each来统计一个区间是合法的,但是远不如accumulate来得清晰
- accumulate直接返回我们要的统计结果,而for_each却返回一个函数对象,我们必须从这个函数对象中提取我们所要的统计信息
6 函数子、函数子类、函数及其它
第38条:遵循按值传递的原则来设计函数子类
- 函数对象在STL中作为参数传递或者返回的时候总是按值方式被拷贝的。这意味着两件事:第一,使它们小巧;第二,使它们成为单态
第39条:确保判别式是”纯函数“
- 一个纯函数是指返回值仅仅依赖于其参数的函数
第40条:若一个类是函数子,则应使它可配接
- 4个标准的函数配接器(not1、not2、bind1st和bind2nd)都需要一些特殊的类型定义,那些非标准的、与STL兼容的配接器通常也是如此。提供了这些必要的类型定义的函数对象被称为可接配的(adaptable)函数对象,反之,如果函数对象缺少这些类型定义,则称为不可配接的
- 特殊的类型定义:argument_type、first_argument_type、second_argument_type及result_type
第41条:理解ptr_fun、mem_fun和men_fun_ref的由来
- ptr_fun每次都加上不会有什么不妥之处。
- men_fun和men_fun_ref:每次将一个成员函数传递给一个STL组件的时候,就必须要用到它们
第42条:确保less< T >与operator<具有相同的语义
7 在程序中使用STL
第43条:算法调用优先于手写的循环
- 效率:算法通常比程序员自己写的循环效率更高
- 正确性:自己写循环比使用算法更容易出错
- 可维护性:使用算法的代码通常比手写循环的代码更加简洁明了
第44条:容器的成员函数优先于同名的算法
- 对于标准的关联容器,选择成员函数而非对应的同名算法,理由有:
- 可以获得对数时间的性能,而非线性时间的性能
- 可以使用等价性来确定两个值是否”相同“,而等价性是关联容器的一个本质定义
- 使用map和multimap的时候,将很自然地只考虑元素的键部分,而不是完整的(key,value)对。
第45条:正确区分count、find、binary_search、lower_bound、upper_bound和equal_range
- P167页有个建议表格,结合容器和我们的目的、性能要求选择合适的算法或成员函数
第46条:考虑使用函数对象而不是函数作为STL算法的参数
- 函数内联使得使用函数对象效率更高;且使用函数对象可以消除一些编译器缺陷