Effective STL 笔记

51 篇文章 1 订阅

虽然用C++很久了,但STL我一直都处于初级阶段,从书上的示例代码断章取义地用,感觉非常不顺手。所以我需要一本讲STL很深,但又不太深(我指的是涉及源码分析)的书,这本无疑是最好的。

第1条:慎重选择容器类型
             STL容器的分类远比我想像中的多。别人意外的是stack,queue等不是STL容器,但这不是这章的重点。在多数应用中,只有可能有一种容器供符合你的要求,尽管我觉得这些容器的操作上雷同。
             ■ 你是否需要在容器的任意位置插入新元素?如果是,就选择序列容器。
             ■ 你是否关心容器中的元素是如何排序的?如是不关心,哈希容器是一个可能的选择;否则,你要避免哈希容器。
             ■ 你需要哪种类型的迭代器?如果必须是随机访问迭代器,就只能限定的vector、deque和string。如果要求使用双向迭代器,就不能使用slist。
             ■ 当发生元素的插入或删除操作时,避免移动容器中的原来的元素是否很重要?如果是,就要避免连续内存的容器。
             ■ 容器中的数据布局是否需要和C兼容?如果是,只能选择vector。
             ■ 元素的查找速度是否是关键考虑因素?如果是,就要考虑哈希容器、排序的vector(二分查找)和标准关联容器。
              ■ 你是否介意容器内使用引用计数?如果是,就要避免使用string和rope,可以考虑vector<char>替代。
             ■ 对插入和删除操作,你需要事务语义(可以回滚)吗?如果需要,list将是你的最好选择。
             ■ 你需要使迭代器、指针和引用变为无效的次数最少吗?如果是,请使用基于节点的容器。
             ■ 如果序列容器的迭代器是随机访问类型,而且只要没有删除操作发生,且插入操作只发生在容器的末尾,则指向数据的指针和引用就不会变为无效,这样的容器是否对你有帮助?这是很特殊的情况,如果真的是这样的,deque是绝佳选择。

第2条:不要试图编写独立于容器类型的代码
             这是现在很流行的泛化(generalization)编程,但作者强调不要在这里使用。原因在于,每个容器所支持的操作不一样,就连erase、insert这样的操作在STL中也不统一。除非你的代码是给自己用的,否则不要使用,天知道那个程序员用的是什么容器!

第3条:确保容器中的对象拷贝正确而高效
             这一条是告诉我们,STL操作中,拷贝发生的频率是相当大的。指针是一个好的想法,但内存泄漏很麻烦。

第4条:调用empty而不是检查size()是否为0
             empty的存在不仅仅是为了少敲几个字符。

第5条:区间成员函数优先于与之对应的单元素成员函数
             如第3条所说,STL操作中,拷贝发生的频率是相当大的。如果用单元素一个一个地操作,这种性能上的损耗将会更多。下面是书中给出的一般区间操作。
            ■ 区间创建
                container::container(Inputiterator begin, Inputiterator end);
             ■ 区间插入
               void container::insert(iterator position, Inputiterator begin, Inputiterator end);
               关联容器不需要position
            ■ 区间删除
              iterator container::eraser(iterator begin, iterator end);
              关联容器有所不同
              void container::eraser(iterator begin, iterator end);
           ■ 区间赋值
              作者非常强调这个,因为很多程序员不使用它
              void container::assign(Inputiterator begin, inputiterator end);

第6条:当心C++编译器最烦人的分析机制
             int g(double ());编译器认为是,g是一个函数,返回值是int,参数是一个函数指针,返回值为double,该参数为空。int g(double d) = int g(double (d)) = int g(double),之所以STL这里要提到,是因为在声明时常常被误解。比如:
             ifstream dataFile("ints.dat");
              list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
             在VS2005中,测试中得到警告,说data被声明库函数,可能这个函数不能被调用。一个解决方法是加上括号以区分   list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());更好的方式是不使用匿名对象。C++的一条普遍规律是,尽可能地解释为函数声明。这真是一个让人恼火的问题。

第7条:如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉
             这个是常识。不过每次都要写for循环,实在很麻烦。书中提出用for_each代替,这正是我讨厌的。

第8条:切勿创建包含auto_ptr的容器对象
             这是很危险的,以至于C++标准都禁止它。

