C++ 标准库
参考资料
- C++ Reference
- STL 源码剖析 TODO
程序控制
std::abort()立即终止程序, 不进行清理std::atexit(clean())注册程序终止时调用的清理函数std::exit()终止程序, 调用已注册的清理函数 (最后注册的首先调用), 不调用局部对象的析构函数std::quick_exit()与std::at_quick_exit()终止程序, 不清理静态对象std::terminate()调用终止处理程序, 默认调用std::abort()std::unreachable()告诉编译器该代码路径不可达, 允许编译器进行优化std::stacktrace::current()获取当前堆栈跟踪信息, 需要 C++23 支持std::stacktrace_entry获取每一层堆栈信息, 包括函数名、文件名、行号等
断言
assert(condition)在调试模式下检查condition, 失败时打印错误信息并调用std::abort()static_assert(condition, "message")在编译时检查condition, 失败时打印message
字符串
std::ssize()返回有符号整数类型, 用于表示容器大小- 字面量后缀
using namespace std::string_literals, 导入std::string的字面量 (适用于类型推导)"hello"s是std::string类型, 注意这是创建了一个新的临时字符串对象"hello"sv是std::string_view类型
std::string_view- 是不可变字符串视图, 不拥有字符串数据, 支持
constexpr - 适用于传递只读字符串参数, 避免不必要的字符串拷贝
- 不会隐式转换为
std::string, 但可以显式转换 - 相较于
const std::string&, 可以接受字符串字面量和 C 风格字符串而不需创建临时std::string对象, 并且不会延长临时对象的生命周期 - 不要使用
std::string字面量初始化std::string_view对象, 会创建临时std::string对象, 并立刻销毁, 导致悬空视图 - 修改原有字符串会导致视图悬空
std::string_view不一定以空字符结尾
- 是不可变字符串视图, 不拥有字符串数据, 支持
std::string_view func(std::string_view sv) {
sv.remove_prefix(1); // 删除第一个字符
sv.remove_suffix(1); // 删除最后一个字符
return sv;
}
std::string_view sv {func("hello"s)}; // 错误: 悬空视图
std::string s {func("hello"s)}; // 正确
if (str.find("abc") != string::npos); // 检查子字符串是否存在
if (str.contains("abc")); // C++23 语法糖
str.c_str(); // 获取 C 风格字符串, 注意返回的指针指向的字符串数据必须以空字符结尾
std::format
TODO
std::print / std::println
随机数
std::random_device用于生成非确定性随机数种子std::chrono::steady_clock亦可std::mt19937Mersenne Twister 算法, 速度快, 周期长, 适合大多数用途std::uniform_int_distribution均匀整数分布
// 使用 steady_clock 为 Mersenne Twister 生成种子
std::mt19937 mt{ static_cast<std::mt19937::result_type>(
std::chrono::steady_clock::now().time_since_epoch().count()
// std::random_device{}() // 也可以使用随机设备, 但有些实现并非非确定性
) };
// 创建可复用的随机数分布, 生成 1 到 6 之间的均匀整数
std::uniform_int_distribution<int> die6{ 1, 6 }; // C++14 可写为 std::uniform_int_distribution<> die6{ 1, 6 };
// 打印一堆随机数
for (int count{ 1 }; count <= 40; ++count)
{
std::cout << die6(mt) << '\t'; // 生成一次掷骰子结果
// 每打印 10 个数字换行
if (count % 10 == 0)
std::cout << '\n';
}
- 考虑
std::seed_seq用于生成更高质量的随机数序列
std::random_device rd{};
std::seed_seq ss{ rd(), rd(), rd(), rd(), rd(), rd(), rd(), rd() }; // 使用多个随机设备值初始化种子序列
std::mt19937 mt{ ss }; // 使用种子序列初始化 Mersenne Twister
std::uniform_int_distribution die6{ 1, 6 };
for (int count{ 1 }; count <= 40; ++count)
{
std::cout << die6(mt) << '\t';
if (count % 10 == 0)
std::cout << '\n';
}
容器
- 实现特定接口的标准库类型才叫容器
std::string不是std::vector<bool>不是- C 风格数组不是
- 通常提供列表初始化函数
- 这可能导致无法使用
{}初始化调用其它构造函数 - 也可以用
std::vector<int> vec{std::vector<int>(10, 42)};规避
- 这可能导致无法使用
- 除了
std::array外, 容器都使用std::allocator分配内存- 这导致容器的
T::size_type都是std::size_t
- 这导致容器的
- 容器都会实现
.size()- 推荐使用
std::size/std::ssize获得非容器的兼容性 - 其实对于容器就是调用它们的
.size()方法
- 推荐使用
.erase()- 序列容器删除元素后可能导致迭代器失效 ->
it = vec.erase(it); - 但是请记住, 用循环说明你没明白 STL, 请使用
std::erase语法糖, 这对于序列容器会调用std::remove_if来移动元素, 然后调用容器的.erase()来删除多余的元素- 显然, 对于关联容器,
std::erase会直接调用容器的.erase()来删除元素
- 显然, 对于关联容器,
- 序列容器删除元素后可能导致迭代器失效 ->
- 自定义比较时, 请严格定义
<运算符, 以满足严格弱排序要求 (等价关系必须返回false)
std::initializer_list
- 编译器看到初始化列表时, 会自动将其转换为
std::initializer_list std::initializer_list是一个轻量级的不可变数组视图, 包含指向元素的指针和大小- 不能使用
[]下标访问元素, 只能使用迭代器或范围for语句 {}会优先匹配std::initializer_list构造函数- 导致无法使用
{}初始化调用其它构造函数
- 导致无法使用
std::vector
- 遗憾的是
std::vector不能是constexpr的 - 容器适配器
std::priority_queue适配器, 默认底层容器为std::vector, 维护一个堆结构, 允许快速访问极值元素
- 所有移除元素的操作都会调用元素的析构函数, 但不会释放内存, 内存会被保留以供未来使用, 直到容器被销毁或显式调用
shrink_to_fit()- 内置类型的析构函数是空的, 因此移除元素不会有任何效果
std::vector<int> vec {1, 2, 3, 4, 5}; // 列表初始化
vec[0u] = 10;
vec.at(0u) = 20; // 带边界检查的访问
vec.data(); // 返回指向底层数组的指针
vec.data()[0] = 30; // 通过指针访问底层数组, 不会有窄化报错
// 容量与大小的区别主要体现在栈行为
vec.resize(10); // 调整大小与容量
vec.reserve(20); // 预留容量, 不改变大小
vec.push_back(42); // 在末尾添加元素
vec.pop_back(); // 移除末尾元素
vec.emplace_back(43); // 直接在末尾构造元素, 完美转发参数, 但可以调用显示构造函数
vec.erase(vec.remove_if([](int x) { return x % 2 == 0; }), vec.end()); // 移除所有偶数元素, 注意 remove_if 返回新的逻辑结尾迭代器
std::erase(vec.begin(), vec.end(), [](int x) { return x % 2 == 0; }); // 语法糖
std::array
- 可以是
constexpr的, 显然长度必须是常量表达式 std::array是一个聚合类型, 可以使用聚合初始化const arr&形参的std::size结果不是constexpr的, 这是一个缺陷
std::array<int, 5> arr {1, 2, 3, 4, 5};
// 问题来了, CTAD 无法只省略一个模板参数, 如果我需要指定类型, 但不想指定大小怎么办
std::array arr2 { std::to_array<short>({1, 2, 3, 4, 5}) }; // 使用 std::to_array 函数, 注意会创建一个临时数组对象
arr[0] = 10;
arr.at(0) = 20; // 带边界检查的访问
std::get<0>(arr) = 30; // 通过 std::get 访问元素, 实现编译时边界检查
std::array arr3 {
MyClass{1},
MyClass{2},
MyClass{3}
}
std::array arr4 {{
{1},
{2},
{3}
}} // 双花括号才可以, 原因好长, 回忆一下哦
std::set 与 std::map
std::map<const Key, T>本质上就是std::set<pair<const Key, T>>std::multiset允许重复键, 相同时插在右边 (排序是稳定的)std::set的元素与std::map的键都是 const 的, 因为它们的排序依赖于该值, 修改会破坏容器的内部结构
std::map<std::string, int> myMap {
{"one", 1},
{"two", 2},
{"three", 3}
};
// 检查键是否存在
if (myMap.find("two") != myMap.end());
if (myMap.contains("two"));
if (myMap["four"] == 1); // 若键不存在, 会插入一个默认值 (0), 然后返回该值的引用, 注意这可能会修改容器
if (myMap.at("four") == 1); // 若键不存在, 会抛出 std::out_of_range 异常
myMap.insert({"four", 4}); // 插入一个键值对, 如果键已存在则不插入
if (myMap.insert_or_assign("four", 4).second); // 插入或更新键值对, 返回一个 pair, 其中 .second 是一个 bool, 表示是否插入了新元素
std::unordered_set 与 std::unordered_map
- 基于哈希表实现, 提供平均常数时间复杂度的查找、插入和删除操作
- 需要提供哈希函数和相等比较函数, 默认使用
std::hash和std::equal_to
std::deque
- 双端队列, 支持在两端高效地添加和删除元素
- 内部实现为分段连续内存, 一个数组指向多个固定大小的块, 每个块存储一定数量的元素
- 容器适配器
std::stack适配器, 默认底层容器为std::deque, 只允许在一端添加和删除元素std::queue适配器, 默认底层容器为std::deque, 只允许在一端添加元素, 另一端删除元素
std::list
std::bitset
std::bitset<8> bits {}; // 8 位, 全部初始化为 0
bits.set(3); // 将第 3 位设置为 1
bits.reset(3); // 将第 3 位设置为 0
bits.flip(2); // 翻转第 2 位
bits.test(1); // 测试第 1 位是否为 1, 返回 bool
bits.count(); // 返回为 1 的位数
bits.size(); // 返回位数
bits.to_string(); // 返回字符串表示
constexpr std::uint8_t mask0{ 0b0000'0001 }; // 位掩码
迭代器
- 支持
++/--/*/->/[]等操作符, 以及与其他迭代器的比较 - 功能
- 随机访问迭代器支持所有操作
- 双向迭代器支持
++和-- - 前向迭代器支持
++, 可重复使用 - 输出迭代器支持单向单次写入
- 输入迭代器支持单向单次读取
- 迭代器类型
- 指向类型
- 距离类型
- 指针类型
- 引用类型
- 分类标签 -> 就是迭代器的功能分类
- 通过 tag dispatching 实现算法对不同类型迭代器的优化
- 迭代器适配器
std::reverse_iterator适配器, 反向迭代器, 使得算法可以从后向前遍历容器std::move_iterator适配器, 移动迭代器, 使得算法可以移动元素而不是复制std::back_insert_iterator适配器, 后插入迭代器, 使得算法可以在容器末尾添加元素std::front_insert_iterator适配器, 前插入迭代器, 使得算法可以在容器前面添加元素std::insert_iterator适配器, 插入迭代器, 使得算法可以在容器的任意位置添加元素
查找与遍历
std::for_each对范围内的每个元素执行函数, 不修改返回值, 但函数可以有副作用std::find/std::find_if/std::find_if_not在范围内查找第一个满足特定条件的元素std::find查找等于给定值的元素std::find_if查找满足谓词的元素
std::find_end在范围内查找最后一次出现的子序列std::find_first_of在范围内查找与另一范围中任意元素匹配的第一个元素std::adjacent_find查找范围内第一对相邻的重复元素std::count/std::count_if统计范围内满足特定条件的元素个数std::mismatch比较两个范围, 返回第一对不匹配元素的迭代器 pairstd::equal检查两个范围内的元素是否完全相等std::search在范围内查找第一个匹配的子序列std::search_n查找连续 n 个满足条件的元素
修改与替换
std::transform对输入范围的每个元素应用一个函数, 将结果存储在输出范围中- 注意输出范围必须至少与输入范围一样大, 否则会导致未定义行为
std::copy/std::copy_if/std::copy_n/std::copy_backward将元素从一个范围复制到另一个范围std::copy_backward从后往前复制, 适用于输出范围与输入范围重叠且输出起点在输入起点之后的情况
std::move/std::move_backward将元素移动(move语义)到另一个范围std::fill/std::fill_n将给定值赋值给范围内的所有元素std::generate/std::generate_n使用生成器函数的返回值赋值给范围内的元素std::replace/std::replace_if/std::replace_copy将范围内满足条件的元素替换为新值std::iota用顺序递增的值填充范围 (位于 numeric 头文件)
删除与重排
std::remove/std::remove_if移除满足条件的元素, 将保留的元素移到范围前端, 返回新的逻辑结尾- 注意此算法不调整容器大小, 通常配合 erase 使用 (Erase-Remove Idiom)
std::remove_copy/std::remove_copy_if将不满足条件的元素复制到输出范围std::reverse/std::reverse_copy将范围内的元素顺序反转std::rotate/std::rotate_copy将范围内的元素进行循环移动- 常用于将缓冲区某部分移到前面
std::shuffle将范围内的元素随机打乱 (C++11起)
排序
std::sort对范围内的元素进行升序排序std::stable_sort保持相等元素的相对顺序, 但可能更慢std::partial_sort/std::partial_sort_copy只对前 N 个元素进行排序std::nth_element将第 N 大的元素放到第 N 个位置, 且左边元素都小于它, 右边都大于它std::is_sorted检查范围是否已经排序std::is_sorted_until返回第一个未排序元素的迭代器- 去重
std::unique移动相邻重复元素到范围末尾, 返回新的逻辑结尾迭代器, 但不删除元素std::unique_copy将相邻重复元素复制到输出范围, 返回输出范围的结尾迭代器
二分查找
std::lower_bound/std::upper_bound/std::equal_range在已排序范围内查找目标元素的位置std::lower_bound返回第一个不小于目标元素的位置std::upper_bound返回第一个大于目标元素的位置std::equal_range返回一个 pair, 包含 lower_bound 和 upper_bound 的结果
std::binary_search在已排序范围内检查元素是否存在, 返回布尔值
有序集合操作
std::set_union/std::set_intersection/std::set_difference/std::set_symmetric_difference对两个已排序范围执行集合操作, 结果存储在输出范围中- 注意输出范围必须至少与输入范围的总大小一样大, 否则会导致未定义行为
std::includes检查一个已排序范围是否包含另一个已排序范围std::merge将两个已排序范围合并成一个已排序范围, 结果存储在输出范围中- 注意输出范围必须至少与输入范围的总大小一样大, 否则会导致未定义行为
std::inplace_merge将一个已排序范围的两个连续子范围合并成一个已排序范围, 结果存储在原范围中
堆操作
- 适用于随机访问迭代器的算法
std::make_heap将范围内的元素组织成一个堆std::push_heap将范围内的最后一个元素添加到堆中std::pop_heap将堆顶元素移到范围内的最后一个位置, 但不删除它std::sort_heap将堆中的元素排序, 结果是一个升序排列
最值与排列
std::min_element/std::max_element返回范围内最小或最大元素的迭代器std::minmax_element同时返回最小和最大元素的迭代器 pairstd::next_permutation将范围内的元素重排为字典序的下一个排列, 如果存在返回 truestd::prev_permutation将范围内的元素重排为字典序的上一个排列
tuple-like 类型
- 实现
std::tuple_size和std::tuple_element模板特化的类型, 可以使用std::get访问元素 - 该类型 + 聚合 + 数组可以被结构化绑定 ->
auto [x, y, z] = tuple_like;
std::tuple
TODO
std::pair
视图
std::span
std::view
std::mdspan
int data[6] = {1, 2, 3, 4, 5, 6};
std::mdspan mdspan1 {data, 2, 3}; // 创建一个 mdspan 视图
mdspan1.extents().extent(0) // 获取第 0 维的大小
mdspan1.extents().extent(1) // 获取第 1 维的大小
mdspan1.data_handle()[3] // 访问底层指针
mdspan1[0, 0] = 20; // 也可以使用下标访问
Ranges
std::ranges::transform
类型
std::function
std::function<int(int, int)> foo1 = [](int a, int b) { return a + b; }; // 使用 lambda 表达式创建一个 std::function 对象
std::function<int(int, int)> foo2 = std::plus{}; // 使用函数对象创建一个 std::function 对象
std::function<int(int, int)> foo3 {&fun1} // 使用函数指针创建一个 std::function 对象, 注意要取地址符
std::bind
std::variants / std::visit
std::reference_wrapper
- 表现出引用的行为
=会改变引用的对象, 而不是被引用的对象- 会被隐式转换为引用类型
.get()返回被引用的对象的引用
int x = 1, y = 2, z = 3;
std::vector<std::reference_wrapper<int>> vec {std::ref(x), std::ref(y), std::ref(z)};
// 没有 CTAD 的原始人会这样做
auto ref = std::ref(x); // 创建一个 std::reference_wrapper<int>
auto cref = std::cref(x); // 创建一个 std::reference_wrapper<const int>
std::optional
- 有可能无值, 解引用前要检查
std::optional<int> divide(int a, int b) {
if (b == 0) {
return std::nullopt; // 返回空的 optional
}
return a / b; // 返回结果
}
int main() {
auto result = divide(10, 2);
if (result) {
std::cout << "Result: " << *result << std::endl; // 解引用 optional 获取值
std::cout << "Result: " << result.value() << std::endl; // 或者使用 value() 方法获取值
std::cout << "Result: " << result.value_or(-1) << std::endl; // 或者使用 value_or() 提供默认值
} else {
std::cout << "Division by zero!" << std::endl;
}
return 0;
}
std::any
TODO
std::expected
- 不可能无值, 但可能有错误
智能指针
std::unique_ptr
std::make_unique<T>(args...)创建一个指向类型T的智能指针, 并完美转发参数args...给T的构造函数- 禁用复制语义, 只支持移动语义保证唯一所有权
std::shared_ptr
- 可以用
unique_ptr初始化, 但反过来不行 - 支持引用计数, 允许多个
shared_ptr指向同一个对象- 一定要复制
shared_ptr, 而不是重复创建
- 一定要复制
std::weak_ptr
- 本质是在观察
shared_ptr指向的对象以及控制块, 不拥有对象的所有权 - 通过
.lock()方法获取一个临时的shared_ptr, 如果对象已经被销毁, 则返回持有nullptr的shared_ptr.expired()方法检查对象是否已经被销毁
协程
TODO
内存
std::allocator
- 容器级
std::pmr
- 内存池
自定义分配
元编程
Concepts
工具
std::numeric_limits<T>提供类型T的数值特性std::numeric_limits<T>::max()返回类型T可表示的最大值std::numeric_limits<T>::min()返回类型T可表示的最小正值 (对于整数类型, 是最小负值)std::numeric_limits<T>::lowest()返回类型T可表示的最小值std::numeric_limits<T>::is_signed如果类型T是有符号类型则为 truestd::numeric_limits<T>::is_integer如果类型T是整数类型则为 true
std::is_...<>检查类型特性- 基础类型
- 复合类型
- 类 / 函数类型
- 构造 / 移动 / 拷贝
- 类型间关系
std::..._t<>类型变换- 移除修饰
- 添加修饰
std::decay_t<T>移除引用和 cv 修饰, 并将数组和函数类型转换为指针类型
- 编译期
std::enabled_if条件编译std::conditional条件类型选择std::void_tSFINAE 占位类型