代码改变世界

《Effective STL 读书笔记》 第一章 容器

2011-08-07 20:38  咆哮的马甲  阅读(766)  评论(0编辑  收藏  举报
作者:咆哮的马甲 
出处:http://www.cnblogs.com/arthurliu/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。

第一条: 慎重选择容器类型

C++所提供的容器类型有如下几种:

  • 标准STL序列容器 vector string deque list
  • 标准STL关联容器 set multiset map multimap
  • 非标准序列容器 slist rope
  • 非标准关联容器 hash_set hash_multiset hash_map hash_multimap
  • vector<char>作为string的替代
  • vector作为标准关联容器的替代
  • 非标准的STL容器 array bitset valarray stack queue priority_queue

标准容器中的vector string和list比较熟悉。
deque是double ended queue,提供了与vector一样的随机访问功能,但同时对头尾元素的增删操作提供了优化。
set和multiset中的数据都是顺序排列的,数据值本身就是键值,set中的数据必须唯一而multiset没有这样的限制。
map和multimap中的数据对按照键值顺序排列,map中不允许出现重复的key,而multimap中可以用相同的key对应不同的value。
slist是single linked list,与STL中标准的list之间的区别就在于slist的iterator是单向的,而list的iterator是双向的。
rope用于处理大规模的字符串。
hash_set hash_multiset hash_map hash_multimap利用hash算法对相对应的关联容器进行了优化。
bitset是专门用来存储bit的容器。
valarray主要用于对一系列的数字进行高速运算。 
priority_queue类似于heap,可以高效的获取最高priority的元素。

  • 连续内存容器,动态申请一块或多块内存,每块内存中存储多个容器中的元素,当发生插入或删除操作时,要对该内存中的其他元素进行新移动操作,这会降低效率。vector,string,rope都是连续内存容器。
  • 基于节点的容器,为容器中的每一元素申请单独的内存,元素中有指针指向其他的元素,插入和删除的操作只需要改变指针的指向。缺点在于占用内存相对连续内存容器较大。list, slist, 关联容器以及hash容器都是基于节点的容器。
如何选择合适的STL容器
如何选择合适的STL容器.png


第二条: 不要编写试图独立于容器的代码
 
  • 数组被泛化为以其所包含对象的类型为参数的容器
  • 函数被泛化为以其使用的迭代器的类型为参数的算法
  • 指针被泛化为以其所指向的对象的类型为参数的迭代器
考虑到以后可能会使用其他的容器替换现有的容器,为了使修改的部分最小化,最好采用如下的方式
 1 class Widget{...};
2 template<typename T>
3 SpecialAllocator{...};
4 typedef vector<Widget, SpecialAllocator<Widget>> WidgetContainer;
5 typedef WidgetContainer::iterator WCIterator;
6
7 WidgetContainer wc;
8 Widget widget;
9 ...
10 WCIterator i = find(wc.begin(), wc.end(), widget);

使用Class将自定义的容器封装起来,可以更好的实现修改部分最小化,同时达到了安全修改的目的
 1 class CustomizedContainer
2 {
3 private:
4
5 typedef vector<Widget> InternalContainer;
6 typedef InternalContainer::Iterator ICIterator;
7
8 InternalContainer container;
9
10 public:
11 ...
12 };

 

 

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


STL的工作方式是Copy In, Copy Out,也就是说在STL容器中的插入对象和读取对象,使用的都是对象的拷贝。


在存放基类对象的容器中存放子类的对象,当容器内的对象发生拷贝时,会发生截断(剥离 slicing)。

1 vector<Widget> vw;
2
3 class SpecialWidget : public Widget
4 {
5 ...
6 };
7
8 SpecialWidget sw;
9 vw.push_back(sw);


正确的方法是使容器包含指针而非对象。

1 vector<Widget*> vw;
2
3 class SpecialWidget : public Widget
4 {
5 ...
6 };
7
8 SpecialWidget sw;
9 vw.push_back(&sw);

  

容器与数组在数据拷贝方面的对比:

当创建一个包含某类型对象的一个数组的时候,总是调用了次数等于数组长度的该类型的构造函数。尽管这个初始值之后会被覆盖掉

Widget w[maxNumWidgets]; //maxNumWidgets 次的Widget构造函数


如果使用vecor,效率会有所提升。

vector<widget> w;     //既不调用构造函数也不调用拷贝构造函数

vector
<widget> w(5); //1次构造 5次拷贝构造

vector
<widget> w; //既不调用构造函数也不调用拷贝构造函数
w.reserve(5); //既不调用构造函数也不调用拷贝构造函数
vector<widget> w(5);  //1次构造 5次拷贝构造
w.reserve(6); //需要移动位置,调用5次拷贝构造

 

 

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


empty()对于所有标准容器都是常数时间,而对list操作,size()耗费线性时间。


list具有常数时间的Splice操作,如果在两个list之间做链接的时候需要记录被链接到当前list的节点的个数,那么Splice操作将会变成线性时间。对于list而言,用户对Splice效率的要求高于取得list长度的要求,所以list的size()需要耗费线性的时间去遍历整个list。所以,调用empty()是判断list是否为空的最高效方法。

 

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


区间成员函数在效率方面的开销要小于循环调用单元素的成员函数,以insert为例

  1. 避免不必要的函数调用
  2. 避免频繁的元素移动
  3. 避免多次进行内存分配


区间创建

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


区间插入

1 void container::insert(Iterator position, InputIterator begin, InputIterator end);
2 void associatedContainer::insert(InputIterator begin, InputIterator end);


区间删除

1 Iterator container::erase(Iterator begin, Interator end);
2 void associatedContainer:erase(Iterator begin, Iterator end);


区间赋值

1 void container::assign(InputIterator begin, InputIterator end);


 

第六条:当心C++编译器最烦人的分析机制


C++会尽可能的将一条语句解释为函数声明。


下列语句都声明了一个函数返回值为int类型的函数f,其参数是double类型。

1 int f(double(d));
2 int f(double d);
3 int f(double);  


下列语句都声明了一个返回值为int类型的函数g,它的参数是返回值为double类型且无参的函数指针

1 int g(double(*pf)());
2 int g(double pf());
3 int g(double ()); //注意与int g(double (f))的区别

  

对于如下语句,编译器会做出这样的解释:声明了一个返回值为list<int>的函数data,该函数有两个参数,一个是istream_iterator<int>类型的变量,另一个是返回值为istream_iterator<int>类型的无参函数指针。

1 ifstream dataFile("ints.dat");
2 list<int> data(istream_iterator<int>(dataFile),istream_iterator<int>());


如果希望构造一个list<int>类型的变量data,最好的方式是使用命名的迭代器。尽管这与通常的STL风格相违背,但是消除了编译器的二义性而且增强了程序的可读性。

1 ifstream dataFile("ints.dat");
2 istream_iterator dataBegin(dataFile);
3 istream_iterator dataEnd;
4 list<int> data(dataBegin,dataEnd); 



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


STL容器在析构之前,会将其所包含的对象进行析构。

 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 widget w; //一次构造函数
9 vector<widget> v;
10 v.push_back(w); //一次拷贝构造函数
11 } // 两次析构函数


