C++ STL 学习笔记

C++ STL 学习笔记

C++ STL(Standard Template Library)是C++标准库的核心,封装了常用数据结构与算法,凭借"泛型编程"思想实现高复用性,让开发者无需重复造轮子,专注于核心业务逻辑。本文基于STL底层原理、关键特性与实战场景,整理成学习笔记,覆盖容器、迭代器、适配器等核心组件。

一、STL 核心组件概览

STL由五大核心组件构成,组件间相互配合,实现"数据存储-访问-操作"的完整闭环:

组件 作用
容器(Containers) 存储数据的载体,底层封装数组、链表、红黑树、哈希表等数据结构
迭代器(Iterators) 连接容器与算法的"桥梁",提供统一的元素访问接口,屏蔽容器底层差异
算法(Algorithms) 基于迭代器的通用操作(排序、查找、遍历等),支持对不同容器批量处理
仿函数(Functors) 重载()运算符的类/结构体,作为算法参数(如自定义排序规则),封装行为
适配器(Adapters) 对现有组件封装改造,改变接口或功能(如stack基于deque实现栈功能)

二、容器详解:分类、原理与场景

容器是STL的核心,按存储逻辑分为顺序容器关联容器无序容器三类,底层实现决定其性能特性与适用场景。

(一)容器分类与核心特性总览

容器类别 底层实现 核心特点 代表容器
顺序容器 动态数组/链表 元素物理位置与插入顺序一致,线性存储 vector、list、deque、array、forward_list
关联容器(有序) 红黑树(自平衡二叉搜索树) 元素按键有序存储,键唯一/可重复 map、set、multimap、multiset
无序容器 哈希表 元素无序存储,基于哈希函数快速访问 unordered_map、unordered_set、unordered_multimap、unordered_multiset

(二)顺序容器:线性存储,注重顺序与访问效率

顺序容器维护元素的线性序列,核心差异体现在内存布局与插入/删除效率,详细特性如下:

1. 各顺序容器对比
容器 底层实现 核心特点 时间复杂度(插入/删除/查找) 适用场景
vector 动态数组 连续内存、支持随机访问,尾部插入/删除高效 尾部O(1)、中间/头部O(n)、查找O(n) 随机访问频繁、尾部增删(如图像处理、动态数据存储)
list 双向链表 非连续内存、不支持随机访问,任意位置增删高效 任意位置O(1)(需迭代器)、查找O(n) 中间频繁增删(如编辑器撤回栈、日志队列)
deque 分段动态数组 双端高效增删,支持随机访问(效率略低于vector) 头尾O(1)、中间O(n)、查找O(n) 双端增删(如滑动窗口算法、LRU缓存)
array 固定大小数组 栈上分配、连续内存,大小编译时确定 访问O(1)、增删不支持(固定大小) 大小固定、性能敏感(如嵌入式开发、方向向量)
forward_list 单向链表 非连续内存、节省空间,仅支持单向遍历 头部O(1)、其他位置O(n)、查找O(n) 内存受限、仅需单向遍历的场景
2. 顺序容器关键问题解析
(1)vector 核心原理
  • 连续存储保证 :内部通过单一内存块存储元素,维护三个核心变量:T* elements(内存起始指针)、size(当前元素个数)、capacity(当前内存容量),通过指针运算实现随机访问。
  • 扩容机制 :当size == capacity(空间不足)时,触发扩容:
    1. 申请原容量1.5~2倍的新内存(GCC为2倍,VS为1.5倍);
    2. 将旧内存中元素复制/移动到新内存(支持移动语义的对象用移动构造,否则用拷贝);
    3. 释放旧内存。
  • 为什么成倍扩容?:避免频繁扩容导致的O(n)时间复杂度,保证增删操作平均O(1);1.5~2倍兼顾内存利用率(3倍及以上易造成内存浪费)。
(2)push_back 与 emplace_back 的区别

两者均用于在容器尾部添加元素,核心差异在于"元素构造方式":

特性 push_back emplace_back
参数类型 已构造的元素(左值/右值) 元素构造函数的参数
构造方式 先创建元素(或移动),再拷贝到容器尾部 直接在容器尾部"就地构造"元素
性能 可能产生临时对象,有拷贝/移动开销 无临时对象,开销更低(尤其构造复杂对象时)
适用场景 添加已存在的对象 需传递多个参数构造新对象

示例代码:

cpp 复制代码
// 自定义类
class Person {
public:
    Person(int age, string name) : age(age), name(name) {}
private:
    int age;
    string name;
};

