【转】STL常见问题
转自 lx青萍之末 的 STL详解及常见面试题
一、各种容器的特点和适用情况
如果想进一步了解 STL 的各容器,可以先看我的另一篇博客:[C++ STL] 各容器简单介绍
二、vector 篇
2.1 vector 的底层原理
vector 底层是一个动态数组,包含三个迭代器,start 和 finish 之间是已经被使用的空间范围,end_of_storage 是整块连续空间包括备用空间的尾部。
- 当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5 倍或者 2 倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector 内存增长机制】。
- 当释放或者删除(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 的元素类型可以是引用吗?
vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。
2.5 vector 迭代器失效的情况
当插入一个元素到 vector 中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase 方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);
。
2.6 如何正确释放 vector 的内存
vec.clear()
:清空内容,但是不释放内存。
vector<int>().swap(vec)
:清空内容,且释放内存,想得到一个全新的 vector。
vec.shrink_to_fit()
:请求容器降低其 capacity 和 size 匹配。
vec.clear();vec.shrink_to_fit();
:清空内容,且释放内存。
2.7 vector 扩容为什么要以 1.5 倍或者 2 倍扩容?
根据查阅的资料显示,考虑可能产生的堆空间浪费,成倍增长倍数不能太大,使用较为广泛的扩容方式有两种,以 2 倍的方式扩容,或者以 1.5 倍的方式扩容。
以 2 倍的方式扩容,导致下一次申请的内存必然大于之前分配内存的总和,导致之前分配的内存不能再被使用,所以最好倍增长因子设置为 (1, 2) 之间:
2.8 vector的常用函数
vector<int> vec(10,100); // 创建10个元素,每个元素值为100
vec.resize(r,vector<int>(c,0)); // 二维数组初始化
reverse(vec.begin(),vec.end()) // 将元素翻转
sort(vec.begin(),vec.end()); // 排序,默认升序排列
vec.push_back(val); // 尾部插入数字
vec.size(); // 向量大小
find(vec.begin(),vec.end(),1); // 查找元素
iterator = vec.erase(iterator) // 删除元素
三、list 篇
3.1 list的底层原理
list 的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
3.2 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) // 删除一个元素,参数是迭代器,返回的是删除迭代器的下一个位置
四、deque 篇
4.1 deque的底层原理
deque是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。
4.2 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。
五、map 、set、multiset、multimap 篇
5.1 map 、set、multiset、multimap 的底层原理
map 、set、multiset、multimap 的底层实现都是红黑树,epoll 模型的底层数据结构也是红黑树,linux 系统中 CFS 进程调度算法,也用到红黑树。
5.2 map 、set、multiset、multimap 的特点
set 和 multiset 会根据特定的排序准则自动将元素排序,set 中元素不允许重复,multiset 可以重复。
map 和 multimap 将 key 和 value 组成的 pair 作为元素,根据 key 的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以 map 默认是按 key 排序的),map 中元素的 key 不允许重复,multimap 可以重复。
map 和 set 的增删改查速度为都是 logn,是比较高效的。
5.3 为何 map 和 set 的插入删除效率比其他序列容器高,而且每次 insert 之后,以前保存的 iterator 不会失效?
因为存储的是结点,不需要内存拷贝和内存移动。
因为插入操作只是结点指针换来换去,结点内存没有改变。而 iterator 就像指向结点的指针,内存没变,指向内存的指针也不会变。
5.4 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 (itor = map.begin(); itor != map.end();)
{
if (itor->second == "target")
map.erase(itor++) ; // erase之后,令当前迭代器指向其后继。
else
++itor;
}
六、迭代器的底层机制和失效的问题
6.1 迭代器的底层原理
迭代器是连接容器和算法的一种重要桥梁,通过迭代器可以在不了解容器内部原理的情况下遍历容器。它的底层实现包含两个重要的部分:萃取技术和模板偏特化。
萃取技术(traits)可以进行类型推导,根据不同类型可以执行不同的处理流程,比如容器是 vector,那么 traits 必须推导出其迭代器类型为随机访问迭代器,而 list 则为双向迭代器。
- 例如 STL 算法库中的distance函数,distance 函数接受两个迭代器参数,然后计算他们两者之间的距离。显然对于不同的迭代器计算效率差别很大。比如对于 vector 容器来说,由于内存是连续分配的,因此指针直接相减即可获得两者的距离;而 list 容器是链式表,内存一般都不是连续分配,因此只能通过一级一级调用 next() 或其他函数,每调用一次再判断迭代器是否相等来计算距离。vector 迭代器计算 distance 的效率为 O(1),而 list 则为 O(n),n 为距离的大小。
使用萃取技术(traits)进行类型推导的过程中会使用到模板偏特化。模板偏特化可以用来推导参数,如果我们自定义了多个类型,除非我们把这些自定义类型的特化版本写出来,否则我们只能判断他们是内置类型,并不能判断他们具体属于是个类型。
template <typename T>
struct TraitsHelper {
static const bool isPointer = false;
};
template <typename T>
struct TraitsHelper<T*> {
static const bool isPointer = true;
};
if (TraitsHelper<T>::isPointer)
...... // 可以得出当前类型int*为指针类型
else
...... // 可以得出当前类型int非指针类型
6.2 一个理解 traits 的例子
// 需要在T为int类型时,Compute方法的参数为int,返回类型也为int,
// 当T为float时,Compute方法的参数为float,返回类型为int
template <typename T>
class Test {
public:
TraitsHelper<T>::ret_type Compute(TraitsHelper<T>::par_type d);
private:
T mData;
};
template <typename T>
struct TraitsHelper {
typedef T ret_type;
typedef T par_type;
};
// 模板偏特化,处理int类型
template <>
struct TraitsHelper<int> {
typedef int ret_type;
typedef int par_type;
};
// 模板偏特化,处理float类型
template <>
struct TraitsHelper<float> {
typedef float ret_type;
typedef int par_type;
};
当函数,类或者一些封装的通用算法中的某些部分会因为数据类型不同而导致处理或逻辑不同时,traits 会是一种很好的解决方案。
6.3 迭代器失效的问题
插入操作:
- 对于 vector 和 string,如果容器内存被重新分配,iterators、pointers、references 失效;如果没有重新分配,那么插入点之前的 iterator 有效,插入点之后的 iterator 失效;
- 对于 deque,如果插入点位于除 front 和 back 的其它位置,iterators、pointers、references 失效;当我们插入元素到 front 和 back 时,deque 的迭代器失效,但 reference 和 pointers 有效;
- 对于 list 和 forward_list,所有的 iterator、pointer 和 refercnce 有效。
删除操作:
- 对于 vector 和 string,删除点之前的 iterators、pointers、references 有效;off-the-end 迭代器总是失效的;
- 对于 deque,如果删除点位于除 front 和 back 的其它位置,iterators、pointers、references 失效;当我们插入元素到 front 和 back 时,off-the-end 失效,其他的 iterators、pointers、references 有效;
- 对于 list 和 forward_list,所有的 iterator、pointer 和 refercnce 有效。
- 对于关联容器 map 来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。
七、STL容器的线程安全性
(1)线程安全的情况
- 多个读取者是安全的。多线程可能同时读取一个容器的内容,这将正确地执行。当然,在读取时不能 有任何写入者操作这个容器;
- 对不同容器的多个写入者是安全的。多线程可以同时写不同的容器。
(2)线程不安全的情况
- 在对同一个容器进行多线程的读写、写操作时;
- 在每次调用容器的成员函数期间都要锁定该容器;
- 在每个容器返回的迭代器(例如通过调用 begin 或 end)的生存期之内都要锁定该容器;
- 在每个在容器上调用的算法执行期间锁定该容器。