Effective STL条款
容器
仔细选择你的容器
vector、deque和list之间选择的指导方案:
vector、list和deque提供给程序员不同的复杂度,因此应该这么用:
vector是一种可以默认使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和删除发生在序列的头或尾时可以选择deque这种数据结构。小心对“容器无关代码”的幻想
使容器里对象的拷贝操作轻量而正确
用empty来代替检查size()是否为0
对于所有的标准容器,empty是一个常数时间的操作,但对于一些list实现,size花费线性时间。
尽量使用区间成员函数代替它们的单元素兄弟
几乎所有目标区间被插入迭代器指定的copy的使用都可以用调用区间成员函数来代替。
尽量使用区间成员函数代替他们的单元素兄弟的利用如下:
1)一般来说使用区间成员函数可以输入更少的代码
2)区间成员函数会导致代码更清晰更直接了当。警惕C++最令人恼怒的解析
当使用new地指针的容器时,记得在销毁容器前delete那些指针
永不建立auto_ptr的容器
在删除选项中仔细选择
去除一个容器中有特定值的所有对象:
如果容器是vector、string或deque,使用erase-remove惯用法
如果容器是list,使用list::remove
如果容器s是标准关联容器,使用它的erase成员函数。
去除一个容器中满足一个特定判定式的所有对象:
如果容器是vector、string或deque,使用erase_remove_if惯用法
如果容器是list,使用list::remove_if
如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。
在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。注意分配器的协定和约束
如果向自定义分配器,需注意以下事情:
把分配器做成一个模板,带有模板参数T,代表要分配内存的对象类型
提供pointer和reference的typedef,但是总是让pointer是T*,reference是T&
决不要给你的分配器每对象状态。通常,分配器不能有非静态的数据成员
记得应该传给分配器的allocate成员函数需要分配的对象个数而不是字节数。也应该记得这些函数返回T*指针(通过pointer typedef),即使还没有T对象被构造
一定要提供标准容器依赖的内嵌rebind模板理解自定义分配器的正确用法
对STL容器线程安全性的期待现实一些
STL容器里对多线程支持的环境规则由SGI定义。大体上包括以下内容:
多个读者时安全的。多线程可能同时读取一个容器的内容,这将正确的执行。当然,在读取时不能有任何写入者操作这个容器。
对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。
一个库可以试图以下列方式实现完全线程安全的容器:
在每次调用容器的成员函数期间都要锁定该容器
在每个容器返回的迭代器(如通过调用begin或end)的生存期之内都要锁定该容器
在每个容器上调用的算法执行期间锁定该容器。
vector和string
尽量使用vector和string来代替动态分配的数组
当决定使用new来进行动态分配,需要负责以下职责:
必须确保有人以后会delete这个分配。如果后面没有delete,new就会产生一个资源泄露;
必须确保使用了delete的正确形式。对于分配一个单独的对象,必须使用“delete”。对于分配的一个数组,必须使用delete p[]。如果使用了delete的错误形式,结果会未定义。在一些平台上,运行期会当掉。另一方面,它会默默地走向错误,有时候会造成资源泄露,一些内存页随之而去。
必须确保只delete一次。如果一个分配被删除了不止一次,结果也会未定义。使用reserve来避免不必要的重新分配
对于vector和string,只要需要更多空间,可以使用与realloc等价的思想来增长。具体包括以下四部分:
分配新的内存块,它有容器目标容量的几倍。在大部分实现中,vector和string的容量每次以2为因子增长。即当容器必须扩展时,它们的容量每次翻倍。
把所有元素从容器的旧内存拷贝到它的新内存
销毁旧内存中的对象
回收旧内存小心string实现的多样性
每个string实现都容纳了下面信息:
字符串的大小,即它包含的字符串的数目
容纳字符串字符的内存容量
资格字符串值,即构成这个字符串的字符
它的配置器的拷贝
引用计数的string的实现也包含了这个值的引用计数
字符串总结如下:
字符串可能是或可能不是引用计数的。默认情况下,很多实现的确是用了引用计数,但它们通常提供了关闭的方法,一般是通过预处理器宏。
string对象的大小可能从1到至少7倍char *指针的大小
新字符串的建立可能需要0、1或2次动态分配
string对象可能是或可能不共享字符串的大小和容量信息
string可能是或可能不支持每对象配置器
不同实现对于最小化字符缓冲区的配置器有不同策略。如何将vector和string的数据传给遗留的API
使用“交换技巧”来修正过剩容量
避免使用vector
作为一个STL容器,vector确实只有两个问题:
第一:它不是一个STL容器
第二:它并不容纳bool。
关联容器
了解相等和等价的区别
为指针的关联容器指定比较类型
永远让比较函数对相等的值返回false
避免原地修改set和multiset的键
如果不关心移植性,想要改变set或multiset中元素的值,而且STL实现让你侥幸成功,继续做。只是要确定不要改变元素的键部分,即会影响容器有序性的元素部分。
如果在乎移植性,就认为set和multiset中的元素不能被修改,至少不能在没有映射的情况下。
如果要总是可以工作而且总是安全地改变set、multiset、map或multimap里的元素,按五个简单的步骤去做:
定位想要改变的容器元素。
拷贝一份要被修改的元素。对map或multimap而言,确定不要把副本的第一个元素声明为const。
修改副本,使它有想要在容器里的值。
从容器里删除元素,通常通过调用erase
把新值插入容器。如果新元素在容器的排序顺序中的位置正好相同或相邻于删除的元素,使用insert的提示形式把插入的效率从对数时间改进到分摊的常数时间。使用从第一部获得的迭代器作为提示。考虑用有序的vector代替关联容器
很多应用中,对数据结构的使用可以总结为以下三个截然不同的阶段:
建立。通过插入很多元素建立一个新的数据结构。在这个阶段,几乎所有的操作都是插入和删除。几乎没有或根本没有查找。
查找。在数据结构中查找指定的信息片。在这个阶段,几乎所有的操作都是查找。几乎没有或根本没有插入和删除。
重组。修改数据结构的内容,也许通过删除所有现有数据和在原地插入新数据。从动作上说,这个阶段等于阶段1.一旦这个阶段完成,应用程序返回阶段2。当关乎效率时应该在map::operator[]和map-insert之间仔细选择
熟悉非标准散列容器
迭代器
尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator
使用iterator取代const或reverse类型的迭代器理由:
insert和erase的一些版本要求iterator。如果需要调用这些函数,就必须产生iterator,而不能用const或reverse iterators
不可能把const_iterator隐式转换成iterator,从一个const_iterator产生一个iterator的技术并不普遍适用,而且不保证高效。
从reverse_iterator转换而来的iterator在转换之后可能需要响应的调整。用distance和advance把const_iterator转化成iterator
了解如何通过reverse_iterator的base得到iterator
需要一个一个字符输入时考虑使用istreambuf_iterator
算法
确保目标区间足够大
了解你的选择排序
排序选择总结如下:
如果需要在vector、string、deque或数组上进行完全排序,可以使用sort或stable_sort
如果有一个vector、string、deque或数组,你只需要排序前n个元素,应该用partial_sort
如果有一个vector、string、deque或数组,需要鉴别出第n个元素或需要鉴别出前n个元素,而且不用知道他们的顺序,nth_element是应该注意和调用的
如果需要把标准序列容器的元素或数组分割为满足和不满足某个标准,大概就要找partition或stable_partition
如果数据是在list中,可以直接使用partition和stable_partition,可以使用list的sort来代替sort和stable_sort。如果需要partial_sort或nth_element提供的效果,就必须间接完成这个任务,会有很多选择。
排序算法需要更少资源(时间和空间)排序如下:
1)partition 2)stable_partition 3)nth_element 4)partitional_sort 5)sort 6)stable_sort如果真的想删除东西的话就在类似remove的算法后接上erase
提防在指针的容器上使用类似remvoe的算法(不注意可能导致内存泄露)
注意哪个算法需要有序区间
只能操作有序数据的算法表如下:
1)binary_search 2)lower_bound 3)upper_bound 4)equal_range 5)set_union 7)set_intersection 8)set_difference 9)set_symmetric_difference 10)merge 11)inplace_merge 12)includes
下面两个算法一般用于有序区间:
1)unique 2)unique_copy通过mismatch或lexicographical比较实现简单的忽略大小写字符串比较
了解copy_if的正确实现
STL中有11个名字带copy的算法:
1)copy 2)copy_backward 3)replace_copy 4)revese_copy 5)replace_copy_if 6)unique_copy 7)remove_copy 8)rotate_copy 9)remove_copy_if 10)partial_sort_copy 11)unitialized_copy用accmulate或for_each来统计区间
仿函数、仿函数类、函数等
把仿函数类设计为用于值传递
用纯函数做判断式
使仿函数类可适配
了解使用ptr_fun、mem_fun和mem_fun_ref的原因
确定less表示operator<
使用STL编程
尽量用算法调用代替手写循环
理由:
效率:算法通常比程序员产生的循环更高效
正确性:写循环时比调用算法更容易产生错误
可维护性:算法通常使用代码比相应的显示循环更干净、更直观。尽量用成员函数代替同名的算法
理由:
成员函数更快
比起算法来,成员函数与容器结合得更好(尤其是关联容器)注意count、find、binary_search、lower_bound、upper_bound和equal_range的区别
考虑使用函数对象代替函数作算法的参数
避免产生只写代码
总是#include适当的头文件
头文件对应总结:
几乎所有的容器都在同名的头文件里,如vector在中声明,list在中声明,声明了set和multiset,学习破解有关STL的编译器诊断信息
让自己熟悉有关STL的网站
参考文献
Effective STL