STL
- 在 C++ 标准模板库(STL)中,主要包含了一系列的容器、迭代器、算法、函数对象、适配器。
容器
- 容器是用于存储数据的类模板。STL 容器可以分为序列型容器、关联型容器和链表型容器三类:
- 序列型容器:
vector
、deque
、array
。 - 关联型容器:
set
、map
、multiset
、multimap
。 - 链表型容器:
forward_list
、list
、unordered_set
、unordered_map
、unordered_multiset
、unordered_multimap
。
迭代器
算法
- STL 算法通过迭代器与容器进行交互,进行数据处理和操作。
- 非修改序列算法:
find
、count
。 - 修改序列算法:
copy
、move
、transform
。 - 排序算法:
sort
、stable_sort
。 - 二分搜索算法:
lower_bound
、upper_bound
。 - 数值算法:
accumulate
、inner_product
。
函数对象
- STL 中的函数对象是实现了
operator()
的对象,常用于算法中作为策略或条件表达式。包括算术运算类、关系运算类、逻辑运算类。
适配器
- 适配器用于修改容器或迭代器的接口。
- 容器适配器:
stack
、queue
、priority_queue
。
一、vector
vector 底层实现原理
- 底层实现了一个动态数组。
- 类构成:
- 以
protected
的方式继承自_Vector_base
,基类的public
在子类中将变为protected
,其他的权限不变。class vector : protected _Vector_base<_Tp, _Alloc> { }
_Vector_base
组成:
_M_start
:指向第一个元素的位置 →vec.begin()
。_M_finish
:指向最后一个实际存储元素的下一个位置 →vec.end()
。_M_end_of_storage
:指向为 vector 所分配的内存块的末尾之后的第一个位置。- 从
_M_start
到_M_finish
之间的内存是vector
实际使用的空间 →vec.size()
。 - 从
_M_start
到_M_end_of_storage
之间的内存是vector
可以用来存储元素的空间 →vec.capacity()
。 - 从
_M_finish
到_M_end_of_storage
之间的内存是已经分配好可以随时使用的,但是目前未使用的。
template<typename _Tp, typename _Alloc> struct _Vector_base { struct _Vector_impl_data { pointer _M_start; pointer _M_finish; pointer _M_end_of_storage; ... } ... }
- 以
- 构造函数:
- 无参构造函数:不会申请动态内存,保证性能优先。
- 初始化元素个数的构造函数:一次性申请足够的动态内存 → 避免多次申请动态内存,影响性能。
- 插入元素:
- 往最后位置插入:
- 检查空间是否需要动态分配内存,是否需要扩容(
_M_finish
是否等于_M_end_of_storage
)。 - 插入到最后:
push_back()
、emplace_back()
→++_M_finish
。
- 检查空间是否需要动态分配内存,是否需要扩容(
- 往其他位置插入。
- 检查空间是否需要动态分配内存,是否需要扩容。
- 将插入位置之后的元素往后平移一位,然后插入元素:
insert()
。
- 往最后位置插入:
- 删除元素:不会释放现有已经申请的内存。
- 删除最后一个元素
pop_back()
:_M_finish
往前移动一位(--_M_finish
)。 - 删除其他元素
erase()
:将待删元素之后的元素向前平移一位,_M_finish
往前移动一位。
- 删除最后一个元素
- 读取元素:返回的是具体元素的引用。
- 操作符
[]
。 at()
:比操作符[]
多了一个检查越界的动作,越界后会抛出错误。
- 操作符
- 修改元素:
vector
不支持直接修改某个位置的元素。- 通过读取元素,获取引用,然后进行修改。
- 先删除后插入。
- 释放空间:
swap()
交换一个空容器。std::vector<int>().swap(vec);
- 先
clear()
,然后shrink_to_fit()
→ 释放掉未使用的内存。
vector 内存增长机制
- 特点:
- 内存空间只会增加不会减少。
vector
的内存是连续的。- 不同平台的增长方式不一样,Linux 下是以翻倍的方式进行增长。
- 增长特征:
- 无参构造,连续插入一个,增长方式:1、2、4、8 …
- 有参构造,连续插入一个,增长方式:10、20、40 …
- 增长时的具体操作:
- 此时
_M_finish == _M_end_of_storage
,将内存空间大小翻倍并产生新的内存空间。 - 将容器原来内存空间中的数据复制到新的内存空间中。
- 释放容器原来的内存空间。
- 插入新元素。
- 此时
- 注意:
vector
中的元素为指针时,不会调用析构函数,需要手动释放内存。
vector 中 reserve 和 resize 的区别
- 共同点:
- 调用它们,容器内原有的元素不受影响。
- 它们都起到增加的作用,对于缩小操作直接无视。
- 区别:
reserve
能增加vector
的capacity
,但是它的size
不会改变。resize
既增加了vector
的capacity
,又增加了size
。- 应用场景:
reserve
用来避免多次内存分配。resize
确保操作符[]
和at()
的安全性。
vector 中的元素类型为什么不能是引用
- 引用的特征:
- 引用必须要进行初始化,不能初始化为空对象,初始化后不能改变指向。
- 引用是别名,不是对象,没有实际的地址,不能定义引用的指针,也不能定义引用的引用。
- 不能为引用分配内存 → 不能定义引用的指针。
push_back()
传入左值时会调用拷贝构造函数 → 不能定义引用的引用。- 基于操作符
[]
和at()
,将会获取引用的引用 → 不能定义引用的引用。
vector 中 push_back 和 emplace_back 的区别
push_back
如果传入的是左值,会调用拷贝构造函数;如果传入的是右值,会调用移动构造函数。emplace_back
利用传入的参数在容器内存中直接构造元素,而无需首先创建临时对象再将其拷贝或移动到容器中,它使用forward
完美转发将参数直接传递给元素的构造函数。vector<string> vec; // 使用 push_back 添加字符串 string str = "Hello"; vec.push_back(str); // 调用拷贝构造函数 vec.push_back(string("World")); // 调用移动构造函数 // 使用 emplace_back 添加字符串 vec.emplace_back("Hello, World"); // 直接在容器中构造字符串
vector<string> vec; string hello = "Hello"; // 这里会调用拷贝构造函数,因为传递的是一个现有的 string 对象 vec.emplace_back(hello); // 这里直接在容器中构造一个新的 string,不调用拷贝或移动构造函数 vec.emplace_back("World");
二、list
- 底层实现了一个双向循环链表。
- 类构成:
- 以
protected
的方式继承自_List_base
。class list : protected _List_base<_Tp, _Alloc> { }
_List_base
→_List_impl
→_List_node
→_M_storage
:存储具体的值。_List_node
继承自_List_node_base
→_M_next
、_M_prev
。_List_node_base* _M_next; _List_node_base* _M_prev;
- 以
- 构造函数:
- 不管如何构造,初始都会构建一个空节点 dummyHead。
- 空节点用来表示整个双向循环链表。
- 迭代器:
++
往下移动指针 →_M_node = _M_node->_M_next
。--
向上移动指针 →_M_node = _M_node->_M_prev
。
- 获取第一个元素:空节点的下一个节点 →
this->_M_impl._M_node._M_next
。 - 获取最后一个元素:空节点的上一个节点 → 先获取空节点
this->_M_impl._M_node
,再--
。 - 插入元素:每插入一个元素,都临时为该节点分配内存,修改指向,size++。
三、deque
- 目的是实现双端数组,底层实现了一个双向开口的连续线性空间。
- 类构成:
- 以
protected
的方式继承自_Deque_base
。class deque : protected _Deque_base<_Tp, _Alloc> { }
_Deque_base
→_Deque_impl
:_M_map
:指针数组。_M_map_size
:_M_map
的容量。_M_start
:记录_M_map
指针数组中首个连续空间的信息。_M_finish
:记录_M_map
指针数组中最后一个连续空间的信息。
- 以
- 迭代器
_Deque_iterator
:_M_cur
:指向当前正在遍历的元素。_M_first
:指向当前连续空间的首地址。_M_last
:指向当前连续空间的尾地址。_M_node
: 指向_M_map
指针数组中存储的指向连续空间的指针。
__deque_buf_size
:连续空间中能容纳的元素个数;如果元素大小小于 512 字节,则能容纳512 / 元素大小
个元素,否则只能容纳一个元素。_M_initialize_map
:- 创建
_M_map
,最小为8
,并配置缓冲区。 _M_start
和_M_finish
指向中间的位置,方便公平地往上或者向下扩展空间。
- 创建
push_back
:- 先看当前连续空间够不够,够就直接插入。
- 不够的话,再看
_M_map
空间够不够,够就生成新的连续空间。 - 不够的话,就生成新的
_M_map
,把旧的_M_map
中的数据挪到新的_M_map
中。
pop_back
:删除当前连续空间的最后一个元素,如果当前连续空间没有数据了,则释放该连续空间。
四、vector、list、deque 使用场景
- 如果需要高效地快速访问(随机存取),并且不在乎插入和删除的效率,使用
vector
。 - 如果需要大量地插入和删除,并且不关心快速访问(随机存取),使用
list
。 - 如果需要快速访问(随机存取),并且关心两端数据的插入和删除,使用
deque
。
五、priority_queue
- 底层实现了一个堆(Heap),堆是一种高效维护集合中最大或最小元素的数据结构。
- 大根堆:根节点最大的堆,用于维护和查询
max
。 - 小根堆:根节点最小的堆,用于维护和查询
min
。 - 堆是一棵树,并且满足堆性质:
- 大根堆任意节点的关键码 ≥ \geq ≥ 它所有子节点的关键码(父 ≥ \geq ≥ 子)
- 小根堆任意节点的关键码 ≤ \leq ≤ 它所有子节点的关键码(父 ≤
- 大根堆:根节点最大的堆,用于维护和查询