第9条:慎重选择删除元素的方法
             没有通用的删除方法。例如,从container<int> c;中删除所有1963的元素。如果你有一个连续的容器(vector,deque,string),最好的方法是erase-remove习惯用法
            c.erase(remove(c.begin(), c.end(), 1963), c.end()); //remove返回删除后的新c.end(),在后面还有一些数据是没有清除掉的(事实上remove没有删除元素,它把找到的元素用后面的填补上)
            对于list,remove更加有效
            c.remove(1963);
            对于关联容器,正确方法调用erase
            c.erase(1963);
            如果找到删除的值没这么简单,remove_if配合erase会派上用场。
            bool badValue(int );
             c.erase(remove_if(c.begin(), c.end(), badValue), c.end()); //连续内存型容器
             c.remove_if(badValue);    // list
            对于关联容器,直接写for循环会比较好,但很容易写出错误的代码。比如
           container<int>::iterator it = ...
           c.erase(it);       // 执行后it变库无效,很多程序员把这个it用到for循环中的++,立马出错!如果c是序列容器,它会返回下一个元素的迭代器,关联容器就没这么好运了。
          for(AssocCaotainer<int>::iterator it = c.begin(); it != c.end(); ) {
               if(badValue(*it)) {
                     c.erase(i++);
               }
                else ++i;
          }
          注意下面的
          for(SeqCaotainer<int>::iterator it = c.begin(); it != c.end(); ) {
               if(badValue(*it)) {
                     i = c.erase(i);   //这里绝对不要用c.erase(i++),因为像vector这类连续内存容器在插入或删除后,原来的指针或引用会无效!!!
               }
                else ++i;
          }

第10条:了解分配子(allocaltor)的约定和限定

第11条:理解自定义分配子的合理用法

第12条:切勿对STL容器的线程安全性有不切实际的依赖
               一般来说,STL不是线程安全的。

第13条:vector和string优先于动态分配的数组
               很明显,在各个方面,vector和string都表现得很优秀。但是可能是受到C的影响,至今还末用到这条,现在决定,以后凡是简单的动态数组,全用vector;如果是字符串,全用string。

第14条:使用reserve来避免不必要的重新分配
               相信这一点是很容易理解的。下面几个函数是vector和string特有的,会常常用到
               ■ capacity() 返回已经分配的内存可以容纳的元素
               ■ resize(Container::size_type n) 强迫容器改变到包含n个元素状态,如果n<capacity(),capacity()的值是不会减少。它的特点就是,执行后,调用size()就是返回n。
               ■ reserve(Container::size_type n) 强迫容器把它的容量变为至少是n。如果n小于capcity(),可能不会做什么事。

第15条:注意string实现的多样性
               string的实现中有引用计数,这一点恐怕你已经清楚了。但它的实现不同版本都不一样,直接的表现是sizeof(string)的值不同,即使相同它们的内存方案也可能不一样。通常来说,对程序员这些没什么影响。

第16条:了解如何把vector和string的数据传给旧的API
               因为它们是内存连续的容器,所以这点很好实现。
               对于vector,简单采用 &v[0] 就可以了;对于string,s.c_str()是唯一方法,不要用&s[0]的方法,因为string的实现多样性,而且很有可能string内部不保证空字符结尾。如果你介意c_str()带来的性能损失,用vector<char>代替string。

第17条:使用“swap技巧”除去多余的容量
               14条中,只有reserve可能有这个能力除去多余容量,但仅仅是可能。比如
                        vector<int>(v).swap(v);
                 vector<int>(v)声明一个隐式对象,有v初始化,然后再与v交换。请放心,原来的指针和引用都能工作。string也类似。string(s).swap(s);

第18条:避免使用vector<bool>
              你也许在怀疑,bool是内置类型,为什么不行?STL的实现者原先对bool做了压缩处理,每个元素只占2个bit,标准的是点8个bit,这样指针指向就出错了。总之C++标准委员会声明vector<bool>是一个失败 的产物,你就不要使用了。取而代之deque<bool>或bitset。

第19条:理解相等(equality)和等价(equivalence)的区别
               相等的概念是基于operator==;等价关系是以“的已排序的区间中对象值的相对顺序”为基础。在关联容器中,需要指定一个排序类(注意,不是函数),默认的比较函数是less<T>。对于w1和w2,等价的关系表示库 !(w1<w2)&&!(w2<w1)。如果人为故意,等价和相等不一样。
                STL中关联函数的比较函数只有一个,当然,一个足够了。

