[置顶] Effective STL 学习笔记

看Effective STL 作的一些笔记,希望对各位有帮助。

以下是50条条款及相关解释。

容器

1. 慎重选择容器类型,根据需要选择高效的容器类型。

2. 不要试图编写独立于容器类型的代码。

3. 确定容器中的对象拷贝正确而高效。也就是防止在存在继承关系时发生剥离。

4. 调用empty而不是检查size()是否为0来判断容器是否为空。原因是调用empty比检查size()更加高效。

5. 尽量使用区间成员,而不是多次使用与之对应的单元素成员函数,原因是这样更加高效。如尽量使用vector的assigninsert成员函数,而不是一直使用push_back

6. 小心C++编译器最烦人的分析机制。如 下面的代码中的第二句被C++解释成为了函数声明,这很奇怪但又符合标准。

ifstream dataFile ("ints.dat")

list <int> data( istream_iterator<int>(dataFile), istream_iterator<int>() ); // 被解释成为函数声明

正确的写法是这样的,注意第一参数两边的括号:

list <int> data( istream_iterator<int>(dataFile), istream_iterator<int>() );

这是因为C++总是尽可能的将语句解释为函数声明。

7. 如果在容器中包含了能过new操作创建的指针,切记在容器对象析构前将指针delete掉。容器没有这么智能,自动帮你释放new出的资源,使用智能指针如shared_ptr(C++11)是个比较好的选择。

8. 切勿创建包含auto_ptr的容器对象。auto_ptr不是采用引用计数实现的,放进容器中就等着出问题吧,特别是当对容器使用sort这类会对元素进行赋值的算法之后。

9. 慎重选择删除元素的方法。这一点很重要,因为在删除一个元素之后会导致一些迭代器、指针及引用失效。所以如果手写循环删除元素一定要小心。最好的方法是vector、string、deque采用erase-remove这样的习惯写法,方便又安全,list应当使用成员函数remove,关联容器使用成员函数erase。

要小心的一点是算法remove并不是真正的删除容器中的元素,因为它没有这个能力,它只能将无用的元素移去末尾。之后你必须用成员函数erase删除无用区间。

10. 了解分配子(allocator)的约定和限制。(这一条和下一条感觉应该用的很少,而且有点复杂,不作解释)

11. 理解自定义分配子合理用法。

12. 切勿对STL容器的线程安全性有不切实际的依赖。请手工保证线程安全,不要依赖于STL。

vector和string

13. 尽量用vector和string代替动态分配的数组。方便而且不用担心内存泄漏。

14. 使用reserve来避免不必要的内存重新分配。成员函数reserve可以为容器预留一块内存,当容器大小(size)增大时,不用重新分配内在,可以提高效率。需要缩减分配的内存时可以使用后面提到的swap技术,或者使用shrink_to_fit(C++11)。

15. 注意string实现的多样性。string的实现可以有很多种,C++并没有规定其实现细节。

16. 了解如何把vector和string数据传给旧的C API。vector请使用 &vector[0],当然先要确定容器不为空。string使用c_str()方法。

17. 使用“swap 技巧”除去多余的容量。其实很简单,如下面的例子。

class Contestant { ... };

vector <Contestant> contestants;

....

vector <Contestant> (contestants).swap(contestants);

关键是拷贝构造一个原先容器的临时变量,然后调用swap方法。

18. 避免使用vector<bool>。因为它不是容器,它是用位来存储bool值的,所以它也没有迭代器,不能对其使用标准算法。最好使用deque <bool> 或bitset代替之。

关联容器

19. 理解相等(equality)和等价(equivalence)的区别。在实际中相等的概念是基于 operator== 的。find对“相同”的定义是相等,是以 operator==为基础的。set::instert对“相同”的定义是等价,是以operator< 为基础的。

如果按照关联容器c的排列顺序,每个都不在另一个的前面,那么称两个对象按照c的排列序列有等价的值。

一般情况下,关联容器的情况下,如果下面的表达式返回true,则按照关联容器c的排序准则,两个对象x和y有等价的值。

