Effective STL之容器

标签: stl c++
4人阅读 评论(0) 收藏 举报
分类:

一、慎重选择容器类型

1、容器分类

  • 标准STL序列容器:vector,string,deque,list;

  • 标准STL关联容器:set,multiset,map,multimap;

  • 非标准关联容器(基于散列表):hash_set,hash_multiset,hash_map,hash_multimap;

  • 几种标准的非STL容器:数组、bitset、stack、queue、priority_queue;

  • 连续内存容器:vector,string,deque;

  • 基于节点的容器:list。

2、主要容器分析

  • 连续内存容器(也叫做基于数组的容器)在一个或多个(动态分配)的内存块中保存它们的元素。如果一个新元素被插入或者已存元素被删除,其他在同一个内存块的元素就必须向上或者向下移动来为新元素提供空间或者填充原来被删除的元素所占的空间。这种移动影响了效率和异常安全;

  • 基于节点的容器在每个内存块(动态分配)中只保存一个元素。容器元素的插入或删除只影响指向节点的指针,而不是节点自己的内容。所以当有东西插入或删除时,元素值不需要移动。

3、容器选择

  • vector、list和deque提供给程序员不同的复杂度,因此应该这么用:vector是一种可以默认使用的序列类型,当很频繁地对序列中部进行插入和删除时应该用list,当大部分插入和删除发生在序列的头或尾时可以选择deque这种数据结构。

  • 你需要“可以在容器的任意位置插入一个新元素”的能力吗?如果是,你需要序列容器,关联容器做不到。

  • 你关心元素在容器中的顺序吗?如果不,散列容器就是可行的选择。否则,你要避免使用散列容器。

  • 你需要哪一类迭代器?如果必须是随机访问迭代器,在技术上你就只能限于vector、deque和string。

  • 当插入或者删除数据时,是否非常在意容器内现有元素的移动?如果是,你就必须放弃连续内存容器。

  • 容器中的数据的内存布局需要兼容C吗?如果是,你就只能用vector。

  • 查找速度很重要吗?如果是,你就应该看看散列容器(优于)排序的vector(优于)标准的关联容器大概是这个顺序。

  • 你需要有可靠地回退插入和删除的能力吗?如果是,你就需要使用基于节点的容器。如果你需要多元素插入的事务性语义,你就应该选择list,因为list是唯一提供多元素插入事务性语义的标准容器。事务性语义对于有兴趣写异常安全代码的程序员来说非常重要。

  • 你要把迭代器、指针和引用的失效次数减到最少吗?如果是,你就应该使用基于节点的容器,因为在这些容器上进行插入和删除不会使迭代器、指针和引用失效(除非它们指向你删除的元素)。一般来说,在连续内存容器上插入和删除会使所有指向容器的迭代器、指针和引用失效。

  • 你需要具有有以下特性的序列容器吗:1)可以使用随机访问迭代器;2)只要没有删除而且插入只发生在容器结尾,指针和引用的数据就不会失效?这个一个非常特殊的情况,但如果你遇到这种情况,deque就是你梦想的容器。(有趣的是,当插入只在容器结尾时,deque的迭代器也可能会失效,deque是唯一一个“在迭代器失效时不会使它的指针和引用失效”的标准STL容器。)

二、确保容器中的对象拷贝正确而高效

  容器可以存储对象。当向容器添加对象(insert或push_back),添加到容器的对象是指定对象的拷贝;同理,取出对象时也是通过拷贝。因为会发生拷贝,如果这个拷贝过程比较“昂贵”,那么这可能会是性能的瓶颈。容器中的对象越多,那么就很可能在拷贝上消耗更大的代价。此外,还有一个非传统意义上的“拷贝”对象,把这样的对象放进容器会导致不幸。
  因为继承的存在,拷贝时可能会发生分割。即,如果用基类对象建立容器,而插入派生类对象,这时通过基类的拷贝构造函数插入,派生类对象会被切割为基类对象(剥离):

vector<Widget> vw; 
class SpecialWidget:public Widget {...};    // SpecialWidget从上面的Widget派生

SpecialWidget sw; 
vw.push_back(sw); // sw被当作基类对象拷入vw,当拷贝时它的特殊部分丢失了

  避免上面的问题的一个解决方法是建立指针容器,这样拷贝更快,且没有分割问题,但是指针容器本身也有问题。要避免这个问题的办法是建立智能指针的容器。

