《Effective STL 读书笔记》 第三章 关联容器

转载 2012年04月23日 14:42:41
作者:咆哮的马甲 
出处:http://www.cnblogs.com/arthurliu/ 
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。 
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。


第十九条: 理解相等(equality)和等价(equivalence)的区别  

  • 相等的概念是基于operator==的,也就是取决于operator==的实现
  • 等价关系是基于元素在容器中的排列顺序的,如果两个元素谁也不能排列在另一个的前面,那么这两个元素是等价的。
标准关联容器需要保证内部元素的有序排列,所以标准容器的实现是基于等价的。标准关联容器的使用者要为所使用的容器指定一个比较函数(默认为less),用来决定元素的排列顺序。

非成员的函数(通常为STL算法)大部分是基于相等的。下列代码可能会返回不同的结果

 1 struct CIStringCompare:
 2     public binary_function<string, string, bool> {
 3     bool operator()(const string& lhs,
 4                     const string& rhs) const
 5     {
 6         int i = stricmp(lhs.c_str(),rhs.c_str());
 7         if(i < 0)
 8             return true;
 9         else
10             return false;
11     }
12 };
14 
15 
16 set<string,CIStringCompare> s; //set的第二个参数是类型而不是函数
17 s.insert("A");
18 
19 if(s.find("a") != s.end())  //true
20 {
21     cout<<"a";
22 }
23 
24 if(find(s.begin(),s.end(),"a") != s.end())   //false
25 {
26     cout<<"a";
27 }


第二十条: 为包含指针的关联容器指定比较类型  

下面的程序通常不会得到用户期望的结果。

1 set<string*> s;
2 s.insert(new string("A"));
3 s.insert(new string("C"));
4 s.insert(new string("B"));
5 
6 for(set<string*>::iterator i = s.begin(); i != s.end(); i++)
7 {
8     cout<<**i;  //输出一定会是ABC么?
9 }

因为set中存储的是指针类型,而它也仅仅会对指针所处的位置大小进行排序,与指针所指向的内容无关。

当关联容器中存储指针或迭代器类型的时候,往往需要用户自定义一个比较函数来替换默认的比较函数。


 1 struct CustomedStringCompare:
 2     public binary_function<string*, string*, bool> {
 3     bool operator()(const string* lhs,
 4                     const string* rhs) const
 5     {
 6         return *lhs < *rhs;
 7     }
 8 };
 9 
10 
11 set<string*,CustomedStringCompare> s;
12 s.insert(new string("A"));
13 s.insert(new string("C"));
14 s.insert(new string("B"));
15 
16 for(set<string*, CustomedStringCompare>::iterator i = s.begin(); i != s.end(); i++)
17 {
18     cout<<**i; //ABC
19 }

  
可以更进一步的实现一个通用的解引用比较类型


1 struct DerefenceLess{
2     template<typename PtrType>
3     bool operator()(PtrType ptr1, PtrType ptr2) const
4     {
5         return *ptr1 < *ptr2;
6     }
7 };
8 
9 set<string*,DerefenceLess> s;

如果用less_equal来实现关联容器中的比较函数,那么对于连续插入两个相等的元素则有
1 set<int,less_equal<int>> s;
2 s.insert(1);
3 s.insert(1);

因为关联容器是依据等价来实现的,所以判断两个1是否等价!

!(1<=1)&& !(1<=1)// false 不等价

所以这两个1都被存储在set中,从而破坏了set中不能有重复数据的约定. 


比较函数的返回值表明元素按照该函数定义的顺序排列,一个值是否在另一个之前。相等的值不会有前后顺序,所以,对于相等的值,比较函数应该返回false。


对于multiset又如何呢?multiset应该可以存储两个相等的元素吧? 答案也是否定的。对于下面的操作:
1 multiset<int,less_equal> s;
2 s.insert(1);
3 s.insert(1);
4 
5 pair<multiset<int,less_equal>::iterator,multiset<int,less_equal>::iterator> ret = s.equal_range(1);

  

返回的结果并不是所期望的两个1。因为equal_range的实现(lower_bound:第一个不小于参数值的元素(基于比较函数的小于), upper_bound:第一个大于参数值的元素)是基于等价的,而这两个1基于less_equal是不等价的,所以返回值中比不存在1。

事实上,上面的代码在执行时会产生错误。VC9编译器Debug环境会在第3行出错,Release环境会在之后用到ret的地方发生难以预测的错误。

  

第二十二条: 切勿直接修改set或multiset的键  

set、multiset、map、multimap都会按照一定的顺序存储其中的元素,但如果修改了其中用于排序的键值,则将会破坏容器的有序性。