vector<Person> vec;
vec.push_back(Person(20, "Tom")); // 先构造临时对象,再移动/拷贝
vec.emplace_back(20, "Tom");      // 直接在vec尾部构造,无临时对象
(3)vector 与 array 的核心对比
特性 vector(动态数组) array(固定数组,C++11引入)
大小 运行时动态可变 编译时固定不变
内存分配 堆内存(动态分配) 栈内存(静态分配)
效率 稍慢(动态扩容开销) 更快(无额外内存管理开销)
适用场景 数据量未知、需动态扩容(如读取文件内容) 大小固定、性能敏感(如嵌入式、算法常量数组)
与C数组对比 支持动态扩容、STL算法 类型安全、避免指针退化,支持STL算法

(三)关联容器:有序存储,注重查找与排序

关联容器基于红黑树实现(自平衡二叉搜索树),元素按键自动排序,查找、插入、删除时间复杂度均为O(logn),核心差异在于存储方式与键的唯一性:

1. 关联容器对比
容器 存储方式 键的特性 访问方式 适用场景
map 键值对(key-value) 键唯一 下标访问(operator[])、迭代器 字典映射(如配置解析、用户ID-信息映射)
set 仅存储键(元素本身) 元素唯一 仅迭代器访问 去重、唯一性判断(如用户ID集合、标签去重)
multimap 键值对(key-value) 键可重复 仅迭代器访问(需遍历相同键元素) 一对多映射(如学生-课程、员工-任务)
multiset 仅存储键(元素本身) 元素可重复 仅迭代器访问 可重复元素的有序存储(如成绩排名、日志级别统计)
2. 关联容器关键操作:元素查找

map/set 提供四种核心查找方法,适用于不同场景:

方法 功能描述 返回值
find(key) 查找指定键,找到返回对应迭代器,否则返回end() 指向目标元素的迭代器
count(key) 返回指定键的元素个数(map/set返回0或1) size_t类型(0表示未找到)
lower_bound(key) 返回指向"不小于key"的首个元素的迭代器 迭代器(有序容器中有效)
upper_bound(key) 返回指向"大于key"的首个元素的迭代器 迭代器(有序容器中有效)
equal_range(key) 返回包含"等于key"所有元素的迭代器对([lower, upper)) pair<iterator, iterator>

示例代码(map查找):

cpp 复制代码
map<int, string> myMap = {{1, "one"}, {2, "two"}};
// 1. find方法
auto it = myMap.find(1);
if (it != myMap.end()) cout << "找到:" << it->second << endl;

// 2. count方法
if (myMap.count(2)) cout << "找到key=2" << endl;

// 3. lower_bound/upper_bound
auto lower = myMap.lower_bound(1);
auto upper = myMap.upper_bound(1);
if (lower != upper) cout << "找到:" << lower->second << endl;

(四)无序容器:哈希存储,注重高效访问

无序容器(C++11引入)基于哈希表实现,元素无序存储,平均查找、插入、删除时间复杂度为O(1),核心特性与关联容器对比:

1. 无序容器与关联容器核心差异
特性 无序容器(unordered_*) 关联容器(map/set)
底层实现 哈希表 红黑树
元素顺序 无序(取决于哈希函数) 有序(按键自动排序)
时间复杂度 平均O(1),极端O(n)(哈希冲突严重) 稳定O(logn)
空间开销 较高(哈希表需存储桶、链表指针) 较低(红黑树仅存储节点数据与颜色信息)
适用场景 无需排序、追求高效查找(如缓存、计数器) 需要有序存储、稳定性能(如排行榜、范围查询)
2. 关键注意点:哈希容器的 rehash

当无序容器的元素个数超过"负载因子"(默认1.0)时,会触发rehash(重建哈希表),此时所有迭代器失效。避免失效的方法:

  • 批量插入前调用reserve(n),预分配足够容量,减少rehash次数;
  • rehash后需重新获取迭代器,不可使用旧迭代器。

三、迭代器:容器的"通用指针"

(一)迭代器的核心作用

迭代器提供容器无关的统一元素访问接口 ,无论容器底层是数组、链表还是红黑树,都可通过++(递增)、*(解引用)等操作遍历元素,实现"算法与容器解耦"。

(二)迭代器失效:高频踩坑点

迭代器失效指容器修改后(如插入、删除、扩容),原有迭代器不再指向有效元素,继续使用会导致未定义行为(UB)。不同容器的失效规则如下:

1. 各容器迭代器失效规则
容器类型 失效场景 不失效场景
vector/string 扩容(push_back/insert)、中间插入/删除 尾部删除(pop_back)、未扩容的尾部插入
deque 首/尾插入删除(部分实现)、中间插入删除 -
list/forward_list 仅被删除元素的迭代器失效 插入、拼接操作
map/set(红黑树) 仅被删除节点的迭代器失效 插入操作
无序容器(哈希) rehash(reserve/插入触发)、被删除元素迭代器 未触发rehash的插入操作
2. 避免迭代器失效的实战技巧
  • 使用 erase 返回值 :C++11起,erase(it)返回下一个有效迭代器,适用于边遍历边删除:

    cpp 复制代码
    // vector安全删除偶数元素
    vector<int> vec = {1,2,3,4};
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (*it % 2 == 0) it = vec.erase(it); // 用返回值更新迭代器
        else ++it;
    }
  • 提前规划容量 :vector/string 批量插入前调用reserve(n),避免扩容导致全迭代器失效;

  • 选择稳定迭代器容器:频繁插删且需迭代器稳定时,优先用list、unordered_map/set;

  • 避免持有临时容器迭代器:局部容器销毁后,其迭代器变为"悬垂迭代器",不可使用。

四、适配器:封装现有组件,改变接口

适配器(Adapters)是对现有容器的"包装",改变其接口或功能,不直接存储数据,依赖底层容器实现:

适配器 底层容器默认选择 核心特性(接口) 适用场景
stack(栈) deque 后进先出(LIFO),支持push/pop/top 函数调用栈、表达式求值、回溯算法
queue(队列) deque 先进先出(FIFO),支持push/pop/front 任务队列、消息队列、广度优先搜索(BFS)
priority_queue(优先队列) vector 优先级最高元素先出,支持push/pop/top 堆排序、迪杰斯特拉算法、TopK问题

关键说明:

  • stack/queue 底层默认用deque,因其支持双端高效增删,且避免vector扩容的开销;

  • priority_queue 底层基于vector实现的"大根堆"(默认),可通过仿函数指定为小根堆:

    cpp 复制代码
    // 小根堆
    priority_queue<int, vector<int>, greater<int>> minHeap;

五、容器选择策略:按需选型

选择容器的核心原则:匹配需求与容器性能特性,以下是常见场景的最优选择:

需求场景 推荐容器
动态数据、频繁随机访问、尾部增删 vector
固定大小、性能敏感、栈内存存储 array
中间频繁插入/删除、无需随机访问 list
双端增删频繁、需随机访问 deque
字典映射、键唯一、有序存储 map
字典映射、键唯一、追求高效访问(无需有序) unordered_map
去重、唯一性判断、有序存储 set
可重复元素、有序存储 multiset/multimap
栈/队列操作 stack/queue(底层deque)
优先级排序、TopK问题 priority_queue(底层vector)

六、实战技巧总结

  1. vector 性能优化 :批量插入前用reserve(n)预分配容量,减少扩容次数;
  2. 元素删除安全写法 :依赖erase返回值更新迭代器,避免迭代器失效;
  3. 复杂对象优先用 emplace_back:避免临时对象拷贝/移动开销,提升效率;
  4. 哈希容器优化 :通过reserve(n)减少rehash,自定义哈希函数降低冲突;
  5. 避免过度依赖 vector:中间频繁插删时,list效率远高于vector;
  6. map 与 unordered_map 选型:需有序/范围查询用map,需高效查找用unordered_map。

总结

STL的核心价值在于"泛型、复用、高效",掌握其关键在于理解容器底层实现性能特性。本文覆盖容器分类、核心原理、迭代器使用、适配器特性与实战技巧,建议结合代码练习(如容器增删查改、算法结合迭代器使用)加深理解。STL的学习无需死记硬背,重点在于"按需选型、避坑高效",熟练运用后能大幅提升编码效率与代码质量。

相关推荐
m0_736919102 小时前
C++中的策略模式实战
开发语言·c++·算法
孞㐑¥2 小时前
算法—位运算
c++·经验分享·笔记·算法
从此不归路2 小时前
Qt5 进阶【9】模型-视图框架实战:从 TableView 到自定义模型的一整套落地方案
开发语言·c++·qt
Stack Overflow?Tan902 小时前
c++constexpr
开发语言·c++
晚风吹长发2 小时前
初步了解Linux中的信号捕捉
linux·运维·服务器·c++·算法·进程·x信号
小程同学>o<2 小时前
嵌入式之C/C++(二)内存
c语言·开发语言·c++·笔记·嵌入式软件·面试题库
qq_417129253 小时前
基于C++的区块链实现
开发语言·c++·算法
2401_832402753 小时前
C++中的命令模式实战
开发语言·c++·算法
水饺编程3 小时前
第4章,[标签 Win32] :系统字体与字符大小
c语言·c++·windows·visual studio