C/C++面试:请介绍一下STL

1059 篇文章 284 订阅

请介绍一下STL

STL一共有六大组件,包括容器、算法、迭代器、仿函数、配置器和配接器,彼此可以组合套用。容器通过配置器取得数据存储空间,算法通过迭代器存取容器内容,仿函数可以协助算法完成不同的策略变化,配接器可以应用于容器、仿函数和迭代器。

  • 容器:各种数据结构,比如vector、list、map等,用来存放数据,从实现的角度来看是一种类模板
  • 算法:各种常用的算法,如 sort(插⼊,快排,堆排序),search(⼆分查找),从实现的角度来看是一种方法模板
  • 迭代器:从实现的角度来看,迭代器是一种将operator*、operator->、operator++、operator--等指针相关操作赋予重载的类模板,所有的STL容器都有自己的迭代器
  • 仿函数:从实现的⻆度看,仿函数是一种重载了operator()的类或者类模板。可以帮助算法实现不同的策略
  • 配接器:一种用来修饰容器或者仿函数或者迭代器接口的东西
  • 配置器:负责空间配置和管理,从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的类模板

内存管理 allocator

SGI设计了双层级配置器

  • 第一级配置器直接使用malloc()和free()完成内存的分配和回收。
  • 第二级配置器则根据需求量的大小选择不同的策略执行。

对于第二级配置器

  • 如果需求块⼤⼩⼤于 128bytes,则直接转而调用第一级配置器,使用malloc()分配内存。
  • 如果需求块大小小于128bytes,第二级配置器中维护了16个自由链表,负责16种小型区块的此配置能力
    • 即当有⼩于 128bytes 的需求块要求时,⾸先查看所需需求块⼤⼩所对应的链表中是否有空闲空间,如果有则直接返回,如果没有,则向内存池中申请所需需求块⼤⼩的内存空间,如果申请成功,则将其加⼊到⾃由链表中。如果内存池中没有空间,则使⽤ malloc() 从堆中进⾏申请,且申请到的⼤⼩是需求ᰁ的⼆倍(或⼆倍+n 附加量),⼀倍放在⾃由空间中,⼀倍(或⼀倍+n)放⼊内存池中。
    • 如果 malloc()也失败,则会遍历⾃由空间链表,四处寻找“尚有未⽤区块,且区块够⼤”的 freelist,找到⼀块就挖出一块返回。如果还是没有,扔交给malloc()处理,因为malloc()有out-of-memory处理机制或者有机会会释放其他的内存拿来用,如果可以就成功,如果不行就报bad_alloc异常

STL中序列式容器的实现

(1)vector

  • 是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。vector维护的是一个连续的线性空间,而且普通指针就可以满足要求作为vector的迭代器(RandomAccessIterator)。
  • vector的数据结构其实就是三个迭代器构成的,一个指向目前使用空间头的iterator,一个指向目前使用空间尾的iterator,一个指向目前可用空间尾的iterator。当有新元素插入时,如果目前容器足够用则直接插入,如果不够容量就扩充两倍,如果两倍还不够,就扩充至足够大的容量
  • 扩充的过程并不是直接在原有空间后面追加容器,而是重新申请一块连续空间,将原有的数据拷贝到新空间中,再释放原有空间,完成一次扩充。需要注意的是,每次扩充是重新开辟的空间,所以扩充后,原有的迭代器将会失效

(2)list

  • 与vector相比,list的好处是每次插入或者删除一个元素,就配置或者释放一个空间,而且原有的迭代器也不会失效
  • STL list是一个双向链表,普通指针已经不能满足list迭代器的需求,因为list的存储空间是不连续的。
  • list的迭代器必须具备前移和后退的功能,所以list提供的是BidirectionalIterator。
  • list 的数据结构中只要⼀个指向 node节点的指针就可以了。

(3)deque

  • vector是单向开口的连续线性空间,deque是一种双向开口的连续线性空间。所谓双向开口,说的是deque支持从头尾两端进行元素的插入和删除。 相比于vector的扩充空间的方式,deque实际上更加贴切的实现了动态空间的概念。deque没有容量的概念,因为它是动态的已分段连续空间组合而成,随时可以增加一段新的空间并连接起来
  • 由于要维护这种整体连续的假象,并提供随机存取的接⼝(即也提供 RandomAccessIterator),避开了“重新配置,复制,释放”的轮回,代价是复杂的迭代器结构。也就是说除非必要,我们应该尽可能的使用vector,而不是deque

那deque是如何做到维护整体连续的假象的呢?

  • deque采用一块所谓的map作为主控,这里的map实际上就是一块大小连续的空间,其中每一个元素,我们称之为节点node,都指向了另一端连续线性空间称为缓冲区,缓冲区才是deque的真正存储空间的主体
  • STL是运行我们指定缓冲区的大小的,默认0表示使用512bytes缓冲区。当map满载时,我们选用一块更大的空间来作为map,重新调整配置。
  • deque另一个关键是它的iterator 的设计,deque 的 iterator 中有四个部分,cur 指向缓冲区现⾏元素,first 指向缓冲区的头,last 指向缓冲区的尾(有时会包含备⽤空间),node指向管控中⼼。所以总结来说,deque的数据结构中包含了,指向第⼀个节点的iterator start, 和指向最后⼀个节点的 iterator finish,⼀块连续空间作为主控 map,也需要记住 map 的⼤⼩,以备判断何时配置更⼤的 map。

