Bootstrap

STL 总结

STL

  • 在 C++ 标准模板库(STL)中,主要包含了一系列的容器迭代器算法函数对象适配器

容器

  • 容器是用于存储数据的类模板。STL 容器可以分为序列型容器、关联型容器和链表型容器三类:
  • 序列型容器:vectordequearray
  • 关联型容器:setmapmultisetmultimap
  • 链表型容器:forward_listlistunordered_setunordered_mapunordered_multisetunordered_multimap

迭代器

算法

  • STL 算法通过迭代器与容器进行交互,进行数据处理和操作。
  • 非修改序列算法:findcount
  • 修改序列算法:copymovetransform
  • 排序算法:sortstable_sort
  • 二分搜索算法:lower_boundupper_bound
  • 数值算法:accumulateinner_product

函数对象

  • STL 中的函数对象是实现了 operator() 的对象,常用于算法中作为策略或条件表达式。包括算术运算类、关系运算类、逻辑运算类。

适配器

  • 适配器用于修改容器或迭代器的接口。
  • 容器适配器:stackqueuepriority_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 能增加 vectorcapacity,但是它的 size 不会改变。
  • resize 既增加了 vectorcapacity,又增加了 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 它所有子节点的关键码(父
;