三、用empty来代替检查size()是否为0

  对于任意容器c,下面的代码:

if(c.size()==0)

  本质上等价于:

if(c.empty())

  empty的典型实现是一个返回size是否为0的内联函数。但首选应该是empty,因为对于所有标准容器,empty是一个常数时间操作,但对于list,size的花费为线性时间。

  list之所以不能提供常数时间的size实现,是因为list特有的splice有很多要处理的东西。例如:

list<int> list1; 
list<int> list2; 
... 
list1.splice(                   // 把list2中
    list1.end(), list2,         // 从第一次出现5到
    find(list2.begin(), list2.end(), 5),        // 最后一次出现10
    find(list2.rbegin(), list2.rend(), 10).base()   // 的所有节点移到list1的结尾。
);
//上面这段代码假设了list2在5后面有个10。执行完上面代码后,list1中有多少元素,在遍历find(list2.begin(), list2.end(), 5)和find(list2.rbegin(), list2.rend(), 10).base()之间有多少元素之前,无法得知list1中元素个数。

  list中如果把size设计成常数时间操作,那么list成员函数在更新list时也要更新size的大小,包括splice。这时splice就是线性时间操作了。size和splice不能都是常数时间操作,必须有一个让步。

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

  使用区间成员函数有以下好处:

  • 更少的函数调用

  • 更少的元素移动

  • 更少的内存分配

   例:将v2后半部的元素赋值给v1:
  单元素操作:

for (auto ci = v2.begin() + v2.size() / 2;ci != v2.end();++ci)
    v1.push_back(*ci);

  使用区间成员函数assign():

v1.assign(v2.begin() + v2.size() / 2, v2.end()); 

五、如果容器中包含了通过new操作创建的指针,切记在容器对象析构前将指针delete掉

void dosomething()
{
    vector<widget*> vwp;
    for (int i=0; i<size; ++i)
        vwp.push_back(new widget);
    ...
}//这里发生了widget的泄露

【Note】:
1)容器销毁前需要自行销毁指针所指向的对象;否则就造成了内存泄漏;
2)使用排序等算法时,需要构造基于对象的比较函数,如果使用默认的比较函数,其结果是基于指针大小的比较,而不是对象的比较;

  最好的办法是:使用智能指针!

六、慎重选择删除元素的方法

1、删除特定值

(1)连续内存容器(vector、deque或string)使用 erase-remove。例:

c.erase(remove(c.begin(), c.end(), 1963), c.end());

  当c是vector、string或deque时,erase-remove惯用法是去除特定值的元素的最佳方法。

(2)以上方法也适合于list,但是,list的成员函数remove更高效。例:

c.remove(1963);

  当c是list时,remove成员函数是去除特定值的元素的最佳方法。

(3)对于关联容器,解决问题的适当方法是调用erase。例:

c.erase(1963);

  当c是标准关联容器时erase成员函数是去除特定值的元素的最佳方法。

2、删除判断式

(1)对于序列容器(vector、string、deque和list),我们要做的只是把每个remove替换为remove_if。例:

bool badValue(int x);   //返回x是否是“bad
//当c是vector、string或deque时这是去掉badValue返回真的对象的最佳方法。
c.erase(remove_if(c.begin(), c.end(), badValue), c.end());
//当c是list时这是去掉badValue返回真的对象的最佳方法。
c.remove_if(badValue);

(2)对于标准关联容器效率低的remove_copy_if。例:

AssocContainerc;
...
AssocContainergoodValues;
remove_copy_if(c.begin(),c.end(),inserter(goodValues,goodValues.end()),badValue);
c.swap(goodValues);

3、要在循环内做某些操作

(1)对于序列容器vector、string、deque和list(用返回值更新迭代器):

for ( SeqContainer<int>::iterator i = c.begin();  i != c.end();)
{
        if (badValue(*i))
         {
                logFile << "Erasing " << *i << '\n';
                i = c.erase(i);            
        }                                 
        else
                ++i;
}

(2)对于标准关联容器(对迭代器递增):

AssocContainer<int> c;
...
// for循环的第三部分是空的;i现在在下面自增对于坏的值,把当前的i传给erase,然后作为副作用增加i;对于好的值,只增加i。
for ( AssocContainer<int>::iterator i = c.begin();   
        i != c.end();                       
        /*nothing*/ ){                        
        if (badValue(*i)) c.erase(i++);      
        else ++i;                                    
}