!c.key_comp() (x, y) && !c.key_com() (y, x)

20. 为包含指针的关联容器指定比较类型。默认情况的比较类型是 less<T>,所以只会比较指针的值,而不是指针指向的值,所以需要手工指定比较类型,以比较指针指向的值。

21. 总是让比较函数在等值情况下返回false。这一点很重要,特别是在关联容器基于比较函数判断等价的情况下。否则会导致相同的值按照定义是不等价的。

22. 切勿直接修改set或multiset中的键。会破坏该数据结构。而map中的键被const修饰,不能更改。

23. 考虑用排序的vector替代关联容器。只有对性能要求很高而且在特定的环境下才可以考虑使用,一般情况下考虑就是浪费时间。

24. 当效率至关重要时,请在map::operator[]map::insert之间做出谨慎选择。选择的标准是:当向map中添加元素时,要优先选择insert; 当更新已经在map中的元素的值时,要优先选择operator[]

25. 熟悉非标准的散列容器。C++11标准已经添加标准散列容器:unordered_mapunordered_multimapunordered_setunordered_multiset

迭代器

26. 尽量使用iterator,而不是const_iterator、reverse_iterator及const_reverse_iterator。使用iterator会减少很多麻烦,所以使用STL时尽量使用iterator。

27. 使用distanceadvance将容器的const_iterator转换成iterator。首先明确的是不能使用const_cast进行转换,因为这两个根本就是两个完全不相关的类,比string和complex<double>之间的关系还远。可以使用下面的技术转换,i是iterator类型,ci是const_iterator类型:

typedef IntDeque::const_iterator ConstIter;

advance(i, distance<ConstIter>(i, ci));   // <ConstIter> 不能少,否则编译通不过

28. 正确理解由reverse_iteratorbase()成员函数所产生的iterator的用法。向右有一个位置的偏移,保证了对于插入操作而言,ri和ri.base()是等价的。对于删除而言则不是等价的,要先++ri,如v.erase((++ri).base());

29. 对非格式化的逐个字符的输入考虑使用 istreambuf_iteratoristream_iterator效率更高而且更方便(不用清ios::skipws标志)。相对的还有ostreambuf_iterator

算法

30. 确保目标区间足够大。最容易犯的错误是这样:

transform(values.begin(), values.end(), results.end(), transmogrify);

可以更改为

transform(values.begin(), values.end(), back_inserter(results), transmogrify);

应该使用插入迭代器。插入迭代器有 inserterback_inserterfront_inserterostream_iterator

31. 了解各种与排序有关的选择。STL提供了很多优质的排序算法,可以满足不同的需求。

非稳定的有(都要求随机访问迭代器 RandomAccessIterator ):

sort:基于快排的排序

partial_sort:部分排序,指定排序区间

nth_element:将排序后位于前n个位置的元素放在前n个位置,但不对它们进行排序。

稳定的有:

stable_sort:稳定排序,排序后等价元素的相对位置不会变

list::sort

分割序列的算法(只需要双向迭代器):

partition

stable_partition:不改变相同元素顺序

32. 如果确实需要删除元素,则需要在remove这一类算法之后调用成员函数erase。算法remove不可能真正删除元素,因为它没有这个能力,必须调用类似erase这样的成员函数才行,list的成员remove可以真正删除元素。类似remove这样的算法有remove_ifunique

33. 对包含指针的容器使用remove这一类算法时要特别小心。因为被“删除“的值可能也不存在于序列最后面,而是被覆盖了。与remove算法的实现有关。

34. 了解哪些算法要求使用排序区间作为参数。如下:

binary_searchlower_bound

upper_boundequal_range

set_unionset_intersection

set_differenceset_symmetric_difference

mergeinplace_merge

includes

下面两个并不一定要求排序区间,但通常情况会与排序区间一起用:

uniqueunique_copy

35. 通过mismatchlexicographical_compare实现简单忽略大小写的字符串比较。主要还是介绍这两个算法的用法。

36. 理解copy_if算法的正确实现。标准算法中没有copy_if。使用while循环实现即可。如下:

