Effective STL学习总结
Author: Milo.Wang
Email: [email protected]
PDF版下载地址:点此进入下载页面
STL真的很棒!它将程序员常用的数据结构和算法以独立于类型的方式提供给程序员使用。程序员无需在关心怎样实现一个堆栈,队列,无需在关心动态分配的数组是否已经正确释放。如果加上散列表和智能指针就基本完美了,这个可以借助boost库来完成,至少在新的STL版本出来之前,这是很好的解决方案了。个人觉得得心应手的使用STL最好具备以下知识:
熟悉基本的数据结构和常见算法,推荐《算法导论》
熟悉C语言及其编译器识别特性,推荐《C专家编程》《C陷阱与缺陷》
熟悉C++基本语言及泛型编程,推荐《C++Primer》
熟悉STL的基本概念与常用类型、算法,推荐《C++标准程序库》。
在以上基础上看EffectiveSTL效率会更高!个人之见,如不苟同,勿喷,谢谢!
以下内容为本人读EffectiveSTL的个人总结,本希望自己今后重温,但郑重其事的写了下来,就尽量写得希望能给看这10多页文档的人一些帮助。如能达此目的,不胜欣喜!当然,我必须强调,这篇文档更适合看过EffectiveSTL的人。
第一部分容器(泛型数据结构->自己起的名字O(∩_∩)O~)
容器包括:序列容器和关联容器
序列容器:string、vector、deque、list
关联容器:set、multiset、map、multimap
备注:stack、queue、priority_queue是容器适配器,不再讨论之内。
问题1、类型歧义问题
如下图所示,在使用模版的情况下,C::const_iterator有些编译器会解析为类C的成员变量,这样就不能通过编译,通过显示使用typename可以去除歧意,而且使用typename不会有副作用,顶多多打一个单词。所以,只要用到类内定义的地方都使用typename关键字是个好主意。
以下为小知识点:(小知识点随即插入,与某个专题无关)
根据容器特性,和场景选择合适的容器,会明显提高效率。
Stl容器使得类型无关性得以可能,但试图进一步泛化,达到容器无关行是不可能的,因为不同的容器有不同的接口,和使用场景,使用方式。
问题2、智能指针取代传统指针
Boost提供了智能指针类,shared_ptr,尽量使用智能指针代替普通指针,会极大降低内存泄露问题发生的几率,什么是智能指针这里不多说了。《C++Primer》中有,很多书中也有。
为了降低容器拷贝对象消耗过多性能,采用容器存对象的智能指针,而不是对象拷贝,这里要注意以下内容:
通过智能指针可以使得容器的拷贝操作高效快速,但在拷贝时必须考虑到指针实际指的对象,而不是指针本身,尤其要注意使用算法时要自己写函数对象,使得比较的不是指针本身而是指针指向的对象。
小知识点:
容器的empty成员函数代替size成员函数检查容器是否为空,效率更高。
问题3、容器的区间成员函数效率高
关键条款:5:尽量使用区间成员函数代替它们的单元素兄弟(原书中的条款,个人感觉重要的我会直接标注是第几条)
利用容器的区间操作成员函数,比利用单元素的成员函数效率高。尤其是区间构造函数,区间赋值函数,区间删除、插入函数。(对于vector减少了重分配内容、移动元素的次数,对于list,减少了调整指针的次数)
区间构造函数
区间插入函数
区间删除函数
区间赋值函数
问题4、C++的语法解析
关键条款:6:警惕c++令人恼怒的解析
C++很多时候会把一些复杂的、长的变量声明,函数参数声明解析成函数。有时通过使用typename或者加()括号的方式可以解决。C++解析方式可以参考《c专家编程》《c陷阱和缺陷》。如下:
Ifstream infile(“hello.txt”);
Listl(istreambuf_iterator<char>(infile),istreambuf_iterator<char>());
会被解析为一个函数声明,其中第一个参数的括号()被忽略。如果要保持本意可以如下使用:
List l( (istreambuf_iterator<char>(infile)),istreambuf_iterator<char>());
没看懂上边写的直接看下边的截图吧。
在第一个参数上加括号(),使得解析器无法将其解析为一个函数参数即可,去除歧义。
以下为错误方式:(虚线框处会被解析为一个参数)
以下为正确方式:(加括号,解除歧义)
分开写不会产生歧义,作者是建议以下写法的,编译不熟悉STL的人轻松读懂你的代码。
问题5、容器的元素删除操作因容器不同而有所不同
关键条款:9:在删除选项中仔细选择
合理选择删除算法或容器的删除成员函数。
1、去除一个容器中有特定值的所有对象:
如果容器是vector、string或deque,使用erase-remove惯用法。
Remove是STL算法库中的算法,不是成员函数,它不真的删除元素,而是用容器后边的元素覆盖要删除的元素,最后返回容器新的逻辑终点,而调用容器本身的end函数返回的还是原先的终点,不信,你自己试试。容器的erase成员真的删除元素。
如果容器是list,使用list::remove,注意list的成员函数remove真的删除了元素。
如果容器是标准关联容器,使用它的erase成员函数,这里也真的删除了元素。
2、去除一个容器中满足特定判定式的元素
如果是vector、string或deque,使用erase-remove-if惯用法。
如果是list,使用list::remove_if。
如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环遍历容器元素,当你把迭代器传给erase时记的后置递增它。这样保证操作的是有效的迭代器。
3、在循环内做某些事情(除了删除对象之外):
如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase是记得都用它的返回值更新你的迭代器。
如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase是记得后置递增它。
问题6、最常用、最重要的序列容器vector和string
尽量用vector和string代替动态分配数组
使用reserve提前预留vector的空间,避免不必要的预分配
这相当于int arr[100] == vector<int> v; v.reserve(100);
String实现具有多样性,很多是基于引用计数实现的,也有不是基于引用计数的。所以sizeof(string)因实现不同而不同。
关键条款:16:如何将vector和string的数据传给遗留的C API
Vector v;
&v[0]可以传给需要数组的API,结合v.size实际就是传递数组首地址和数组元素个数。
String s;
S.c_str()可以传给需要c风格字符串的API,传递C风格字符串。
关键条款:17:使用“交换技术”来休整过剩容量
利用swap成员函数可以实现将过剩容量减少。Vector在装了1000个元素后,其容量是1000(注意,我说的是容量,不是大小尺寸),及时清空其中存放的元素,容量仍然不变。
采用以下方法可以将容量减到和容器所装的元素个数一样大。
小知识点:
Vector<bool>不是容器类型,每个bool只占一个位,内部用代理类实现,取某个元素地址获得的是代理类的地址。可以用deque<bool>或bitset代替,deque<bool>内部装的是用一个自己表示的bool,它是容器,bitset是位集合,提供丰富的位操作。
模版化函数对象内部的调用操作符,在使用函数对象时,不用显示指出对象类型,实现指针容器的多态。
问题7、学STL必须区别相等和等价。
相等:关于operator==两元素返回真,即 a == b;
等价:满足!(a op b)&&!(b op a) == true成立,即,对于操作符op,
a op b与 b op a都不成立。其实这是离散数学中的知识点,不清楚,可以回去复习一下。
关键条款:21:永远让比较函数对相等的值返回false
基于等价关系的关联容器,排序算法等,其比较函数不能有“等于的成分”,小于等于不行,大于等于也不行,只能是小于或者大于。如下图例示:
等价关系绝对不能出现相等,>=是不对的。
关键条款:22:避免原地修改set和multiset的键
Const_cast对象的引用或对象的指针可以修改对象值,但对于内置类型可能有编译器优化,保证原常量值不变,而应用已经变了,其实现方式应该是变量对应内存已变,但其原先值拷贝到另一个地方。
Set和multiset的键一般不是常量,可以修改,但尽量不要修改,主要是不要修改用于排序的部分。为了可移植性,可以修改不用于排序的成分,可以使用const_cast<T &>()的方式。如上图所示。
Map和multimap要求键不可修改,键是const,但值可以修改。
小知识点:
Map::operator[]和map-insert,其实insert方式效率要高,因为前者需要先调用类的默认构造,再赋值。
重要的非标准散列容器:
Hash_set
Hash_multiset
Hash_map
Hash_multimap
第二部分迭代器和仿函数(函数对象)->容器和算法的接口
迭代器:行为类似于C++基本语言中的指针,根据不同容器包装成的类。
仿函数:内部定义了调用操作符的类,其行为类似于函数。
其中迭代器使得算法独立于容器,独立于数据结构;仿函数,使得算法的排序、关联容器的排序,已经判断性算法处理条件是带有状态,具有动态改变条件的功能。这是泛型编程的两把利器。
小知识点:
尽量用iterator代替const_iterator、reverse_iterator和const_reverse_iterator,因为前者对任何算法、容器成员函数都可使用,他可以转换为其他任何一个,反之则不然。
Iterator转换为reverse_iterator需要显示调用构造函数,因为一个参数的构造函数是exciplit的。
用distance和advance可以将const_iterator转换为iterator:
Iter it;
ConstIter cit;
Advance(it,distance<ConstIter>(v.begin(),cit));显示指定模版函数的类型。或者强制转换第一个参数的类型,如下图,显示指定函数模版参数。
删除反向迭代器当前元素的错误方法,如下图:
以上为错误代码,c++里函数返回的指针是个右值地址。
以上为正确做法。
通过reverse_iterator的base()函数可以获得相应的正向迭代器。位置是当前位置的下一位置。对于插入元素操作两者是等价的,对于删除元素操作,应该采用如下方式:
v.erase((++rit).base());
对比:v.erase(--rit.base());这样是不行的,对于vector迭代器是用指针实现的,直接返回的指针是常量,不能直接自减操作。
小知识点:
需要一个字符一个字符输入时用istreambuf_iterator代替istream_iterator,前者不用考虑流格式,不会跳过空格。
如下使用:
Ifstream infile(“hello.txt”);
Stringname(istreambuf_iterator<char>(infile),isteambuf_iterator<char>());
使用istreambuf_iterator比istream_iterator效率高。(注意加括号,避免解析错误)
上图为去空格的读入,以下两图为带空格的读入:
带空格读入
快速而高效的带空格输入
关键条款:38:把仿函数类设计为用于值传递。
无论是算法用到的仿函数还是普通函数,都是按值传递的,普通函数传递函数指针,仿函数传递仿函数对象。
判断式(predicate)是返回bool(或者其他可以隐式转化为bool的东西)。
纯函数是返回值只依赖于参数的函数。如果f是一个纯函数,x和y是对象,f(x, y)的返回值仅当x或y的值改变的时候才会改变。
一个判断式类是一个仿函数类,它的operator()函数是一个判断式,也就是,它的operator()返回true或false。
关键条款:39:用纯函数做判断式
由于stl的某些算法的限制,判断式类和判断式函数都应该是纯函数,即判断式类中不应该有状态,判断式函数中不用应该有局部静态函数。//对于局部静态函数这一点,我在VS下测试结果不是作者说的那样。
关键条款:40:使仿函数类可适配
一元可适配函数对象类
二元可适配函数对象类
每次写仿函数类都应该继承自unary_function或binary_function,它们使得仿函数类具有可适配性。就是对于函数适配器not1、not2、bind1st、bind2nd只能使用继承自上边两个类模版的仿函数对象。继承使得仿函数类里边多了一些typedef。这是适配函数需要的,非标准的仿函数也多半有这个要求。所以,一般写仿函数都使其可适配。
关键条款:41:了解使用ptr_fun、mem_fun和mem_fun_ref的原因
Ptr_fun给一个普通函数加上typedef,使其可以适用于函数适配器;
Mem_fun用于算法中调用指针构成的容器中的指针所指对象的成员函数;
Mem_fun_ref用于算法中调用对象的副本构成的容器调用其成员函数。
第三部分算法
STL提供上百个泛型算法,这些算法都是常用的一些算法,掌握这些算法,可以有效提高编程效率,这些算法是高效,是计算机hacker编写的优质程序,他们充分考虑了效率问题。当然,如果,你本身是个效率框,hacker,你可以写自己的,但对于多数程序员,它们已经很好用了。
对于每个算法的细节,参看《C++标准程序库》
Transform等算法要求目标区间的大小至少和源区间的大小一样大。
常见的以下几种排序算法:list提供自己专用的sort成员函数。排序算法需要随机访问迭代器。不能用于关联容器。
Sort
Partial_sort
Stable_sort
Nth_element
Partition
Stable_partition
关键条款:32:如果你真的想删除东西的话就在类似remove的算法后接上erase
Erase和remove惯用法
v.erase(remove(v.begin(),v.end(),99),v.end());使用这种惯用法的还有remove_if和unique等。Remove不真的删除元素。需要调用容器的相关函数。因为算法没有容器的指针,无法调用其成员函数。对于list容器,remove真的删除元素,而且比erase和remove惯用法高效,关联容器有erase函数完成这项任务。
关键条款:33:提防在指针的容器上使用类似remove的算法
容器元素类型是指针的话很危险,使用不当容易内存泄露,尽量用智能指针代替,可以使用boost库中的shared_ptr智能指针模版。
基于有序区间的算法:
Binary_seach
Lower_bound
Upper_bound
Equal_range
Merge
Set_union
Set_intersection
Set_difference
Set_symmectric_difference
Unique
Unique_copy
Mistmach和lexicographical_compare容器比较函数
Accumulate and for_each算法
Copy_if算法的正确实现
第四部分杂项
STL相关资源:
SGI STL:http://www.sgi.com/tech/stl
提供了完整的文档说明。
提供hash_set/hash_multiset/hash_map/hash_multimap。
提供单链表slist。
提供超长字符串rope。
提供多个非标准的函数对象和适配器。
STLport:http://www.stlport.org
提供20多个平台的可移植性,基于sgi stl。
提供调试模式,可以调试很多在标准库中未定义的行为。
Boost:http://www.boost.org
提供智能指针类。
提供很多函数对象。
关于STL的编译器错误信息,往往很长,因为其提供的是代码错误行的库内部实现形式。
内部采用很多typedef。所以显示的内容很多。通过将内部形式替换为用户可见的形式,可以缩短错误信息。这个需要经验积累。
Stl算法与数据结构所在头文件,及头文件相互包含的情况,在不同的stl实现版本上可能不同,所以要在不同的编译平台上包含适当的头文件。
使用stl算法嵌套层次不要太高,把一个深层嵌套stl算法的语句拆为多个语句,便于不谙熟stl的程序员阅读。
关键条款:46:考虑使用函数对象代替函数作算法的参数。
使用stl算法时,尽量用函数对象代替普通函数。最主要的是效率原因,函数对象使得stl内联调用操作符的函数体,而普通函数,即便是显示声明成内联函数,通过传递函数指针给stl算法,从而抑制了内联行为,所以,对每个元素,stl算法如果传递普通函数,都是一个元素调用一次函数,而函数对象通过内联,没有提供函数调用的性能损耗!所以stl的sort算法比c的qsort快很多倍。其他用函数对象代替普通函数的原因多半与移植性和编译器特性有关。总之,尽量用函数对象代替普通函数。
==操作符定义相等,!comp(x,y) &&!comp(y,x)表达式为真,定义等价关系。关联容器内部排序准则采用等价关系。
关键条款:45:注意count findbinary_search lower_bound upper_bound和equal_range的区别。
关键条款:19:了解相等和等价的区别
Count和find算法不要求容器中元素有序,是线性时间算法;binary_search、lower_bound、upper_bound和equal_range要求容器有序,他们是对数时间算法。这一般说的是作用在顺序容器上,对于关联容器,尽量使用其自身的成员函数兄弟。
Stl中涉及到容器内元素排序的比较函数都是利用的等价而不是相等。
尽量用同名的成员函数代替同名的算法。同名成员函数效率更高
尽量用算法调用代替手写循环,当算法调用需要的仿函数明显复杂时,改用手写循环。
写完了,有部分内容重复,本来是安排问题编号的,可后来没注上,时间紧迫,不好意思了,13也得总结,希望对学习STL的朋友有帮助!
Milo