相关系列文章
面试指南:C++之STL知识点
C++内存分配策略
深入理解STL空间分配器(一): new_allocator
深入理解STL空间分配器(二):mt_allocator
深入理解STL空间分配器(三):pool_allocator
深入理解STL空间分配器(四):bitmap_allocator
目录
1.讲讲STL的六大组件
2.vector
2.1.简单说说vector
2.2.vector底层原理
2.3.vector为什么要用加倍扩容而不是每次增加一个固定的扩容容量
2.4.vector扩容的过程
2.5.vector的扩容方式为什么是1.5倍或2倍
2.6.size、resize、reserve、capacity的区别
2.7.vector迭代器失效问题
2.8.对于vector可能导致其迭代器失效的操作有哪些
2.9.push_back和emplace_back的区别
2.10.频繁对vector调用push_back()对性能的影响和原因
3.list
3.1.聊聊STL库中的list
3.2.list的底层原理
3.3.list的常用函数
3.4.vector插入、list插入区别
3.5.vector和list的优缺点
3.6.vector和list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?
3.7.总结一下vector和list具体是怎么实现的呢?常见操作的时间复杂度是多少?
4.deque
4.1.简单说说deque
4.2.详细讲讲deque实现原理
4.3.你了解deque的中控器吗
4.4.deque的迭代器是怎么回事呢
4.5.说一说deque的数据结构
4.6.deque常用的函数
4.7.vector、list、deque的选择原则
5.说说STL迭代器是怎么删除元素的
6.了解优先级队列吗?细节讲讲
7.你知道STL容器动态链接可能产生的问题吗
8.map、unordered_map和set
8.1.你了解map和unordered_map嘛?底层实现呢
8.2.map和unordered_map的优缺点
8.3.set的底层实现为什么不用哈希表而是用红黑树
8.4.为什么map和set和插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效
8.5.为什么map和set不能像vector一样有个reserve函数来预分配数据
8.6.你知道map和set有什么区别嘛?分别是怎么实现的呢
8.7.vector越界访问下标,map越界访问下标,vector删除元素时会不会释放空间
8.8.map中[ ]与find的区别
8.9.hash_map与map的区别?什么时候用hash_map,什么时候用map
8.10.讲一讲set的用法和它的特点
9.你了解STL的内存优化吗
1.讲讲STL的六大组件
- 容器(Containers):各种数据结构,如Vector,List,Deque,Set,Map,用来存放数据,STL容器是一种Class Template,就体积而言,这一部分很像冰山载海面的比率。
- 算法(Algorithms):各种常用算法如Sort,Search,Copy,Erase,从实现的角度来看,STL算法是一种Function Templates。
- 迭代器(Iterators):扮演容器与算法之间的胶合剂,是所谓的“泛型指针”,共有五种类型,以及其它衍生变化,从实现的角度来看,迭代器是一种将:Operators*,Operator->,Operator++,Operator--等相关操作予以重载的Class Template。所有STL容器都附带有自己专属的迭代器——是的,只有容器设计者才知道如何遍历自己的元素,原生指针(Native pointer)也是一种迭代器。
- 仿函数(Functors):行为类似函数,可作为算法的某种策略(Policy),从实现的角度来看,仿函数是一种重载了Operator()的Class 或 Class Template。一般函数指针可视为狭义的仿函数。
- 适配器(配接器)(Adapters):一种用来修饰容器(Containers)或仿函数(Functors)或迭代器(Iterators)接口的东西,例如:STL提供的Queue和Stack,虽然看似容器,其实只能算是一种容器配接器,因为 它们的底部完全借助Deque,所有操作有底层的Deque供应。改变Functor接口者,称为Function Adapter;改变Container接口者,称为Container Adapter;改变Iterator接口者,称为Iterator Adapter。配接器的实现技术很难一言蔽之,必须逐一分析。
- 分配器(Allocators):负责空间配置与管理,从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的Class Template。
2.vector
2.1.简单说说vector
动态开辟的二维数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
2.2.vector底层原理
- 数据安排及操作方式与array非常相似。两者的唯一差别在于空间运用的灵活性。
- 静态空间,一旦配置好了就不能改变了,如果程序需要一个更大的array,只能自己再申请一个更大的array,然后将以前的array中的内容全部拷贝到新的array中。
- 动态开辟的空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新的元素。vector的关键技术在于对大小的控制以及重新分配时的数据移动效率。
- 采用的数据结构是线性的连续空间(泛型的动态类型顺序表),他以两个迭代器start和finish分别指向配置得来的连续空间中目前已将被使用的空间。迭代器end_of_storage指向整个连续的尾部。
- 在增加元素时,如果超过自身最大的容量,vector则将自身的容量扩充为原来的两倍。扩充空间需要经过的步骤:重新配置空间,元素移动,释放旧的内存空间。一旦vector空间重新配置,则指向原来vector的所有迭代器都失效了,因为vector的地址改变了。
2.3.vector为什么要用加倍扩容而不是每次增加一个固定的扩容容量
- vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。
- 为了防止申请内存的浪费,现在使用较多的有2倍与1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。
2.4.vector扩容的过程
- 完全弃用现有的内存空间,重新申请更大的内存空间;
- 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
- 最后将旧的内存空间释放。
2.5.vector的扩容方式为什么是1.5倍或2倍
- 假如说我们是以2倍方式扩容(1,2,4,8,16),则第i次扩容期间所需要的空间总量就是2^i次方,如果第4次扩容时总共需要8个元素大小的空间,但是前3次已经释放的空间加起来的总量,刚好是7,而7小于8,不足以我们第4次扩容时所需要的空间,也就是说,如果恰巧以2倍方式扩容,那么每次扩容时前面释放的空间它都不足以支持本次的扩容!!!那么如果是以更高倍数的方式进行扩容,则这个空间它的浪费情况就会更高!!!也就是说,以2倍或者更高倍数的方式进行扩容,会存在2个问题:
- 空间浪费可能比较大
- 无法使用前面已经释放的空间
- STL标准没有严格说明我们应该按照哪一种方式进行扩容,因此不同STL的实现厂商都是按照自己的方式进行扩容的。
- 一般情况下,在Windows的VS系列编译器下,是按照1.5倍的方式进行扩容,在Linux的g++中,是按照2倍的方式进行扩容的。
2.6.size、resize、reserve、capacity的区别
- size表示当前vector中有多少个元素(即finish – start);当前容器所存储的个数,
- resize可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。可以有多个参数。创建指定数量的元素并指定vector的存储空间。既分配空间又创建对象。
- reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),从而达到提高效率的目的,其次还可以减少多次要拷贝数据的问题。reserve它只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。并且它只有一个参数。指定vector的元素总数,不创建对象。
- capacity函数表示它已经分配的内存中可以容纳多少元素(即end_of_storage – start)。即容器在分配新的存储空间能存储的元素总数。返回vector中能存储元素的最大数。
2.7.vector迭代器失效问题
迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了 封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的 空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器, 程序可能会崩溃)。
2.8.对于vector可能导致其迭代器失效的操作有哪些
- resize、reserve、insert、assign、push_back等会引起其底层空间改变的操作,都有可能使迭代器失效。
- 指定位置元素的删除操作--erase
#include <iostream>
using namespace std;
#include <vector>
int main()
{
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos);
cout << *pos << endl; // 此处会导致非法访问
return 0;
}
erase删除pos位置元素后,pos位置之后的元素会往前移动,没有导致底层空间的改变,理论上讲迭代器应该不会失效,但是如果pos刚好是最后一个元素,删完之后pos刚好是end位置,而end位置是没有元素的,那么pos就失效了。所以删除vector中任意位置上元素时,vs就认为该位置迭代器失效了。
2.9.push_back和emplace_back的区别
emplace_back() 和 push_back() 的主要区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
2.10.频繁对vector调用push_back()对性能的影响和原因
在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间。
3.list
3.1.聊聊STL库中的list
- list 是顺序容器的一种。list 是一个双向链表。使用 list 需要包含头文件 list。双向链表的每个元素中都有一个指针指向后一个元素,也有一个指针指向前一个元素。
- 在 list 容器中,在已经定位到要增删元素的位置的情况下,增删元素能在常数时间内完成。
- list不支持根据下标随机存取元素。
- 在任何位置都能高效地插入和删除元素,只要改变元素的指针值,不需要拷贝元素。
3.2.list的底层原理
- list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
- 和vector容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器的指针,并且该指针还可以借助重载的 *、++、--、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。
3.3.list的常用函数
list.push_back(elem) 在尾部加入一个数据
list.pop_back() 删除尾部数据
list.push_front(elem) 在头部插入一个数据
list.pop_front() 删除头部数据
list.size() 返回容器中实际数据的个数
list.sort() 排序,默认由小到大
list.unique() 移除数值相同的连续元素
list.back() 取尾部迭代器
list.erase(iterator) 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
3.4.vector插入、list插入区别
- vector中插入大量连续的数据时,如果预知数据量大小,可以通过resize来调整原先vector的空间大小,同时利用赋值“=”,可以减少耗时。
- list可以通过遍历list然后insert插入,但还可以通过成员方法splice来进行插入,且效率较高,耗时较少。
3.5.vector和list的优缺点
1) vector的优点
- 下标随机访问
vector的底层是一段连续的物理空间,所以支持随机访问。
- 尾插尾删效率高
跟数组类似,我们能够很轻易的找到最后一个元素,并完成各种操作。
- cpu高速缓存命中率高
因为系统在底层拿空间的时候,是拿一段进cpu,不是只拿单独一个,会提前往后多拿一点,vector的物理地址是连续的,所以我们再拿到数据的时候,cpu访问后面的数据会更快。
2)vector的缺点
- 前面部分的插入删除数据效率低
如果我们要在前面或中间插入或者删除数据,我们不能直接删,我们需要挪动数据,去覆盖或者增加一段空间,这样我们挪动数据的效率就是O(N)。
- 扩容有消耗,可能存在一定的空间浪费
正常情况下我们vector的扩容机制是一旦达到当前空间的capacity(容量)那么我们扩容原空间的1.5倍或者2倍数(vs一般是1.5倍而g++是2倍),这样扩容就有可能导致空间浪费,而且频繁扩容也会影响效率。
3) list的优点
- 按需申请释放,不需要扩容
list是一个带头双向循环链表,那么链表就是一个个独立的空间链接起的,需要多少,就new多少,不存在空间浪费。
- 任意位置的插入删除效率高(对比vector)
因为list是双向循环链表,我们需要插入新的元素只需要改变原数据的next和prev,所以我们的插入删除效率是O(1)。
4)list的缺点
- 不支持下标随机访问
因为list是链表,在存放数据的物理地址并不是连续的,所以我们也就不能支持随机访问。
- CPU高速缓存命中率低
主要还是跟它的物理地址不连续有关,CPU提前存的一段数据,可能跟下一个数据完全没有联系,因为它们空间不连续,所以就命中率低。
3.6.vector和list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?
- 迭代器和指针之间的区别
- 迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,-->、++、--等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。
- 迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
- vector和list特性
- vector特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。
- list特性 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
- vector和list增删元素
- 对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
- 对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
3.7.总结一下vector和list具体是怎么实现的呢?常见操作的时间复杂度是多少?
- vector 一维数组(元素在内存连续存放)
- 倍放开辟三倍的内存
- 旧的数据开辟到新的内存
- 释放旧的内存
- 指向新内存
- 是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新增大小当前大小时才会重新分配内存。
- 扩容方式:
- list 双向链表(元素存放在堆中)
- 随机访问不方便
- 删除插入操作方便
- 元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
- 特点:
- 常见时间复杂度
- vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
- list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。
4.deque
4.1.简单说说deque
- deque是一个双向开口的容器,所谓双向开口就是再头尾两端均可以做元素的插入和删除操作。
- deque相比于vector最大的差异就在于支持常熟时间内对首尾两端进行插入和删除操作,而且deque没有容量的概念,其内部采用分段连续内存空间来存储元素,在插入元素的时候随时都可以重新增加一段新的空间并链接起来。
- deque提供了Ramdon Access Iterator,同时也支持随机访问和存取,但是它也为此付出了昂贵的代价,其复杂度不能跟vector的原生指针迭代器相提并论。在下面的讲解中会一一为大家介绍STL是怎样”辛苦地”维持一个随机访问迭代器的。
4.2.详细讲讲deque实现原理
动态开辟的二维数组空间,第二维固定长度的数组空间,扩容的时候(第一维的数组进行2倍扩容)。
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
4.3.你了解deque的中控器吗
deque为了维持整体连续的假象,设计一个中控器,其用来记录deque内部每一段连续空间的地址。大体上可以理解为deque中的每一段连续空间分布在内存的不连续空间上,然后用一个所谓的map作为主控,记录每一段内存空间的入口,从而做到整体连续的假象。
4.4.deque的迭代器是怎么回事呢
- deque提供的是一个随机访问迭代器,由于是分段连续空间,其必须记录当前元素所在段的信息,从而在该段连续空间的边缘进行前进或者后退的时候能知道跳跃到的上一个或下一个缓冲区。deque必须完完整整的掌握和控制这些信息,以达到正确的跳跃。
- buffer_size函数:
static size_t buffer_size(){
return __deque_buf_size(BufSiz, sizeof(T));
}
//如果n不为0,传回n,表示buffer size 由自己定义
如果n为0,表示buffer_size 采用默认值
如果sz(元素大小) < 512,传回512/sz,如果不小于512 ,传回1
inline size_t __deque_buf_size(size_t n, size_t sz)
{
return n != 0 ? n : (sz < 512 ? size_t(512 / sz) : size_t(1));
}
3. set_node函数
当迭代器处在当前缓冲区的边缘时,一旦前进或者后退,就要考虑超过当前缓冲区的情况,此时需要跳转到下一个缓冲区,这时候set_node就派上用场了。
void set_node(map_pointer new_node)
{
node = new_node; // 跳转到相应缓冲区
first = *new_node; // 更新跳转后缓冲区first信息
last = first + difference_type(buffer_size()); // 更新跳转后缓冲区last的信息
}
4.5.说一说deque的数据结构
deque维护着一个map,用来记录每个缓冲区的位置。除了map外,deque的数据结构还维护着start和finish两个迭代器,分别指向deque的首尾。此外,他还必须知道map的大小,一旦map提供的节点不足,就需要配置一块更大的map。
4.6.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。
4.7.vector、list、deque的选择原则
- vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
- list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
- 需要从首尾两端进行插入或删除操作的时候需要选择deque。
也即:
- 需要对数据高效的随机访问(存取),而不在乎插入和删除的效率,采用vector。
- 需要大量插入、删除数据,而不关心随机访问数据,采用list。
- 需要随机访问数据,而且关心前后增删数据的能力,采用deque。
- 对数据中间的增删操作比较多,采用list,建议在排序的基础上,批量进行增删可以对运行效率提供最大的保证。
5.说说STL迭代器是怎么删除元素的
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器;
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
6.了解优先级队列吗?细节讲讲
底层数据结构一般以vector为底层容器,heap为处理规则来管理底层容器实现。
优先队列(priority_queue)容器与队列一样,只能从队尾插入元素,从队首删除元素。但是它有一个特性,队列中最大的元素总是位于队首。出队时,并非按照先进先出的原则进行,而是将当前队列中最大的元素出队。这点类似于给队列里的元素进行了由大到小的顺序排序。元素的比较规则默认按元素值由大到小排序,可以重载“<”操作符来重新定义比较规则。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
7.你知道STL容器动态链接可能产生的问题吗
- 能产生的问题
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
- 产生问题的原因
容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
8.map、unordered_map和set
8.1.你了解map和unordered_map嘛?底层实现呢
- map实现机理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
- unordered_map实现机理
unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
8.2.map和unordered_map的优缺点
- map的优点
- 有序
- 基于红黑树实现,查找的时间复杂度是O(nlogn)
- map的缺点
- 空间占用率比较高,虽然说底层是红黑树实现的,提高了运行效率,但是每个节点都要保存父节点和孩子节点和红黑树的性质,使得每一个节点都占用胆量的空间。
- 适用情况
- 对于有序的结构
- unordered_map的优点
- 底层是用哈希表实现的,查找效率非常高,时间复杂度为O(1)。
- unordered_map的缺点
- 哈希表的建立比较费时。
- 适用场景
- 对于查找问题,使用unordered_map更好。
8.3.set的底层实现为什么不用哈希表而是用红黑树
- set中元素是经过排序的,红黑树也是有序的,哈希是无序的
- 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(logn)。
8.4.为什么map和set和插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效
因为存储的是节点,不需要内存拷贝和内存移动。
插入操作只是节点指针换来换去,节点内存没有改变,而iterator就像指向节点的指针,内存没变,指向内存de指针也不会变。
8.5.为什么map和set不能像vector一样有个reserve函数来预分配数据
因为在map和set内部存储的已经不是元素本身了,而是包含元素的节点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。
8.6.你知道map和set有什么区别嘛?分别是怎么实现的呢
- set是一种关联式容器,其特性如下:
- set以RBTree作为底层容器
- 所得元素的只有key没有value,value就是key
- 不允许出现键值重复
- 所有的元素都会被自动排序
- 不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的
- map和set一样是关联式容器,其特性如下:
- map以RBTree作为底层容器
- 所有元素都是键+值存在
- 不允许键重复
- 所有元素是通过键进行自动排序的
- map的键是不能修改的,但是其键对应的值是可以修改的
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。
8.7.vector越界访问下标,map越界访问下标,vector删除元素时会不会释放空间
- 通过下标访问vector中的元素时会做边界检查,但该处的实现方式要看具体IDE,不同IDE的实现方式不一样,确保不可访问越界地址。
- map的下标运算符[]的作用是:将key作为下标去执行查找,并返回相应的值;如果不存在这个key,就将一个具有该key和value的某人值插入这个map。
- erase()函数,只能删除内容,不能改变容量大小;
erase成员函数,它删除了itVect迭代器指向的元素,并且返回要被删除的itVect之后的迭代器,迭代器相当于一个智能指针;clear()函数,只能清空内容,不能改变容量大小;如果要想在删除内容的同时释放内存,那么你可以选择deque容器。
8.8.map中[ ]与find的区别
- map的下标运算符[ ]的作用是:将关键码作为下标去执行查找,并返回对应的值;如果不存在这个关键码,就将一个具有该关键码和值类型的默认值的项插入这个map。
- map的find函数:用关键码执行查找,找到了返回该位置的迭代器;如果不存在这个关键码,就返回尾迭代器。
8.9.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.10.讲一讲set的用法和它的特点
- 用法:count()-返回某个值元素的个数(set中最多为1)、find()-返回一个指向被查找到元素的迭代器、equal_range()-返回集合中与给定值相等的上下限的两个迭代器。
- 特点:元素不允许有重复,在默认情况下会对元素进行自动排序,数据被组织成一棵红黑树,查找的速度非常快(二分),时间复杂度是O(logN),set中的元素不能被修改,只能删除后再添加。
9.你了解STL的内存优化吗
STL内存管理使用二级内存配置器。
1) 第一级配置器:
- 第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。一级空间配置器分配的是大于128字节的空间,如果分配不成功,调用句柄释放一部分内存,如果还不能分配成功,抛出异常。
- 第一级配置器只是对malloc函数和free函数的简单封装,在allocate内调用malloc,在deallocate内调用free。同时第一级配置器的oom_malloc函数,用来处理malloc失败的情况。
2)第二级配置器
第一级配置器直接调用malloc和free带来了几个问题:
- 内存分配/释放的效率低。
- 当配置大量的小内存块时,会导致内存碎片比较严重。
- 配置内存时,需要额外的部分空间存储内存块信息,所以配置大量的小内存块时,还会导致额外内存负担。
如果分配的区块小于128bytes,则以内存池管理,第二级配置器维护了一个自由链表数组,每次需要分配内存时,直接从相应的链表上取出一个内存节点就完成工作,效率很高。
- 自由链表数组:自由链表数组其实就是个指针数组,数组中的每个指针元素指向一个链表的起始节点。数组大小为16,即维护了16个链表,链表的每个节点就是实际的内存块,相同链表上的内存块大小都相同,不同链表的内存块大小不同,从8一直到128。如下所示,obj为链表上的节点,free_list就是链表数组。
- 内存分配:allocate函数内先判断要分配的内存大小,若大于128字节,直接调用第一级配置器,否则根据要分配的内存大小从16个链表中选出一个链表,取出该链表的第一个节点。若相应的链表为空,则调用refill函数填充该链表。默认是取出20个数据块。
- 填充链表 refill:若allocate函数内要取出节点的链表为空,则会调用refill函数填充该链表。refill函数内会先调用chunk_alloc函数从内存池分配一大块内存,该内存大小默认为20个链表节点大小,当内存池的内存也不足时,返回的内存块节点数目会不足20个。接着refill的工作就是将这一大块内存分成20份相同大小的内存块,并将各内存块连接起来形成一个链表。
- 内存池:chunk_alloc函数内管理了一块内存池,当refill函数要填充链表时,就会调用chunk_alloc函数,从内存池取出相应的内存。
- 在chunk_alloc函数内首先判断内存池大小是否足够填充一个有20个节点的链表,若内存池足够大,则直接返回20个内存节点大小的内存块给refill;
- 若内存池大小无法满足20个内存节点的大小,但至少满足1个内存节点,则直接返回相应的内存节点大小的内存块给refill;
- 若内存池连1个内存节点大小的内存块都无法提供,则chunk_alloc函数会将内存池中那一点点的内存大小分配给其他合适的链表,然后去调用malloc函数分配的内存大小为所需的两倍。若malloc成功,则返回相应的内存大小给refill;若malloc失败,会先搜寻其他链表的可用的内存块,添加到内存池,然后递归调用chunk_alloc函数来分配内存,若其他链表也无内存块可用,则只能调用第一级空间配置器。
知识点看完了,最后还是希望大家都能拿到自己“心动的offer”!文章来源:https://www.toymoban.com/news/detail-833525.html
参考书籍:<<STL源码剖析>>文章来源地址https://www.toymoban.com/news/detail-833525.html
到了这里,关于面试指南:C++之STL知识点的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!