一、初识STL
1、STL六大组件
(1)容器(containers):各种数据结构,用于存放数据。如vector,list,deque,set,map
(2)算法(algorithms):常用sort,search,copy
(3)迭代器(iterators):提供一种访问容器中每个元素的方法。扮演容器和算法之间的胶合剂,是所谓的“泛型指针”
(4)仿函数(functors):一个行为类似函数的对象,调用它就像调用函数一样。
(5)配接器(adapters):用于修饰容器、仿函数、迭代器的接口,比如queue和stack,底层借助了deque。
(6)空间配置器(allocators):负责空间配置和管理
2、 请你讲讲STL有什么基本组成
STL主要由六大组件组成:容器、算法、迭代器、仿函数、配接器、空间配置器
它们之间的关系是:空间配置器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
3、STL线程不安全的情况
二:空间配置器(allocators)
空间配置器中的空间不单指内存,还可以是磁盘或其他辅助存储介质
(1)传统:
new:调用::operator new 分配内存 + 构造函数
delete:析构函数 + 调用 ::operator delete 释放内存
(2)STL allocator:
分配内存:alloc:allocate()
释放内存:alloc:deallocate()
构造对象:::construct()
析构对象:::destroy()
考虑小型区块造成的内存破碎问题,SGI设计了双层级配置器:
第一级配置器:allocate()直接使用malloc()、deallocate()直接使用free()。
第二级配置器:视情况使用不同的策略,当配置区块大于128bytes时,调用第一级配置器;当配置区块小于128bytes时,采用内存池的整理方式:配置器维护16个(128/8)自由链表,负责16种小型区块的此配置能力。内存池以malloc配置而得,如果内存不足转调用第一级配置器。
(从内存池中取空间给 free list 使用)
1、请你来介绍一下STL的allocator
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()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
2、空间配置器的标准接口
allocator::allocator() => 默认构造
allocator::allocator(const allocator&) =>拷贝构造
allocator::~allocator() => 析构
pointer allocator::allocate(size_type n,const void* =0) => 分配内存
void allocator::deallocate(pointer p,size_type n) =>释放内存
void allocator::construct(pointer p, const T& x)
void allocator::destroy(pointer p )
3、空间配置器存在的问题
-
自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费。
-
由于配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。释放时机就是程序结束,这样子会导致自由链表一直占用内存,自己进程可以用,其他进程却用不了。
三、迭代器
1、迭代器的五种类别
template <class Iterator>
struct iterator_traits{
typedef typename Iterator::iterator_category iterator_category;
typedef typename Iterator::value_type value_type;
typedef typename Iterator::pointer pointer;
typedef typename Iterator::reference reference;
typedef typename Iterator::difference_type difference_type;
};
(1)value type:迭代器所指对象的类别
(2)difference type:两个迭代器之间的距离,可表示一个容器的最大容量
(3)reference type:如 T&
(4)pointer type: 如 T*
(5)iterator_category
traits就像一台特性萃取机,榨取各个迭代器的特性(相应类别)
2、迭代器的种类
(1)输入迭代器(Input Iterator):是只读迭代器,在每个被遍历的位置上只能读取一次。如find函数参数就是输入迭代器。
(2)输出迭代器(Output Iterator):是只写迭代器,在每个被遍历的位置上只能被写一次。
(3)前向迭代器(Forward Iterator):兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。
(4)双向迭代器(Bidirectional Iterator):可双向移动。
(5)随机访问迭代器(Random Access Iterator):有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。(前三种支持 operator++, 第四种再加上 operator--, 第五种支持前面四种Iterator的所有操作,并另外支持it + n、it - n、it += n、 it -= n、it1 - it2和it[n]等操作)
3、迭代器的底层原理
迭代器是连接容器和算法的一种重要桥梁,通过迭代器可以在不了解容器内部原理的情况下遍历容器。它的底层实现包含两个重要的部分:萃取技术和模板偏特化。
(1)萃取技术(traits)可以进行类型推导,根据不同类型可以执行不同的处理流程,比如traits推导出容器vector的迭代器类型为随机访问迭代器,而list则为双向迭代器。
例如STL算法库中的distance函数,distance函数接受两个迭代器参数,然后计算他们两者之间的距离。显然对于不同的迭代器计算效率差别很大。比如对于vector容器来说,由于内存是连续分配的,因此指针直接相减即可获得两者的距离;而list容器是链式表,内存一般都不是连续分配,因此只能通过一级一级调用next()或其他函数,每调用一次再判断迭代器是否相等来计算距离。vector迭代器计算distance的效率为O(1),而list则为O(n),n为距离的大小。
(2)使用萃取技术(traits)进行类型推导的过程中会使用到模板偏特化,模板偏特化可以用来推导参数。如果类模板拥有一个以上的模板参数,可以对模板参数的一个(或多个,非全部)进行特化。
4、迭代器失效的问题(请你来说一说STL迭代器删除元素)
STL 中某些容器调用了某些成员方法后会导致迭代器失效。 例如 vector 容器,如果调用reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始 迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能 已经被复制或移到了新的内存地址。
1. 序列式容器迭代器失效
对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器 被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。
解决:erase 返回下一个有效的迭代器。
2. 关联式容器迭代器失效
对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删 除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
3、对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
5、 请你来说一下STL中迭代器的作用,有指针为何还要迭代器
(1)Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
(2)迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。
他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、*、++、--等。
迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象, 本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值而不能直接输出其自身。
四、容器
1、STL 容器用过哪些,查找的时间复杂度是多少,为什么?
容器的时间复杂度取决于其底层实现方式。
(1)序列式容器
1) vector 采用一维数组实现,元素在内存连续存放,
不同操作的时间复杂度为: 插入: O(N) 删除: O(N)查看: O(1)
2)deque 采用双向队列实现,元素在内存连续存放,
不同操作的时间复杂度为: 插入: O(N) 删除: O(N)查看: O(1)
3) list 采用双向链表实现,元素存放在堆中,
不同操作的时间复杂度为: 插入: O(1) 删除: O(1)查看: O(N)
4)stack, queue 其实是deque衍生而来的配接器
(2)关联式容器
关联式容器的每个元素都有一个键值(key)和一个实值(value)。关联式容器没有所谓的头尾(只有最大元素和最小元素),所以不会有所谓的 push_back(), push_front(), pop_back(), pop_front(), begin(), end().
4) map、set、multimap、multiset上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。 不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
5)unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,
不同操作的时间复杂度为: 插入: O(1) 查看: O(1) 删除: O(1),最坏情况下均为 O(N)
2、vector
2.1 vector的底层原理
vector底层是一个动态数组,维护一块连续的空间。
包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。
当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间。
当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了。
2.2 vector中的reserve和resize的区别
reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。
resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。
2.3 vector中的size和capacity的区别
size表示当前vector中有多少个元素(finish - start),
而capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage - start)。
2.4 vector中 erase 方法与 algorithn 中的 remove 方法区别
2.5 vector的元素类型可以是引用吗?
vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。
2.6 vector迭代器失效的情况
当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。
erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。
2.7 正确释放vector的内存(clear(), swap(), shrink_to_fit())
vec.clear():清空内容,但是不释放内存。
vector<int>().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
2.8 vector 扩容为什么要以1.5倍或者2倍扩容?
根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以2倍的方式扩容,或者以1.5倍的方式扩容。
以2倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为(1,2)之间
2.9 正确释放vector的内存 ( clear(), swap(), shrink_to_fit() )
2.10 vector的常用函数
vector<int> vec(10,100); 创建10个元素,每个元素值为100
reverse(vec.begin(),vec.end()) 将元素翻转
sort(vec.begin(),vec.end()); 排序,默认升序排列
vec.push_back(val); 尾部插入数字
vec.size(); 向量大小
find(vec.begin(),vec.end(),1); 查找元素
c.assign(n,elem) 复制n个elem,赋值给c
c.assign(begin,end) 将区间[begin,end]内的元素赋值给c
iterator erase(iterator position){} 删除某个位置的元素
iterator erase(iterator first, iterator last){} 删除 [first, last) 中所有的元素
//vector::insert 从position 开始,插入n 个元素,元素初值为 x
void vector<T, Alloc>::insert(interator position, size_type n, constT& x){...}
3、list
(1)list的底层原理
list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。(不仅是一个双向链表,而且是一个环状双向链表,所以只需要一个指针,就可以完整表现整个链表)
list不支持随机存取,如果需要大量的插入和删除,而不关心随机存取可以使用list
(2)list的常用函数
list.size() 返回容器中实际数据的个数
list.sort() 排序,默认由小到大
list.unique() 移除数值相同的连续元素
list.back() 取尾部迭代器
// 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
iterator erase(iterator position)
// 在迭代器 position 所指位置插入一个节点,内容为 x
iterator insert(interator position, const T& x){...}
// 插入一个节点,作为头节点
void push_front(const T& x){}
// 插入一个节点,作为尾节点
void push_back(const T& x){}
// 移除头节点
void pop_front(){}
// 移除尾节点
void pop_back(){}
4、deque
(1)deque的底层原理
deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。(vector 是单向开口的连续线性空间)
deque 是分段连续空间。维持整体“连续”的假象,依靠的是迭代器 operator++ 和 operator--
(2)什么情况下用vector,什么情况下用list,什么情况下用deque
1) vector可以随机存储元素(即不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。
除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
2)list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
3)需要从首尾两端进行插入或删除操作的时候需要选择deque。
(3)deque的常用函数
deque.push_back(elem) 在尾部加入一个数据。
deque.pop_back() 删除尾部数据。
deque.push_front(elem) 在头部插入一个数据。
deque.pop_front() 删除头部数据。
deque.size() 返回容器中实际数据的个数。
deque.at(idx) 传回索引idx所指的数据,如果idx越界,抛出out_of_range。
// 清除 pos 所指的元素,pos为清除点
iterator erase(interator pos){}
// 清除 [first, last) 区间内的所有元素
deque<T, Alloc, Bufsize>::erase(interator first, interator last){}
// 在 position 处插入一个元素,其值为 x
interator insert(interator position, const value_type& x){}
4.1 stack
先进后出的数据结构,只允许操作最顶端的元素,即 stack 没有遍历行为,故也没有迭代器。
除了使用 deque 作为底层容器外,同样可以使用 list 作为底层容器。
4.2 queue
先进先出的数据结构,只允许操作最顶端(出)和最低端的元素(入),即 queue 没有遍历行为,故也没有迭代器。
除了使用 deque 作为底层容器外,同样可以使用 list 作为底层容器。
5、priority_queue
(1)priority_queue的底层原理
priority_queue:优先队列,其底层是用堆来实现的。
在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
(2)priority_queue的常用函数
priority_queue<int, vector<int>, greater<int>> pq; 最小堆
priority_queue<int, vector<int>, less<int>> pq; 最大堆
pq.empty() 如果队列为空返回真
pq.pop() 删除对顶元素
pq.push(val) 加入一个元素
pq.size() 返回优先队列中拥有的元素个数
pq.top() 返回优先级最高的元素
6、map 、set、multiset、multimap
6.1 请你说一说STL中 map 数据存放形式
红黑树。unordered map底层结构是哈希表
6.2 map 、set、multiset、multimap 的底层原理
map 、set、multiset、multimap的底层实现都是红黑树 RB-tree
红黑树的特性:
1. 每个结点不是红色就是黑色;
2. 根结点是黑色;
3. 如果结点为红,则子节点必须为黑;
4. 任一结点到达NULL(树尾端)的任何路径,包含相同数目的黑色结点。
对于STL的map容器,count方法与find方法,都可以用来判断一个key是否出现,mp.count(key) > 0统计的是key出现的次数,因为只能为0/1,而mp.find(key) != mp.end()则表示key存在。
6.3 set的底层实现实现为什么不用哈希表而使用红黑树?
6.4 map中红黑树最长路径和最短路径的差值
最短路径为全黑,最长路径就是红黑节点交替(因为红色节点不能连续),每条路径的黑色节点相同,则最长路径和最短路径的差值、刚好是最短路径的两倍。
6.5 map 、set、multiset、multimap的特点
1)set和multiset会根据特定的排序准则自动将元素递增排序,set中元素不允许重复,multiset可以重复。( set 使用的是红黑树的 insert_unique(), multiset 使用的是红黑树的 insert_equal())
(不能通过 set 的迭代器改变 set 的元素值,因为 set 元素值就是键值,关系到 set 元素的排序规则,set 底层迭代器是红黑树的 const_interator)
2)map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素递增排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的)。map中元素的key不允许重复,multimap可以重复,所以 map 适用于有序键值对不重复映射,multimap 适用于有序键值对可重复映射。
(map 的key 不能被修改,而value 是可以被修改的)
map和set的增删改查速度为都是logn,是比较高效的。
6.6 为何 map 和 set 的插入删除效率比其他序列容器高,而且每次 insert 之后,以前保存的iterator不会失效?
因为存储的是结点,不需要内存拷贝和内存移动。
因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。
6.7 为何map和set不能像vector一样有个 reserve 函数来预分配数据?
因为在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。
6.8 当数据元素增多时(从10000到20000),map和set的查找速度会怎样变化?
6.9 map 、set、multiset、multimap的常用函数
it map.begin() 返回指向容器起始位置的迭代器(iterator)
it map.end() 返回指向容器末尾位置的迭代器
bool map.empty() 若容器为空,则返回true,否则false
it map.find(k) 寻找键值为k的元素,并用返回其地址
int map.size() 返回map中已存在元素的数量
map.insert({int,string}) 插入元素
for (iter = map.begin(); iter != map.end();)
{
if (iter->second == "target")
map.erase(iter++) ; // erase之后,令当前迭代器指向其后继。
else
++iter;
}
7、hashtable
思想:采用某种映射函数,将大数映射为小数。
可能会有不同的元素被映射到相同的位置(碰撞问题)。
解决碰撞问题:线性探测,二次探测,开链
7.1 请你来说一说hash表的实现,包括STL中的哈希桶长度常数
hash表的实现主要包括构造哈希和处理哈希冲突两个方面:
对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数法等。
解决碰撞问题:线性探测,二次探测,开链
虽然链地址法并不要求哈希桶长度必须为质数,但SGI STL仍然以质数来设计哈希桶长度,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
7.2 hash_map与map的区别?什么时候用hash_map,什么时候用map?
8、unordered_map、unordered_set
8.1 unordered_map、unordered_set的底层原理
unordered_map的底层是一个哈希表。哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。
8.2 unordered_map 与map的区别?使用场景?
存储结构:unordered_map 采用hash表存储,map一般采用红黑树(RB Tree) 实现。因此其memory数据结构是不一样的。
总体来说,unordered_map 查找速度会比map快,而且查找速度基本和数据数据量大小,属于常数级别; 而map的查找速度是log(n)级别。
8.3 unordered_map、unordered_set的常用函数
unordered_map.begin() 返回指向容器起始位置的迭代器(iterator)
unordered_map.end() 返回指向容器末尾位置的迭代器
unordered_map.cbegin() 返回指向容器起始位置的常迭代器(const_iterator)
unordered_map.cend() 返回指向容器末尾位置的常迭代器
unordered_map.size() 返回有效元素个数
unordered_map.insert(key) 插入元素
unordered_map.find(key) 查找元素,返回迭代器
unordered_map.count(key) 返回匹配给定主键的元素的个数
9、 请你回答一下STL里resize和reserve的区别
(1)resize():改变当前容器内含有元素的数量(size()).
eg: vector<int>v; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
(2)reserve():改变当前容器的最大容量(capacity).
它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;
五、算法
完成了常见的算法:排序(sort),查找(find),复制(copy),交换(swap),反转(reverse)等
// 将指定元素插入区间之内而不改变原有排列顺序的最低位置
lower_bound(forwardIterator first, forwardIterator last, const T& value){}
// 将指定元素插入区间之内而不改变原有排列顺序的最高位置
upper_bound(forwardIterator first, forwardIterator last, const T& value){}
六、仿函数
仿函数:所谓仿函数,是一个定义了operator()的对象,它不是函数,而是一个类,该类重载了()操作符
仿函数三大优点:
(1)仿函数比一般函数更灵巧,因为它可以拥有状态。
(2)每个仿函数都有其类型。
(3)执行速度上,仿函数通常比函数指针更快。
仿函数按功能划分
(1)算术类仿函数:
加: plus<T>
减: minus<T>
乘: multiplies<T>
除: divides<T>
模数:modulus<T>
否定:negate<T>
(2)关系运算类仿函数
等于: equal_to<T>
不等于: not_equal_to<T>
大于: greater<T>
大于或等于:greater_equal<T>
小于: less<T>
小于或等于:less_equal<T>
(3)逻辑运算类仿函数
逻辑运算and: logical_and<T>
逻辑运算or: logical_or<T>
逻辑运算not: logical_not<T>
七、配接器(adapters)
将一个类的接口转换成另一个类的接口,使原本因接口不兼容而不能合作的类可以一起运作,即配接器用于改变接口。(相当于转换器)
STL主要提供三种配接器:
- 改变仿函数接口,functor adapter
- 改变容器接口,container adapter
- 改变迭代器接口,iterator adapter
(1)应用于仿函数:是所有配接器中数量最为庞大的一个族群,可以多次配接,配接操作包括系结、否定、组合,以及对一般函数或成员函数的修饰(使其成为一个仿函数)。C++标准规定配接器的接口可由<functional>获得。
作用:仿函数配接器可以通过它们之间的绑定、组合、修饰能力,几乎可以无限制地创造出各种可能的表达式。
(2)应用于容器:标准程序库提供的queue和stack,其实都只不过是一种配接器,都是对deque接口的修饰而成就自己的容器风貌,序列式容器set和map是对其内部所维护的平衡二叉树接口改造
(3)应用于迭代器:STL提供应用于迭代器身上的配接器,这些接口可以由<iterator>获得。
1) insert iterators:将一般迭代器的赋值操作转变为插入操作
2)reverse iterators:将一般迭代器的行进方向逆转
3)iostream iterators:将迭代器绑定到某个 iostream 对象身上