NO.6.STL容器和算法|allocator|序列式容器|vector|map|set|STL迭代器|resize|reserve

![[Pasted image 20250311200948.png]]

1. C++ 的 STL 介绍(内存管理, allocator,函数,实现机理,多线程实现等)

六大组件

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

  1. 容器: 各种数据结构,如 vector, list, deque, set, map,⽤来存放数据, 从实现的⻆度来讲是⼀种类模板。
  2. 算法: 各种常⽤的算法,如 sort(插⼊,快排,堆排序), search(⼆分查找), 从实现的⻆度来讲是⼀种⽅法模板。
  3. 迭代器: 从实现的⻆度来看,迭代器是⼀种将 operator*,operator->,operator++, operator–等指针相关操作赋予重载的类模板,所有的 STL 容器都有⾃⼰的迭代器。
  4. 仿函数: 从实现的⻆度看,仿函数是⼀种重载了 operator()的类或者类模板。 可以帮助算法实现不同的策略。
  5. 配接器: ⼀种⽤来修饰容器或者仿函数或迭代器接⼝的东⻄。
  6. 配置器: 负责空间配置与管理,从实现的⻆度讲,配置器是⼀个实现了动态空间配置、空间管理,空间释放的类模板。
内存管理 allocator

SGI 设计了双层级配置器,第⼀级配置器直接使⽤ malloc()和 free()完成内存的分配和回收。第⼆级配置器则根据需求量的⼤⼩选择不同的策略执⾏。

对于第⼆级配置器,如果需求块⼤⼩⼤于 128bytes,则直接转⽽调⽤第⼀级配置器,使⽤malloc()分配内存。如果需求块⼤⼩⼩于 128bytes,第⼆级配置器中维护了 16 个⾃由链表,负责 16 种⼩型区块的次配置能⼒。

即当有⼩于 128bytes 的需求块要求时,⾸先查看所需需求块⼤⼩所对应的链表中是否有空闲空间,如果有则直接返回,如果没有,则向内存池中申请所需需求块⼤⼩的内存空间,如果申请成功,则将其加⼊到⾃由链表中。如果内存池中没有空间,则使⽤ malloc() 从堆中进⾏申请,且申请到的⼤⼩是需求量的⼆倍(或⼆倍+ n 附加量),⼀倍放在⾃由空间中,⼀倍(或⼀倍+ n)放⼊内存池中。

如果 malloc()也失败,则会遍历⾃由空间链表,四处寻找“尚有未⽤区块,且区块够⼤”的freelist,找到⼀块就挖出⼀块交出。如果还是没有,仍交由 malloc()处理,因为 malloc() 有out-of-memory 处理机制或许有机会释放其他的内存拿来⽤,如果可以就成功,如果不⾏就报bad_alloc 异常。

序列式容器的实现
  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 的真正存储空间主体。

SGI STL 是允许我们指定 缓冲区的⼤⼩的,默认 0 表示使⽤ 512bytes 缓冲区。当 map 满载时,我们选⽤ ⼀块更⼤的空间来作为 map,重新调整配置。 deque 另外⼀个关键的就是它的iterator 的设计, deque 的 iterator 中有四个部分, cur 指向缓冲区现⾏元素, first 指向缓冲区的头, last 指向缓冲区的尾(有时会包含备⽤空间), node 指向管控中⼼。所以总结来说,deque的数据结构中包含了,指向第⼀个节点的iterator start, 和指向最后⼀个节点的 iteratorfinish,⼀块连续空间作为主控 map,也需要记住 map 的⼤⼩,以备判断何时配置更⼤的map。

stack

是⼀种先进后出的数据结构,只有⼀个出⼝, stack 允许从最顶端新增元素,移除最顶端元素,取得最顶端元素。 deque 是双向开⼝的数据结构,所以使⽤ deque 作为底部结构并封闭其头端开⼝,就形成了⼀个 stack。

queue

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

heap

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

⼤根堆,是⼀个满⾜每个节点的键值都⼤于或等于其⼦节点键值的⼆叉树(具体实现是⼀个vector,⼀块连续空间,通过维护某种顺序来实现这个⼆叉树),新加⼊元素时,新加⼊的元素要放在最下⼀层为叶节点,即具体实现是填补在由左⾄右的第⼀个空格(即把新元素插⼊在底层 vector 的 end()),然后执⾏⼀个所谓上溯的程序:将新节点拿来与 ⽗节点⽐较,如果其键值⽐⽗节点⼤,就⽗⼦对换位置,如此⼀直上溯,直到不需要对换或直到根节点为⽌。当取出⼀个元素时,最⼤值在根节点,取⾛根节点,要割舍最下层最右边的右节点,并将其值重新安插⾄最⼤堆,最末节点放⼊根节点后,进⾏⼀个下溯程序:将空间节点和其较⼤的节点对调,并持续下⽅,直到叶节点为⽌。