对于map和multimap而言,其存储元素的类型为pair<const key, value>,修改map中的key值将不能通过编译(除非使用const_cast)。
对于set和multiset,其存储的键值并不是const的,在修改其中元素的时候,要小心不要修改到键值。

 1 class Employee
 2 {
 3 public:
 4     int id;
 5     string title;
 6 };
 7 
 8 struct compare:
 9     public binary_function<Employee&, Employee&, bool> {
10     bool operator()(const Employee& lhs,
11                     const Employee& rhs) const
12     {
13         return lhs.id < rhs.id;
14     }
15 };
16 
17 
18 set<Employee,compare> s;
19 
20 Employee e1,e2;
21 
22 e1.id = 2;
23 e1.title = "QA";
24 
25 e2.id = 1;
26 e2.title = "Developer";
27 
28 s.insert(e1);
29 s.insert(e2);
30 
31 set<Employee,compare>::iterator i = s.begin();
32 i->title = "Manager"; //OK to update non-key value
33 i->id = 3; // 破坏了有序性

  
有些STL的实现将set<T>::iterator的operator*返回一个const T&,用来保护容器中的值不被修改,在这种情况下,如果希望修改非键值,必须通过const_case。

1 set<Employee,compare>::iterator i = s.begin();
2 const_cast<Employee&>(*i).title = "Manager"; //OK
3 const_cast<Employee*>(&*i).title = "Arch"; //OK
4 const_cast<Employee>(*i).title = "Director"; // Bad 仅仅就修改了临时变量的值 set中的值没有发生改变

  
对于map和multimap而言,尽量不要修改键值,即使是通过const_cast的方式,因为STL的实现可能将键值放在只读的内存区域当中。

相对安全(而低效)的方式来修改关联容器中的元素

  1. 找到希望修改的元素。
  2. 将要被修改的元素做一份拷贝。(注意拷贝的Map的key值不要声明为const)
  3. 修改拷贝的值。
  4. 从容器中删除元素。(erase 见第九条)
  5. 插入拷贝的那个元素。如果位置不变或邻近,可以使用hint方式的insert从而将插入的效率从对数时间提高到常数时间。
 1 set<Employee,compare> s;
 2 
 3 Employee e1,e2;
 4 
 5 e1.id = 2;
 6 e1.title = "QA";
 7 
 8 e2.id = 1;
 9 e2.title = "Developer";
10 
11 s.insert(e1);
12 s.insert(e2);
13 
14 set<Employee,compare>::iterator i = s.begin();
15 Employee e(*i);
16 e.title = "Manager";
17 
18 s.erase(i++);
19 s.insert(i,e);
 


第二十三条: 考虑使用排序的vector替代关联容器  

哈希容器大部分情况下可以提供常数时间的查找效率,标准容器也可以达到对数时间的查找效率。

标准容器通常基于平衡二叉树实现, 这种实现对于插入、删除和查找的混合操作提供了优化。但是对于3步式的操作(首先进行插入操作,再进行查找操作,再修改元素或删除元素),排序的vector能够提供更好的性能。
因为相对于vector,关联容器需要更大的存储空间。在排序的vector中存储数据比在关联容器中存储数据消耗更少的内存,考虑到页面错误的因素,通过二分搜索进行查找,排序的vector效率更高一些。

如果使用排序的vector替换map,需要实现一个自定义的排序类型,该排序类型依照键值进行排序。
 

第二十四条: 当效率至关重要时,请在map:operator[]和map:insert之间谨慎作出选择 

从效率方面的考虑,当向map中添加元素时,应该使用insert,当需要修改一个元素的值的时候,需要使用operator[]

如果使用operator[]添加元素


1 class Widget{
2 };
3 
4 
5 map<int,Widget> m;
6 Widget w;
7 
8 m[0] = w;
9 //Widget构造函数被调用两次 

对于第8行,如果m[0]没有对应的值,则会通过默认的构造函数生成一个widget对象,然后再用operator=将w的值赋给这个widget对象。 使用insert可以避免创建这个中间对象。

1 map<int,Widget> m;
2 Widget w; 
3 
4 m.insert(map<int,Widget>::value_type(0,w));  //没有调用构造函数

  
如果使用insert修改元素的值(当然,不会有人这样做)


 1 map<int,Widget> m;
 2 Widget w(1); 
 3 m.insert(map<int,Widget>::value_type(0,w)); 
 4 
 5 Widget w2(2);
 6 
 7 m.insert(map<int,Widget>::value_type(0,w2)).first->second = w2;  //构造了一个pair对象
 8 
 9 // 上面这段代码比较晦涩
10 // map::insert(const value_type& x)的返回值为pair<iterator,bool> 
11 // 当insert的值已经存在时,iterator指向这个已经存在的值,bool值为false。
12 // 反之,指向新插入的值,bool值为true。

使用operator[]则轻便且高效的多