七、STL的线程安全性

1、线程安全的情况

  • 多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能有任何写入者操作这个容器。

  • 对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。

2、线程不安全的情况

  • 在对同一个容器进行多线程的读写、写操作时。

  • 在每次调用容器的成员函数期间都要锁定该容器。

  • 在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器。

  • 在每个在容器上调用的算法执行期间锁定该容器。

  看到风险了吧?在工程中多线程操作STL的场景应该还是比较常见的,一个典型的例子就是用其来做生产者——消费者模型的队列或者其他共享队列,这样为了应对线程安全问题我们必须自己对容器操作进行封装。

template<typename Container>    // 获取和释放容器的互斥量
class Lock {                    // 的类的模板核心;
public:                         // 忽略了很多细节
        Lock(const Containers container)
                        : c(container)
        {
                getMutexFor(c); // 在构造函数获取互斥量
        }
        ~Lock()
        {
                releaseMutexFor(c); // 在析构函数里释放它
        }
private:
        const Container& c;
};
查看评论

Effective STL 中文版(完整版)

 Winter总算找到《Effective STL》的完整中文版了,奉献给大家。书中作者解释了怎样结合STL组件来在库的设计得到最大的好处。这样的信息允许你对简单、直接的问题开发简单、直接的解决方案,...
  • WinterTree
  • WinterTree
  • 2005年01月16日 01:23
  • 16436

Effective STL目录

    从去年12月开始,我花了40天看了《Effective STL》这本书,并写了相关的读书笔记,我在考虑是否将这部分笔记在BLOG上贴出来。在此之前,先贴上Effective STL目录,并且强...
  • hanyu1980
  • hanyu1980
  • 2006年06月20日 15:22
  • 1226

Effective STL 中文版(完整版)

Winter总算找到《Effective STL》的完整中文版了,奉献给大家。书中作者解释了怎样结合STL组件来在库的设计得到最大的好处。这样的信息允许你对简单、直接的问题开发简单、直接的解决方案,也...
  • sdnxiaotao
  • sdnxiaotao
  • 2008年07月05日 13:23
  • 501

【C++】STL常用容器总结之十二:string类

13、string类声明string类本不是STL的容器,但是它与STL容器有着很多相似的操作,因此,把string放在这里一起进行介绍。 之所以抛弃char*的字符串而选用C++标准程序库中的st...
  • hero_myself
  • hero_myself
  • 2016年08月25日 13:02
  • 3764

《Effective STL》学习笔记

该篇笔记转自以下两个连接:(另外附件里有STL源码分析和编写高质量代码:改善C++程序的150个建议) http://my.csdn.net/swordll80   http://blog.c...
  • zhongguoren666
  • zhongguoren666
  • 2012年12月22日 09:53
  • 2422

《Effective STL》条款3-条款4

条款3使容器里对象的拷贝操作轻量而正确 条款4用empty来代替检查size是否为0
  • KangRoger
  • KangRoger
  • 2015年10月08日 23:31
  • 947

Effective STL 读书总结

(Effective STL 中文版 潘爱民 陈铭 邹开红 译) 这里是看书过程做的读书笔记点击打开链接,花了不少功夫写的,对每个条款做了一个概括性的描述。看这本书之前最好是对STL的基本的操作熟悉,...
  • xuzhezhaozhao
  • xuzhezhaozhao
  • 2013年08月01日 19:47
  • 1526

带你深入理解STL之List容器

上一篇博客中介绍的vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,很好的支持了随机存取,但由于是连续空间,所以在中间进行插入、删除等操作时都造成了内存块的拷贝和移动,另外在内存空间...
  • terence1212
  • terence1212
  • 2016年08月24日 17:29
  • 3150

STL基础6:list容器的使用总结

一.list使用构造函数的四种初始化方式 list的初始化方式和vector基本一样: //1.默认构造函数,长度为0的列表  list lis1;  //2.带有单个整形参数的构造函数,长度...
  • longhuahaha
  • longhuahaha
  • 2012年12月26日 17:08
  • 3214

effective stl 第19条:理解相等(equality)和等价(equivalence)的区别

#include #include #includeusing namespace std;bool ciStringCompare(const string l, const string r) {...
  • u014110320
  • u014110320
  • 2016年09月20日 23:36
  • 264
    个人资料
    持之以恒
    等级:
    访问量: 8万+
    积分: 1761
    排名: 2万+
    最新评论