第20条:为包含指针的关联容器指定比较类型
               上面一条提到比较函数,通常有一个默认的比较函数。考虑set<string*> s,如果你向里面插入数据后,你会发现没有按照顺序输出。原因在于,s是这样声明的set<string*, less<string*> s,比较的是string*,结果输出是按指针地址的值来排序。所以,对于指针关联容器,我们要指定自己的排序类型
               struct StringPtrLess : pblic binary_function<const string*, const string*, bool>
                {
                    bool operator()(const string* ps1, const string* ps2) const
                    {
                          return *ps1<*ps2;
                    }
                 };
                set<string*, StringPtrLess> s;
                对于map,用法类似。

第21条:总是让比较函数在等值的情况下返回false
               如果把它代入到等价的关系式,这就明白了。

第22条:切勿直接修改set或multiset中的键
               键 = key。key是排序的依据,随意修改会打乱容器结构。

第23条:考虑用排序vector替代关联容器
               连续内存窗口有它的优势,调用一些排序算法和查找算法可以达到set和map的效果,不过大量元素插入和删除的代价太高了,只能用来查找。

第24条:当效率至关重要时,请在map::operator[]与map::insert之间谨慎选择
               产生这个误会的原因是,operator[]也可以达到insert的效果,但它的代价确实偏高,却常常不被查觉。本条的要点概括为:如果要更新一个已有的映射表元素,优先使用operator[];如果是添加一个新元素,最好还是insert。
              (也许你会觉得insert有些麻烦
                  typedef map<int, double> MyMap;
                 Map m;
                 m[1] = 1.0;            //       m.insert(Map::value_type(1, 1,0);
                 m[2] = 1.1;            //       m.insert(Map::value_type(1, 1,1);
                 ... ...)

第25条:熟透非标准的哈希容器
               哈希容器没包括在C++标准库中,据说是因为标准委员会为了赶时间。

第26条:iterator优于const_iterator、reverse_iterator以及const_reverse_iterator
               iterator可以直接转换到其它的iterator,反过来则不行。

第27条:使用distance和advance将容器的const_iterator转换成iterator
               代码上看去像
              const_itrator ci = ...
              iterator i(c.begin());
               advance(i, distance(i ,ci));
              如果能避免const_iterator,就没有这些事了。

第28条:正确理解由reverse_iterator的base()成员函数所生成的iterator用法
               base()返回的iterator指向的位置不是原来的位置,而是后面一个元素,这一点很迷惑人。

第29条:对于逐个字符的输入请考虑使用istreambuf_iterator
              如果纯粹读文件中的内容,不用格式化(如读出一个整数,路过空格等),首选istreambuf_iterator代替istream_iterator。当然,与之对应的还有ostreambuf_iterator替代ostream_iterator.

第30条:确保目标区间足够大
               如果你通过STL算法来向容器中增加元素,那么这是一个值得重视的问题。虽然STL能自动管理内存,但我们常常高估了它的能力。常见的例子是:
               int trans(int x);
              vector<int> v1, v2;
              ... ...
              transform(v1.begin(), v1.end(), v2.end(), trans); //将v1的值经过trans变换加到v2.end()上
              在STL算法中,迭代的样子是 v2.end()++。这时你就会明白,v2.end()后面没有可能内存,会访问非法地址。这时你在前面用 v2.reserve(v2.size()+v1.size()),很抱歉,又错了,原因在于后面的空间没有初始化,结果将未知。正确的方法是
              transform(v1.begin, v1.end(), back_insert(v2), trans);
             这样返回迭代器前v2.push_back()将被调用,这样插入就正确了。类似的还有front_insert, inserter等

第31条:了解各种与排序有关的选择
               STL提供的排序算法要比你自己写的好得多,何必自寻烦恼呢?共有6种可用的排序算法(按消耗资源由少至多排列),partition, stable_partition, nth_element, partial_sort, sort, stable_sort。并不是说,消耗少的就一定好,是因为它们实现的功能有限。在不同的场合请选择合适的算法。

第32条:如果确实要删除元素,则需要在remove这一类算法之后调用erase
              相信你已经知道vector经典的remove-erase组合,所有的内存连续容器都适用。remove算法没有删除元素的能力,因为它不可册通过一个迭代器判断出能否调用erase,即使标准的STL容器erase使用方式也不尽相同,因此,这个工作就交给用户完成。

第33条:对包含指针的容器使用remove这一类算法时要特别小心
              对于这类容器就不应该使用remove算法,手工写for循环将是更好的选择。如果你参照remove的实现,或许你可以把for循环加以改进。

第34条:了解哪些算法要求使用排序的区间作为参数
                需要的有 binary_search, lower_bound, upper_bound, equal_range, set_union, set_intersection, set_difference, set_symmetric_difference, merge, inplace_merge, includes. unique和unique_copy虽然没有强制使用,但它们确实需要工作的排序的容器中。另一个要注意的是,上面这些算法通常都需要一个比较函数,而且要与提供给排序的比较函数相一致。

