如何使用好STL?这些经验你必须知道(进阶必备,建议收藏)!

  1. 慎重选择容器类型

    • 需要在容器任意位置插入元素,就选择序列容器(vector string deque list )
    • 不关心容器中的元素是否是排序的,哈希容器可行
    • 你选择的容器是c++标准的一部分,就排除了哈希容器,slist(单链表)和rope(“重型”的string)
    • 随机访问迭代器:vector,deque,string .rope, 双向迭代器:避免使用slist与哈希容器的一个实现
    • 当发生元素的插入和删除,避免移动原来容器的元素移动很重要,那么避免使用连续内存的容器
    • 需要兼容c,就vector
    • 元素的查找速度为关键:哈希容器,排序的vector,标准关联容器(按速度顺序)
    • 介意引用计数,就要避免string与rope
    • 回滚能力(错了能改):就要基于节点的容器。if 对多个元素的插入操作需要事务语义就list ;使用连续内存的容器也可以获得事务语义
    • 基于节点的容器不会使迭代器、指针、引用变为无效
    • 在string上使用swap会使迭代器、指针、或引用变为无效
    • 如果序列容器的迭代器是随机访问,只要没有删除操作发生,且插入操作只在末尾,则指向数据的引用和指针不会失效-deque
  2. 不要试图编写独立于容器类型的代码:例如:不要编写对序列和关联容器都适用的代码

  3. 确保容器中的对象拷贝正确而高效

    • 存入容器的是你的对象的拷贝

    • 剥离问题:向基类对象的容器添加派生类对象会导致,派生类对象所特有的信息被抹去。智能指针是个解决问题的好办法。

  4. 调用empty而不是检查size()是否为0

  5. 区间成员函数优先于与之对应的单元素成员函数

    • 好处:效率高易于理解,更能表达你的意图
    • 区间创建、删除、赋值(assign)、插入可以用到区间成员函数
  6. 当心c++编译器的烦人的分析机制—尽可能的解释为函数声明:

    • int f(double d) ; int f(double (d)) ; int f(double) -三种同样形式的声明

      int g(double (* pf) ()) ; int g(double pf ()) ; int g(double ()) -g是一个函数,该函数的参数是一个指向不带任何参数的函数的指针,

      围绕参数名的括号被忽略,独立的括号则表明参数列表的存在

    • list data(istream_iterator(datafile),istream_iterator());

      • 由上面分析可知,这是一个函数声明,跟我们想要做的事,大相径庭
      • 第一个参数的名称是datafile ,其括号可省略,类型是istream_iterator
      • 第二个参数是一个函数指针,返回一个istream_iterator
    • class Weight{…};

      Weight w();

      我是我们刚开始学习类的时候容易犯下的错误,我们想声明一个Weight函数,向进行默认初始化,编译器却给我什么了一个函数声明。该函数不带任何参数,并返回一个Weight

    • 解决方法一:给函数参数加上括号,

      list data((istream_iterator(datafile)),istream_iterator());

      解决方法二:避免是使用匿名的istream_iterator迭代器对象,而是给这些迭代器一个名称(更好一点)

      ifstream  datafile("ints.dat");
      istream_iterator<int> dateBegin(datafile);
      istream_iterator<int> dataEnd;
      list<int>data(dataBegin,dateEnd);
      
  7. 如果在容器中包含了通过new操作创建的对象的指针,切记在容器对象调用析构函数之前将指针delete掉

  8. 解决方法:最简单的方法用智能指针代替指针容器,这里的智能指针通常是被引用计数的指针