1 map<int,Widget> m;
2 Widget w(1); 
3 m.insert(map<int,Widget>::value_type(0,w));
4 
5 Widget w2(2);
6 
7 m[0] = w2;

 
一个通用的添加和修改map中元素的方法


 1 template<typename MapType,
 2          typename KeyType,
 3          typename ValueType>
 4 typename MapType::iterator InsertOrUpdate(MapType& map,const KeyType& k, const ValueType& v) // 注意typename的用法 从属类型前一定要使用typename
 5 {
 6     typename MapType::iterator i = map.lower_bound(k); // 如果i!=map.end(),则i->first不小于k
 7 
 8     if(i!=map.end() && !map.key_comp()(k,i->first)) // k不小于i->first 等价!
 9     {
10         i->second = v;
11         return i;
12     }
13 
14     else
15     {
16         return map.insert(i,pair<const KeyType, ValueType>(k,v));
17     }
18 };
19 
20 
21 map<int,Widget> m;
22 Widget w(1); 
23 
24 map<int,Widget>::iterator i  = InsertOrUpdate<map<int,Widget>,int,Widget>(m,0,w);


第二十五条: 熟悉非标准的哈希容器

如果你和我一样对于hash容器仅仅停留在知道的层次,这篇文章是我看到的国内对于hash_map讲解的最为认真的文章,建议参考一下。

常见的hash容器的实现有SGI和Dinkumware,SGI的hashset的声明类似于


1 template<typename T,
2          typename HashFunction = hash<T>,
3          typename CompareFunction = equal_to<T>,
4          typename Allocator = allocator<T>>
5 class hashSet;

  
Dinkumware的hash_set声明


1 template<typename T,
2          typename CompareFunction>
3 class hash_compare;
4 
5 template<typename T,
6          typename HashingInfo = hash_compare<T,less<T>>,
7          typename Allocator = allocator<T>>
8 class hash_set;

  
SGI使用传统的开放式哈希策略,由指向元素的单向链表的指针数组(桶)构成。Dinkumware同样使用开放式哈希策略,由指向元素的双向链表的迭代器数组(桶)组成。从内存的角度上讲,SGI的设计要节省一些。

相关文章推荐

effective STL 读书笔记——第三章:关联容器

条款19:了解相等和等价的区别相等:一般表示operator==操作符返回true 等价:一般用于关联容器,表示两个对象x和y如果在关联容器c的排序顺序中没有哪个排在另一个之前,一般以Operato...

STL源码剖析_读书笔记:第五章 关联式容器 红黑树篇

关联式容器 实现:红黑树,按照键值大小将元素放于适当位置 内部结构:平衡二叉树 含义:每个元素都有键值与实值 二叉搜索树:对数时间的元素插入和访问,节点键值大于左子树中每个节点键值,小于右子树...

effective java 读书笔记---第三章对于所有对象都通用的方法

20170409 8.覆盖 equals 方法需要遵守的约定 自反性:非空对象,自身与自身equals返回 true 对称性:非空对象a.equals(b) 与 b.equals(a)结果一致 ...

第三章:构造函数,析构函数和赋值操作(Effective C++ Second Edition 读书笔记)

类的构造函数,析构函数,赋值函数,拷贝函数需要特别注意,因为这几个操作常常隐藏在日常变量操作中。 重点:为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符(条款11) 1) 要写自...

effective STL 读书笔记——第四章:迭代器

条款26:尽量用iterator代替const_iterator,reverse_iterator和const_reverse_iterator1.四个迭代器的转换关系iterator可以转换为其他三...

《Effective STL》读书笔记

http://www.linuxso.com/linuxbiancheng/2462.html   第一条: 慎重选择容器类型 C++所提供的容器类型有如下几种: 标准STL序列容器 ...

effective STL 读书笔记——第二章:vector和string

条款13:尽量使用vector和string来代替动态分配的数组理由如下: 通过vector、string代替动态分配的数组,你可以享受标准stl算法库的好处 你不需要考虑何时放内存,不会存在麻烦的内...

effective STL读书笔记

1、  序列容器和关联容器使用情况,注重点不同而不同,比如插入、删除、元素顺序 4、用empty来判断是否为0 5、用assign来进行区间成员复制 6、注意函数声明和函数参数的区别 7、用指...
  • necrazy
  • necrazy
  • 2013年11月09日 10:38
  • 793

Effective STL读书笔记-第一章

博客已搬家,请移步

STL源码剖析_读书笔记:第三章 迭代器概念与traits编程技法

迭代器模式:提供能够按序访问容器中元素且屏蔽容器差异性的方法 迭代器本质:是行为类似指针的对象,对operator* 与operator->重载。 迭代器的特点:每个容器有自己的迭代器 迭代器...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:《Effective STL 读书笔记》 第三章 关联容器
举报原因:
原因补充:

(最多只允许输入30个字)