Effective STL第一部分: 容器

第一条:慎重地选择容器

C++提供了以下一些容器:

n  标准STL序列容器:vector、string、deque和list。

n  标准STL关联容器:set、multiset、map和multimap。

n  非标准序列容器:slist和rope。slist是一个单向链表,rope本质上是一个“重型”string。

n  非标准关联容器:hash_set、hash_multiset、hash_map和hash_multimap。

n  vector<char>作为string的代替

n  vector作为标准关联容器的代替

n  几种标准的非STL容器,包括数组、bitset、valarry、stack、queue和priority_queue。

 

在选择容器时考虑但不限于如下因素:

n  你是否需要在容器的任意位置插入新元素?如果需要,选择序列容器。

n  你是否关心容器中的元素是排序的? 如果关心,你要避免使用哈希容器。

n  你选择的容器必须是C++的一部分吗?如果是,就排除了非标准的容器(哈希容器、slist、rope)。

n  你需要哪种类型的迭代器?vector、deque、string提供随机访问迭代器。

n  当发生元素插入或删除操作时,避免移动容器中原来的元素是否很重要?如果是,避免使用连续内存的容器。

n  容器中数据的布局是否要和c兼容?如果是,只能选择vector。

n  元素的查找速度是否是关键的考虑因素?如果是,就要考虑哈希容器,排序的vector和标准关联容器----或许这就是优先顺序。

n  如果容器内部使用引用计数,你是否介意?如果是,则避免使用string,因为string的实现都使用了引用计数。Rope也需要避免。可以考虑使用vector<char>代替string

n  对于插入和删除操作,你需要事务语意吗?也就是插入和删除失败时,需要回滚的能力吗?如果需要,最好使用基于节点的容器。如果需要对多个元素的插入操作需要事务语意,则需要选择list,因为在标准容器中,只有list提供这功能。连续内存的容器也可以获得事务语意,但是要付出性能上的代价,而且代码也显得不那么直截了当。

n  你需要使迭代器、指针和引用变成无效的次数最小吗?如果是这样,就需要使用基于节点的容器

n  如果在容器上使用swap,使得迭代器、指针和引用变成无效,你会在意吗?如果在意,避免使用string,因为string是在swap过程中使迭代器、指针和引用变成无效的唯一容器。

n 如果序列容器的迭代器是随机访问类型,而且只要没有发生删除操作,且插入操作只是发生在容器的末尾,则指向数据的指针和引用就不会变成无效,这样的容器是否对你有帮助?这是很特别的情形,如果是,deque是你希望的容器。deque是唯一的,迭代器可能变成无效而指针和引用不会变得无效的stl标准容器。



第二条:不要试图编写独立于容器类型的代码

容器类型被泛化为序列容器和标准容器,类似的容器被赋予相似的功能。

n 标准的连续内存容器提供了随机访问迭代器

n 标准的基于节点的容器提供了双向迭代器。

n 序列容器支持push_front和/或push_back操作,而关联容器没有

n 关联容器提供对数时间的lower_bound、upper_bound和equal_range成员函数,但是序列容器却没有。

因此试图编写对各容器试用的代码,你的程序只能试用他们的交集。这意味着要放弃所有容器独特的优势,因此没有太大意义。



第三条确保容器中的对象拷贝正确而高效

(1)内置类型的实现总是简单的按位拷贝

(2)往容器中填充对象是不明智的,原因如下:

n 对象的拷贝操作费时,放入对象越多,占用内存和时间就越多。

n 往基类对象容器拷贝派生类对象时,会发生剥离,派生类特有的信息会丢失。

n 解决这个问题的一个简单方法是是容器包含指针,智能指针是很好的选择。



第四条:调用empty而不是检查size()是否为0

原因很简单:empty对于所有标准容器都是常数时间操作,而对一些list实现,size耗费线性时间。 

这条好简单啊!


第五条:区间成员函数优先于与之对应的单元素成员函数

