1. list
一个线性的双向链表(底层),具有链表的特性,不使用连续的内存空间,可以快速的插入和删除,不支持随机的内部访问。
push_back(),push_front(),
pop_back(),pop_front(),
.assgin(2,100);//添加2个100的元素
成员函数:erase(删除元素),swap(交换),clear(清空),remove(移除指定元素),unique(删除重复值),sort(排序) 默认升序,可自写cmp函数,reverse(逆序),merge(合并有序的list)。
访问:随机访问性能很差,只能快速访问头尾节点。
适用场景:经常插入删除大量数据。
2. stack
后进先出,不能遍历。
3. queue
先进先出,不能遍历。
4. priority_queue
将优先级最高的元素始终置于队头。底层通过heap(堆)来实现,所以默认为一个大根堆。
priority_queue <int> que;//创建实例,默认降序
priority_queue <int, vector<int>, greater<int> > que2;//升序
5. vector
连续存储的容器,动态数组,在堆上分配空间。
底层就是数组。
如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。
适用场景:经常随机访问,且不经常对非尾节点进行插入删除。
6. set(集合)
红黑树实现,实现了一个自动排序,元素值唯一的容器。查找的复杂度为(logn),set中的元素值不能直接被修改,在其中的查找属于二分查找。
//find(查找某个值)
se.find(2);//返回2所在的迭代器,否则返回end()
//lower_bound(查找第一个大于等于key的值)upper_bound(查找第一个大于key的值)
se.lower_bound(2);
se.upper.bound(2);
mutiset(可重复插入的set)
7. map&&pair(关联)
特点:增加和删除节点对迭代器的影响很小。
map内部也是通过红黑树实现的(自动有序的),map的形式为一个键值对,和set一样查找的复杂度为o(logn)可以修改value值,但不能修改key值。快速查找,快速增删结点。
- 优点:有序
- 缺点:空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间。
- 适用性:有顺序要求的问题,用map会更高效一些
map与set的区别:
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
区别:
- map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
- set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
- map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
8. Multimap
多重映射。底层实现:红黑树。所有元素都会根据元素的键值自动被排序。允许键值重复。
适用场景:有序键值对可重复映射
9. unordered_map
内部实现了一个哈希表,查找的时间复杂度可达到O(1),其元素的排列顺序是无序的。
- 优点:查找速度非常的快
- 缺点:哈希表的建立比较耗费时间
- 适用性:查找问题
用法:
常用算法
include<algorithm>
//sort(快速排序)stable_sort(稳定排序)
sort(start,end,排序方法);
//reserve(反转容器)
reserve(vec.begin(),vec.end());
//lower_bound,upper_bound(二分查找)//返回的是位置,前提要有序
int num[6]={1,2,4,7,15,34};
sort(num,num+6);
int pos1=lower_bound(num,num+6,7)-num; //返回数组中第一个大于或等于被查数的值
int pos2=upper_bound(num,num+6,7)-num; //返回数组中第一个大于被查数的值
//集合操作(前提容器序列有序)
includes(s1.begin(), s1.end(), s2.begin(), s2.end());
//s1是否包含s2,递增序列使用less<int>(),递减序列使用greater<int>()。
set_union(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(s3));
//求并集,并输入到支持insert操作的s3中,也可以使用back_inserter(s3)输入到支持push_back操作的s3
set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(s3));
//求交集
set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), inserter(s3));
//求差集
//堆操作
make_heap(begin(),end());//对一个序列建立一个堆,默认大根堆,greater<int>()则是小根堆
pop_heap(begin(),end());//将堆顶元素移到序列末尾,一般搭配pop_back();使用
push_heap(begin(), end());//有新元素插入序列末尾后的加入操作。
STL的allocaotr
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
- new运算分两个阶段:(1)调用::operator new配置内存;(2)调用对象构造函数构造对象内容
- delete运算分两个阶段:(1)调用对象希构函数;(2)调用::operator delete释放内存。
为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
STL迭代器删除元素
这个主要考察的是迭代器失效的问题。
- 对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,但是erase会返回下一个有效的迭代器;
- 对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
STL构成
容器 迭代器 仿函数 算法 分配器 配接器
分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数。
STL中迭代器的作用
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
-
迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符->、++、--等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
- 迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
C++ STL 的内存优化
1)二级配置器结构
STL内存管理使用二级内存配置器。
1、第一级配置器
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。
一级空间配置器分配的是大于128字节的空间;
如果分配不成功,调用句柄释放一部分内存;
如果还不能分配成功,抛出异常。
2、第二级配置器
在STL的第二级配置器中多了一些机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。
3、分配原则
如果要分配的区块大于128bytes,则移交给第一级配置器处理。
如果要分配的区块小于128bytes,则以内存池管理(memory pool),又称之次层配置(sub-allocation):每次配置一大块内存,并维护对应的16个空闲链表(free-list)。下次若有相同大小的内存需求,则直接从free-list中取。如果有小额区块被释放,则由配置器回收到free-list中。
当用户申请的空间小于128字节时,将字节数扩展到8的倍数,然后在自由链表中查找对应大小的子链表;
如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块;
如果内存池空间足够,则取出内存;
如果不够分配20块,则分配最多的块数给自由链表,并且更新每次申请的块数;
如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器。
2)二级内存池
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24......120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
空间配置函数allocate
首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。空间释放函数deallocate
首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。重新填充空闲链表refill
在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。
假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。
3)总结
- 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
- 如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。
a. 如果链表不为空,返回第一个node,链表头改为第二个node。
b. 如果链表为空,使用blockAlloc请求分配node。- x. 如果内存池中有大于一个node的空间,分配尽可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。
- y. 如果内存池只有一个node的空间,直接返回给用户。
- z. 若果如果连一个node都没有,再次向操作系统请求分配内存。
①分配成功,再次进行b过程。
②分配失败,循环各个自由链表,寻找空间。(I. 找到空间,再次进行过程b。
II. 找不到空间,抛出异常。)
- 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。
- 否则按照其大小找到合适的自由链表,并将其插入。