MySTL项目常见面试题目(C++)

一、初识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线程不安全的情况

在对同一个容器进行多线程的读写、写操作时;
在每次调用容器的成员函数期间都要锁定该容器;
在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
在每个在容器上调用的算法执行期间锁定该容器。

二:空间配置器(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. 序列式容器迭代器失效

对于序列式容器,例如 vectordeque,由于序列式容器是组合式容器,当当前元素的迭代器 被删除后,其后的所有元素的迭代器都会失效,这是因为 vectordeque都是连续存储的一段空,所以当对其进行 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只能输出迭代器使用*取值后的值而不能直接输出其自身。

 四、容器

1STL 容器用过哪些,查找的时间复杂度是多少,为什么?

容器的时间复杂度取决于其底层实现方式。

(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 方法区别

vector中erase方法真正删除了元素,迭代器不能访问了
remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。


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() )

vec.clear():清空内容,但是不释放内存。
vector().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.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的底层实现实现为什么不用哈希表而使用红黑树?

set中元素是经过排序的,红黑树也是有序的,哈希是无序的
如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时 间复杂度为O(lgn)


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的查找速度会怎样变化?

RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到 log20000=15次,多了1次而已。


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?

构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。
存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。 总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的 查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。
如果考虑效率,特别当元素达到一定数量级时,用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 对象身上

STL 中某些容器调用了某些成员方法后会导致迭代器失效。 例如 vector 容器,如果调用reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始 迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能 已经被复制或移到了新的内存地址

1. 序列式容器迭代器失效

对于序列式容器,例如 vectordeque,由于序列式容器是组合式容器,当当前元素的迭代器 被删除后,其后的所有元素的迭代器都会失效,这是因为 vectordeque都是连续存储的一段空,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。

解决:erase 返回下一个有效的迭代器。

2. 关联式容器迭代器失效

对于关联容器,例如如 map set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树实现,插入、删 除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。

3、对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。

猜你喜欢

转载自blog.csdn.net/weixin_47887421/article/details/125953774
今日推荐