作者:咆哮的马甲
出处:http://www.cnblogs.com/arthurliu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
转载请保持文档的完整性,严禁用于任何商业用途,否则保留追究法律责任的权利。
第十九条: 理解相等(equality)和等价(equivalence)的区别
- 相等的概念是基于operator==的,也就是取决于operator==的实现
- 等价关系是基于元素在容器中的排列顺序的,如果两个元素谁也不能排列在另一个的前面,那么这两个元素是等价的。
非成员的函数(通常为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;
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的键
对于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的实现可能将键值放在只读的内存区域当中。
相对安全(而低效)的方式来修改关联容器中的元素
- 找到希望修改的元素。
- 将要被修改的元素做一份拷贝。(注意拷贝的Map的key值不要声明为const)
- 修改拷贝的值。
- 从容器中删除元素。(erase 见第九条)
- 插入拷贝的那个元素。如果位置不变或邻近,可以使用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的设计要节省一些。