但如果容器中包含的是指针的话,一旦没有特别将指针delete掉将会发生内存泄漏

 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 widget* w = new widget();
9 vector<widget*> v;
10 v.push_back(w);
11 } // memory leak!!!

  

最为方便并且能够保证异常安全的做法是将容器所保存的对象定义为带有引用计数的智能指针

 1 class widget
2 {
3 ...
4 };
5
6 doSth()
7 {
8 shared_ptr<widget> w(new widget()); //构造函数一次
9 vector<shared_ptr<widget>> v;
10 v.push_back(w);
11 } //析构函数一次 没有内存泄漏

 

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


由于auto_ptr对于其"裸指针"必须具有独占性,当将一个auto_ptr的指针赋给另一个auto_ptr时,其值将被置空。

1 auto_ptr<int> p1(new int(1)); // p1 = 1
2 auto_ptr<int> p2(new int(2)); // p2 = 2
3
4 p2 = p1; // p2 = 1 p1 = emtpy;


第三条提到STL容器中的插入对象和读取对象,使用的都是对象的拷贝,并且基于STL容器的算法也通常需要进行对象的copy,所以,创建包含auto_ptr的容器是不明智的。


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


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

1 //对于vector、string、deque 使用erase-remove方法
2 container.erase(remove(container.begin(),container.end(),value),container.end());
3
4 //对于list 使用remove方法
5 list.remove(value);
6
7 //对于标准关联容器 使用erase方法
8 associatedContainer.erase(value);


要删除容器中满足特定条件的所有对象

 1 bool condition(int );