太多的STL程序员滥用了copy,copy中肯定会有循环,效率会低下。通过利用插入迭代器的方式来限定目标区间的copy调用,几乎都应该换成对区间成员函数的调用。原因:

n  通过使用区间成员函数,通常可以少写一些代码。

n  使用区间成员函数通常会得到意图清晰和更加直接的代码

一句话,区间成员函数使代码更加易懂易写。

例如:

V1.clear();

Copy(v2.begin(),  v2.end(), back_inserter(v1));

可以替换成:

V1.insert(v1.end,  v2.begin(), v2.end());

 

单元素的insert对于区间insert在三个方面影响效率:

第一种是不必要的函数调用。把n个元素逐个插入到v中导致对insert的n次调用。而区间形式的insert,只做一次调用。

第二种是把容器内原有的元素频繁地移动到新元素插入后他们所处的位置,且插入n个元素,最多会导致log2n次的内存分配。而区间形式的insert将原有元素直接移动到它们的最终位置,即只需要付出每个元素移动一次的代价。

 

总结一下区间形式的操作:

区间创建:

所有标准容器提供如下形式的构造函数:

Container::container(InputIterator  begin, InputIterator end);

当传入这种构造函数的迭代器是istream_iterator和istreambuf_iterator时,可能遇到c++最烦人的parse机制,编译器把这条语句解释为函数声明,而不是定义新的容器对象。

区间插入:

所有标准序列容器提供如下形式的insert:

Void container::insert(Iterator  position,    //在何处插入区间、

InputIterator  begin,      //区间开始   

InputIterator end);         //区间结束

关联容器利用比较函数来决定元素插入何处,提供一个省去position的函数原型。

Void container::insert(InputIterator  begin, InputIterator end);

区间删除:

所有标准容器都提供了区间形式的删除操作,但对于序列和关联容器,其返回值有所不同。序列容器提供了这样的形式。

Iterator container::erase(Iterator begin,  Iterator end);

关联容器提供了如下形式,据说是因为返回iterator会影响效率:

void container::erase(Iterator begin,  Iterator end);

         要特别注意区间删除的erase-remove用法。

区间赋值:

所有标准序列容器提供如下形式的区间赋值:

Void container::assign(InputIterator begin, InputIterator end);


 

第六条:当心c++最烦人的分析机制

假设你有一个存有整数的文件,想把这些整数复制到一个list中。下面是很合理的一种做法:

Ifstream datefile(“ints.dat”);

list<int> date(istream_iterator<int>(datafile),  istream_iterator<int>());

这种做法的思路是,把一对istream_iterator传入list的区间构造函数中,见第五条,从而将文件中的整数复制到list中。

 

注意,结果并不是你想像的那样。这里会被解释为声明一个函数data,其返回值是list<int>。这个data函数有两个参数:

n  第一个参数的名称是datafile。它的类型是istream_iterator<int>。datafile两边的括号是多余的。

n  第二个参数没有名称。它的类型是指向不带参数的函数的指针,该函数返回一个istream_iterator<int>。

非常令人吃惊!但是却和c++的一条普遍规律符合,尽可能的解释为函数声明。

 

正确的做法:在对data的声明中避免使用匿名的istream_iterator对象(尽管使用匿名对象是一种趋势),而是给这些迭代器一个名称。下面的代码应该总是可以工作的:

Ifstream datefile(“ints.dat”);

istream_iterator<int>  dataBegin(datafile);

istream_iterator<int>  dataEnd;

list<int> date(dataBegin, dataEnd);

使用命名的迭代器对象与通常的STL程序风格相违背。但是为了保证代码对所有编译器都没有二义性,且使得代码维护人员理解更容易,这一代价是值得的。


 

第七条:如果容器中包含了通过new操作创建的指针,切记在容器析构前将指针delete掉。

这篇讲的比较复杂,总结起来主要有如下两个方面:

1.stl中的容器删除时,能够正确地删除对象,但是不能正确地删除new的方式分配的指针。指针容器在析构时会析构包含的每个元素,但指针的析构函数不作任何事情!他当然不会调用delete。因此会造成内存泄露。

