STL是C++重要的组件之一,大学时看过《STL源码剖析》这本书,这几天复习了一下,总结出如下LZ认为比较重要的知识点,内容有点略多 :)html
STL提供六大组件,彼此能够组合套用:前端
STL六大组件的交互关系node
一些可能使人困惑的C++语法糖:ios
STL的中心思想是:将数据容器和算法分隔开,彼此独立设计,最后再用黏合剂将它们撮合在一块儿。容器和算法的泛型化,能够用C++的class template和function template来实现,而两者的黏合剂就是迭代器了。程序员
与其说迭代器是一种指针,不如说迭代器是一种智能指针,它将指针进行了一层封装,既包含了原生指针的灵活和强大,也加上不少重要的特性,使其能发挥更大的做用以及能更好的使用。迭代器对指针的一些基本操做如*、->、++、==、!=、=进行了重载,使其具备了遍历复杂数据结构的能力,其遍历机制取决于所遍历的数据结构。下面上一段代码,了解一下迭代器的“智能”:算法
template<typename T> class Iterator { public: Iterator& operator++(); //... private: T *m_ptr; };
对于不一样的数据容器,以上Iterator类中的成员函数operator++的实现会各不相同,例如,对于数组的可能实现以下:编程
//对于数组的实现 template<typename T> Iterator& operator++() { ++m_ptr; retrun *this; }
对于链表,它会有一个相似于next的成员函数用于获取下一个结点,其可能实现以下:windows
//对于链表的实现 template<typename T> Iterator& operator++() { m_ptr = m_ptr->next();//next()用于获取链表的下一个节点 return *this; }
iterator首先要对iterator指向对象的实现细节有很是丰富的了解,因此iterator为了避免暴露所指向对象的信息,干脆就将iterator的实现由各个容器的设计者来实现好了。STL将迭代器的实现交给了容器,每种容器都会以嵌套的方式在内部定义专属的迭代器。各类迭代器的接口相同,内部实现却不相同,这也直接体现了泛型编程的概念。数组
#include <iostream> #include <vector> #include <list> #include <algorithm> using namespace std; int main(int argc, const char *argv[]) { int arr[5] = { 1, 2, 3, 4, 5 }; vector<int> iVec(arr, arr + 5);//定义容器vector list <int> iList(arr, arr + 5);//定义容器list //在容器iVec的头部和尾部之间寻找整形数3 vector<int>::iterator iter1 = find(iVec.begin(), iVec.end(), 3); if (iter1 == iVec.end()) cout << "3 not found" << endl; else cout << "3 found" << endl; //在容器iList的头部和尾部之间寻找整形数4 list<int>::iterator iter2 = find(iList.begin(), iList.end(), 4); if (iter2 == iList.end()) cout << "4 not found" << endl; else cout << "4 found" << endl; system("pause"); return 0; }
从上面迭代器的使用中能够看到,迭代器依附于具体的容器,即不一样的容器有不一样的迭代器实现,同时,咱们也看到,对于算法find来讲,只要给它传入不一样的迭代器,便可对不一样的容器进行查找操做。经过迭代器的穿针引线,有效地实现了算法对不一样容器的访问,这也是迭代器的设计目的。数据结构
所谓序列式容器,其中的元素均可序,但未必有序,C++自己内建了一个序列式容器array,STL另外提供了vector、list、deque、stack、queue、priority-queue等序列式容器。其中stack和queue因为只是deque改头换面而来,技术上被归为一种配接器 (adapter)。
vector采用的数据结构很是简单:线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间(含备用空间)的尾端。
template <class T, class Alloc = alloc> class vector { ... protected: iterator start; //表示 iterator finish; iterator end_of_storage; ... };
注意:所谓动态增长大小,并非在原来空间以后接续新空间(由于没法保证原空间以后尚有可供分配的空间),而是以原来大小的的两倍另外分配一块较大空间,而后将原内容拷贝过来,而后才开始在原内容以后构造新元素,并释放原空间。所以,对vector的任何操做,一旦引发空间从新配置,指向原vector的全部迭代器就都失效啦。
vector变量的大小分析
vector类中有3个迭代器域(也就是指针域),因此大小至少为12字节。
测试环境:win7 64位 VS2013
测试代码:
#include <iostream> #include <vector> using namespace std; int main(void) { vector<int> a(5, 0); //16 cout << sizeof(a) << endl; cout << (int)(void *)&a << endl; cout << (int)(void *)&a[0] << endl; cout << (int)(void *)&a[a.size() - 1] << endl; cout << endl; cout << *((int *)&a) << endl; cout << *(((int *)&a) + 1) << endl; cout << *(((int *)&a) + 2) << endl; cout << *(((int *)&a) + 3) << endl; cout << endl; cout << a.size() << endl; cout << a.capacity() << endl; system("pause"); return 0; }
测试结果显示此时vector的大小为16字节,分别包括start、finish、end_of_storage成员,剩下的4个字节暂时不知道表明什么意思… :(
测试环境:Ubuntu12.04 codeblocks10.05
测试代码:
#include <iostream> #include <vector> using namespace std; int main(void) { vector<int> a(5, 0); //12 cout << sizeof(a) << endl; cout << (int)(void *)&a << endl; cout << (int)(void *)&a[0] << endl; cout << (int)(void *)&a[a.size() - 1] << endl; cout << endl; cout << *((int *)&a) << endl; cout << *(((int *)&a) + 1) << endl; cout << *(((int *)&a) + 2) << endl; cout << *(((int *)&a) + 3) << endl; cout << endl; cout << a.size() << endl; cout << a.capacity() << endl; return 0; }
测试结果显示此时vector的大小为12字节,包括start、finish、end_of_storage成员
小结
win和Ubuntu所用的STL的版本是不同的,不一样的STL所使用的vector类也不一样,有着不一样的容器管理方式。
相对于vector的连续线性空间,list就显得复杂许多,它的好处就是插入或删除一个元素,就配置或删除一个元素空间。对于任何位置的元素的插入或删除,list永远是常数时间。
list自己和节点是不一样的结构,须要分开设计。如下是STL list的节点node结构:
template <class T> class __list_node { typedef void* void_pointer; void_pointer prev; void_pointer next; T data; };
这是一个双向链表
list数据结构
SGI list不只是一个双向链表,并且是一个环状双向链表。只需一个指针就可遍历整个链表。
deque和vector的最大差别,一在于deque容许常数时间内对起头端进行插入或移除操做,二在于deque没有所谓容量(capacity)概念,由于它是以分段连续空间组合而成,随时能够增长一段新的空间链接起来。
deque由一段一段连续空间组成,一旦有必要在deque的前端或尾端增长新空间,便配置一段连续空间,串接在整个deque的前端或尾端。deque的最大任务,即是在这些分段的连续空间上,维护其总体连续的假象,并提供随机存取的接口,避开了“从新配置、复制、释放”的轮回,代价是复杂的迭代器结构。
迭代器首先必须指出分段连续空间在哪里,其次它必须可以判断本身是否已经处在缓冲区的边缘,若是是,一旦前进或后退就必须跳跃下一个缓冲区,为了可以正常跳跃,deque必须随时掌握管控中心。
迭代器结构:
template <class T, class Ref, class Ptr, size_t BufSiz> struct __deque_iterator { // 未继承 std::iterator // 保持迭代器的链接 T* cur; // 此迭代器所指之缓冲区的现行( current)元素 T* first; // 此迭代器所指之缓冲区的的头 T* last; // 此迭代器所指之缓冲区的的尾(含备用空间) map_pointer node; // 指向管控中心 ... };
假如deque中已经包含了20个元素了,缓冲区大小为8,则内存布局以下:
注意:deque最初状态(无任何元素)保有一个缓冲区,所以,clear()完成以后回到初始状态,也同样会保留一个缓冲区。
tack是一种先进后出(First In Last Out,FILO)的数据结构,它只有一个出口。stack容许增长元素、移除元素、取得最顶端元素。但除了最顶端外,没有任何其余方法能够存取,stack的其余元素,换言之,stack不容许有遍历行为。stack默认以deque为底层容器。
queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,容许增长元素、移除元素、从最底端加入元素、取得最顶端元素。但除了最底端能够加入、最顶端能够取出外,没有任何其余方法能够存取queue的其余元素,换言之,queue不容许有遍历行为。queue默认以deque为底层容器。
heap并不归属于STL容器组件,它是个幕后英雄,扮演prority queue的助手。priority queue容许用户以任何次序将任何元素推入容器内,但取出时必定是按照优先级最高的元素开始取。binary max heap正好具备这样的特性,适合做为priority queue的底层机制。heap默认创建的是大堆。
heap测试用例:
#include <iostream> #include <queue> #include <algorithm> using namespace std; template <class T> struct display { void operator()(const T &x) { cout << x << " "; } }; /// heap默认为大堆,如下设置为创建小堆 template <typename T> struct greator { bool operator()(const T &x, const T &y) { return x > y; } }; int main(void) { int ia[9] = { 0, 1, 2, 3, 4, 8, 9, 3, 5 }; vector<int> ivec(ia, ia + 9); make_heap(ivec.begin(), ivec.end(), greator<int>()); //注意:此函数调用时,新元素应已止于底部容器的尾端 for_each(ivec.begin(), ivec.end(), display<int>()); cout << endl; ivec.push_back(7); push_heap(ivec.begin(), ivec.end(), greator<int>()); for_each(ivec.begin(), ivec.end(), display<int>()); cout << endl; pop_heap(ivec.begin(), ivec.end(), greator<int>()); cout << ivec.back() << endl; ivec.pop_back(); for_each(ivec.begin(), ivec.end(), display<int>()); cout << endl; sort_heap(ivec.begin(), ivec.end(), greator<int>()); for_each(ivec.begin(), ivec.end(), display<int>()); cout << endl; system("pause"); return 0; }
priority_queue是一个拥有权值的queue,它容许加入新元素、移除旧元素、审视元素值等功能。因为是一个queue,因此只容许在底端加入元素,从顶端取出元素,除此以外别无其余存取元素方法。priority_queue内的元素并不是按照被推入的顺序排列,而是自动按照元素的权值排列。权值最高者排在前面。
默认状况下priority_queue利用max-heap按成,后者是一个以vector为底层容器的complate binary tree。
priority_queue测试用例:
#include <iostream> #include <queue> #include <algorithm> using namespace std; int main(void) { int ia[9] = { 0, 1, 2, 3, 4, 8, 9, 3, 5 }; vector<int> ivec(ia, ia + 9); priority_queue<int> ipq(ivec.begin(), ivec.end()); ipq.push(7); ipq.push(23); while (!ipq.empty()) { cout << ipq.top() << " "; ipq.pop(); } cout << endl; system("pause"); return 0; }
set和map底层数据结构都是红黑树,红黑树的data域段为pair<key, value>类型。关于红黑树更多知识请点击:深刻理解红黑树。
set的全部元素都会根据元素的键值自动排序。set的元素不像map那样能够同时拥有实值(value)和键值(key),set元素的键值就是实值,实值就是键值,set不容许有两个相同的元素。Set元素不能改变,在set源码中,set<T>::iterator被定义为底层TB-tree的const_iterator,杜绝写入操做,也就是说,set iterator是一种constant iterators(相对于mutable iterators)
测试用例(让set从大到小存放元素):
#include <iostream> #include <set> #include <functional> using namespace std; /// set默认是从小到大排列,如下是让set从大到小排列 template <typename T> struct greator { bool operator()(const T &x, const T &y) { return x > y; } }; int main(void) { set<int, greator<int>> iset; iset.insert(12); iset.insert(1); iset.insert(24); for (set<int>::const_iterator iter = iset.begin(); iter != iset.end(); iter++) { cout << *iter << " "; } cout << endl; system("pause"); return 0; }
map的全部元素都会根据元素的键值自动排序。map的全部元素都是pair,同时拥有实值(value)和键值(key)。pair的第一元素为键值,第二元素为实值。map不容许有两个相同的键值。
若是经过map的迭代器改变元素的键值,这样是不行的,由于map元素的键值关系到map元素的排列规则。任意改变map元素键值都会破坏map组织。若是修改元素的实值,这是能够的,由于map元素的实值不影响map元素的排列规则。所以,map iterator既不是一种constant iterators,也不是一种mutable iterators。
测试用例(map从大到小存放元素):
#include <iostream> #include <string> #include <map> #include <functional> using namespace std; /// map默认是从小到大排列,如下是让map从大到小排列 template <typename T> struct greator { bool operator()(const T x, const T y) { return x > y; } }; int main(void) { map<int, string, greator<int>> imap; imap[3] = "333"; imap[1] = "333"; imap[2] = "333"; for (map<int, string>::const_iterator iter = imap.begin(); iter != imap.end(); iter++) { cout << iter->first << ": " << iter->second << endl; } system("pause"); return 0; }
multiset的特性以及用法和set彻底相同,惟一的差异在于它容许键值重复,所以它的插入操做采用的是底层机制RB-tree的insert_equal()而非insert_unique()。
multimap的特性以及用法和map彻底相同,惟一的差异在于它容许键值重复,所以它的插入操做采用的是底层机制RB-tree的insert_equal()而非insert_unique()。
二叉搜索树具备对数平均时间表现,但这样的表现构造在一个假设上:输入数据有足够的随机性。hashtable这种结构在插入、删除、查找具备“常数平均时间”,并且这种表现是以统计为基础,不需依赖元素的随机性。
hashtable底层数据结构为分离链接法的hash表,以下所示:
hashtable中的buckets使用的是vector数据结构,当插入一个元素时,找到该插入哪一个buckets的插槽,而后遍历该插槽指向的链表,若是有相同的元素,就返回;不然的话就将该元素插入到该链表的头部。(固然,若是是multi版本的话,是能够插入重复元素的,此时插入过程为:当插入一个元素时,找到该插入哪一个buckets的插槽,而后遍历该插槽指向的链表,若是有相同的元素,就将新节点插入到该相同元素的后面;若是没有相同的元素,产生新节点,插入到链表头部)
当调用成员函数clear()后,buckets vector并未释放空间,仍保留原来大小,只是删除了buckets所链接的链表。
hash_multimap插入式的图示说明
运用set,为的是快速搜寻元素。这一点,不论其底层是RB-tree或是hashtable,均可以完成任务,可是,RB-tree有自动排序功能而hashtable没有,即set的元素有自动排序功能而hash_set没有。
测试代码:
#include <iostream> #include <hash_set> #include <cstring> using namespace std; using namespace __gnu_cxx; // gcc编译器要加上这一句,不然编译出错 struct eqstr { bool operator()(const char *s1, const char *s2) { return strcmp(s1, s2) == 0; } }; void lookup(const hash_set<const char *> &Set, const char *word) { hash_set<const char *>::const_iterator iter = Set.find(word); cout << word << ": " << (iter != Set.end() ? "present" : "not present") << endl; } int main(void) { hash_set<const char *> Set; Set.insert("kiwi"); Set.insert("plum"); Set.insert("apple"); Set.insert("mango"); Set.insert("apricot"); Set.insert("banana"); lookup(Set, "mango"); lookup(Set, "apple"); lookup(Set, "durian"); hash_set<const char *>::const_iterator iter; for (iter = Set.begin(); iter != Set.end(); iter++) cout << *iter << " "; cout << endl; return 0; }
hash_map以hashtable为底层结构,因为hash_map所提供的操做接口,hashtable都提供了,因此几乎全部的hash_map操做行为都是转调用hashtable的操做行为结果。RB-tree有自动排序功能而hashtable没有,反映出来的结果就是,map的元素有自动排序功能而hash_map没有。
测试代码:
#include <iostream> #include <hash_map> #include <cstring> #include <algorithm> using namespace std; template <typename T> struct print { void operator()(const T &x) { cout << x.first << ": " << x.second << endl; } }; int main(void) { hash_map<char *, int> days; days["january"] = 31; days["february"] = 28; days["march"] = 31; days["april"] = 30; days["may"] = 31; days["june"] = 30; cout << "march: " << days["march"] << endl; cout << "june: " << days["june"] << endl; cout << "the total elements of hash_map:" << endl; for_each(days.begin(), days.end(), print<pair<char *, int>>()); system("pause"); return 0; }
hash_multiset的特性与multiset彻底相同,惟一的差异在于它的底层机制是hashtable,所以,hash_multiset的元素是不会自动排序的。
hash_multimap的特性与multimap彻底相同,惟一的差异在于它的底层机制是hashtable,所以,hash_multimap的元素是不会自动排序的。
hash_multimap测试用例:
#include <iostream> #include <hash_map> #include <cstring> #include <algorithm> #include <string> using namespace std; template <typename T> struct print { void operator()(const T &x) { cout << x.first << ": " << x.second << endl; } }; int main(void) { hash_multimap<int, string> hmap; hmap.insert(pair<int, string>(2, "32")); hmap.insert(pair<int, string>(2, "22")); hmap.insert(pair<int, string>(2, "12")); hmap.insert(pair<int, string>(2, "2")); for_each(hmap.begin(), hmap.end(), print<pair<int, string>>()); return 0; }
在vs2013(windows 7 64位)下运行结果为:
在Kali2.0中运行(程序需添加using namespace __gun_cxx)结果为:
由运行结果可知,不一样的系统所用的STL是有差异的,不一样的STL的hash_table冲突解决方法不同。
参考:
一、《STL源码剖析》