2
3 //对于vector、string、deque 使用erase-remove_if方法
4 container.erase(remove_if(container.begin(),container.end(),condition),container.end());
5
6 //对于list 使用remove_if方法
7 list.remove_if(condition);
8
9 //对于标准关联容器 第一种方法是结合remove_copy_if和swap方法
10 associatedContainer.remove_copy_if(associatedContainer.begin(),
11 associatedContainer.end(),
12 insert(tempAssocContainer,tempAssocContainer.end()),
13 condition);
14
15 //另外一种方法是遍历容器内容并在erase元素之前将迭代器进行后缀递增
16 for(assocIt = associatedContainer.begin(); assocIt != associatedContainer.end())
17 {
18 if(condition(*assoIt))
19 {
20 ///当关联容器中的一个元素被删除掉时,所有指向该元素的迭代器都被设为无效,所以要提前将迭代器向后递增
21 associatedContainer.erase(assoIt++);
22 }
23 else
24 {
25 assocIt++;
26 }
27 }


如果出了在删除容器内对象的同时还需要进行额外的操作时

 1 bool condition(int );
2 void dosth();
3
4 //对于标准序列容器,循环遍历容器内容,利用erase的返回值更新迭代器
5 for(containerIt = container.begin(); containerIt != container.end())
6 {
7 if(condition(*containerIt))
8 {
9 doSth();
10 //当标准容器中的一个元素被删除掉时,所有指向该元素以及该元素之后的迭代器都被设为无效,所以要利用erase的返回值
11 containerIt = container.erase(containerIt++);
12 }
13 else
14 {
15 containerIt++;
16 }
17 }
18
19 //对于标准关联容器,循环遍历容器内容,并在erase之前后缀递增迭代器
20 for(assocIt = associatedContainer.begin(); assocIt != associatedContainer.end())
21 {
22 if(condition(*assoIt))
23 {
24 dosth();
25 associatedContainer.erase(assoIt++);
26 }
27 else
28 {
29 assocIt++;
30 }
31 }


我觉得第三种情况下可以用第二种情况的实现代替,我们需要做的仅是将额外做的事情和判断条件包装在一个函数内,并用这个函数替代原有的判断条件。 

 1 bool condition_doSth(int i)
2 {
3 bool ret = condition(i);
4 if(ret)
5 {
6 doSth();
7 }
8
9 return ret;
10 }



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

如果需要编写自定义的分配子,有以下几点需要注意

  • 当分配子是一个模板,模板参数T代表你为其分配内存的对象的类型
  • 提供类型定义pointer和reference,始终让pointer为T*而reference为T&
  • 不要让分配子拥有随对象而不同的状态,通常,分配子不应该有非静态数据成员
  • 传递给allocator的是要创建元素的个数而不是申请的字节数,该函数返回T*,尽管此时还没有T对象构造出来
  • 必须提供rebind模板,因为标准容器依赖于该模板


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

如果需要在共享的内存空间中手动的管理内存分配,下列代码提供了一定的参考
 1 //用户自定义的管理共享内存的malloc和free
2 void* mallocShared(size_t bytesNeeded);
3 void* freeShared(void* ptr);
4
5 template<typename T>
6 class sharedMemoryAllocator
7 {
8 public:
9 ...
10
11 point allocator(size_type numObjects, const void* localityHint=0)
12 {
13 return static_cast<pointer>(mallocShared(numObjects*sizeof(T)));
14 }
15
16 void deallocate(pointer ptrMemory, size_type numObjects)
17 {
18 freeShared(ptrMemory);
19 }
20
21 ...
22 }

  
如果不仅仅是将容器的元素放在共享内存,而且要将容器对象本身也放在共享内存中,参考如下代码

1 void* ptrVecMemory = mallocShared(sizeof(SharedDoubleVec));
2 SharedDoubleVec* sharedVec = new(ptrVecMemory) SharedDoubleVec;
3
4 ...
5
6 sharedVec->~SharedDoubleVec();
7 freeShared(sharedVec);



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

  • 对于STL容器的多线程读是安全的
  • 对于多个不同的STL容器

采用面向对象的方式对STL容器进行加锁和解锁

 1 template<typename Container>
2 class lock
3 {
4 public:
5
6 Lock(const Container& container):c(container)
7 {
8 getMutexFor(c);
9 }
10
11 ~Lock()
12 {
13 releaseMutex(c);
14 }
15
16 private:
17
18 Container& c;
19 },
20
21
22
23 vector<int> v;
24 ...
25
26 {
27 Lock<vector<int>> lock(v); //构造lock,加锁v
28 doSthSync(v); //对v进行多线程的操作
29 } //析构lock,解锁v