标准库(STL)

C++ STL(Standard Template Library,标准模板库)是 C++ 标准库的核心组成部分,基于泛型编程思想实现,提供了一系列通用数据结构(容器) 和算法,旨在提高代码复用性、效率和规范性。STL 的设计遵循 “数据与操作分离” 原则:容器存储数据,算法通过迭代器操作容器数据,二者通过迭代器无缝协作。

STL 的六大核心组件概述

STL 由以下六大组件构成,彼此协同工作:

  1. 容器(Containers):存储数据的模板类(如动态数组、链表、哈希表等)。
  2. 迭代器(Iterators):连接容器与算法的 “桥梁”,提供访问容器元素的统一接口。
  3. 算法(Algorithms):通用操作函数(如排序、查找、复制等),通过迭代器操作容器。
  4. 函数对象(Functors):重载 () 运算符的类 / 结构体,可作为算法的参数(如自定义比较规则)。
  5. 适配器(Adapters):转换已有组件的接口(如将容器转为栈 / 队列,或调整函数参数)。
  6. 分配器(Allocators):负责容器的内存管理(默认无需手动干预)。

一、容器(Containers)

容器是 STL 中最常用的组件,用于存储同类型元素,分为三大类:序列容器关联容器无序关联容器

1. 序列容器(Sequence Containers)

特点:元素按插入顺序排列,可通过位置访问,不自动排序。

容器底层结构核心特性适用场景
vector动态数组连续内存,随机访问快(O (1));尾部插入 / 删除快(O (1)),中间插入 / 删除慢(O (n))。频繁随机访问、尾部操作,如存储列表数据。
list双向链表非连续内存,随机访问慢(O (n));任意位置插入 / 删除快(O (1))。频繁插入 / 删除(尤其是中间位置),如链表操作。
deque分段连续数组双端队列,头部 / 尾部插入 / 删除快(O (1)),随机访问较快(O (1))。双端频繁操作,如队列、缓冲区。
array固定大小数组编译期确定大小,连续内存,效率与 C 数组相当,更安全(支持边界检查)。已知固定大小的数组,替代 C 风格数组。
forward_list单向链表list 更节省空间,仅支持单向遍历,插入 / 删除效率同 list内存受限场景,单向遍历为主。

2. 关联容器(Associative Containers)

特点:元素按键(Key) 排序(默认升序),基于红黑树实现,查找效率高(O (log n))。

容器存储内容核心特性适用场景
set键(Key)即值(Value)键唯一,自动排序,支持快速查找、插入、删除。存储不重复元素,需排序和快速查找(如字典)。
multiset键即值,可重复set 类似,但键可重复。存储可重复元素,需排序(如成绩统计)。
map键值对(Key-Value)键唯一,按键排序,通过键快速访问值(类似字典)。存储键值映射(如 ID→用户信息)。
multimap键值对,键可重复map 类似,键可重复(一个键对应多个值)。一对多映射(如班级→学生列表)。

3. 无序关联容器(Unordered Associative Containers)

特点:元素无序,基于哈希表实现,平均插入 / 查找 / 删除效率为 O (1)(最坏 O (n)),C++11 新增。

容器存储内容核心特性适用场景
unordered_set键即值,唯一无序,哈希表存储,查找速度通常快于 set(无排序开销)。无需排序,仅需快速查找 / 去重(如黑名单)。
unordered_multiset键即值,可重复无序,键可重复,哈希表存储。无需排序,允许重复元素(如频率统计)。
unordered_map键值对,键唯一无序,哈希表存储,查找速度通常快于 map无需排序的键值映射(如缓存)。
unordered_multimap键值对,键可重复无序,键可重复,哈希表存储。无需排序的一对多映射。

二、迭代器(Iterators)

迭代器是容器元素的 “指针抽象”,提供统一的接口(如 ++*)访问容器元素,使算法可独立于容器类型。

  • 迭代器分类(按功能强弱):