template< typename Inputlterator, // a correct
typename Outputlterator, // implementation of
typename Predicate> //copy_if
Outputlterator copy_if( Inputlterator begin,
Inputlterator end,
Outputlterator destBegin,
Predicate p)
{
while (begin != end) {
if (p(*begin)) *destBegin++ = *begin;
++begin;
}
return destBegin;
}

37. 使用accumulate或者for_each进行区间统计。accumulate是4个位于<numeric>头文件中算法之一,其余三个分别为:adjacent_differenceinner_productpartial_sum。C++11新增了一个itoa

函数子、函数子类、函数及其他

38. 遵循按值传递的原则来设计函数子类。在STL中,函数对象往往会按值传递和返回,所以在设计函数对象时要保证经过了值传递之后还能正常工作。

39. 确保判别式(predicate)是”纯函数(pure function)“。“纯函数”是指返回值仅仅依赖于其参数的函数。

40. 若一个类是函数子,则就使它可配接(adaptable)。为了使函数对象可配接我们要以 unary_functionbinary_function作为函数子的基类。因为它们提供了函数对象配接器所需要的类型定义。通过简单的继承,我们就产生了可配接的函数对象。如下:

struct WidgetNameCompare:
std::binary_function<Widget, Widget, bool> {
bool operator()(cost Widget& lhs, const Widget& rhs) const;
}

unary_function有两个参数,第一个是 operator() 函数参数,第二个是其返回值。 binary_function类似,有三个参数,前两个是operator()函数参数,第三个是其返回值。

41. 理解ptr_funmem_funmem_fun_ref的来由。用来配接函数指针及成员函数。

42. 确保 less<T>与operator< 具有相同的语义。一般情况下不要特化less<T>让less做别的事情。operator< 是less默认的实现方式,也是程序员期望less所做的事情。让less不调用operator< 而去做别的事情,会无端违背程序员的意愿。要需要改变less的情况下只需要创建一个函数子类让这个函数子类完成期望的操作就可以了。

在程序中使用STL

43.  算法调用优先于手写的循环。能调用算法完成操作的情况下不要手写循环,调用算法效率高、正确性高、可维护性好。如使用for_eachtransformcopy之类的算法代替循环。适当的情况下配合绑定器使用:bind1stbind2nd,这两个配接器位于<functional>头文件中。

44. 容器的成员函数优于同名的算法。因为成员函数更高效,成员函数可以根据特定的容器及实现作出优化。

45. 正确区分 countfindbinary_searchlower_boundupper_boundequal_range根据需要选择最高效的算法。下图总结其常用用法。

[置顶] Effective STL 学习笔记

46. 考虑使用函数对象而不是函数作为STL算法参数。一个事实是STL的sort比C语言标准中的qsort更快。原因就是sort使用函数对象,而qsort使用函数指针作为参数。如果一个函数对象的operator()已经被声明为内联的,那么其在函数体内也是内联的,但对于函数指针就不一样了,通过函数指针传递的函数不大可能会被优化成内联的,即使该函数被声明为内联的。

47. 避免产生“直写型(write-only)”代码。"直写型"代码复杂而难以理解,不便于维护。”直写型“代码就是那种一句话完成n多功能的代码,晦涩难懂。最好将”直写型“代码分解为易读的代码。

48. 总是包含(#include)正确的头文件。有些编译器的头文件会相互包含,不过最好不要依赖这种相互包含的关系,这种依赖不利于程序的移植性,每次都记住包含正确的头文件。与STL 有关的头文件除各容器定义的头文件外,还有 <algorithm> 、<numeric>、<iterator> <functional>。


49. 学会分析与STL相关的编译器诊断信息。不同的STL平台给出的诊断信息一般都不相同,不过关于STL的诊断信息一般都比较复杂,而且出错位置大多不会定位在出错源码上,可以借助文本编译工具处理相关的出错信息,这也需要一定的经验才能准确定位出错位置及原因。

50. 熟悉与STL相关的Web站点。

上一篇:《C++编程规范:101条规则、准则与最佳实践》学习笔记


下一篇:Python基本数据类型之str