第35条:通过mismatch或lexicographical_compare实现简单的忽略大小写的字符串比较
               这种程序,说难也难,说简单也简单。

第36条:理解copy_if算法的正确正确实现
               首先,标准STL中没有copy_if,但各大厂商的“好事者”几乎者实现了它。本书作者也加入到这个行列中了。

第37条:使用accumulate或者for_each进行区间统计
               accumulate绝不仅仅是将几个数加在一起求合那么简单,可以自定义运算子。相比之下,for_each稍显繁琐但更灵活。原因在于 accumulate返回的是size_type,而for_each返回Function。

第38条:遵循按值传递的原则来设计函数子类
               这一条我不太理解,我看到很多operator()按引用传递值。

第39条:确保判别式是“纯函数”
               有几个名词可以来解释一下
               ■ 判别式(predicate)返回bool类型的函数
               ■ 纯函数(pure fuction)指返回值仅与输入有关
               ■ 判别式类 一个函数子类,它的operator()函数是一个判别式
               问题根源在于:在一个算法中,你无法确定函数子被调用多少次。像for_each这类简单的算法调用次数就是迭代器的个数,但其它的较复杂的就不一定了,比如find_if就不一样。设计函数功能单一是一个重要的原则。

第40条:若一个类是函数子,则应使它可配接
               总共有4个配接器not1, not2, bind1st, bind2nd。如果你要使用它们,你必须确保函数子类从unary_function, binary_function等继承。或者使用ptr_fun, mem_fun等(这个用起来不太方便)。对于前一种方法,有一个使用特点
              1、对于无状态函数子类(指没有私有成员变量的),通常定义为struct;这是STL的风格,当然用class也完全一样。
              2、一般情况下,传递给unary_function或binary_function的非指针类型需要去掉const和引用(&)部分。(作者不愿意解释其中的原因,但我的编译器加上它们后也没错)
             struct Cmp: public std::binary_function<int, int, bool> {
                       bool operator()(const int& i, const int& j);
              }
             如果是指针,就要写成一致
             struct Cmp: public std::binary_function<const int*, const int*, bool> {
                       bool operator()(const int* i, const int* j);
              }
              其实写函数子也不是很麻烦

第41条:理解ptr_fun、mem_fun和mem_fun_ref的由来
               用一句话来说就是“函数调用方式的多样性,使STL必须规定一种方式来约束”(哦,估计下次我自己看到也无法理解)。在第40条中,我们看到函数子的声明是从其它类中继承过来的,如果不继承或者是内部成员函数,可能需要这几个适配器的支持,因此,在编译无法通过的时候,查查它们的用法也许有意想不到的收获。

第42条:确保less<T>与operator<具有相同的语义
               在排序容器中,有一个默认的比较函数子less<T>,见第20条。确保比较函数子可行性是很重要的,同时,为了避免出现混乱,less<T> (如果需要自己实现,就不能用less这个名字)与operator<不同有歧义。

第43条:算法调用优先于手写循环
               诚然,不论是简洁,速度,正确性,算法都比手写循环好,只是我原来一直都想避免写函数子,因为我不了解(我的记忆中只在一个项目中写了一个,仅此一个)。不过现在情况好多了,函数子,算法,适配器都不是难事了。

第44条:窗口的成员函数优先于同名算法
              比如说查找,对于set,可以使用成员函数find,也可以用算法find。但是很明显,成员函数对list知道得更多技术细节,就如同手写循环和算法一样。

第45条:正确区分count, find, binary_search, lower_bound, upper_bound和equal_range
               确切地说,它们是不同的算法,作用也不同,关键在于,有时候我们可以在它们中间任选一个来解决问题,我是说在某些方面,它们的共同点。现在我还记得它们的特点,也许以后还有印象。

第46条:考虑使用函数对象而不是函数作为STL算法的参数
                STL中的sort算法比qsort快,这一点确实让人吃惊。这样你就会喜欢函数对象了。

第47条:避免产生“直写型”(write-only)的代码
               直写型 —— 直接写所有函数。这是编程风格问题,意思是请不要所STL的算法写得太复杂了,我还达不到这个境界。

第48条:总是包含(#include)正确的头文件
               不同的平台可能需要不同的头文件。

第49条:学会分析STL相关的编译器诊断信息
               stlsplit是可以选用的工具。

第50条:熟悉与STL相关的Web站点
               boost将是我下一个关注的对象

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值