priority_queue

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

slist:

STL list 是⼀个双向链表, slist 是⼀个单向链表。

2. vector 使⽤的注意点及其原因,频繁对 vector 调⽤ push_back() 性能影响

使⽤注意点:

注意插⼊和删除元素后迭代器失效的问题;
清空 vector 数据时,如果保存的数据项是指针类型,需要逐项 delete,否则会造成内存泄漏。

频繁调⽤ push_back()影响:

向 vector 的尾部添加元素,很有可能引起整个对象 存储空间的重新分配,重新分配更⼤的内存,再将原数据拷⻉到新空间中,再释 放原有内存,这个过程是耗时耗⼒的,频繁对 vector调⽤ push_back()会导致性能的下降。

在 C++11 之后, vector 容器中添加了新的⽅法: emplace_back() ,和 push_back()⼀样的是都是在容器末尾添加⼀个新的元素进去,不同的是 emplace_back() 在效率上相⽐较于 push_back() 有了⼀定的提升。

emplace_back() 函数在原理上⽐ push_back() 有了⼀定的改进,包括在内存优化⽅⾯和运⾏效率⽅⾯。内存优化主要体现在使⽤了就地构造(直接在容器内构造对象,不⽤拷⻉⼀个复制品再使⽤) +强制类型转换的⽅法来实现,在运⾏效率⽅⾯,由于省去了拷⻉构造过程,因此也有⼀定的提升。

3. map 和 set 有什么区别,分别⼜是怎么实现的?

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

由于 map 和 set 所开放的各种操作接⼝, RB-tree 也都提供了,所以⼏乎所有的 map 和 set的操作⾏为,都只是转调 RB-tree 的操作⾏为。

map 和 set 区别在于:

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

4. 请你来说⼀说 STL 迭代器删除元素

这个主要考察的是迭代器失效的问题。

对于序列容器 vector, deque来说,使⽤ erase(itertor) 后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动⼀个位置,但是 erase 会返回下⼀个有效的迭代器;
对于关联容器 map set 来说,使⽤了 erase(iterator) 后,当前元素的迭代器失效,但是其结构是红⿊树,删除当前元素的,不会影响到下⼀个元素的迭代器,所以在调⽤ erase 之前,记录下⼀个元素的迭代器即可。

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

5. 请你来说⼀下 STL 中迭代器的作⽤,有指针为何还要迭代器

迭代器

Iterator(迭代器)模式⼜称 Cursor(游标)模式,⽤于提供⼀种⽅法顺序访问⼀个聚合对象中各个元素, ⽽⼜不需暴露该对象的内部表示。或者这样说可能更容易理解: Iterator模式是运⽤于聚合对象的⼀种模式,通过运⽤该模式,使得我们可以在不知道对象内部表示的情况下,按照⼀定顺序(由iterator提供的⽅法)访问聚合对象中的各个元素。

由于Iterator模式的以上特性:与聚合对象耦合,在⼀定程度上限制了它的⼴泛运⽤,⼀般仅⽤于底层聚合⽀持类,如STL的list、 vector、 stack 等容器类及ostream_iterator等扩展iterator。

迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的⼀些功能,通过重载了指针的⼀些操作符, ->*++--等。迭代器封装了指针,是⼀个“可遍历STL( StandardTemplate Library)容器内全部或部分元素”的对象, 本质是封装了原⽣指针,是指针概念的⼀种提升(lift),提供了⽐指针更⾼级的⾏为,相当于⼀种智能指针,他可以根据不同类型的数据结构来实现不同的++, --等操作。

迭代器返回的是对象引⽤⽽不是对象的值,所以cout只能输出迭代器使⽤*取值后的值⽽不能直接输出其⾃身。

迭代器产⽣原因

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

6. 回答⼀下 STL ⾥ resize 和 reserve 的区别

resize()

改变当前容器内含有元素的数量(size()),
eg: vectorv; v.resize(len);
v的size变为len,如果原来v的size⼩于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;

reserve()

改变当前容器的最⼤容量(capacity) ,它不会⽣成元素,只是确定这个容器允许放⼊多少对象,如果reserve(len)的值⼤于当前的capacity(),那么会重新分配⼀块能存len个对象的空间,然后把之前v.size()个对象通过 copy construtor 复制过来,销毁之前的内存;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值