STL源码

 

 

 

(1)请你讲讲STL有什么基本组成:

 

容器、迭代器、仿函数、算法、分配器、配接器

 

他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数

 

偏特化:如果class template拥有一个以上的template参数,我们可以针对其中某个template参数进行特化工作(我们可以在泛化设计中提供一个特化版本)

 

 

13C++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()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。

 

 

19C ++内存管理,内存池技术(热门问题),与csapp中几种内存分配方式对比学习加深理解

 

13STL的内存优化

1. 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc

2. 如果需要的内存大小小于128bytesallocate根据size找到最适合的自由链表。

a. 如果链表不为空,返回第一个node,链表头改为第二个node

b. 如果链表为空,使用blockAlloc请求分配node

x. 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20),将一个node返回,其他的node添加到链表中。

y. 如果内存池只有一个node的空间,直接返回给用户。

z. 若果如果连一个node都没有,再次向操作系统请求分配内存。

①分配成功,再次进行b过程。

②分配失败,循环各个自由链表,寻找空间。

I. 找到空间,再次进行过程b

II. 找不到空间,抛出异常。

3. 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free

4. 否则按照其大小找到合适的自由链表,并将其插入。

 

 

 


14STL源码中的hash表的实现

 


15STLunordered_mapmap的区别和应用场景

 

Map底层结构是红黑树。unordered map底层结构是哈希表

 

map map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树存储的,使用中序遍历可将键值按照从小到大遍历出来。

 

unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的

 

15)请你说说STLMultimapMap的区别

 

1Map映射,map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。

 

底层实现:红黑树

 

适用场景:有序键值对不重复映射

 

2Multimap

 

多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。

 

底层实现:红黑树

 

适用场景:有序键值对可重复映射

 

16STLvector的实现

 

关于vector简单的说就是一个动态增长的数组,里面有一个指针指向一片连续的内存空间,当空间装不下要容纳的数据的时候会自动申请一片更大的空间(空间配置器)将原来的数据拷贝到新的空间,然后就会释放旧的空间。当删除的时候空间并不会释放只是清空了里面的数据。

 

vector的数据安排以及操作方式与数组非常相似,两者唯一区别在于空间运用的灵活性,数组是静态空间一旦配置了就不能再改变大小,如果要增容的话,就要把数据搬到新的数组里面,然后再把原来的空间释放掉还给操作系统。vector是动态的随着元素的增加,它的内部机制会自动的扩充空间来容纳新的元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们不必害怕空间不足而一开始就开辟一块很大的内存。

 

vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector的旧空间满载了,如果客户端每新增加一个元素,vector的内部只是扩充了一个元素空间,其实这样是比较不明智的。因为所谓的扩充空间(无论多大),过程都是配置新空间——数据移动——释放旧空间,成本还是比较高的。vector维护的是一个连续的线性空间,所以vector支持随机访问。

 

 

vector的动态增加大小的时候,并不是在原有的空间上持续增加成新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原来的内容拷贝过来,并释放原来的空间。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了,这是比较容易犯的一个错误。

 

 

 

17STL容器的几种迭代器以及对应的容器(输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器)

 

顺序容器:vector,deque是随机访问迭代器;list是双向迭代器

 

容器适配器:stack,queue,priority_queue没有迭代器

 

关联容器:set,map,multiset,multimap是双向迭代器

 

unordered_set,unordered_map,unordered_multiset,unordered_multimap是前向迭代器

 

 

18STL中的traits技法

STL源码剖析

type_traits

iterator_traits

char traits

allocator_traits

pointer_traits

array_traits

 

19 vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间

 

 

20)请你来说一下mapset有什么区别,分别又是怎么实现的?

 

mapset都是C++的关联容器,其底层实现都是红黑树(RB-Tree。由于 map set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map set的操作行为,都只是转调 RB-tree 的操作行为。

 

mapset区别在于:

 

map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。

 

set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为mapset是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了mapset的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。

 

map支持下标操作,set不支持下标操作map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,

 

mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find

 https://www.nowcoder.com/discuss/609412

 

21)请你来说一说STL迭代器删除元素

 

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

 

1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,erase会返回下一个有效的迭代器

2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。 

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

 

 

22)请你来说一下STL中迭代器的作用,有指针为何还要迭代器

 

1、迭代器

 

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

 

由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STLlistvectorstack等容器类及ostream_iterator等扩展iterator

 

 

 

2、迭代器和指针的区别

 

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

 

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

 

3、迭代器产生原因

 

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

 

 

 

23)请你回答一下STLresizereserve的区别

 

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

 

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

 

posted @   淡然那片天  阅读(196)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示