迭代器类型支持操作适用容器
输入迭代器++*(只读)输入流(如 istream_iterator
输出迭代器++*(只写)输出流(如 ostream_iterator
前向迭代器++*(读写),单向遍历forward_list
双向迭代器++--*(读写),双向遍历listsetmap
随机访问迭代器支持双向迭代器所有操作 + 随机访问(+n[]vectordequearray

三、算法(Algorithms)

STL 提供了约 100 个通用算法(定义在 <algorithm> 中),按功能可分为:排序与查找修改与复制数值计算等。算法通过迭代器操作容器,不依赖具体容器类型。

  • 排序与查找std::sort(排序)、std::binary_search(二分查找)、std::find(线性查找)。
  • 修改与转换std::for_each(遍历元素)、std::transform(转换元素)、std::copy(复制元素)。
  • 集合操作std::set_union(并集)、std::set_intersection(交集)、std::remove(移除元素)。
  1. 排序算法std::sort(快速排序变种,平均 O (n log n))
#include <vector>
#include <algorithm>
 
int main() {
	std::vector<int> nums = {3, 1, 4, 1, 5};
	std::sort(nums.begin(), nums.end());  // 升序排序(默认)
	// 结果:1,1,3,4,5
 
	// 自定义排序(降序)
	std::sort(nums.begin(), nums.end(), std::greater<int>());  // 用标准函数对象
	// 结果:5,4,3,1,1
	return 0;
}
  1. 查找算法std::find(线性查找)、std::binary_search(二分查找,需先排序)
 
#include <vector>
 
#include <algorithm>
 
int main() {
 
std::vector<int> nums = {1, 3, 5, 7};
 
// 线性查找
 
auto it = std::find(nums.begin(), nums.end(), 3); // 找到3,返回迭代器
 
// 二分查找(需先排序)
bool has_5 = std::binary_search(nums.begin(), nums.end(), 5);  // true
return 0;
 
}
  1. 遍历与修改std::for_each(遍历元素并执行操作)、std::transform(转换元素)
#include <vector>
#include <algorithm>
#include <iostream>
 
int main() {
	std::vector<int> nums = {1, 2, 3};
	// 遍历并打印(用lambda作为操作)
	std::for_each(nums.begin(), nums.end(),
		[](int x) { std::cout << x << " "; });  // 1 2 3
 
	// 转换元素(每个数乘2)
	std::transform(nums.begin(), nums.end(), nums.begin(),
		[](int x) { return x * 2; });  // nums变为{2,4,6}
	return 0;
}

四、函数对象(Functors)

函数对象(仿函数)是重载 () 运算符的类 / 结构体,可像函数一样被调用。其优势是:可存储状态(成员变量),比普通函数更灵活,常用于算法的自定义参数(如比较规则、转换逻辑)。

示例:自定义函数对象

#include <vector>
#include <algorithm>
 
// 自定义函数对象:比较两个数的平方
struct SquareCompare {
    bool operator()(int a, int b) const {
        return a*a < b*b;  // 按平方值升序
    }
};
 
int main() {
    std::vector<int> nums = {3, -2, 1};
    // 用自定义函数对象排序
    std::sort(nums.begin(), nums.end(), SquareCompare());
    // 结果:1, -2, 3(平方分别为1,4,9)
    return 0;
}

标准函数对象(定义在 <functional> 中)

STL 提供了常用函数对象,如 std::plus(加法)、std::less(小于)、std::greater(大于)等,可直接用于算法:

#include <functional>  // 标准函数对象
#include <vector>
#include <algorithm>
 
int main() {
    std::vector<int> nums = {3, 1, 4};
    // 用std::greater排序(降序)
    std::sort(nums.begin(), nums.end(), std::greater<int>());  // 4,3,1
    return 0;
}

五、适配器(Adapters)

适配器用于转换已有组件的接口,使其满足新的需求,分为三类:

1. 容器适配器(Container Adapters)

基于现有容器实现特定数据结构,隐藏底层容器的部分接口,只暴露特定功能。

适配器底层默认容器核心功能适用场景
stackdeque后进先出(LIFO)栈操作(如表达式求值)
queuedeque先进先出(FIFO)队列操作(如任务调度)
priority_queuevector优先级队列(最大元素先出)按优先级处理任务(如堆排序)

2. 迭代器适配器(Iterator Adapters)

转换迭代器的行为,如反向遍历、插入元素等。常见的有:

  • reverse_iterator:反转迭代器方向(rbegin()rend());
  • insert_iterator:将赋值操作转为插入操作(如 back_inserter)。

3. 函数适配器(Function Adapters)

调整函数的参数或行为,如绑定参数、否定逻辑等。C++11 后常用 std::bind(替代旧版 bind1st/bind2nd)。

示例:用 std::bind 绑定函数参数

#include <functional>
#include <iostream>
 
// 原函数:a + b * c
int func(int a, int b, int c) {
    return a + b * c;
}
 
int main() {
    // 绑定b=2,c=3,只保留a作为参数:func(a, 2, 3) = a + 6
    auto bound_func = std::bind(func, std::placeholders::_1, 2, 3);
    std::cout << bound_func(4) << std::endl;  // 4 + 2*3 = 10
    return 0;
}

六、分配器(Allocators)

分配器负责容器的内存分配与释放,是容器模板的可选参数(默认使用 std::allocator)。用户通常无需关注,但可自定义分配器优化特定场景(如内存池、共享内存)。

示例:默认分配器与容器

#include <vector>
#include <memory>  // std::allocator
 
int main() {
    // 显式指定分配器(默认就是std::allocator<int>)
    std::vector<int, std::allocator<int>> vec;
    vec.push_back(1);
    return 0;
}

其他库

多线程与并发见 多线程 <memory> 见内存管理

  • <utility>:包含 std::pair(键值对)、std::swap(交换)、std::move(移动语义)等基础工具。
  • <tuple>:元组类型(std::tuple),可存储多个不同类型的值(类似结构体的轻量替代)。
  • <optional>(C++17):std::optional 表示 “可能存在的值”,避免用特殊值(如 -1)表示 “无结果”。
  • <variant>(C++17):std::variant 表示 “多类型中的一种”,类型安全的联合体。
  • <chrono>(C++17):时间库,用于时间点、时间段的计算(如秒、毫秒级计时)。
  • 输入输出(<iostream><fstream><sstream>
    • <iostream>:标准输入输出(std::cin / std::cout),用于控制台交互。
    • <fstream>:文件输入输出(std::ifstream / std::ofstream),支持文件读写。
    • <sstream>:字符串流(std::stringstream),用于字符串与其他类型的转换(如数字转字符串)。
  • 数值工具(<cmath><numeric><random>
    • <cmath>:数学函数库,包含三角函数(sin/cos)、指数对数(exp/log)、取整(ceil/floor)等。
    • <numeric>:数值算法,如 std::accumulate(累加)、std::gcd(最大公约数,C++17)。
    • <random>:高质量随机数生成器,替代 C 风格的 rand()(分布不均匀)。
  • 字符串处理(<string><string_view>
    • <string>:动态字符串类,封装了字符串的创建、拼接、查找、替换等操作,比 C 风格字符串(char*)更安全易用。
    • <string_view>(C++17):轻量级字符串视图,用于 “只读访问字符串”,避免不必要的字符串拷贝(性能优化)。
  • 文件系统(C++17)
    • std::filesystem::path, create_directories, exists
  • 范围库(C++20)
    • std::views::filter, std::views::transform
  • 协程(C++20)
    • 异步编程的新范式(如 co_await, co_yield)。
  • 概念(Concepts, C++20)
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;
};

STL 详解

顺序容器 (Sequence Containers)

顺序容器的核心特点是:它们以线性方式存储元素,元素的位置由它们被插入的顺序决定(或者由插入位置的迭代器决定),而不是由元素的值决定。这与关联容器(如 set, map)形成鲜明对比。

主要的顺序容器

C++ STL 提供了以下几种主要的顺序容器:

  1. std::vector: 动态数组
  2. std::deque: 双端队列 (Double-Ended Queue)
  3. std::list: 双向链表 (Doubly Linked List)
  4. std::forward_list: 单向链表 (Singly Linked List) - C++11 引入
  5. std::array: 静态数组 - C++11 引入
  6. std::string: 字符串 (可以看作是 std::vector<char> 的特化,但有自己独特的成员函数)

详解每个容器

1. std::vector (动态数组)

  • 特点:

    • 连续存储: 元素在内存中是连续存放的,就像一个动态大小的数组。
    • 随机访问: 支持通过下标 []at() 方法进行高效的 O(1) 随机访问。
    • 尾部操作高效: 在容器尾部进行插入 (push_back) 和删除 (pop_back) 操作非常高效,通常是摊销 O(1)。
    • 中间/头部操作低效: 在容器中间或头部插入或删除元素需要移动大量元素,效率为 O(n),非常慢。
    • 自动扩容: 当容量不足时,vector 会自动分配更大的内存块,将旧元素复制过去,并释放旧内存。这个过程会使得迭代器失效。
    • 迭代器失效: 尾部插入可能导致所有迭代器失效(如果发生扩容);任何位置的插入都会使插入点及之后的所有迭代器失效;删除会使被删除元素及之后的所有迭代器失效。
  • 适用场景:

    • 需要频繁随机访问元素。
    • 主要在尾部进行插入和删除。
    • 存储元素数量在运行时可能变化,但变化主要发生在尾部。
    • 需要将数据传递给期望 C 风格数组的 C 函数(使用 &vec[0]vec.data())。

2. std::deque (双端队列)

  • 特点:

    • 分段连续存储: 内部通常由多个固定大小的、不连续的内存块(数组)组成,通过指针链接。逻辑上是连续的,物理上是分段的。
    • 双端高效: 在头部尾部进行插入 (push_front, push_back) 和删除 (pop_front, pop_back) 操作都非常高效,通常是 O(1)。
    • 随机访问: 支持 O(1) 的随机访问(通过下标 []at()),但比 vector 稍慢(因为需要计算内存块和偏移)。
    • 中间操作低效: 在中间插入或删除元素仍然需要移动元素,效率为 O(n)。
    • 迭代器失效: 尾部或头部插入通常只使所有迭代器失效(但指向元素的指针和引用通常不失效,这是与 vector 的重要区别);中间插入/删除会使所有迭代器失效。
  • 适用场景:

    • 需要在两端高效地进行插入和删除(如实现队列、双端队列)。
    • 需要随机访问,但插入/删除主要发生在两端。
    • 不需要 vector 的连续内存特性。

3. std::list (双向链表)

  • 特点:

    • 非连续存储: 元素通过指针链接,内存不连续。
    • 任意位置高效插入/删除: 只要拥有指向插入/删除位置的迭代器,就可以在 O(1) 时间内完成插入 (insert) 和删除 (erase) 操作,不会使其他元素的迭代器、指针或引用失效(被删除的元素除外)。
    • 无随机访问: 不支持通过下标直接访问。访问特定位置的元素需要从头或尾遍历,效率为 O(n)。
    • 额外内存开销: 每个元素都需要额外的内存来存储前向和后向指针。
    • 迭代器失效: 只有被删除的元素的迭代器失效,其他所有迭代器都保持有效。
  • 适用场景:

    • 需要在任意位置频繁地插入和删除元素。
    • 不需要随机访问。
    • 需要保持指向容器中元素的迭代器、指针或引用在插入/删除操作后仍然有效(除了被删除的元素)。

4. std::forward_list (单向链表)

  • 特点:

    • 非连续存储: 元素通过指针链接,内存不连续。
    • 最小开销: 每个元素只需要一个指针,内存开销比 list 更小。
    • 单向遍历: 只能从前往后遍历,不支持反向迭代器。
    • 任意位置高效插入/删除: 与 list 类似,只要拥有指向位置的迭代器,插入/删除是 O(1)。但插入操作通常在给定迭代器之后进行 (insert_after)。
    • 无随机访问: 访问效率为 O(n)。
    • 迭代器失效: 与 list 类似,只有被删除的元素的迭代器失效。
  • 适用场景:

    • 对内存非常敏感,且只需要单向遍历。
    • 需要在任意位置频繁插入/删除,但不需要双向遍历。
    • 作为栈或某些特定算法(如哈希表的桶)的底层实现。

5. std::array (静态数组)

  • 特点:

    • 固定大小: 大小在编译时确定,不能改变。
    • 连续存储: 元素在内存中连续存放。
    • 高性能: 没有动态内存分配开销,性能与 C 风格数组相当。
    • 安全: 提供 at() 方法进行边界检查,size() 方法获取大小。
    • 可拷贝: 可以像普通对象一样被拷贝和赋值(C 风格数组不行)。
    • 迭代器: 支持标准迭代器。
    • 无迭代器失效问题: 因为大小固定,没有插入/删除操作(std::array 没有 push_back, pop_back, insert, erase 等成员函数)。
  • 适用场景:

    • 知道确切的、固定的元素数量。
    • 需要 C 风格数组的性能,但希望获得 STL 容器的安全性和便利性。
    • 作为函数参数传递数组时,避免退化为指针。

6. std::string (字符串)

  • 特点:

    • 本质上是 std::basic_string<char>,可以看作是 std::vector<char> 的特化,但提供了大量针对字符串操作的便利函数。
    • 连续存储: 通常(但不是强制要求)是连续存储的(C++11 起要求)。
    • 动态大小: 大小可以在运行时改变。
    • 丰富的字符串操作: substr, find, replace, append, c_str(), data() 等。
    • 尾部操作高效: push_back, pop_back 高效。
    • 迭代器失效: 与 vector 类似,插入可能导致所有迭代器失效。
  • 适用场景:

    • 存储和操作文本字符串。

通用操作 (Common Operations)

几乎所有顺序容器都支持以下通用操作(具体细节可能略有差异):

操作描述示例
构造
C c;默认构造,创建空容器vector<int> v;
C c(n);创建包含 n 个默认构造元素的容器vector<int> v(10); // 10个0
C c(n, val);创建包含 n 个值为 val 的元素的容器vector<int> v(5, 1); // {1,1,1,1,1}
C c(c2);拷贝构造vector<int> v2(v1);
C c(b, e);用迭代器范围 [b, e) 构造vector<int> v(arr, arr+5);
C c{a, b, c};列表初始化 (C++11)vector<int> v{1,2,3};
赋值与交换
c1 = c2拷贝赋值v1 = v2;
c1 = il列表赋值 (C++11)v = {1,2,3};
c1.swap(c2)交换两个容器的内容 (高效,通常 O(1))v1.swap(v2);
swap(c1, c2)同上,非成员函数swap(v1, v2);
大小与容量
c.size()返回元素数量v.size()
c.max_size()返回容器能容纳的最大元素数v.max_size()
c.empty()检查容器是否为空if (v.empty()) ...
c.resize(n)调整容器大小到 n。如果 n > size(),用默认值填充;如果 n < size(),末尾元素被删除。v.resize(10);
c.resize(n, val)调整大小,新增元素值为 valv.resize(10, 0);
c.shrink_to_fit()(vector, string) 请求释放未使用的内存 (非强制)v.shrink_to_fit();
c.capacity()(vector, string) 返回当前分配的存储容量v.capacity()
c.reserve(n)(vector, string) 预留至少能容纳 n 个元素的空间v.reserve(100);
访问元素
c[i]返回索引 i 处元素的引用(不检查越界int x = v[0];
c.at(i)返回索引 i 处元素的引用(会检查越界,越界抛出 std::out_of_rangeint x = v.at(0);
c.front()返回第一个元素的引用int first = v.front();
c.back()返回最后一个元素的引用int last = v.back();
c.data()(vector, array, string) 返回指向内部连续存储的首元素的指针int* p = v.data();
迭代器
c.begin() / c.cbegin()返回指向第一个元素的迭代器 / const 迭代器auto it = v.begin();
c.end() / c.cend()返回指向最后一个元素之后的迭代器 / const 迭代器auto it = v.end();
c.rbegin() / c.crbegin()返回指向最后一个元素的反向迭代器 / const 反向迭代器auto rit = v.rbegin();
c.rend() / c.crend()返回指向第一个元素之前的反向迭代器 / const 反向迭代器auto rit = v.rend();
插入与删除
c.push_back(val)尾部插入元素 valv.push_back(42);
c.emplace_back(args...)尾部原地构造元素 (C++11)v.emplace_back(42);
c.pop_back()删除最后一个元素v.pop_back();
c.push_front(val)头部插入元素 vald.push_front(42); // deque, list, forward_list
c.emplace_front(args...)头部原地构造元素 (C++11)d.emplace_front(42);
c.pop_front()删除第一个元素d.pop_front(); // deque, list, forward_list
c.insert(p, val)在迭代器 p 指向的位置之前插入 val,返回新元素的迭代器auto it = v.insert(v.begin()+2, 10);
c.emplace(p, args...)在迭代器 p 指向的位置之前原地构造元素 (C++11)v.emplace(v.begin()+2, 10);
c.erase(p)删除迭代器 p 指向的元素,返回下一个元素的迭代器auto it = v.erase(v.begin());
c.erase(b, e)删除迭代器范围 [b, e) 内的元素,返回 eauto it = v.erase(v.begin(), v.begin()+2);
c.clear()删除所有元素v.clear();
c.remove(val)(list, forward_list) 删除所有值等于 val 的元素lst.remove(0);
c.remove_if(pred)(list, forward_list) 删除所有满足谓词 pred 的元素lst.remove_if([](int n){return n<0;});
c.unique()(list, forward_list) 删除连续的重复元素lst.unique();
c.sort()(list, forward_list) 对容器内元素进行排序lst.sort();
c.splice(p, c2, q)(list, forward_list) 将 c2q 指向的元素移动到 cp 之前lst1.splice(lst1.begin(), lst2, it);

总结与选择指南

选择哪个顺序容器取决于你的具体需求:

  • 首选 std::vector: 如果你需要随机访问,并且插入/删除主要发生在尾部,vector 通常是最佳选择,因为它缓存友好,性能优秀。
  • 需要两端操作选 std::deque: 如果你需要在头部和尾部都高效地插入/删除,deque 是理想选择。
  • 频繁中间插入/删除选 std::liststd::forward_list: 如果你需要在任意位置(尤其是中间)频繁插入/删除,并且不关心随机访问,listforward_list 非常适合。list 支持双向遍历,forward_list 内存开销更小。
  • 大小固定选 std::array: 如果元素数量在编译时已知且不变,array 是最安全、最高效的选择。
  • 处理文本用 std::string: 专门用于字符串操作。

理解每个容器的底层数据结构(数组 vs 链表)、内存布局(连续 vs 非连续)以及操作的时间复杂度是做出正确选择的关键。

关联容器 (Associative Containers)

关联容器的核心特点是:它们根据元素的(key) 来存储和组织元素,而不是像顺序容器那样依赖于插入顺序。这使得查找、插入和删除操作通常非常高效(通常是 O(log n))。


主要的关联容器

C++ STL 提供了以下几种主要的关联容器:

  1. std::set: 集合
  2. std::multiset: 多重集合
  3. std::map: 映射 (字典)
  4. std::multimap: 多重映射
  5. std::unordered_set: 无序集合 (C++11 引入)
  6. std::unordered_multiset: 无序多重集合 (C++11 引入)
  7. std::unordered_map: 无序映射 (C++11 引入)
  8. std::unordered_multimap: 无序多重映射 (C++11 引入)

这些容器可以分为两大类:

  • 有序关联容器 (Ordered Associative Containers): set, multiset, map, multimap

    • 内部通常基于平衡二叉搜索树(如红黑树)实现。
    • 元素按键的排序顺序存储(默认是升序)。
    • 提供对数时间复杂度 O(log n) 的查找、插入和删除。
    • 支持基于排序的范围操作(如 lower_bound, upper_bound)。
  • 无序关联容器 (Unordered Associative Containers): unordered_set, unordered_multiset, unordered_map, unordered_multimap

    • 内部基于哈希表(Hash Table) 实现。
    • 元素的存储位置由其键的哈希值决定,不保证任何特定的顺序。
    • 在理想情况下,提供平均常数时间复杂度 O(1) 的查找、插入和删除。
    • 性能高度依赖于哈希函数的质量和哈希表的负载因子。

详解每个容器

1. std::set (集合)

  • 特点:

    • 存储唯一的键(key)。
    • 键是只读的(不能通过迭代器修改键,因为这会破坏排序)。
    • 内部元素按键排序。
    • 不允许重复的键。
    • 查找、插入、删除:O(log n) (有序) / 平均 O(1) (无序)。
  • 适用场景:

    • 存储一组唯一的、不重复的元素。
    • 需要快速判断某个元素是否存在。
    • 需要元素自动排序。

2. std::multiset (多重集合)

  • 特点:

    • set 基本相同,但允许键重复。
    • 可以有多个相同的键。
    • 查找、插入、删除:O(log n) (有序) / 平均 O(1) (无序)。
  • 适用场景:

    • 需要存储可能重复的元素,并且希望它们自动排序。
    • 统计频率(虽然 map<Key, int> 可能更直观)。

3. std::map (映射)

  • 特点:

    • 存储键值对(key-value pairs)。
    • 键是唯一的,值与键关联。
    • 键是只读的(不能通过迭代器修改键)。
    • 内部元素按键排序。
    • 不允许重复的键。
    • 提供类似数组的访问语法 operator[]
    • 查找、插入、删除:O(log n) (有序) / 平均 O(1) (无序)。
  • 适用场景:

    • 建立键到值的唯一映射关系(如字典、电话簿)。
    • 需要通过键快速查找对应的值。

4. std::multimap (多重映射)

  • 特点:

    • map 基本相同,但允许键重复。
    • 一个键可以关联多个值。
    • 没有 operator[] 成员函数(因为一个键可能对应多个值,[] 无法确定返回哪个)。
    • 查找、插入、删除:O(log n) (有序) / 平均 O(1) (无序)。
  • 适用场景:

    • 一个键需要对应多个值(如一个作者对应多本书)。

5. std::unordered_set / std::unordered_multiset / std::unordered_map / std::unordered_multimap

  • 特点:

    • 功能分别对应 set / multiset / map / multimap
    • 基于哈希表实现。
    • 不保证元素的顺序。
    • 需要为键类型提供或定义一个哈希函数(hash function) 和一个相等比较函数(equality function)。
    • 在哈希函数良好的情况下,性能通常优于有序容器。
    • 可能发生哈希冲突,性能退化为 O(n) 最坏情况(但平均是 O(1))。
    • 迭代器可能在插入/删除时失效(当哈希表重新散列时)。
  • 适用场景:

    • 当查找速度是首要考虑因素,且不需要元素有序时。
    • 数据量较大,且哈希函数分布均匀。

通用操作 (Common Operations)

关联容器支持许多通用操作。以下表格主要针对有序关联容器,无序容器操作类似,但前缀为 unordered_

操作描述示例
构造
C c;默认构造,创建空容器set<int> s;
C c(comp);使用自定义比较函数 comp 构造set<int, greater<int>> s; // 降序
C c(c2);拷贝构造set<int> s2(s1);
C c(b, e);用迭代器范围 [b, e) 构造set<int> s(v.begin(), v.end());
C c(il);列表初始化 (C++11)set<int> s{1,2,3};
赋值与交换
c1 = c2拷贝赋值s1 = s2;
c1 = il列表赋值 (C++11)s = {1,2,3};
c1.swap(c2)交换两个容器的内容 (高效)s1.swap(s2);
swap(c1, c2)同上,非成员函数swap(s1, s2);
大小与容量
c.size()返回元素数量s.size()
c.max_size()返回容器能容纳的最大元素数s.max_size()
c.empty()检查容器是否为空if (s.empty()) ...
访问元素
c.count(k)返回键 k 的元素数量(set/map 为 0 或 1,multiset/multimap 可能大于 1)if (s.count(5)) ...
c.find(k)查找键 k,如果找到返回指向该元素的迭代器,否则返回 c.end()auto it = s.find(5); if (it != s.end()) ...
c.lower_bound(k)返回第一个不小于 k 的元素的迭代器auto it = s.lower_bound(5);
c.upper_bound(k)返回第一个大于 k 的元素的迭代器auto it = s.upper_bound(5);
c.equal_range(k)返回一个 pair,包含 lower_bound(k)upper_bound(k) 的结果,表示所有键为 k 的元素的范围auto range = s.equal_range(5); for (auto it = range.first; it != range.second; ++it) ...
m[k](map/unordered_map) 访问与键 k 关联的值。如果 k 不存在,则插入一个以 k 为键、值被默认初始化的元素。慎用,可能意外插入元素!int val = m["key"]; m["new_key"] = 10;
m.at(k)(map/unordered_map) 访问与键 k 关联的值。如果 k 不存在,抛出 std::out_of_range 异常。比 [] 更安全。int val = m.at("key");
迭代器
c.begin() / c.cbegin()返回指向第一个元素的迭代器 / const 迭代器auto it = s.begin();
c.end() / c.cend()返回指向最后一个元素之后的迭代器 / const 迭代器auto it = s.end();
c.rbegin() / c.crbegin()返回指向最后一个元素的反向迭代器 / const 反向迭代器auto rit = s.rbegin();
c.rend() / c.crend()返回指向第一个元素之前的反向迭代器 / const 反向迭代器auto rit = s.rend();
插入与删除
c.insert(val)插入元素 val。对于 set/map,如果键已存在则插入失败。返回一个 pair<iterator, bool>bool 表示是否插入成功。auto result = s.insert(5); if (result.second) cout << "Inserted";
c.insert(hint, val)提供插入位置的提示 hint,可能提高插入效率(尤其在有序容器中)。s.insert(s.end(), 5);
c.insert(b, e)插入迭代器范围 [b, e) 内的所有元素s.insert(v.begin(), v.end());
c.insert(il)插入初始化列表 il 中的所有元素 (C++11)s.insert({1,2,3});
c.emplace(args...)原地构造元素并插入 (C++11)s.emplace(5);
c.emplace_hint(hint, args...)原地构造元素并插入,提供提示s.emplace_hint(s.end(), 5);
c.erase(k)删除所有键等于 k 的元素,返回删除的元素数量size_t n = s.erase(5);
c.erase(p)删除迭代器 p 指向的元素,返回下一个元素的迭代器auto it = s.erase(it);
c.erase(b, e)删除迭代器范围 [b, e) 内的元素,返回 es.erase(s.begin(), s.find(5));
c.clear()删除所有元素s.clear();

无序容器的特有操作

无序容器还提供一些与哈希表内部状态相关的操作:

操作描述
c.bucket_count()返回桶的数量
c.max_bucket_count()返回桶的最大数量
c.bucket_size(n)返回第 n 个桶中的元素数量
c.bucket(k)返回键 k 所在的桶的索引
c.load_factor()返回当前的负载因子(元素总数 / 桶数)
c.max_load_factor()获取或设置最大负载因子
c.rehash(n)将桶的数量设置为至少 n,并重新散列元素
c.reserve(n)请求容器至少能容纳 n 个元素而不必重新散列

总结与选择指南

选择关联容器的关键在于理解你的需求:

  1. 需要键值对还是仅键?

    • 需要映射关系:选 mapmultimap
    • 只需要存储唯一键:选 setmultiset
  2. 键是否需要唯一?

    • 键必须唯一:选 setmap
    • 键可以重复:选 multisetmultimap
  3. 是否需要元素有序?

    • 需要元素按键排序,或需要 lower_bound/upper_bound 等范围操作:选有序容器 (set, map, multiset, multimap)。
    • 不需要顺序,只追求最快的平均查找速度:选无序容器 (unordered_set, unordered_map 等)。
  4. 性能考量:

    • 有序容器: 稳定的 O(log n) 性能,内存开销相对较小,支持有序遍历。
    • 无序容器: 平均 O(1) 性能,但最坏情况 O(n),可能有较高的内存开销(哈希表),且性能受哈希函数影响大。

简单决策树:

  • 我需要一个字典(键 值)且键唯一?
    • 需要按键排序或范围查询 std::map
    • 只求最快查找 std::unordered_map
  • 我需要一个集合(只存键)且键唯一?
    • 需要排序 std::set
    • 只求最快查找 std::unordered_set
  • 我需要一个键对应多个值?
    • 需要排序 std::multimap
    • 只求最快查找 std::unordered_multimap
  • 我需要存储可能重复的元素?
    • 需要排序 std::multiset
    • 只求最快查找 std::unordered_multiset

理解这些容器的内部机制(树 vs 哈希表)和操作的时间复杂度,是高效使用 STL 的基础。

算法库(Algorithm Library)

核心理念与设计哲学

STL 算法库的设计基于以下几个关键原则:

  1. 泛型 (Genericity):

    • 算法不直接操作具体的容器(如 vector, list),而是通过迭代器(Iterators)来访问和操作元素。
    • 这使得同一个算法可以无缝地应用于任何提供相应迭代器类型的容器。例如,std::sort 可以对 vector<int>, deque<double>, 甚至是原生数组进行排序。
  2. 分离关注点 (Separation of Concerns):

    • 容器负责数据的存储和管理。
    • 迭代器负责提供对数据的访问接口。
    • 算法负责执行具体的计算或操作逻辑。
    • 这种分离使得组件高度可复用。
  3. 效率 (Efficiency):

    • STL 算法经过精心设计和优化,通常能达到理论上的最佳时间复杂度。
    • 算法会根据传入的迭代器类型选择最优的实现策略(例如,std::sort 对随机访问迭代器使用高效的混合排序算法)。
  4. 就地操作 (In-Place Operation):

    • 大多数算法直接在输入数据上进行修改,而不是创建新的副本,这节省了内存和复制开销。
    • 例如,std::sort 直接修改原容器中的元素顺序。
  5. 函数式编程思想:

    • 许多算法接受可调用对象(函数指针、函数对象、Lambda 表达式)作为参数,用于自定义操作逻辑(如比较、转换、判断)。
    • 这使得算法非常灵活和可定制。

算法的通用接口

几乎所有 STL 算法都遵循一个统一的接口模式:

template <typename Iterator, typename ...Args>
ReturnType algorithm_name(Iterator first, Iterator last, Args... args);
  • [first, last): 这是最核心的参数。它定义了一个左闭右开的迭代器范围,表示算法操作的数据区间。first 指向第一个元素,last 指向最后一个元素的后一个位置
  • 返回值: 通常是迭代器,用于指示操作的结果位置(如新逻辑末尾、找到的元素位置等)。
  • 可选参数: 如自定义的比较函数、操作函数、目标迭代器等。

主要算法分类详解

1. 非修改性序列操作 (Non-modifying Sequence operations)

这些算法遍历序列但不修改元素。

  • std::find / std::find_if / std::find_if_not:

    • 原理: 从 first 开始,逐个比较元素,直到找到匹配项或到达 last

    • 方法:

      auto it = std::find(vec.begin(), vec.end(), 42); // 找值为42的元素
      auto it = std::find_if(vec.begin(), vec.end(), [](int n){ return n > 10; }); // 找第一个大于10的元素
    • 复杂度: O(n)

  • std::count / std::count_if:

    • 原理: 遍历整个范围,对满足条件的元素进行计数。

    • 方法:

      int n = std::count(vec.begin(), vec.end(), 1); // 统计1的个数
      int even_count = std::count_if(vec.begin(), vec.end(), [](int n){ return n % 2 == 0; }); // 统计偶数个数
    • 复杂度: O(n)

  • std::all_of / std::any_of / std::none_of:

    • 原理: 检查范围内所有元素是否都满足、任意一个满足、或都不满足某个条件。

    • 方法:

      bool all_positive = std::all_of(vec.begin(), vec.end(), [](int n){ return n > 0; });
    • 复杂度: O(n),但可能提前终止(短路求值)。

  • std::equal / std::lexicographical_compare:

    • 原理: equal 比较两个范围是否相等。lexicographical_compare 按字典序比较两个范围(常用于字符串比较)。
    • 复杂度: O(min(n1, n2))

2. 修改性序列操作 (Modifying Sequence operations)

这些算法会修改序列中的元素或重新排列它们。

  • std::copy / std::copy_if / std::copy_n:

    • 原理: 将一个范围 [first, last) 内的元素复制到从 d_first 开始的目标范围。

    • 方法:

      std::vector<int> src = {1, 2, 3, 4, 5};
      std::vector<int> dest(5); // 必须预先分配足够空间!
      std::copy(src.begin(), src.end(), dest.begin());
    • 复杂度: O(n)

    • 注意: 目标范围必须有足够的空间,否则行为未定义。可以使用插入迭代器适配器(如 std::back_inserter)自动增长目标容器。

  • std::move:

    • 原理: 与 copy 类似,但使用移动语义。源元素被 ” 移动 ” 到目标,源元素处于有效但未指定的状态(通常为空或可析构)。
    • 方法: 用于高效转移资源(如大对象、动态内存)。
    • 复杂度: O(n)
  • std::fill / std::fill_n / std::generate / std::generate_n:

    • 原理: fill 将范围内的所有元素设置为指定值。generate 使用一个函数(或可调用对象)为每个元素生成新值。

    • 方法:

      std::fill(vec.begin(), vec.end(), 0); // 所有元素设为0
      std::generate(vec.begin(), vec.end(), std::rand); // 用随机数填充
    • 复杂度: O(n)

  • std::transform:

    • 原理: 对输入范围内的每个元素应用一个一元函数,并将结果写入输出范围。或者,对两个输入范围的对应元素应用一个二元函数

    • 方法:

      // 一元:将vec中每个元素平方,存入squares
      std::transform(vec.begin(), vec.end(), squares.begin(), [](int n){ return n * n; });
      // 二元:将vec1和vec2对应元素相加,存入result
      std::transform(vec1.begin(), vec1.end(), vec2.begin(), result.begin(), std::plus<int>());
    • 复杂度: O(n)

  • std::replace / std::replace_if:

    • 原理: 将范围内等于 old_value 的元素替换为 new_value,或替换满足条件的元素。

    • 方法:

      std::replace(vec.begin(), vec.end(), 0, -1); // 将所有0替换为-1
    • 复杂度: O(n)

  • std::sort / std::stable_sort:

    • 原理: sort 通常使用一种混合排序算法(如 introsort,结合了快速排序、堆排序和插入排序),平均和最坏情况复杂度均为 O(n log n)。stable_sort 保证相等元素的相对顺序不变,可能使用归并排序,需要额外内存。

    • 要求: 需要随机访问迭代器vector, deque, array)。

    • 方法:

      std::sort(vec.begin(), vec.end()); // 升序
      std::sort(vec.begin(), vec.end(), std::greater<int>()); // 降序
    • 复杂度: O(n log n)

  • std::partial_sort / std::nth_element:

    • 原理: partial_sort 将范围中最小的 n 个元素排序并放在前面。nth_element 将第 n 小的元素放在其最终排序后的位置,左边都小于等于它,右边都大于等于它(但不保证左右部分有序)。
    • 用途: 当只需要前 K 个最小元素或找中位数时,比完全排序更高效。
    • 复杂度: partial_sort O(n log k), nth_element 平均 O(n)
  • std::is_sorted / std::lower_bound / std::upper_bound / std::binary_search:

    • 原理: 这些算法要求范围已排序lower_bound 找到第一个不小于 val 的位置,upper_bound 找到第一个大于 val 的位置,binary_search 判断 val 是否存在。
    • 复杂度: O(log n)

4. 删除与移除操作 (Removal operations)

重要概念: STL 中的 “remove” 算法不真正删除容器中的元素,而是将需要 ” 保留 ” 的元素移动到范围的前面,并返回新的逻辑末尾迭代器。真正的删除需要调用容器的 erase 方法。

  • std::remove / std::remove_if:

    • 原理: 将不等于 value(或不满足谓词)的元素向前移动,覆盖掉需要移除的元素。返回新的逻辑末尾。

    • 方法 (经典的 “erase-remove” 惯用法):

      vec.erase(std::remove(vec.begin(), vec.end(), 0), vec.end()); // 删除所有0
      vec.erase(std::remove_if(vec.begin(), vec.end(), [](int n){ return n < 0; }), vec.end()); // 删除所有负数
    • 复杂度: O(n)

  • std::unique / std::unique_copy:

    • 原理: 移除相邻的重复元素。只比较相邻元素。

    • 方法:

      vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); // 删除相邻重复元素

5. 集合操作 (Set operations)

这些算法要求两个输入范围都已排序

  • std::merge:

    • 原理: 将两个已排序的范围合并成一个已排序的范围。

    • 方法:

      std::merge(vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), result.begin());
    • 复杂度: O(n1 + n2)

  • std::set_union / std::set_intersection / std::set_difference / std::set_symmetric_difference:

    • 原理: 分别计算两个已排序集合的并集、交集、差集、对称差集。
    • 复杂度: O(n1 + n2)

6. 最小/最大操作 (Minimum/Maximum operations)

  • std::min / std::max / std::minmax:

    • 原理: 返回两个值或初始化列表中的最小值、最大值或两者。

    • 方法:

      int a = 3, b = 7;
      int m = std::min(a, b);
      auto [min_val, max_val] = std::minmax({1, 5, 3, 9, 2});
  • std::min_element / std::max_element / std::minmax_element:

    • 原理: 在范围内查找最小/最大元素的迭代器。
    • 复杂度: O(n)

7. 数值算法 (Numerical operations) - <numeric>

  • std::accumulate:

    • 原理: 计算范围内元素的和(或通过二元操作符计算的累积值)。

    • 方法:

      int sum = std::accumulate(vec.begin(), vec.end(), 0); // 求和
      int product = std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>()); // 求积
    • 复杂度: O(n)

  • std::inner_product:

    • 原理: 计算两个范围的点积(内积)。

    • 方法:

      int dot = std::inner_product(vec1.begin(), vec1.end(), vec2.begin(), 0);
  • std::adjacent_difference / std::partial_sum:

    • 原理: adjacent_difference 计算相邻元素的差值。partial_sum 计算部分和。

    • 方法:

      // vec: {1, 3, 6, 10} -> diff: {1, 2, 3, 4}
      std::adjacent_difference(vec.begin(), vec.end(), diff.begin());

使用 STL 算法的最佳实践

  1. 优先使用算法: 遇到循环处理容器的场景,先想想是否有现成的 STL 算法可用。
  2. 善用 Lambda: 用 Lambda 表达式提供简洁的谓词或操作,避免定义繁琐的函数对象。
  3. 理解迭代器要求: 确保选择的算法与容器的迭代器类型兼容(如 sort 需要随机访问迭代器)。
  4. 掌握 “erase-remove” 惯用法: 这是安全删除容器元素的标准方法。
  5. 注意目标空间: 使用 copy, transform 等算法时,确保目标范围有足够的空间,或使用 back_inserter, inserter 等适配器。
  6. 利用算法的返回值: 返回的迭代器通常包含重要信息(如新末尾、找到的位置)。
  7. 保持范围有序: 如果需要频繁使用 binary_search, lower_bound 等,考虑使用 setmap,或者在插入后手动维护有序性。

Vector

std::vectoremplace_backpush_back 都用于在容器末尾添加新元素,但它们在实现机制和性能上存在关键区别。理解这些差异对于编写高效的 C++ 代码至关重要。


1. push_back

push_back 是一个成员函数,用于将一个已经构造好的对象的副本或移动版本添加到 vector 的末尾。

语法:

void push_back( const T& value );    // 添加 const 引用(复制)
void push_back( T&& value );         // 添加右值引用(移动)

工作流程:

  1. 对象构造:首先在调用 push_back 之前或作为参数传递时,对象必须已经完全构造。
  2. 传递给 push_back:将构造好的对象传递给 push_back
  3. 复制或移动push_backvector 内部为新元素分配内存,并通过复制构造函数(如果是左值)或移动构造函数(如果是右值)将传入的对象“放置”到新分配的内存中。

示例:

#include <vector>
#include <string>
#include <iostream>
 
struct Person {
    std::string name;
    int age;
 
    Person(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Person constructed: " << name << ", " << age << std::endl;
    }
 
    // 演示复制构造
    Person(const Person& other) : name(other.name), age(other.age) {
        std::cout << "Person copied: " << name << ", " << age << std::endl;
    }
};
 
int main() {
    std::vector<Person> people;
 
    // 使用 push_back:先构造临时对象,再复制(或移动)到 vector 中
    people.push_back(Person("Alice", 30)); // 输出两次构造:1次临时对象,1次复制
 
    return 0;
}

输出:

Person constructed: Alice, 30
Person copied: Alice, 30

注意:这里发生了两次构造调用——一次是创建临时 Person 对象,另一次是将其复制到 vector 中。


2. emplace_back

emplace_back 是 C++11 引入的成员函数,它允许你直接在 vector 的末尾原地构造对象,而无需先创建临时对象。

语法:

template< class... Args >
void emplace_back( Args&&... args );

工作流程:

  1. 传递参数:你将构造新对象所需的参数直接传递给 emplace_back
  2. 原地构造emplace_back 使用这些参数,在 vector 内部为新元素分配的内存位置上,直接调用对象的构造函数来构造对象。
  3. 零次复制/移动:由于对象是在其最终位置上直接构造的,因此不会发生复制或移动操作

示例:

int main() {
    std::vector<Person> people;
 
    // 使用 emplace_back:直接在 vector 内部构造对象
    people.emplace_back("Bob", 25); // 只调用一次构造函数
 
    return 0;
}

输出:

Person constructed: Bob, 25

注意:这里只发生了一次构造调用——直接在 vector 的内存中构造 Person 对象。


关键区别总结

特性push_backemplace_back
对象构造时机调用前或作为参数时构造vector 内存中原地构造
复制/移动可能发生(复制或移动构造)不会发生
性能可能有额外开销(复制/移动)通常更高效,尤其对复杂对象
参数接受一个已构造的对象接受构造对象所需的参数包
适用性所有情况需要对象支持对应的构造函数

何时使用?

  • 优先使用 emplace_back:当你需要通过传递参数来构造新对象时,emplace_back 通常是更好的选择,因为它更高效,避免了不必要的复制或移动。尤其对于 std::string、自定义类等大型或资源密集型对象,性能提升明显。

  • 使用 push_back

    • 当你已经有了一个现成的对象(变量)要添加时。
    • 当对象类型不支持 emplace_back 所需的构造方式时(虽然很少见)。
    • 在某些复杂模板推导或完美转发有歧义的情况下(但这种情况较少)。

注意事项

  • emplace_back 使用完美转发(perfect forwarding)将参数传递给构造函数,确保参数的值类别(左值/右值)被正确保留。
  • 对于内置类型(如 int, double),两者性能差异通常可以忽略,但 emplace_back 仍然是推荐做法。
  • vector 因容量不足而重新分配内存时,所有元素仍需被移动(如果可移动)或复制,但这与 push_back/emplace_back 的选择无关。

结论

emplace_back 是更现代、更高效的选择,因为它允许在容器内部直接构造对象,避免了中间的复制或移动开销。在大多数情况下,应优先使用 emplace_back 来向 vector 添加新元素。而 push_back 适用于添加已存在的对象。理解两者的区别有助于编写更高效、更现代的 C++ 代码。