void dosomething(){
typedef boost::shard_ptr<Weight>SPW; //令SPW=shard_ptr<Weight>
vector<SPW>vwp;
for(int i=0;i<SOME_MAGIC_NUMBER;++i){
    vwp.push_back(SPW(new Weight))//这里不会发生内存泄露,即使前面抛出异常。
        ...
}
}
  1. 切勿创建包含auto_ptr 的容器

  2. 当年你复制一个auto_ptr时,它所指对象的所有权被移交到复制的对象,而他自身被置为NULL

  3. 慎重选择删除元素的方法

    • 要删除容器 中有特定值的所有对象

      • 如果容器时vector、string或duque, 则使用erase—remove
      • 如果是list 则使用list::remove
      • 如果容器是一个关联容器,则使用它的erase成员函数
    • 要在循环内部做某些(除了删除对象的操作之外)操作

      • 如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次调用erase时,要用它的返回值更新迭代器

      • 如果是关联容器,写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对他进行后缀递增

        for(specalcontainer<int>::iterator i=c.begin();i!=c.end();){
            if(badvalue(*i)){
                logFile<<"..."<<*i<<endl;
                i=c.erase(i);//对于vector,string,deque删除的一个元素不仅会导致这个元素的迭代器失效,同时会导致所有的迭代器失效,所以必须更新迭代器。
            }
              else ++i;
        }
        
    • 要删除容器中满足判别式的所有对象

      • 如果容器时vector、string或duque, 则使用erase—remove_if

      • 如果是list 则使用list::remove_if

      • 如果是关联容器,则使用remove_copy_if和swap(把我们需要的值复制到新容器,然后交换容器),或者写一个循环来编译容器中的元素,记住当把迭代器传给erase时,要对他进行后缀递增

        container<int>c;
         ...
        for(container<int>::iterator i=c.begin();i!=c.end();){
            if(badvalue(*i))
                c.erase(i++);//当该元素被删除的时候,该元素所有的迭代器都会失效,所以我们使用i++
              else ++i;
        }
        
  4. 了解分配子的约定与概念(接下来的两节的知识有点难度,只简单的写点概念)

    • 分配子最初是作为内存模型的抽象,后来为了有利于开发作为对象形式的内存管理器,STL内存分配子负责分配和释放内存

      • 首先分配子能够为它所定义的内存模型中的指针和引用提供类型定义,分别为allocator::pointer与allocator::reference,用户定义的分配子也应该提供这些类型定义,创建这种具有引用行为特点的对象是使用代理对象的一个例子,而代理对象会导致很多问题

      • 库实现者可以忽略类型定义,而直接使用指针和引用,允许每个库实现者假定每个分配子的指针类型等同于T*,引用为T &

      • STL实现者可以假定所有属于同一类型的分配子都是等价的

      • 大多数标准容器从来没有单独使用过对应的分配子,例如list ,但我们添加一个节点的时候我们并不是需要T的内存,而是要包含T的listNode的内存,所有说list从未需要allocator做任何内存分配,该list的分配子不能够提供list所需的分配内存的功能。所以它会利用分配子提供的一个模板,根据list中T来决定listNode的分配子类型为:Allocator::rebind::other,这样就得到listNode 的分配子,就可以为list分配内存

      • new与allocator在分配内存的时候,他们的接口不同

        void* operator new(size_t bytes);
        pointer allocator<T>::allocator(size_type numberjects);//pointer是个类型定义总是T*
        //两者都带参数说名要分配多少内存,但是new 是指明一定的字节,而allocator ,它指明的内存中要容纳多少个T对象
        //new返回的是一个void*,而allocator<T>::allocate返回的是一个T*,但是返回的指针并未指向T对象,因为T为被构造,STL会期望allocator<T>::allocate的调用者最终在返回的内存中创建一个或者多个T对象
        
  5. 编写自定义分配子需要什么

    • 你的分配子是个模板,T代表为它分配对象的类型
    • 提供模板类型定义,分别为allocator::pointer与allocator::reference
    • 通常,分配子不应该有非静态对象
    • new返回的是一个void *,而allocator::allocate返回的是一个T *,但是返回的指针并未指向T对象,因为T为被构造,STL会期望allocator::allocate的调用者最终在返回的内存中创建一个或者多个T对象
    • 一定要提供rebind模板
  6. 理解分配子的用法

    • 把STL容器中的内容放在共享内存中
    • 把STL容器中的内容放到不同的堆中
  7. 切勿对STL容器的线程安全性有不切实际的依赖

  8. 当你在动态分配数组的时候,请使用vector和string

  9. 使用reserve来避免不必要的重新分配

    • STL容器会自动增长以便容纳下其中的数据,只要没有超出他们的限制

    • vector与string 的增长实现过程:

      1.分配一块大小为旧内存两倍的新内存,把容器中所有的元素复制到新内存中,析构旧内存的对象,释放旧内存。

    • reserve函数能够把你重新分配内存的次数减到最小,从而避免重新分配和指针、迭代器、引用失效带来的开销,所以应该尽早的使用reserve,最好是容器在被刚刚构造出来的时候就使用

    • 4个易混函数(只有vector与string提供所有的这4个函数)

      1. size()告诉你容器中有多少个元素
      2. capacity()告诉你该容器利用已分配的内存能够容纳多少个元素,这是容器能够容纳元素的总数
      3. resize(Container::size_type n)强迫容器改变到包含n个元素的状态,如果size返回的数<n,则容器尾部的元素就会被析构,如果>n,则默认构造新的元素添加到容器的末尾,如果n要比当前的容器容量大,那么就会在添加元素之前,重新分配内存
      4. reserve(Container::size_type n),强迫容器改变容量变为至少n,前提是不比当前的容量小,这会导致重新分配。
    • 有两种方式避免不必要的内存分配

      1. 你提前已经知道要用多少的元素,你此时就可以使用reserve。
      2. 先预留足够大的空间,然后在去除多余的容量(如何去除,参照使用swap技巧)
  10. string实现的多样性

    1. string 的值可能会被引用计数
    2. string对象的大小可能是char*的大小的1~7倍
    3. 创建一个新的字符串可能会发生0,1,2次的动态分配内存
    4. string也可能共享其容量,大小信息
    5. string可能支持针对单个对象的分配子
    6. 不同的实现对字符内存的最小分配单位有不同的策略
  11. 了解如何把vector和string数据传给旧的API

    if(!v.empty())

    dosomething(&v[0],v.size());

    or dosomething(v.c_str());