(4)stack

  • stack是一种先进后出的数据结构,只有一个出口,stack运行从最顶端新增元素,移除最顶端元素,取得最顶端元素
  • deque是一种双向开口的数据结构,所以使用deque作为底部结构并封装其头部开口,就形成了一个stack

(5)queue

  • 是⼀种先进先出的数据结构,有两个出⼝,允许从最底端加⼊元素,取得最顶端元素,从最底端新增元素,从最顶端移除元素。
  • deque 是双向开⼝的数据结构,若以 deque 为底部结构并封闭其底端的出⼝,和头端的⼊⼝,就形成了⼀个 queue。(其实 list 也可以实现 deque)

(6)heap

  • 堆并不属于STL容器组件,它是个幕后英雄,扮演 priority_queue 的助⼿
  • priority_queue 允许用户以任何次序将任何元素压入容器中,但是取出时一定是从优先级最高(数值最高)的元素开始取。⼤根堆(binary max heap)正具有这样的性质,适合作为 priority_queue 的底层机制。

(7)priority_queue

  • 底层时⼀个 vector,使⽤ heap 形成的算法,插⼊,获取 heap 中元素的算法,维护这个 vector,以达到允许⽤户以任何次序将任何元素插⼊容器内,但取出时⼀定是从优先权最⾼(数值最⾼)的元素开始取的⽬的。

(8)slist:STL list 是⼀个双向链表, slist 是⼀个单向链表

map和set有什么区别?

map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。

由于map和set所开放的各种操作接口,RB-tree也都提供了,所以几乎所有的map和set的操作行为,都只是转掉RB-tree的操作行为。

map和set区别在于:

  • map中的元素是key-value对,key起到索引的作用,value表示与索引相关联的数据;set是关键字的简单集合,set中每个元素只包含一个关键字
  • set的迭代器但是const的,不允许修改元素的值;map运行修改value,但是不允许修改key。原因是map和set都是根据根据关键字排序来保证其有序性的,如果运行修改key的话,那么首先要删除该key,然后调节平衡,再插⼊修改后的键值,调节平衡,如此⼀来,严᯿破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key的值,运行修改map的值
  • map支持下标操作,set不支持下标操作。map可以⽤key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果key不存在,则插入一个具有该key和mapped_type类型默认值的元素到map中,因此下标运算符[]在map应用中要慎用,cosnt_map不能用,只希望确定某一个关键值是否存在而不希望插入元素也不应该使用,mapped_type类型没有默认值也不应该使⽤。如果find能解决需要,尽可能有find

请介绍下STL迭代器删除元素

  • 对于序列容器vector、deque来说,使用erase(iterator)后,后面的每个元素的迭代器都会失效,但是后面每个元素都会往前移动一个位置,erase会返回下一个有效的迭代器
  • 对于关联容器map、set来说,使用erase(iterator)后,当前元素的迭代器会失效,但是其结构是红黑叔,删除当前元素不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器就可以了
  • 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种方法都可以正确使用

STL中迭代器的作用,有了指针为什么还要迭代器

迭代器的作用:

  • Iterator(迭代器)模式⼜称 Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不续约暴露该对象的内部表示。或者这样说可能更容易理解:I迭代器模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不适用对象内部表示的情况下,按照一定的顺序(由iterator提供的⽅法)访问聚合对象中的各个元素
  • 由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,比如STL的list、vector、stack 等容器类及ostream_iterator等扩展iterator。

迭代器和指针的区别:

  • 迭代器不是指针,是类模板,表现的像指针。它支持模拟量指针的一些功能,通过重载了指针的一些操作符,->、*、++、–等。迭代器封装了指针,是一个“可遍历STL容器内全部或者部分元素”的对象,本质是封装了原生指针,值指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,它可以根据不同类型的数据结构来实现不同过的++、–等操作
  • 迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用*取值后的值⽽不能直接输出其⾃身。

迭代器产⽣原因:

  • Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果

STL ⾥ resize 和 reserve 的区别

  • resize():改变当前容器内含有元素的数量(size()),eg: vector< v>; v.resize(len);v的size变为len,如果原来v的size⼩于len,那么容器新增(len-size)个元素,元素的值为默认为0。当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
  • reserve ():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器重新分配⼀块能存len个对象的空间,然后把之前v.size()个对象通过 copy construtor 复制过来,销毁之前的内存;

vector和list的区别

  • vector用用段连续的内存空间,因此支持随机存取,如果需要高效的随机存取,而不在乎插入和删除的效率,用vector
  • list拥有一段不连续的内存空间,因此不支持随机存储,如果需要大量的插入和删除,而不关心随机存取,用list

容器分类

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值