2.effective c++第十三款和这条意思差不多:即用对象来管理资源。用智能指针来替代普通指针放入容器。容器在删除元素时,智能指针会调用析构函数并删除资源。



第八条切勿创建包含auto_ptr的容器对象

原因如下:

n  当auto_ptr对象被拷贝时,它所指向的对象的所有权会被移交到复制的auto_ptr上,它自身被置为NULL。你可以理解为:拷贝一个auto_ptr对象意味着改变它的值。

n 假设容器包含auto_ptr对象。容器进行一次排序操作,将对象的值拷贝到一个临时对象,即意味着原来对象被置为0,其他操作也会有类似的情况。

这是一个令人讨厌的陷阱,因此,千万别创建包含auto_ptr的容器对象。



第九条 慎重选择删除元素的方法

(1)要删除容器中所有特定值的元素

n  如果是连续内存的容器(例如vector、deque或string),最好的方法是使用erase-remove习惯用法。

n  如果是list,则使用list::remove

n  如果是标准关联容器,如果任何名为remove的操作都是完全错误的。这样的容器没有名为remove的成员函数,使用remove算法可能会覆盖容器的值。正确的方法是调用erase

 

(2)要删除容器中所有满足特定判别式(条件)的元素

n  vector、deque或string,最好的方法是使用erase-remove习惯用法。

n  如果是list,则使用list::remove_if

n  如果是标准关联容器,正确的方法是调用remove_copy_ifswap或者写一个循环来遍历,记住把迭代器传给erase时,要对它进行后缀递增(类似c.erase(it++))。

 

(3)要在循环内部做某些(除了删除对象之外的)操作


n 如果容器是一个标准序列容器,记住每次调用erase时,要用它的返回值更新迭代器

n 如果容器时一个关联序列容器,记得当把迭代器传给erase时,要对它进行后缀递增


第十条了解分配子的约定和限制

分配子这个概念现在只需要简单了解一下,在stl源码剖析中会有详细的介绍。

主要记住以下分配子的特性:

n  你的分配子是一个模板,模板参数T代表你为它分配内存的对象的类型。

n  提供类型定义pointer和reference,但是始终让pointer为T*,reference为T&。

n  千万别让你的分配子拥有虽对象而不同的状态(per-object state)通常,分配子不应该有非静态的数据成员。

n  记住,传给分配子的allocate成员函数的是那些要求内存是的对象的个数,而不是所需的字节数。同时要记住,这些函数返回T*指针(通过pointer类型定义),及时尚未有T对象被构造出来。

n  一定要提供嵌套的rebind模板,因为标准容器依赖该模板.



第十一条理解自定义分配子的合理用法

考虑使用自定义分配子的几种情况:

n  Stl自己的内存管理器(即allocate<T>)太慢,或者浪费内存,你相信可以实现地更好。

n  你发现allocate是线程安全的,而你所感兴趣的是在单线程环境下运行,不愿为多线程同步付出不必要的代价。

n  你知道容器中的对象通常是一起使用的,所以你想把它们放在一个特殊堆中的相邻位置上,以便于尽可能地做到引用局部化。

n  你想建立一个与共享内存相对应的特殊的堆,然后在这块内存中存放一个或多个容器,以便使其它进程可以共享这些容器。



第十二条 切勿对stl容器的线程安全性有不切实际的依赖

对一个stl你只能期望:

n  多个线程读是安全的。

n  多个线程对不同的容器做写入操作是安全的

考虑当一个库要实现完全的容器线程安全性时可能采取的方式:

n  对容器成员函数的每次调用,都锁住容器直到调用结束。

n  容器所返回的每个迭代器的生存周期结束前,都锁住容器。

n  对作用于容器的每个算法,都锁住容器直到算法结束。

可以考虑使用autolock类,在构造函数中获得一个互斥体,在析构函数中释放它

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值