​ 如何用来至C API的元素初始化一个vector

size_t fillArray(double*pArray,size_t arraySize);

vector< double >vd(maxnumbers);

vd.resize(fillArray(&v[0],vd.size()));

size_t fillString(char*pArray,size_t arraySize);

vector< char >vc(maxnumbers);

size_t charWritten=fillString(&v[0],vd.size(0))

string s(vc.begin(),v.end()+charWrittrn)

  1. 使用“swap技巧”删去多余的容量

    vector< C>cs.swap(cs);

    string s ; string (s).swap(s);

    • swap还可以删去一个容器

      vector< C>().swap(cs);

      string s ; string ().swap(s);

  2. 在我swap的时候,不仅两个容器的元素被交换了,他们的迭代器,指针和引用依然有效(string除外),只是他们的元素已经在另一个容器里面。

  3. 避免使用vector< bool >,用deque< bool >和bitset代替它。

  4. 理解等价与相等

    • 相等基于operator==,一旦x==y则返回真,则x与y相等
    • 等价关系是在已经排好序的的区间中对象值的相对顺序,每一个值都不在另一个值的前面。!=(x<y)&&!=(y<x)。每个标注关联容器的比较函数是用户自定义的判别式,每个标准关联容器都是通过key_comp成员函数使排序判别式可被外部使用
  5. 熟悉非标准散列容器:hash_map,hash_set hash_multimap hash_multiset

  6. 包含指针的关联容器指定比较类型,而不是比较函数,最好是准备一个模板

    struct Dfl{
        template<typename ptrtype>
        bool operator()(ptrtype pT1,ptrtype pT2)const{
            return *pT1<*pT2;
        }
    }
    
  7. 切勿直接修改set或multiset中的键

    set/multiset 的值不是const,map/multimap的键是const。

    如何修改元素:

    • 找到想要修改的元素
    • 为将要修改的元素做一份拷贝。在map/multimap的情况下,不要把该拷贝的第一部分申明为const
    • 修改拷贝
    • 把该元素重容器中删除,一般用erase
    • 把新的值插入到容器中
  8. 考虑用排序的vector替代关联容器

    • 当程序使用数据结构的方式是:设置阶段、查找阶段、重组阶段,使用排序的vector容器可能比使用关联容器的效率要更好一点(当在使用数据结构的时候,查找操作不与删除添加操作混在一起的时候在考虑vector)

    • 好处:消耗更少的内存,运行的更快一些

    • 注意:当你使用vector来模仿map<const k,v>时,存储在vector中的是pair<k,v>,而不是pair<const k,v>;需要自己写3个自定义比较函数(用于排序的比较函数,用于查找的比较函数)

      typedef pair<string,int>Data;
      class Datacompare{
          public:
            bool operator()(const Data&lhs,const Data&rhs)const{
                return keyless(lhs.first,rhs.first)
            }//用于排序的比较函数
            bool operator()(const Data& lhs,const Data::first_type& k)const{
                return keyless(lhs.first,k)
            }//用于查找的比较函数
            bool operator()(const Data::first_type& k,const Data&rhs)const{
                return keyless(k,rhs.first)
            }//用于查找的比较函数
          private:
            bool keyless(const Data::first_type&k1,const Data::first_type&k2)const{
                return k1<k2;
            }//为了保证operator()的一致性
      }
      
  9. 如果要更新一个已有的映射表元素,则应该选择operator[],如果是添加元素,那么最好还是选择insert。

  10. iterator优先于const_iterator,reserve_iterator,const_reserve_iterator

  11. 使用distance和advance将容器的const_iterator转换为iterator

    typedef deque<int>IntDeque;
    typedef IntDeque::iterator Iter;
    typedef IntDeque::const_iterator ConstIter;
    IntDeque d;
    ConstIter ci;
    ...
    Iter i(d.begin());
    advance(i,distance<ConstIter>(i,ci));
    
  12. 正确理解由reserve_iterator的base()成员函数所产生的iterator的用法

    • 对于插入操作,ri和ri.base()是等价的

    • 对于删除操作,ri和ri.base()不是等价的

      v.erase((++ri).base());
      
  13. 对于逐个字符的输入请考虑使用istreambuf_iterator

    ifstream inputFile("sdsdsa.txt");
    string filedate((istreambuf_iterator<char>(inputFile),istreambuf_iterator<char>());
    
  14. 如果所使用的算法需要指定一个目标空间,确保目标区间足够大或确保它会随着算法的运行而增大。要在算法执行过程中中增大目标区间,请使用插入型迭代器:back_inserter,front_inserter,ostream_iterator【插入器,它接受一个容器,生成一个迭代器,能实现向给定容器添加元素】

  15. 了解各种与排序相关的选择

    • 如果需要对vector string,deque,或者数组中的元素进行一次完全排序,那么可以使用sort和stable_sort
    • 如果有一个vector,string,deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那就是可以使用partial_sort
    • 如果有一个vector,string,deque或者数组,并且只需要找到第n个位置上的元素,或者,并且只需要找到等价性最前面的n个元素,并不需要排序,那么nth_element就行
    • 如果需要将一个标准序列容器中的元素按照是否满足某个特定区间区分开,那么就选择partition、stable_partition
    • 如果你的数据在list中,那么你可以选择内置的sort和stable_sort算法。同时如果你需要活得partition_sort或nth_element算法的效果可采用一些间接的方法【effective stl p114】
    • 对于排序算法的选择应该基于功能而选择,而不是基于性能
  16. 如果要删除元素,需要在remove后面使用erase

    • remove并没有删除容器中的元素:因为remove它不是成员函数(list除外),所以它不知道要删除那个容器的元素。它会把不被删除的元素排在前面,要删除的元素排在后面,所有它返回的一个指针指向最后一个不被删除的元素的后面的那个元素
    • 我们就要使用在remove后面使用erase函数。v.erase(remove(v.begin(),v.end(),elemnet),v.end())
    • 还有两类函数也是这种情况:remove_if,unique;
    • 但是list函数把remove与erase结合在一起生成的list::remove比原先的remove-erase的效率高。unique和remove_if同理
  17. 对包含指针的容器使用remove这一类算法要小心

    原因:由于remove是将那些要被删除的指针被那些不需要被删除的指针覆盖了,所以没有指针指向那些被删除指针所指向的内存和资源,所有资源就泄露了

    做法:使用智能指针或者在使用remove-erase之前手动删除指针并把他们置为空

  18. 了解那些算法要求使用排序的区间作为参数

    binary_search lower_bound upper_bound equal_range

    set_union set_intersection set_differerce set_symmetric_differerce

    merge implace_merge

    includes

    unique unique_copy

    要确保提供给他们排序的区间并且保证这些算法使用的比较函数与排序所使用的函数一致

  19. 通过mismatch或者lexicographical_compare实现简单的忽略大小写的字符串比较

  20. 理解copy_if算法的正确实现

    template<typename InputIterator
             typename OutputIterator
             typename predicate>
    OutputIterator copy_if(InputIterator begin ,InputIterator end,OutputIterator destbegin,predicate p)
    {
        while(begin!=end){
            if(p(*begin))
                *destbegin++=*begin;
                ++begin;
        }
         return destbegin;
    }
    
  21. 使用accumulate或者for_each进行区间统计

    • accumulate (inner_producet、adjacent_difference、partial_sum)位于< numeric >中
    • for_each(区间,函数对象)
    • accumulate(begin,end,初始值);accumulate(初始值,统计函数)
  22. 遵循按值传递的原则来设计函数子类

- 如果做能够允许函数对象可以很大、或者保留多态,又可以与STL所采用的按值传递函数指针的习惯保持一致:将数据和虚函数从函数子类中分离出来,放到一个新的类中;然后在函数子类中包含一个指针,指向这一个心类的对象。

  ```c++
  template <typename T>
  class Bs:public:unary_function<T,void>{
      private:
         Weight w;
         int x;
         ...
         virtual ~Bs();
         virtual void operator()(const T& val) const;
         friend  class B<T>
  }
  template <typename T>
  class B:public:unary_function<T,void>{
      private:
         BS<T> *p;
      public:
         virtual void operator()(const T& val) const;{
             p->operator()(val);
         }
         
  }
  //这样的设计模式:effective c++ 34有介绍
  ```
  1. 确保判别式是“纯函数”

  2. 使你的函数子类可配接

    为什么:1.(可配接的函数对象能够与其他STL组件默契的协同工作)2.能够让你的函数子类拥有必要的类型定义。

    为什么:4个标准的函数配接器(not1,not2,bind1st,bind2nd)要求这些类型定义

    为什么not1等需要这些定义:能够辅助他们完成一些功能

    如何使函数可配接:让函数子重特定的基类继承:unary_funtion与binary_function

    注意:unary_funtion<operator所带参数类型,返回类型> binary_function<operator 1,operator 2,返回类型

  3. 理解ptr_fun\mem_fun\mem_fun_ref

    在函数和函数对象被调用的时候,总是使用非成员函数形式发f(),而当你使用成员函数的形式时x.f(),p->f();

    这将通不过编译,所有使用上面的那些东西,就能调整成员函数,使其能够以成员函数的形式调用函数和函数对象。

    每次将成员函数传给STL组件的时候,就要使用他们。

  4. 确保less< T >与operator<的语义相同

    一般情况下我们使用less< T >都是默认通过operator<来排序。

    如果你想要实现不同的比较,最好是重新写一个类,而不是修改特化修改less

  5. 算法的调用优先于手写的循环

    1. 效率高
    2. 自己手写的循环更容易出错
    3. 算法代码比我们自己写的更简单明了,利于维护
  6. 容器的成员函数优先于同名函数

  7. 正确区分count、find、binary_search、lower_bound,upper_bound、equal_range

  8. 使用函数对象作为STL算法的参数

  9. 避免产生“直写行”的代码,不利于阅读和维护

  10. 包含正确的头文件

    1. 几乎所有的STL容器都被声明在与之同名的头文件中
    2. 除了4个STL算法外,其他所有的算法都被声明在< algorithm >中;accumulate (inner_producet、adjacent_difference、partial_sum)位于< numeric >中
    3. 特殊类型的迭代器(istream_iterator,istreambuf_iterator)被声明在< iterator >
    4. 标注的函数子(less< T >)和函数配接器< not1,bind2nd >b被声明在头文件< funtional >中
  11. 学会分析于STL相关的编译器的诊断信息

  12. 熟悉于STL相关的web站点

    1. SGI STL
    2. STLport
      c/s/02e93901045672)
  13. 使用函数对象作为STL算法的参数

  14. 避免产生“直写行”的代码,不利于阅读和维护

  15. 包含正确的头文件

    1. 几乎所有的STL容器都被声明在与之同名的头文件中
    2. 除了4个STL算法外,其他所有的算法都被声明在< algorithm >中;accumulate (inner_producet、adjacent_difference、partial_sum)位于< numeric >中
    3. 特殊类型的迭代器(istream_iterator,istreambuf_iterator)被声明在< iterator >
    4. 标注的函数子(less< T >)和函数配接器< not1,bind2nd >b被声明在头文件< funtional >中
  16. 学会分析于STL相关的编译器的诊断信息

  17. 熟悉于STL相关的web站点

    1. SGI STL
    2. STLport
    3. Boost
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值