容器使用的12条军规——《Effective+STL中文版》试读
还记的自己早年在学校学习c++的时候,老师根本就没有讲STL,导致了自己后来跟人说 起会C++的时候总是被鄙视,后来就下定决心一定要搞定STL。但是说实话,后来学了STL之后,我还是没有能够把它运用好,有的时候觉得STL太好了, 太强大了,大大减少了我编程的难度,但是另一方面,STL又有些复杂,自己还不能够确定哪里会产生错误以及错误的原因,这些陷进导致自己运用的时候总是蹑手蹑脚,只用到了STL比较基础的一些功能(用容器放一些数据,用到一点儿基础的如find,sort之类的算法),不能充分发挥STL的能力,不能运用自如。 这就好比金毛狮王费了很大劲抢得了屠龙刀,但是他却不懂怎么发挥屠龙刀的真正威力。本书的试读,仿佛让我看到了一线希望,因为本书的体例是以一种类似于条款的方式来指导STL应用实践的,作者给我们揭示的是实实在在地在STL应用方面的专家级经验技巧和to do or not to do的规则。 (应该与不应该)
第一章阅读笔记
1.容器使用关注点
基本上书中所有写到的内容都是从这些方面出发来分析,我们在遇到非书中提到的情况时,可以基于以上这些关注点来具体展开分析。
2.基础知识点说明
(1)(STL提供的)容器有: 序列容器:标准(vector、string、deque、list)和非标准(slist、rope);
关联容器:标准(set、multiset、map、multimap)和非标准(hash_set、hash_map、hash_multiset、hash_multimap)
就存储方式而言,其中vector、string、deque、rope是连续内存的,其余表示链表的(list、slist)和关联容器是基于节点的。
(2)标准非STL容器:数组、bitset、valarray、stack、queue、priority_queue
(3)STL提供的迭代器按所支持的操作分类:输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器
(4)STL提供计算复杂性保证,用n表示元素个数,所提供操作的快慢程度:n足够大时,线性>对数>常数(>表示快于);相对较小的n时,理论上复杂的操作比理论上简单的操作性能好。
(5)容器中数据布局和C兼容的只有vector;string和rope是基于引用计数的;只有list对多元素的插入提供事务语义;只有string在swap过程中会导致迭代器、指针和引用变为无效;deque是唯一的迭代器可能无效而指针和引用不会变为无效的标准容器。
3.实际经验使用条款
(1)不要编写独立于容器的代码,虽然泛型很好,但是还达不到这样的抽象程度。因为如果要编写独立于容器的代码,那么必然只能实现所有容器所提供的功能的交集,而这个交集的功能很少,这样做没有意义。
但是可以对容器提供封装,用typedef或类来实现,使改变容器类型(更换容器)的操作变得容易。
(2)不符合标准的代码,通过了编译将更危险。
(4)避免使用vector
(3)STL的工作方式是基于拷贝的。这种方式通过拷贝构造函数和拷贝赋值操作符来实习。因此,向基类对象的容器中插入派生类对象几乎总是错误的,这回导致派生类对象的特化特性被“剥离”,避免这种现象而利用多态性的选择是智能指针。
(4)检查容器为空时用empty()而不是size()==0,因为empty总是线性时间的,size()对于list而言则需要有所取舍,所以可能不同的STL平台实现不同,有的会是线性时间,有的会是常数时间。
(5)使用时区间成员函数优先于对应的单元素成员函数。此时进行比较的关注点有:编码风格(代码长度(减少敲击)、表达意思的清晰度)和效率(调用内存分配子分配内存的次数、拷贝对象(对象移动)频繁程度、冗余操作、函数调用次数)。支持区间的成员函数:
①区间创建:container::container(InputIteratorbegin,InputIteratorend); 适用范围:所有标准容器
②区间插入:void container::insert(iteratorposition,InputIteratorbegin,InputIteratorend); 适用范围:所有标准序列容器
void container::insert(InputIterator begin,InputIteratorend); 适用范围:所有标准关联容器
③区间删除:iteratorcontainer::erase(iteratorbegin,iteratorend);适用范围:所有标准序列容器
voidcontainer::erase(iteratorbegin,iteratorend); 适用范围:所有标准关联容器
④区间赋值:void container::assign(InputIteratorbegin,InputIteratorend); 适用范围:所有标准容器
功能弱一些的:operator=用于把一个容器拷贝到相同类型的另一个容器 适用范围:所有标准容器
(6)由于函数类型的存在,所以要小心语法分析可能带来的问题。如:
list
这个定义可能会被编译器理解为一个data函数的声明,正确的做法是:
list
但并不是所有编译器都支持,所以为了对所有编译器都没有二义性,更好的方式是在对data 的声明中避免使用匿名的istream_iterator 对象,如下:
1. ifstream dataFile("ints.dat");
2. istream_iterator<int> dataBegin(dataFile);
3. istream_iterator<int> dataEnd;
4. list<int> data(dataBegin,dataEnd);
(7)容器不是万能的,不能知道程序员真正做了什么,所以,如果容器中包含了通过 new操作创建的指针,切记在容器对象析构前将指针delete掉。可以通过创建函数对象来完成这项工作(两种不同模板化程度的实现提供不同的特性),但是不能够做到异常安全,所以建议采用的是智能指针。
(8)介于auto_ptr“复制一个auto_ptr意味着改变它的值”的特殊特性,避免创建包含auto_ptr的容器对象,auto_ptr不是智能指针!
(9)删除元素的方法,其中要考虑到迭代器实现啊:
(10)为了满足标准同一类型的分配子是等价的的条件,分配子(allocator)对象不可以有状态,这意味着可移植的分配子不可以有任何非静态的数据成员,至少不能有会影响其行为的数据成员;如果要编写分配子并且将它们同标准容器一起使用,分配子必须提供rebind模板,因为标准容器假定它是存在的;编写分配子时,传给分配子的allocate 成员函数的是那些要求内存的对象的个数,并不是类似于编写operator new 那样的字节数。简单的说,分配子的作用就是帮助我们自己定义内存管理的策略。
(11)在编程时,只能期望STL提供的操作具有线程安全性而不能依赖。所以,如果想要实现线程安全,可以自己动手通过获得互斥量(getMutexFor)和释放互斥量(releaseMutexFor)的方法来实现。考虑到防止忘记调用releaseMutexFor的可能性,好的解决方案是使用一个lock类来封装,构造函数获得互斥量,析构函数释放互斥量,只要在需要互斥访问同步的地方定义一个lock对象就可以了。
试读感受
整个第一章读下来,其实不是那么顺利,需要投入时间来“啃”。虽然作者所讲的内容都是实际的经验和技术,但是作者讲解的是非常深入的,所以正如作者书中提示的那样,需要停下来,瞪大眼睛看,仔细思考才能够真正理解背后的原理,只有这样也许在未来的应用中才能运用的得心应手。这本书的写作风格非常严谨,比如作者在引言中所说的书中所要用到的不是通用概念的STL平台,一些特殊样式所表达的意思,一些STL相关的书中出现的表达方式都给出说明,单从这方面而言就已经是专家级水准了。从内容而言,这本书所讲绝不仅仅是告诉读者怎么做,作者还会向我们一步步阐明这样做背后的原因,从多个角度去分析各种实现手段的优劣,步步深入地从简单的能够达到任务的方法到最好的方案向我们展示整个分析过程和分析方法,这一整套的思维体系和框架值得每个程序设计人员去学习和应用。本书的体例是条款式的,这样的好处就是虽然本书有些地方讲解的比较深奥,但是各个条款的内容又不是太多,所以可以让读者无形中增加读下去的信心而不轻易放弃!总之,这是一本精心编排的重量级的经验技巧书籍,每一个希望用好STL的人都应该人手必备,每个程序员都该学习其中的思考方法。当然,我也希望能够通篇的研读!
——L0mio
【美】梅耶 (Meyers,S.) 著
潘爱民 陈铭 邹开红 译.
电子工业出版社 出版