025 《C++ 标准库深入 (In-depth Standard Library)》


作者 Lou Xiao, gemini 创建时间 "2025-04-22 19:55:30" 更新时间 "2025-04-22 19:55:30"

🌟🌟🌟本文由Gemini 2.0 Flash Thinking Experimental 01-21生成,用来辅助学习。🌟🌟🌟

书籍大纲

1. C++ 标准库概览 (Overview of C++ Standard Library)

本章介绍 C++ 标准库的整体框架和设计思想,概述其主要组成部分,并探讨标准库在现代 C++ 开发中的重要性。

1.1 标准库的历史与演变 (History and Evolution)

追溯 C++ 标准库的起源,从 STL 的引入到 C++ 标准的不断发展,了解标准库的演变历程。

1.1.1 STL 的诞生与贡献 (Birth and Contribution of STL)

介绍 STL 的设计初衷、核心概念(容器 (Containers)、算法 (Algorithms)、迭代器 (Iterators)),及其对 C++ 标准库的奠基作用。

在深入探讨 C++ 标准库的旅程伊始,我们必须首先回溯其辉煌的开端,聚焦于 STL (Standard Template Library, 标准模板库) 的诞生及其划时代的贡献。STL 的出现,不仅是 C++ 发展史上的一个重要里程碑,也深刻地影响了现代软件开发的范式。

STL 的设计初衷:
▮▮▮▮STL 的设计并非偶然,而是为了解决当时 C++ 编程中普遍存在的代码复用性效率问题。在 STL 诞生之前,程序员在面对不同的数据结构和算法需求时,往往需要从零开始编写代码,这不仅耗时耗力,而且容易出错,代码的维护性和可读性也难以保证。
▮▮▮▮亚历山大·斯捷潘诺夫 (Alexander Stepanov) 和 孟-林 (Meng Lee) 在惠普实验室主导了 STL 的设计与实现。他们的目标是创建一个通用的、高效的、可复用的 C++ 库,以支持泛型编程 (Generic Programming) 的思想。泛型编程的核心理念是将算法从特定的数据类型中解耦出来,使得算法可以应用于各种不同的数据结构,只要这些数据结构满足特定的概念 (Concepts)接口 (Interfaces) 即可。

STL 的核心概念:
STL 的强大之处在于其精巧的设计,主要围绕着三个核心组件展开,它们协同工作,共同构建了一个灵活而强大的库:

▮▮▮▮ⓐ 容器 (Containers): 容器是 STL 的基石,它们是用于存储和组织数据的数据结构 (Data Structures)。STL 提供了多种类型的容器,每种容器都有其特定的数据组织方式和性能特点,以适应不同的应用场景。常见的容器包括:
▮▮▮▮▮▮▮▮❷ 序列容器 (Sequence Containers): 如 vector (向量)、deque (双端队列)、list (列表)、array (数组) 和 forward_list (单向链表)。这些容器按照元素插入的顺序存储元素,并提供了对元素进行顺序访问的能力。
▮▮▮▮▮▮▮▮❸ 关联容器 (Associative Containers): 如 set (集合)、map (映射)、multiset (多重集合) 和 multimap (多重映射)。这些容器根据键值对 (key-value pairs) 存储元素,并提供了基于键的快速查找能力。关联容器通常基于平衡树 (Balanced Trees)哈希表 (Hash Tables) 实现,保证了高效的查找、插入和删除操作。
▮▮▮▮▮▮▮▮❹ 容器适配器 (Container Adaptors): 如 stack (栈)、queue (队列) 和 priority_queue (优先队列)。容器适配器不是独立的容器,而是基于现有的序列容器(如 dequevector)构建的,它们提供了特定的接口,以满足特定的应用需求。例如,stack 适配器提供了后进先出 (LIFO, Last-In-First-Out) 的访问方式。
▮▮▮▮ⓔ 算法 (Algorithms): 算法是 STL 的灵魂,它们是独立于特定容器的通用函数 (Generic Functions),用于执行各种常见的操作,如排序 (sorting)、搜索 (searching)、复制 (copying)、转换 (transforming) 等。STL 提供了大量的算法,涵盖了数据处理的各个方面。算法的设计遵循泛型编程的原则,通过迭代器 (Iterators) 与容器进行交互,使得同一个算法可以应用于不同的容器类型。例如,std::sort 算法可以用于排序 vectordequearray 等序列容器,甚至可以用于排序自定义的数据结构,只要这些数据结构提供了迭代器接口。
▮▮▮▮ⓕ 迭代器 (Iterators): 迭代器是 STL 的桥梁,它们是连接容器和算法的抽象接口 (Abstract Interfaces)。迭代器类似于指针,用于遍历容器中的元素。STL 定义了五种类型的迭代器:输入迭代器 (Input Iterators)输出迭代器 (Output Iterators)前向迭代器 (Forward Iterators)双向迭代器 (Bidirectional Iterators)随机访问迭代器 (Random Access Iterators)。每种迭代器类型都提供了不同的操作集合,以满足不同算法的需求。例如,std::sort 算法需要随机访问迭代器,因为排序算法需要能够随机访问容器中的元素;而 std::copy 算法只需要输入迭代器输出迭代器,因为复制算法只需要顺序读取源容器的元素,并顺序写入目标容器即可。

STL 的奠基作用:
STL 的引入对 C++ 标准库产生了深远的影响,可以毫不夸张地说,STL 是现代 C++ 标准库的基石。

▮▮▮▮ⓐ 提升代码复用性: STL 的泛型设计极大地提升了代码的复用性。程序员可以使用 STL 提供的容器和算法,而无需从零开始编写重复的代码。例如,当需要对数据进行排序时,可以直接使用 std::sort 算法,而无需关心数据的具体类型和存储方式。
▮▮▮▮ⓑ 提高开发效率: 由于 STL 提供了丰富的、经过良好测试的组件,程序员可以更加专注于解决业务逻辑问题,而无需花费大量时间在底层数据结构和算法的实现上,从而显著提高了开发效率。
▮▮▮▮ⓒ 增强程序性能: STL 的组件都是经过精心设计和优化的,例如,vector 容器的动态数组实现,map 容器的红黑树实现,sort 算法的快速排序或内省排序 (Introsort) 实现等,都保证了在各种场景下都能获得良好的性能。
▮▮▮▮ⓓ 促进泛型编程思想的普及: STL 的成功实践,有力地推广了泛型编程的思想,使得泛型编程成为现代 C++ 开发的重要范式。泛型编程不仅提高了代码的复用性和效率,也增强了代码的灵活性和可维护性。
▮▮▮▮ⓔ 为后续标准库扩展奠定基础: STL 的设计思想和组件,为后续 C++ 标准库的扩展奠定了坚实的基础。例如,C++11 引入的 智能指针 (Smart Pointers)并发 (Concurrency) 库等,都借鉴了 STL 的泛型设计和组件化思想。

总之,STL 的诞生是 C++ 标准库发展史上的一次革命性的事件。它不仅为 C++ 程序员提供了强大的工具库,也深刻地影响了 C++ 语言的设计和发展方向。理解 STL 的设计初衷、核心概念及其贡献,对于深入学习和掌握 C++ 标准库至关重要。

1.1.2 C++ 标准的演进与标准库的扩展 (Evolution of C++ Standards and Expansion of Standard Library)

概述 C++98, C++03, C++11, C++14, C++17, C++20 等标准对标准库的扩充和改进,以及未来发展趋势。

C++ 语言自标准化以来,经历了多次重要的修订和扩展。每一次 C++ 标准的发布,都伴随着标准库的扩充和改进。了解 C++ 标准的演进历程,以及标准库在不同版本中的变化,有助于我们更好地理解现代 C++ 的特性,并充分利用标准库提供的强大功能。

C++98 (第一版 ISO C++ 标准):
▮▮▮▮C++98 是第一个正式的 ISO C++ 标准,它标志着 C++ 语言的标准化进程取得了里程碑式的成果。C++98 标准库的核心是 STL (Standard Template Library, 标准模板库),它奠定了现代 C++ 标准库的基础。
▮▮▮▮除了 STL 之外,C++98 标准库还包括了:
▮▮▮▮ⓐ I/O 流 (Input/Output Stream) 库: 提供了用于输入输出操作的类和函数,如 iostreamfstreamsstream 等。
▮▮▮▮ⓑ 字符串 (String) 库: 提供了字符串类 std::string,以及相关的字符串操作函数。
▮▮▮▮ⓒ 数值 (Numerics) 库: 提供了数值计算相关的类和函数,如复数 std::complex、数值算法 std::accumulate 等。
▮▮▮▮ⓓ 本地化 (Localization) 库: 提供了本地化相关的类和函数,用于处理不同地域和语言的文化差异。
▮▮▮▮ⓔ C 标准库: C++98 标准库包含了 ISO C 标准库 (如 C89/C90 标准) 的所有内容,并进行了一些 C++ 化的改造,如将 C 标准库的头文件从 .h 扩展名改为 c 前缀,例如 <stdio.h> 变为 <cstdio>

C++03 (C++98 的修订):
▮▮▮▮C++03 是对 C++98 标准的一个小的修订版本,主要目的是修复 C++98 标准中发现的缺陷和不一致之处,并对标准库进行了一些小的改进。C++03 并没有引入新的语言特性或大型的标准库组件,它更多的是一个技术勘误 (Technical Corrigendum) 版本,旨在提高 C++98 标准的质量和稳定性。

C++11 (现代 C++ 的开端):
▮▮▮▮C++11 是 C++ 标准发展史上的一次重大飞跃,它引入了大量的新语言特性 (New Language Features)标准库组件 (Standard Library Components),使得 C++ 语言焕发了新的活力,被认为是现代 C++ (Modern C++) 的开端。
▮▮▮▮C++11 标准库的扩展主要体现在以下几个方面:
▮▮▮▮ⓐ 核心语言支持: 为了支持新的语言特性,如 移动语义 (Move Semantics)右值引用 (Rvalue References)Lambda 表达式 (Lambda Expressions)类型推导 (Type Inference, auto 关键字)范围 for 循环 (Range-based for loop) 等,标准库进行了相应的扩展和改进。例如,容器类增加了移动构造函数和移动赋值运算符,算法可以更好地利用移动语义提高效率。
▮▮▮▮ⓑ 并发 (Concurrency) 库: C++11 引入了 线程 (Threads)互斥量 (Mutexes)条件变量 (Condition Variables)期物 (Futures)原子操作 (Atomic Operations) 等并发编程相关的库组件,使得 C++ 能够更好地支持多线程编程和并发应用开发。
▮▮▮▮ⓒ 智能指针 (Smart Pointers): C++11 引入了 std::unique_ptrstd::shared_ptrstd::weak_ptr 等智能指针,用于自动管理动态分配的内存,避免内存泄漏,并更好地支持 RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 编程范式。
▮▮▮▮ⓓ 哈希表 (Hash Tables) 容器: C++11 引入了 std::unordered_setstd::unordered_mapstd::unordered_multisetstd::unordered_multimap 等无序容器,基于哈希表实现,提供了平均时间复杂度为O(1)的查找、插入和删除操作。
▮▮▮▮ⓔ 元组 (Tuple): C++11 引入了 std::tuple,用于表示固定大小的不同类型元素的集合,类似于增强版的 std::pair
▮▮▮▮ⓕ 正则表达式 (Regular Expressions) 库: C++11 引入了 std::regex 库,提供了正则表达式匹配和操作的功能。
▮▮▮▮ⓖ 随机数 (Random Numbers) 生成库: C++11 引入了 <random> 头文件,提供了更强大、更灵活的随机数生成器和分布。
▮▮▮▮ⓗ 时间 (Time) 库: C++11 引入了 <chrono> 头文件,提供了更精确、更易用的时间库,用于处理时间点、时间段和时钟。

C++14 (C++11 的小幅改进):
▮▮▮▮C++14 是对 C++11 标准的一个小的改进版本,主要目的是完善 C++11 标准,并引入一些小的、实用的新特性和标准库组件。C++14 标准库的扩展相对较小,主要包括:
▮▮▮▮ⓐ std::make_unique: C++14 引入了 std::make_unique 函数,用于创建 std::unique_ptr 智能指针,与 std::make_shared 类似,提高了异常安全性和效率。
▮▮▮▮ⓑ 二进制字面量 (Binary Literals)数位分隔符 (Digit Separators): C++14 允许使用二进制字面量(如 0b101010)和数位分隔符(如 1'000'000),提高了数值字面量的可读性。
▮▮▮▮ⓒ 泛型 Lambda 表达式 (Generic Lambdas): C++14 允许 Lambda 表达式的参数类型使用 auto 关键字,使得 Lambda 表达式更加通用和灵活。

C++17 (C++11/14 的重要演进):
▮▮▮▮C++17 是继 C++11 之后又一个重要的 C++ 标准版本,它引入了更多的新语言特性和标准库组件,进一步提升了 C++ 的表达能力和开发效率。C++17 标准库的扩展主要包括:
▮▮▮▮ⓐ 并行算法 (Parallel Algorithms): C++17 引入了并行版本的 STL 算法,例如 std::for_eachstd::transformstd::sort 等,可以通过执行策略 (Execution Policies) 来指定算法的执行方式,包括顺序执行、并行执行和向量化执行,充分利用多核处理器的性能。
▮▮▮▮ⓑ std::optional: C++17 引入了 std::optional 类型,用于表示可能存在也可能不存在的值,可以更好地处理函数返回值可能为空的情况,避免使用空指针。
▮▮▮▮ⓒ std::variant: C++17 引入了 std::variant 类型,用于表示可以存储多种不同类型值中的一种类型,是一种类型安全的联合体 (Union)
▮▮▮▮ⓓ std::any: C++17 引入了 std::any 类型,用于表示可以存储任意类型的值,但需要在运行时进行类型检查,使用时需要谨慎。
▮▮▮▮ⓔ 文件系统 (Filesystem) 库: C++17 引入了 <filesystem> 头文件,提供了跨平台的文件和目录操作功能,例如创建目录、删除文件、遍历目录等。
▮▮▮▮ⓕ 结构化绑定 (Structured Bindings): C++17 引入了结构化绑定,可以方便地将 pairtuple 或结构体的成员解包到独立的变量中,提高了代码的可读性。
▮▮▮▮ⓖ 内联变量 (Inline Variables): C++17 允许在头文件中定义 inline 变量,简化了头文件代码的编写。
▮▮▮▮ⓗ 折叠表达式 (Fold Expressions): C++17 引入了折叠表达式,可以方便地对参数包 (Parameter Packs) 进行递归展开和运算。

C++20 (C++ 的持续现代化):
▮▮▮▮C++20 是最新的 C++ 标准版本,它继续沿着现代 C++ 的方向发展,引入了更多强大的语言特性和标准库组件,进一步提升了 C++ 的抽象能力和编程效率。C++20 标准库的扩展主要包括:
▮▮▮▮ⓐ 概念 (Concepts): C++20 引入了 Concepts,用于在编译时对模板参数进行约束,提高了模板代码的类型安全性和错误诊断能力。Concepts 可以看作是对泛型编程的进一步增强。
▮▮▮▮ⓑ 范围 (Ranges) 库: C++20 引入了 Ranges 库,对 STL 的容器、迭代器和算法进行了重新设计和抽象,提供了更简洁、更灵活、更高效的范围操作方式,可以更好地支持函数式编程 (Functional Programming) 风格。
▮▮▮▮ⓒ 协程 (Coroutines): C++20 引入了协程,用于简化异步编程和并发编程,可以编写出更高效、更易于理解的异步代码。
▮▮▮▮ⓓ std::format: C++20 引入了 std::format 库,提供了更安全、更强大、更易用的格式化输出功能,替代了传统的 printfstd::iostream 格式化方式。
▮▮▮▮ⓔ 日历和时区 (Calendar and Time Zone) 库: C++20 对 <chrono> 时间库进行了扩展,增加了日历和时区相关的支持,可以更好地处理日期、时间和时区转换。
▮▮▮▮ⓕ std::span: C++20 引入了 std::span 类型,用于表示连续内存区域的视图,类似于数组切片 (Array Slice),可以避免不必要的数据拷贝。
▮▮▮▮ⓖ constexpr 的扩展: C++20 对 constexpr 关键字进行了扩展,允许更多的函数和对象在编译时求值,进一步提升了编译时计算的能力。
▮▮▮▮ⓗ 模块 (Modules): C++20 引入了模块,用于改进 C++ 的编译模型,解决头文件包含带来的编译效率和命名空间污染问题,但模块的普及和应用还需要时间。

未来发展趋势:
▮▮▮▮C++ 标准的演进仍在继续,未来的 C++ 标准(如 C++23 及以后的版本)将继续关注以下几个方面:
▮▮▮▮ⓐ 持续现代化: 继续引入新的语言特性和标准库组件,使得 C++ 语言更加现代化、易用、高效。
▮▮▮▮ⓑ 并发和并行: 进一步加强对并发和并行的支持,例如,改进协程、扩展并行算法、引入执行器 (Executors) 等。
▮▮▮▮ⓒ 模块化: 推动模块的普及和应用,改进 C++ 的编译模型和代码组织方式。
▮▮▮▮ⓓ 反射 (Reflection) 和元编程 (Metaprogramming): 探索和引入反射和元编程相关的特性,提高 C++ 的抽象能力和代码生成能力。
▮▮▮▮ⓔ 网络 (Networking) 库: 考虑引入标准化的网络库,简化网络编程。
▮▮▮▮ⓕ 机器学习 (Machine Learning) 和人工智能 (Artificial Intelligence): 关注机器学习和人工智能领域的需求,探索 C++ 在这些领域的应用和扩展。

总而言之,C++ 标准库在 C++ 标准的演进过程中不断扩展和完善,它已经成为现代 C++ 开发不可或缺的重要组成部分。了解 C++ 标准库的演变历程,可以帮助我们更好地把握 C++ 的发展脉络,并充分利用标准库提供的强大功能,编写出更高效、更可靠、更现代的 C++ 程序。

1.2 标准库的设计原则与哲学 (Design Principles and Philosophy)

深入探讨标准库的设计原则,例如泛型编程、效率、类型安全、异常安全等,理解其背后的设计哲学。

C++ 标准库之所以能够如此强大、灵活和高效,并非偶然,而是得益于其背后一套清晰而深刻的设计原则与哲学。理解这些设计原则,有助于我们更深入地理解标准库的设计意图,并更好地使用标准库。

1.2.1 泛型编程与模板 (Generic Programming and Templates)

阐述泛型编程在标准库中的应用,以及模板在实现通用算法和数据结构中的关键作用。

泛型编程 (Generic Programming) 的核心思想:
▮▮▮▮泛型编程 是一种编程范式,旨在编写不依赖于特定数据类型的代码。其核心思想是将算法从特定的数据类型中抽象出来,使得算法可以应用于各种不同的数据结构,只要这些数据结构满足特定的概念 (Concepts)接口 (Interfaces) 即可。
▮▮▮▮泛型编程的目标是提高代码的 复用性 (Reusability)灵活性 (Flexibility)可维护性 (Maintainability)。通过泛型编程,我们可以编写出更通用的组件,减少代码重复,并更容易适应需求变化。

模板 (Templates) 是 C++ 泛型编程的基石:
▮▮▮▮在 C++ 中,实现泛型编程的主要工具是 模板 (Templates)。模板允许我们编写参数化类型 (Parameterized Types) 的代码,即代码的类型可以在编译时才确定。C++ 提供了两种类型的模板:
▮▮▮▮ⓐ 函数模板 (Function Templates): 函数模板允许我们编写可以接受不同类型参数的函数。例如,我们可以编写一个通用的 max 函数模板,它可以比较任意可比较类型的两个值:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <typename T>
2T max(T a, T b) {
3 return (a > b) ? a : b;
4}

这个 max 函数模板可以用于比较整数、浮点数、字符串,甚至自定义类型的对象,只要这些类型支持 > 运算符。

▮▮▮▮ⓑ 类模板 (Class Templates): 类模板允许我们编写可以操作不同类型数据的类。例如,STL 中的 vectorlistmap 等容器都是类模板。std::vector 类模板可以存储任意类型的元素:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::vector<int> intVector; // 存储整数的向量
2std::vector<double> doubleVector; // 存储浮点数的向量
3std::vector<std::string> stringVector; // 存储字符串的向量

类模板使得我们可以创建通用的数据结构,而无需为每种数据类型都编写一个特定的类。

泛型编程在 C++ 标准库中的广泛应用:
▮▮▮▮C++ 标准库的设计几乎完全基于泛型编程的思想。STL 就是泛型编程的典范之作。STL 中的 容器 (Containers)算法 (Algorithms)迭代器 (Iterators) 都是通过模板实现的,它们共同构成了一个高度泛化的框架。
▮▮▮▮ⓐ 容器的泛型性: STL 的容器都是类模板,例如 std::vector<T>std::list<T>std::map<K, V> 等。这里的 TKV 都是类型参数,可以是内置类型,也可以是用户自定义类型。容器的泛型性使得我们可以使用同一种容器来存储不同类型的数据,而无需编写针对特定类型的容器类。
▮▮▮▮ⓑ 算法的泛型性: STL 的算法都是函数模板,例如 std::sortstd::findstd::copy 等。这些算法不依赖于特定的容器类型,而是通过 迭代器 (Iterators) 来操作容器中的元素。只要容器提供了满足算法要求的迭代器,算法就可以应用于该容器。例如,std::sort 算法可以用于排序 std::vectorstd::dequestd::array 等序列容器,甚至可以用于排序 C 风格的数组。
▮▮▮▮ⓒ 迭代器的泛型性: 迭代器 (Iterators) 本身也是泛型概念的体现。STL 定义了五种迭代器类别:输入迭代器 (Input Iterators)输出迭代器 (Output Iterators)前向迭代器 (Forward Iterators)双向迭代器 (Bidirectional Iterators)随机访问迭代器 (Random Access Iterators)。每种迭代器类别都定义了一组操作,例如递增、解引用、比较等。算法通过迭代器来访问容器中的元素,而无需关心容器的具体实现细节。迭代器的泛型性使得算法可以独立于容器类型,只要容器提供了相应类型的迭代器即可。

模板在实现通用算法和数据结构中的关键作用:
▮▮▮▮模板是实现泛型编程的关键工具。在 C++ 标准库中,模板被广泛应用于实现通用的算法和数据结构,例如:
▮▮▮▮ⓐ 通用算法: std::sortstd::findstd::copystd::transform 等算法都是函数模板,它们可以应用于各种不同的数据类型和容器类型。模板使得这些算法具有高度的通用性,可以处理各种不同的数据处理任务。
▮▮▮▮ⓑ 通用数据结构: std::vectorstd::liststd::map 等容器都是类模板,它们可以存储各种不同类型的数据。模板使得这些数据结构具有高度的通用性,可以满足各种不同的数据存储和组织需求。
▮▮▮▮ⓒ 编译时多态 (Compile-time Polymorphism): 模板在编译时进行类型绑定和代码生成,实现了静态多态 (Static Polymorphism)编译时多态 (Compile-time Polymorphism)。与运行时多态 (Runtime Polymorphism) (通过虚函数实现) 相比,编译时多态具有更高的效率,因为它避免了运行时的虚函数调用开销。模板的编译时多态特性,使得 C++ 标准库既具有泛型性,又具有高性能。

总之,泛型编程是 C++ 标准库的核心设计原则之一。模板作为 C++ 泛型编程的基石,在实现标准库的通用算法和数据结构中发挥了至关重要的作用。通过泛型编程和模板,C++ 标准库实现了高度的代码复用性、灵活性和效率,为现代 C++ 开发提供了强大的支持。

1.2.2 效率与性能考量 (Efficiency and Performance Considerations)

分析标准库在设计时对效率和性能的追求,以及如何在实际应用中利用标准库的性能优势。

效率与性能是 C++ 标准库设计的核心目标之一:
▮▮▮▮C++ 语言本身就以性能 (Performance) 著称,C++ 标准库的设计也充分体现了对效率和性能的极致追求。标准库的组件,无论是容器、算法还是其他工具,都经过精心的设计和优化,力求在各种应用场景下都能达到最佳的性能。

标准库在设计时对效率和性能的考量:
▮▮▮▮ⓑ 零开销抽象 (Zero-overhead Abstraction): C++ 的设计哲学之一是 “零开销抽象” (Zero-overhead Abstraction)。这意味着,C++ 提供的抽象机制(如模板、类、RAII 等)不应该引入额外的运行时开销,或者开销应该尽可能小。C++ 标准库的设计也遵循了这一原则,力求提供高效的抽象,使得程序员可以使用高级的抽象概念,而无需牺牲性能。例如,STL 的泛型算法通过模板和迭代器实现,避免了虚函数调用等运行时开销,实现了与手写 C 代码相媲美的性能。
▮▮▮▮ⓒ 针对常见场景的优化: 标准库的组件都针对常见的应用场景进行了优化。例如,std::vector 容器在内存中连续存储元素,提供了快速的随机访问能力,适合于频繁访问元素的场景;std::map 容器基于红黑树实现,提供了对数时间复杂度的查找、插入和删除操作,适合于需要高效键值查找的场景;std::sort 算法通常采用快速排序或内省排序等高效的排序算法,保证了在大多数情况下都能获得良好的排序性能。
▮▮▮▮ⓓ 内存管理优化: 标准库在内存管理方面也进行了优化。例如,std::vector 容器的动态内存分配策略,采用了指数增长 (Exponential Growth) 的方式,减少了内存重新分配的次数,提高了性能;智能指针 (Smart Pointers) (如 std::unique_ptrstd::shared_ptr) 实现了自动的内存管理,避免了内存泄漏,并减少了手动内存管理的开销。
▮▮▮▮ⓔ 编译时计算 (Compile-time Computation): C++ 不断增强 编译时计算 (Compile-time Computation) 的能力,例如 constexpr 关键字允许在编译时求值表达式。标准库也利用了编译时计算的特性,例如,类型 traits (Type Traits) 可以在编译时进行类型检查和类型判断,避免了运行时的类型判断开销,提高了性能。C++20 引入的 Concepts (概念) 也可以在编译时对模板参数进行约束,提高了编译时错误诊断能力和性能。
▮▮▮▮ⓕ 并行与并发支持: 随着多核处理器 (Multi-core Processors) 的普及,并行和并发编程变得越来越重要。C++17 引入了 并行算法 (Parallel Algorithms),C++11 引入了 并发 (Concurrency) 库,标准库正在不断加强对并行和并发的支持,以充分利用多核处理器的性能,提高程序的执行效率。

如何在实际应用中利用标准库的性能优势:
▮▮▮▮为了充分利用 C++ 标准库的性能优势,我们在实际应用中应该注意以下几点:
▮▮▮▮ⓐ 选择合适的容器和算法: 根据具体的应用场景和需求,选择最合适的容器和算法。例如,如果需要频繁随机访问元素,应该选择 std::vectorstd::array;如果需要频繁在容器中间插入和删除元素,应该选择 std::liststd::forward_list;如果需要高效的键值查找,应该选择 std::mapstd::unordered_map。选择合适的算法同样重要,例如,如果只需要查找元素是否存在,可以使用 std::binary_search (对于已排序的序列) 或 std::find (对于未排序的序列),而不是使用排序算法。
▮▮▮▮ⓑ 避免不必要的拷贝: C++ 中对象的拷贝可能会带来性能开销。应该尽量避免不必要的拷贝操作,特别是在处理大型对象时。移动语义 (Move Semantics)右值引用 (Rvalue References) 是 C++11 引入的重要特性,可以有效地减少对象的拷贝开销。在标准库中,许多容器和算法都支持移动语义,例如,std::vectorpush_back 操作,当参数是右值时,会优先调用移动构造函数,而不是拷贝构造函数。
▮▮▮▮ⓒ 合理使用智能指针: 智能指针 (Smart Pointers) (如 std::unique_ptrstd::shared_ptr) 不仅可以自动管理内存,避免内存泄漏,还可以提高程序的异常安全性。合理使用智能指针,可以减少手动内存管理的错误和开销。
▮▮▮▮ⓓ 利用并行算法: 如果程序需要处理大量数据,并且计算密集型任务可以并行化,可以考虑使用 C++17 引入的 并行算法 (Parallel Algorithms)。通过指定合适的 执行策略 (Execution Policies),可以充分利用多核处理器的性能,显著提高程序的执行效率。
▮▮▮▮ⓔ 性能测试与分析: 在关键性能路径上,应该进行 性能测试 (Performance Testing)性能分析 (Performance Analysis),找出性能瓶颈,并进行针对性的优化。可以使用性能分析工具 (如 profiler) 来帮助定位性能瓶颈。

总之,效率与性能是 C++ 标准库设计的重要考量。通过理解标准库的设计原则和性能特点,并在实际应用中合理选择和使用标准库的组件,我们可以充分利用标准库的性能优势,编写出高效、可靠的 C++ 程序。

1.2.3 异常安全与资源管理 (Exception Safety and Resource Management)

讲解标准库如何处理异常安全,以及 RAII (Resource Acquisition Is Initialization) 原则在资源管理中的应用。

异常安全 (Exception Safety) 是 C++ 标准库设计的重要原则:
▮▮▮▮异常安全 (Exception Safety) 是指当程序抛出异常时,程序的状态仍然保持良好,不会发生资源泄漏、数据损坏等问题。C++ 标准库的设计非常注重异常安全,力求提供的组件在各种异常情况下都能保持程序的正确性和可靠性。

C++ 标准库的异常安全级别:
▮▮▮▮C++ 标准库的组件通常提供以下三种级别的异常安全保证:
▮▮▮▮ⓐ 不抛出异常保证 (No-throw guarantee): 也称为 最强异常安全保证 (Strongest exception safety guarantee)。保证操作永远不会抛出异常,即使在内存不足等极端情况下。标准库中一些基本的、低级别的操作通常提供不抛出异常保证,例如,移动构造函数、移动赋值运算符、析构函数等。
▮▮▮▮ⓑ 强异常安全保证 (Strong exception safety guarantee): 保证操作要么完全成功,要么完全失败,并且程序状态保持在操作之前的状态。如果操作失败并抛出异常,程序不会有任何副作用,资源不会泄漏,数据不会损坏。例如,std::vector::push_back 操作,如果内存分配失败抛出异常,vector 的状态会保持在 push_back 操作之前的状态。
▮▮▮▮ⓒ 基本异常安全保证 (Basic exception safety guarantee): 保证操作抛出异常后,程序不会发生资源泄漏,数据不会损坏,程序状态保持一致性,但程序状态可能不是操作之前的状态。这是大多数标准库组件提供的默认异常安全级别。例如,std::vector::operator[] 操作,如果索引越界抛出异常,vector 的状态仍然是有效的,没有资源泄漏,但 vector 的内容可能已经被修改。
▮▮▮▮ⓓ 无异常安全保证 (No exception safety guarantee): 也称为 最弱异常安全保证 (Weakest exception safety guarantee)。不提供任何异常安全保证,操作抛出异常后,程序状态可能变得不可预测,可能发生资源泄漏、数据损坏等问题。标准库中很少有组件提供无异常安全保证,除非是一些特殊情况,例如,C 风格的函数库。

RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 原则:
▮▮▮▮RAII (Resource Acquisition Is Initialization, 资源获取即初始化) 是 C++ 中一种重要的资源管理技术。其核心思想是将资源的生命周期与对象的生命周期绑定在一起。当对象被创建时,资源被获取 (初始化);当对象被销毁时,资源被释放 (清理)。通过 RAII,可以确保资源在任何情况下都能被正确释放,即使程序抛出异常或提前退出。
▮▮▮▮RAII 的关键在于利用 C++ 的 析构函数 (Destructor) 机制。析构函数在对象生命周期结束时自动调用,无论对象是正常销毁还是由于异常而销毁。因此,我们可以将资源的释放操作放在对象的析构函数中,从而保证资源在任何情况下都能被释放。

RAII 原则在标准库资源管理中的应用:
▮▮▮▮C++ 标准库广泛应用了 RAII 原则来管理各种资源,例如:
▮▮▮▮ⓐ 内存管理: 智能指针 (Smart Pointers) (如 std::unique_ptrstd::shared_ptr) 是 RAII 的典型应用。智能指针对象在构造时获取动态分配的内存,在析构时自动释放内存。通过使用智能指针,可以避免手动 newdelete 带来的内存泄漏风险,并提高程序的异常安全性。
▮▮▮▮ⓑ 文件句柄管理: std::fstream 等文件流类也应用了 RAII 原则。文件流对象在构造时打开文件,在析构时自动关闭文件。通过使用文件流类,可以确保文件句柄在文件操作完成后被正确关闭,避免资源泄漏。
▮▮▮▮ⓒ 锁管理: 锁管理器 (Lock Managers) (如 std::lock_guardstd::unique_lock) 是 RAII 在并发编程中的应用。锁管理器对象在构造时获取互斥锁 (Mutex),在析构时自动释放互斥锁。通过使用锁管理器,可以确保互斥锁在临界区 (Critical Section) 结束后被正确释放,避免死锁和资源泄漏,并简化锁的管理。
▮▮▮▮ⓓ 其他资源管理: RAII 原则可以应用于管理各种类型的资源,例如,网络连接、数据库连接、GDI 对象、互斥锁、条件变量等。只要资源需要在对象的生命周期内进行获取和释放,都可以使用 RAII 技术进行管理。

异常安全与资源管理的最佳实践:
▮▮▮▮为了编写异常安全且资源管理良好的 C++ 代码,我们应该遵循以下最佳实践:
▮▮▮▮ⓐ 使用 RAII 管理资源: 尽可能使用 RAII 技术来管理各种资源,例如,使用智能指针管理内存,使用文件流类管理文件句柄,使用锁管理器管理互斥锁。
▮▮▮▮ⓑ 提供强异常安全保证: 在设计和实现自己的类和函数时,尽量提供 强异常安全保证 (Strong exception safety guarantee)。如果无法提供强异常安全保证,至少要提供 基本异常安全保证 (Basic exception safety guarantee)。避免提供 无异常安全保证 (No exception safety guarantee) 的代码。
▮▮▮▮ⓒ 避免在析构函数中抛出异常: 析构函数应该尽量避免抛出异常。如果在析构函数中抛出异常,可能会导致程序崩溃或其他不可预测的行为。如果析构函数中可能抛出异常,应该进行适当的异常处理,例如,捕获异常并进行日志记录,或者终止程序。
▮▮▮▮ⓓ 使用 noexcept 声明不抛出异常的函数: 对于确定不会抛出异常的函数(如移动构造函数、移动赋值运算符、析构函数等),应该使用 noexcept 关键字进行声明。noexcept 声明可以帮助编译器进行优化,并提高程序的性能。
▮▮▮▮ⓔ 谨慎使用裸指针和手动内存管理: 应该尽量避免使用裸指针和手动内存管理。如果必须使用裸指针,一定要小心谨慎,确保内存被正确分配和释放,避免内存泄漏和悬 dangling 指针问题。

总之,异常安全和资源管理是 C++ 标准库设计的重要原则。通过理解异常安全的概念、RAII 原则,并在实际编程中遵循最佳实践,我们可以编写出更健壮、更可靠、更易于维护的 C++ 程序。C++ 标准库提供的智能指针、容器、算法等组件,都为我们提供了良好的异常安全和资源管理支持,我们应该充分利用这些工具,提高程序的质量。

1.3 如何学习和使用标准库 (How to Learn and Use Standard Library)

提供学习和使用 C++ 标准库的有效方法和资源,包括文档查阅、实践练习、社区参与等。

C++ 标准库内容丰富、功能强大,但同时也意味着学习曲线相对陡峭。掌握 C++ 标准库,需要系统的方法和持续的实践。本节将介绍一些学习和使用 C++ 标准库的有效方法和资源,帮助读者更好地入门和精进。

1.3.1 在线文档与参考资源 (Online Documentation and Reference Resources)

介绍 cppreference.com, cplusplus.com 等权威在线文档资源,以及其他学习资料。

权威在线文档:
▮▮▮▮在线文档是学习和使用 C++ 标准库最重要、最权威的参考资源。以下是一些常用的权威在线文档网站:

▮▮▮▮ⓐ cppreference.com: https://en.cppreference.com/
▮▮▮▮▮▮▮▮cppreference.com 是公认的最权威、最全面的 C++ 标准库在线文档。它提供了 C++ 语言和标准库的详细参考,包括:
▮▮▮▮▮▮▮▮❶ 语言特性: C++ 语言的语法、语义、关键字、操作符等。
▮▮▮▮▮▮▮▮❷ 标准库: C++ 标准库的各个组件,如容器、算法、迭代器、函数对象、I/O 流、并发库、智能指针、实用工具等。
▮▮▮▮▮▮▮▮❸ C 兼容性: C++ 与 C 语言的兼容性,C++ 标准库中包含的 C 标准库组件。
▮▮▮▮▮▮▮▮cppreference.com 的文档组织清晰、内容准确、更新及时,并且提供了大量的代码示例,是学习和查阅 C++ 标准库的首选资源。它通常被认为是 C++ 程序员的 “圣经” (Bible)。

▮▮▮▮ⓑ cplusplus.com: http://www.cplusplus.com/
▮▮▮▮▮▮▮▮cplusplus.com 是另一个流行的 C++ 在线文档网站。它也提供了 C++ 语言和标准库的参考文档,以及教程、文章、论坛等学习资源。cplusplus.com 的文档相对 cppreference.com 来说,可能没有那么全面和深入,但对于初学者来说,可能更容易上手和理解。cplusplus.com 也提供了大量的代码示例,可以帮助读者快速学习和应用标准库。

▮▮▮▮ⓒ MSDN (Microsoft Developer Network) C++ Library Reference: https://docs.microsoft.com/en-us/cpp/standard-library/cpp-standard-library-reference
▮▮▮▮▮▮▮▮MSDN C++ Library Reference 是微软官方提供的 C++ 标准库文档,主要面向使用 Microsoft Visual C++ 编译器的开发者。MSDN 的文档与 cppreference.com 和 cplusplus.com 相比,可能没有那么通用,但对于 Windows 平台上的 C++ 开发,MSDN 文档也是一个重要的参考资源。MSDN 文档通常与 Visual Studio 集成,可以方便地在 IDE 中查阅。

其他学习资料:
▮▮▮▮除了在线文档之外,还有许多其他的学习资料可以帮助我们学习和使用 C++ 标准库:

▮▮▮▮ⓐ 书籍: 有许多优秀的 C++ 书籍深入讲解了 C++ 标准库,例如:
▮▮▮▮▮▮▮▮❷ 《The C++ Standard Library: A Tutorial and Reference (2nd Edition)》 (Nicolai M. Josuttis 著): 这本书是 C++ 标准库的权威指南,全面而深入地讲解了 C++ 标准库的各个组件,包括 STL 容器、算法、迭代器、函数对象、I/O 流、并发库等。这本书既可以作为教程,也可以作为参考手册,适合不同层次的 C++ 程序员阅读。
▮▮▮▮▮▮▮▮❸ 《Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library》 (Scott Meyers 著): 这本书以经验总结的方式,提出了 50 条关于如何更有效地使用 STL 的建议,涵盖了 STL 的各个方面,例如容器选择、迭代器使用、算法应用、函数对象设计等。这本书适合有一定 STL 使用经验的 C++ 程序员阅读,可以帮助他们提高 STL 的使用技巧和效率。
▮▮▮▮▮▮▮▮❹ 《C++ Primer (5th Edition)》 (Stanley B. Lippman, Josée Lajoie, Barbara E. Moo 著): 这本书是 C++ 入门的经典之作,全面而系统地介绍了 C++ 语言和标准库的基础知识。这本书的讲解清晰易懂,代码示例丰富,适合 C++ 初学者入门。书中也用相当的篇幅讲解了 C++ 标准库的常用组件,如容器、算法、迭代器、I/O 流等。

▮▮▮▮ⓑ 在线教程和博客: 互联网上有很多优秀的 C++ 在线教程和博客,也包含了很多关于 C++ 标准库的学习资料。例如,LearnCpp.comGeeksforGeeks 等网站提供了大量的 C++ 教程和文章,其中很多都涉及 C++ 标准库的讲解和应用。一些 C++ 大牛的博客 (如 Herb Sutter's blog, Scott Meyers' blog) 也经常分享关于 C++ 标准库的深入见解和使用技巧。

▮▮▮▮ⓒ 视频教程: 视频教程也是一种有效的学习方式。YouTube、B站、Coursera、Udemy 等平台上有许多 C++ 和 C++ 标准库的视频教程,可以根据自己的学习习惯选择合适的视频教程进行学习。

▮▮▮▮ⓓ 社区论坛和问答网站: 参与 C++ 社区的讨论和交流,可以帮助我们更好地学习和理解 C++ 标准库。Stack OverflowReddit (r/cpp, r/learncpp)C++ 用户组 (C++ User Groups) 等社区论坛和问答网站,是 C++ 程序员交流和学习的重要平台。在这些社区中,我们可以提问、解答问题、分享经验、获取帮助。

如何有效利用在线文档和参考资源:
▮▮▮▮ⓑ 明确学习目标: 在查阅文档之前,先明确自己的学习目标。例如,是想学习某个特定的容器 (如 std::vector),还是想了解某个算法 (如 std::sort),或者想解决某个具体的问题。明确学习目标可以帮助我们更高效地查阅文档,避免迷失在浩如烟海的文档中。
▮▮▮▮ⓒ 善用搜索功能: 在线文档网站通常都提供了强大的搜索功能。善用搜索功能,可以快速定位到我们需要的文档页面。可以使用关键词 (如容器名、算法名、函数名、头文件名等) 进行搜索。
▮▮▮▮ⓓ 阅读文档结构: 在阅读文档时,注意文档的结构。通常,一个标准库组件的文档会包括以下几个部分:概览 (Overview)头文件 (Header)命名空间 (Namespace)类/函数定义 (Class/Function Definition)成员函数/参数/返回值 (Member Functions/Parameters/Return Values)异常 (Exceptions)示例 (Examples)复杂度 (Complexity)版本 (Version) 等。了解文档结构可以帮助我们更快地找到我们需要的信息。
▮▮▮▮ⓔ 关注代码示例: 代码示例是学习标准库的重要辅助。在线文档和书籍通常都提供了大量的代码示例,演示了标准库组件的使用方法。仔细阅读和运行代码示例,可以帮助我们更好地理解标准库的用法和特性。
▮▮▮▮ⓕ 结合实践练习: 学习标准库最好的方法是 实践练习 (Practice Exercises)。在学习文档的同时,应该结合实际的编程任务,尝试使用标准库的组件来解决问题。通过实践练习,可以加深对标准库的理解,并掌握标准库的使用技巧。

总之,在线文档和参考资源是学习和使用 C++ 标准库的重要工具。通过善用这些资源,结合实践练习和社区交流,我们可以逐步掌握 C++ 标准库,并将其应用于实际的 C++ 开发中。

1.3.2 实践练习与代码示例 (Practice Exercises and Code Examples)

强调通过编写代码来学习标准库的重要性,提供一些实用的练习案例和代码示例。

实践练习是学习标准库的关键:
▮▮▮▮学习任何编程技术,都离不开 实践练习 (Practice Exercises)。对于 C++ 标准库的学习来说,实践练习尤为重要。仅仅阅读文档和书籍,是远远不够的。只有通过编写代码,亲手使用标准库的组件,才能真正理解标准库的设计思想、掌握标准库的使用方法、并发现标准库的强大之处。
▮▮▮▮“纸上得来终觉浅,绝知此事要躬行” (陆游)。学习 C++ 标准库,也是如此。只有通过大量的实践练习,才能将理论知识转化为实际技能,并最终成为 C++ 标准库的熟练使用者。

代码示例是学习标准库的有效辅助:
▮▮▮▮代码示例 (Code Examples) 是学习标准库的有效辅助工具。标准库的文档、书籍、教程等通常都提供了大量的代码示例,演示了标准库组件的使用方法。研究和运行这些代码示例,可以帮助我们快速入门,并了解标准库的基本用法。
▮▮▮▮但是,代码示例仅仅是入门的引导,更重要的是要自己动手编写代码,解决实际问题。

实用的练习案例:
▮▮▮▮以下是一些实用的练习案例,可以帮助读者通过实践练习来学习 C++ 标准库:

▮▮▮▮ⓐ 使用 std::vector 实现动态数组:
▮▮▮▮▮▮▮▮练习使用 std::vector 容器,实现一个动态数组类,支持动态扩容、插入、删除、访问等操作。
▮▮▮▮```cpp

include

include

template
class DynamicArray {
private:
std::vector data;

public:
void push_back(const T& value) {
data.push_back(value);
}

void insert(size_t index, const T& value) {
if (index > data.size()) {
throw std::out_of_range("Index out of range");
}
data.insert(data.begin() + index, value);
}

void erase(size_t index) {
if (index >= data.size()) {
throw std::out_of_range("Index out of range");
}
data.erase(data.begin() + index);
}

T& operator[](size_t index) {
if (index >= data.size()) {
throw std::out_of_range("Index out of range");
}
return data[index];
}

size_t size() const {
return data.size();
}

bool empty() const {
return data.empty();
}

void print() const {
for (const auto& item : data) {
std::cout << item << " ";
}
std::cout << std::endl;
}
};

int main() {
DynamicArray arr;
arr.push_back(10);
arr.push_back(20);
arr.push_back(30);
arr.insert(1, 15);
arr.print(); // Output: 10 15 20 30
arr.erase(2);
arr.print(); // Output: 10 15 30
std::cout << "Size: " << arr.size() << std::endl; // Output: Size: 3
std::cout << "arr[0]: " << arr[0] << std::endl; // Output: arr[0]: 10
return 0;
}

1.双击鼠标左键复制此行;2.单击复制所有代码。
1## 2. 容器 (Containers)
2本章全面讲解 C++ 标准库中的各种容器 (Containers),包括序列容器 (Sequence Containers)、关联容器 (Associative Containers)、容器适配器 (Container Adaptors) 和无序容器 (Unordered Containers),深入剖析其特性、适用场景和使用方法。
3### 2.1 序列容器 (Sequence Containers)
4详细介绍 `vector` (向量)、`deque` (双端队列)、`list` (列表)、`array` (数组)、`forward_list` (单向链表) 等序列容器的内部实现、性能特点和常用操作。序列容器强调元素**插入**和**删除**时的**相对位置**,元素会按照严格的线性顺序排列。
5#### 2.1.1 vector (向量): 动态数组 (Dynamic Array)
6深入剖析 `vector` 的动态内存管理、随机访问特性、插入删除操作的效率,以及 `capacity` (容量) 和 `size` (大小) 的区别。`vector` 封装了动态大小数组,可以根据需要自动增长。
7① **内部实现**:
8▮ `vector` 的内部实现依赖于**动态分配的连续内存块**。当 `vector` 的大小超过其当前容量时,它会自动重新分配一块更大的内存(通常是当前容量的 1.5 倍或 2 倍),并将原有元素复制或移动到新的内存位置。
9▮ `vector` 维护了三个关键的迭代器(或指针):
10▮▮▮▮ⓐ `begin()`: 指向 `vector` 中**第一个元素**的位置。
11▮▮▮▮ⓑ `end()`: 指向 `vector` 中**最后一个元素之后的位置**(past-the-end)。
12▮▮▮▮ⓒ `capacity()`: 指向分配内存的**末尾位置**。
13② **性能特点**:
14▮ **优点**:
15▮▮▮▮ⓐ **随机访问高效**:由于元素存储在连续的内存空间中,`vector` 支持通过**下标** `[]` 或 `at()` 方法进行**常数时间**\(O(1)\)的**随机访问**。这使得 `vector` 在需要频繁访问任意位置元素的场景中非常高效。
16▮▮▮▮ⓑ **尾部插入和删除高效**:在 `vector` 的**尾部**进行插入和删除操作,通常只需要**常数时间**\(O(1)\)。
17▮▮▮▮ⓒ **缓存友好**:由于数据连续存储,`vector` 的内存布局对 CPU 缓存非常友好,有助于提高程序的整体性能。
18▮ **缺点**:
19▮▮▮▮ⓐ **头部和中部插入删除低效**:在 `vector` 的**头部或中部**进行插入和删除操作,需要移动插入&#x2F;删除位置之后的所有元素,时间复杂度为**线性时间**\(O(n)\),其中\(n\)是元素数量。
20▮▮▮▮ⓑ **动态扩容开销**:当 `vector` 需要扩容时,会发生内存重新分配和元素复制&#x2F;移动,这会带来一定的性能开销。虽然扩容操作的平均时间复杂度是**均摊常数时间**\(O(1)\),但在**最坏情况下**(例如频繁插入导致多次扩容),性能可能会受到影响。
21③ **常用操作**:
22▮ **构造与赋值**:
23```cpp
24std::vector<int> v1; // 默认构造,空 vector
25std::vector<int> v2(10); // 构造包含 10 个默认初始化元素的 vector
26std::vector<int> v3(10, 42); // 构造包含 10 个值为 42 的元素的 vector
27std::vector<int> v4 = {1, 2, 3, 4, 5}; // 初始化列表构造
28std::vector<int> v5(v4); // 拷贝构造
29std::vector<int> v6(std::make_move_iterator(v4.begin()), std::make_move_iterator(v4.end())); // 移动构造 (C++11)
30v1 = v4; // 拷贝赋值
31v1 = std::move(v4); // 移动赋值 (C++11)

容量与大小

1.双击鼠标左键复制此行;2.单击复制所有代码。
1v.size(); // 返回 vector 中元素的数量 (size)
2v.capacity(); // 返回 vector 当前分配的内存容量 (capacity)
3v.empty(); // 判断 vector 是否为空
4v.reserve(100); // 预留至少能容纳 100 个元素的空间,但不改变 size
5v.shrink_to_fit(); // 释放多余的容量,使 capacity 匹配 size (C++11)

元素访问

1.双击鼠标左键复制此行;2.单击复制所有代码。
1v[0]; // 访问第一个元素,不进行边界检查
2v.at(0); // 访问第一个元素,进行边界检查,越界抛出 std::out_of_range 异常
3v.front(); // 访问第一个元素
4v.back(); // 访问最后一个元素
5v.data(); // 返回指向 vector 内部数组的指针 (C++11)

修改操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1v.push_back(10); // 在尾部添加元素
2v.pop_back(); // 删除尾部元素
3v.insert(v.begin(), 0); // 在指定位置插入元素,这里是在头部插入 0
4v.erase(v.begin()); // 删除指定位置的元素,这里是删除头部元素
5v.clear(); // 清空 vector 中的所有元素,size 变为 0,但 capacity 通常不变
6v.resize(20); // 改变 vector 的 size 为 20,如果 size 增大,则默认初始化新元素;如果 size 减小,则删除尾部元素
7v.assign(5, 8); // 将 vector 赋值为包含 5 个值为 8 的元素

迭代器

1.双击鼠标左键复制此行;2.单击复制所有代码。
1for (auto it = v.begin(); it != v.end(); ++it) {
2std::cout << *it << " ";
3}
4for (auto it = v.rbegin(); it != v.rend(); ++it) { // 反向迭代器
5std::cout << *it << " ";
6}
7for (int x : v) { // 范围 for 循环 (C++11)
8std::cout << x << " ";
9}

capacity (容量) 和 size (大小) 的区别
size 指的是 vector实际存储的元素数量,可以通过 size() 方法获取。
capacity 指的是 vector 已分配的内存空间,可以容纳的最大元素数量,可以通过 capacity() 方法获取。capacity 通常大于或等于 size
▮ 当向 vector 中添加元素,且 size 达到 capacity 时,vector 会自动扩容,重新分配更大的内存空间。
reserve() 方法可以预先分配一定的 capacity,避免频繁扩容,提高性能。shrink_to_fit() 方法可以释放多余的容量

适用场景
vector 是最常用和通用的序列容器,适用于以下场景:
▮▮▮▮ⓐ 需要高效的随机访问:例如,需要频繁通过下标访问元素的场景。
▮▮▮▮ⓑ 尾部插入和删除操作频繁:例如,动态数组、栈等数据结构的实现。
▮▮▮▮ⓒ 数据量大小可变:例如,存储读取的文件内容、用户输入等。

2.1.2 deque (双端队列): 双端动态数组 (Double-ended Dynamic Array)

讲解 deque 的分段连续存储结构、双端插入删除的优势、与 vector 的比较,以及适用场景。deque 允许在头部和尾部进行快速插入和删除操作。

内部实现
deque 的内部实现通常采用分段连续存储结构,而不是像 vector 那样的单一连续内存块。deque 由多个独立的内存块(缓冲区)组成,这些块之间通过索引数组(或映射表)进行连接,逻辑上表现为连续存储。
▮ 这种分段结构允许 deque头部和尾部进行高效的插入和删除操作,而无需像 vector 那样移动大量元素。

性能特点
优点
▮▮▮▮ⓐ 双端插入删除高效:在 deque头部和尾部进行插入和删除操作,都只需要常数时间O(1)
▮▮▮▮ⓑ 随机访问性能接近 vectordeque 也支持随机访问,虽然需要经过两次指针跳转(先到索引数组,再到数据块),但相对于链表等结构,其随机访问性能仍然很高,通常认为是接近常数时间O(1)
▮▮▮▮ⓒ 动态扩容开销较低deque 的扩容发生在内存块的分配和索引数组的扩展上,相对于 vector 的整体内存重新分配,开销通常较低。
缺点
▮▮▮▮ⓐ 中部插入删除性能略逊于 list:虽然 deque 在中部插入删除时不需要像 vector 那样移动大量元素,但仍然需要移动部分内存块中的元素,性能略逊于 list 的链表结构。
▮▮▮▮ⓑ 迭代器相对复杂deque 的迭代器需要维护块的信息块内偏移量,因此比 vector 的迭代器略为复杂。
▮▮▮▮ⓒ 缓存局部性不如 vector:由于 deque 的内存不是完全连续的,其缓存局部性可能不如 vector

常用操作
deque 的常用操作与 vector 非常相似,主要区别在于 deque 提供了头部操作push_front()pop_front()

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::deque<int> d;
2d.push_back(10); // 尾部添加元素
3d.push_front(5); // 头部添加元素
4d.pop_back(); // 删除尾部元素
5d.pop_front(); // 删除头部元素
6d[0]; // 随机访问
7d.at(0); // 边界检查的随机访问
8d.front(); // 访问头部元素
9d.back(); // 访问尾部元素
10d.size(); // 返回元素数量
11d.empty(); // 判断是否为空
12d.clear(); // 清空 deque
13// ... 其他操作与 vector 类似

vector 的比较
共同点
▮▮▮▮ⓐ 都是动态数组,可以动态增长和收缩。
▮▮▮▮ⓑ 都支持随机访问,且性能接近常数时间O(1)
▮▮▮▮ⓒ 迭代器失效规则类似:插入和删除操作可能导致迭代器失效。
不同点
▮▮▮▮ⓐ 插入删除效率deque头部和尾部插入删除效率更高,为O(1)vector 仅在尾部插入删除高效,头部和中部为O(n)
▮▮▮▮ⓑ 内存结构deque分段连续存储vector单一连续存储
▮▮▮▮ⓒ 扩容机制deque 扩容开销通常较低,vector 扩容可能涉及大量数据复制。
▮▮▮▮ⓓ 缓存局部性vector 的缓存局部性更好,deque 稍逊。
▮▮▮▮ⓔ 内存使用:在某些情况下,deque 可能比 vector 占用更多的内存,因为 deque 需要维护索引数组和多个内存块。

适用场景
deque 适用于以下场景:
▮▮▮▮ⓐ 需要在头部和尾部都进行频繁插入和删除操作:例如,双端队列消息队列等数据结构的实现。
▮▮▮▮ⓑ 对随机访问性能有一定要求,但头部插入删除比尾部插入删除更重要的场景
▮▮▮▮ⓒ 内存使用相对不敏感,但需要更灵活的动态扩容能力的场景

2.1.3 list (列表) 与 forward_list (单向链表): 链式存储 (Linked List)

对比 listforward_list 的双向链表和单向链表结构,分析其在插入删除操作上的优势,以及迭代器的失效问题。listforward_list 基于链表实现,擅长任意位置的快速插入和删除。

内部实现
list (列表)
▮▮▮▮ⓐ list 基于双向链表实现。每个元素(节点)都包含数据两个指针,分别指向前一个元素后一个元素
▮▮▮▮ⓑ 双向链表允许双向遍历,可以在常数时间内访问元素的前驱和后继节点。
forward_list (单向链表)
▮▮▮▮ⓐ forward_list 基于 单向链表实现。每个元素(节点)只包含数据一个指针,指向后一个元素
▮▮▮▮ⓑ 单向链表只能进行单向遍历,访问前驱节点需要从链表头开始重新遍历。
▮▮▮▮ⓒ forward_list内存开销更小性能更高(在单向遍历和插入删除的场景下),但功能相对 list 较少。

性能特点
优点
▮▮▮▮ⓐ 任意位置插入删除高效:在 listforward_list任意位置(包括头部、中部、尾部)进行插入和删除操作,都只需要常数时间O(1)。这是链表结构最显著的优势,只需要修改相邻节点的指针即可。
▮▮▮▮ⓑ 迭代器失效影响小listforward_list 的插入和删除操作只影响被操作位置的迭代器,其他迭代器仍然有效。对于 list,删除元素只会使指向被删除元素的迭代器失效;对于 forward_list,由于单向链表的特性,删除元素时可能需要前驱节点的迭代器,因此删除操作相对复杂一些。
▮▮▮▮ⓒ 动态内存分配灵活:链表不需要连续的内存空间,内存分配和释放更加灵活。
缺点
▮▮▮▮ⓐ 随机访问低效listforward_list 不支持随机访问(即下标访问)。要访问链表中的元素,必须从头节点指定位置开始顺序遍历,时间复杂度为线性时间O(n)
▮▮▮▮ⓑ 额外内存开销:链表的每个节点都需要额外的指针来维护链表结构,相对于连续存储的容器(如 vector),内存开销较大
▮▮▮▮ⓒ 缓存局部性差:链表节点的内存地址通常不是连续的,缓存局部性较差,可能影响程序的性能。forward_list 由于节点更小,缓存局部性稍好于 list

常用操作
list (列表):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::list<int> l;
2l.push_back(10); // 尾部添加元素
3l.push_front(5); // 头部添加元素
4l.pop_back(); // 删除尾部元素
5l.pop_front(); // 删除头部元素
6l.insert(std::next(l.begin()), 8); // 在指定位置(第二个元素前)插入元素
7l.erase(l.begin()); // 删除指定位置的元素
8l.remove(8); // 删除所有值为 8 的元素
9l.unique(); // 删除相邻的重复元素(需要先排序)
10l.sort(); // 排序
11l.merge(l2); // 合并两个已排序的 list
12l.splice(it, l2, it2); // 将 l2 中 it2 指向的元素移动到 l 中 it 指向的位置
13// ... 其他操作

forward_list (单向链表):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::forward_list<int> fl;
2fl.push_front(5); // 头部添加元素 (forward_list 只有头部操作)
3fl.pop_front(); // 删除头部元素 (forward_list 只有头部操作)
4fl.insert_after(fl.begin(), 8); // 在指定位置之后插入元素
5fl.erase_after(fl.begin()); // 删除指定位置之后的元素
6fl.remove(8); // 删除所有值为 8 的元素
7fl.unique(); // 删除相邻的重复元素(需要先排序)
8fl.sort(); // 排序
9fl.merge(fl2); // 合并两个已排序的 forward_list
10fl.splice_after(it, fl2, it2); // 将 fl2 中 it2 之后的元素移动到 fl 中 it 之后的位置
11// ... 其他操作

注意forward_list 没有 back()size()end() 成员函数。要获取 forward_list 的大小,需要手动遍历;要访问尾部元素,需要遍历到最后一个元素。为了高效地在指定位置之前插入或删除元素,forward_listinsert_after()erase_after() 操作需要前一个位置的迭代器

listforward_list 的对比
共同点
▮▮▮▮ⓐ 都基于链表实现,擅长任意位置插入删除
▮▮▮▮ⓑ 插入删除操作的迭代器失效影响小
▮▮▮▮ⓒ 随机访问低效,只能顺序遍历。
不同点
▮▮▮▮ⓐ 链表类型list双向链表forward_list单向链表
▮▮▮▮ⓑ 功能list 功能更完整,支持双向遍历、尾部操作等;forward_list 功能受限,只支持单向遍历和头部操作,但性能更高内存开销更小
▮▮▮▮ⓒ 内存开销forward_list 的节点比 list 的节点小,内存开销更低。
▮▮▮▮ⓓ 迭代器forward_list 的迭代器类型是前向迭代器 (Forward Iterator)list 的迭代器类型是双向迭代器 (Bidirectional Iterator)

适用场景
list (列表) 适用于以下场景:
▮▮▮▮ⓐ 需要频繁在任意位置进行插入和删除操作:例如,文本编辑器操作历史记录等。
▮▮▮▮ⓑ 对随机访问性能要求不高,主要进行顺序访问和迭代的场景。
▮▮▮▮ⓒ 需要双向遍历的场景。
▮▮▮▮ⓓ 元素大小较大,移动代价较高的场景,链表的节点移动只是指针的修改,代价很小。
forward_list (单向链表) 适用于以下场景:
▮▮▮▮ⓐ list 类似,但更注重性能和内存效率,且只需要单向遍历的场景。
▮▮▮▮ⓑ 内存资源受限的环境,例如嵌入式系统
▮▮▮▮ⓒ 作为其他数据结构(例如邻接表)的底层实现

2.1.4 array (数组): 固定大小数组 (Fixed-size Array)

介绍 array 的静态数组特性、与 C 风格数组的区别、边界检查,以及在栈上分配内存的优势。array 是对C 风格数组类型安全的封装,提供了固定大小的数组容器。

内部实现
array 是对静态数组的封装,其内部就是一个固定大小的数组,大小在编译时确定。
array 的内存分配发生在栈上(如果 array 对象是局部变量)或静态存储区(如果 array 对象是全局变量或静态变量)。
array 不支持动态扩容或收缩,其大小在创建时就固定不变。

性能特点
优点
▮▮▮▮ⓐ 高效的随机访问array 就像 C 风格数组一样,支持常数时间O(1)随机访问
▮▮▮▮ⓑ 栈上分配,速度快array 的内存分配通常在栈上进行,分配和释放速度非常快,效率高。
▮▮▮▮ⓒ 类型安全array 是一个模板类,提供了类型安全的数组访问,避免了 C 风格数组的一些潜在错误,例如类型不匹配。
▮▮▮▮ⓓ 边界检查array 提供了 at() 方法进行边界检查,可以在运行时检测数组越界错误(虽然 operator[] 默认不进行边界检查)。
▮▮▮▮ⓔ 迭代器支持array 提供了迭代器,可以方便地使用标准库算法进行操作。
▮▮▮▮ⓕ 聚合类型array 是一个聚合类型 (aggregate type),在某些情况下可以进行聚合初始化
缺点
▮▮▮▮ⓐ 大小固定array 的大小在编译时就必须确定,不能动态改变大小,缺乏灵活性。
▮▮▮▮ⓑ 栈空间限制:由于 array 通常在栈上分配,其大小受到栈空间的限制,不适合存储过大的数据。
▮▮▮▮ⓒ 插入删除低效array 不支持插入和删除操作,或者说插入删除操作的效率很低,因为需要手动移动元素。

常用操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <array> // 需要包含头文件 <array>
2std::array<int, 5> arr1; // 默认构造,元素值未初始化
3std::array<int, 5> arr2 = {1, 2, 3}; // 部分初始化,剩余元素默认初始化为 0
4std::array<int, 5> arr3 = {1, 2, 3, 4, 5}; // 初始化列表构造
5std::array<int, 5> arr4;
6arr4.fill(0); // 将所有元素填充为 0
7arr3.size(); // 返回数组大小,编译时常量
8arr3.empty(); // 判断数组是否为空,总是返回 false,因为 array 大小固定
9arr3[0]; // 访问第一个元素,不进行边界检查
10arr3.at(0); // 访问第一个元素,进行边界检查
11arr3.front(); // 访问第一个元素
12arr3.back(); // 访问最后一个元素
13arr3.data(); // 返回指向 array 内部数组的指针
14std::array<int, 5>::iterator it_begin = arr3.begin(); // 获取迭代器
15for (auto it = arr3.begin(); it != arr3.end(); ++it) {
16std::cout << *it << " ";
17}
18for (int& x : arr3) { // 范围 for 循环
19x *= 2;
20}
21std::swap(arr3, arr4); // 交换两个 array 的内容 (大小必须相同)

与 C 风格数组的区别
共同点
▮▮▮▮ⓐ 都是固定大小的数组。
▮▮▮▮ⓑ 都支持常数时间O(1)随机访问
▮▮▮▮ⓒ 内存都连续存储
不同点
▮▮▮▮ⓐ 类型安全std::array模板类,提供类型安全;C 风格数组类型检查较弱。
▮▮▮▮ⓑ 边界检查std::array 提供 at() 进行边界检查;C 风格数组没有内置边界检查,容易越界。
▮▮▮▮ⓒ 大小信息std::array 可以通过 size() 方法获取大小;C 风格数组需要手动记录大小或通过 sizeof 计算,但不总是可靠。
▮▮▮▮ⓓ 迭代器支持std::array 提供迭代器,可以使用标准库算法;C 风格数组需要手动使用指针进行迭代。
▮▮▮▮ⓔ 作为对象std::array对象,可以进行拷贝、赋值、比较等操作;C 风格数组退化为指针,不能直接进行这些操作。
▮▮▮▮ⓕ 栈上分配std::array 默认在栈上分配,更安全高效;C 风格数组如果声明在函数内部,也在栈上,但动态分配的 C 风格数组在堆上,需要手动管理内存。

适用场景
array 适用于以下场景:
▮▮▮▮ⓐ 需要固定大小数组,且大小在编译时已知
▮▮▮▮ⓑ 追求性能和效率,利用栈上分配的优势。
▮▮▮▮ⓒ 需要类型安全和边界检查,避免 C 风格数组的潜在问题。
▮▮▮▮ⓓ 作为小型固定大小数据集合,例如固定大小的缓冲区矩阵(固定维度)等。
▮▮▮▮ⓔ 替代 C 风格数组,提高代码的可读性、安全性、和可维护性

2.2 关联容器 (Associative Containers)

深入探讨 set (集合)、map (映射)、multiset (多重集合)、multimap (多重映射) 等关联容器的基于键值对的存储方式,以及红黑树等底层数据结构。关联容器强调元素(或)的有序性快速查找

2.2.1 set (集合) 与 multiset (多重集合): 有序唯一/可重复元素集合 (Ordered Unique/Multiple Element Set)

讲解 setmultiset 的有序性、唯一性/可重复性,以及查找、插入、删除操作的效率,和自定义比较器的使用。setmultiset 用于存储一组元素,并自动排序

内部实现
setmultiset 的内部实现通常基于红黑树 (Red-Black Tree),一种自平衡二叉搜索树
▮ 红黑树保证了在最坏情况下,基本操作(插入、删除、查找)的时间复杂度仍然是对数时间O(logn),其中n是元素数量。
▮ 红黑树的有序性是通过比较器 (Comparator) 来定义的,默认使用 std::less,即小于运算符 <。用户可以自定义比较器来改变元素的排序规则。

性能特点
优点
▮▮▮▮ⓐ 有序存储setmultiset 中的元素总是有序排列的(根据比较器)。
▮▮▮▮ⓑ 快速查找:基于红黑树的实现,查找、插入、删除操作的时间复杂度都是对数时间O(logn)
▮▮▮▮ⓒ 唯一性 (set)set 保证元素的唯一性,重复插入相同元素会被忽略。
▮▮▮▮ⓓ 可重复性 (multiset)multiset 允许存储重复元素
▮▮▮▮ⓔ 迭代器稳定:除了删除操作会使指向被删除元素的迭代器失效外,其他操作对迭代器的影响较小。
缺点
▮▮▮▮ⓐ 不支持随机访问setmultiset 不支持随机访问(下标访问),只能通过迭代器进行顺序访问。
▮▮▮▮ⓑ 额外内存开销:红黑树需要额外的节点结构颜色信息,内存开销比序列容器略大。
▮▮▮▮ⓒ 插入删除略慢于哈希表:虽然红黑树的插入删除效率很高,但平均情况下,哈希表unordered_set 等)的插入删除效率更高O(1)

常用操作
set (集合):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <set> // 需要包含头文件 <set>
2std::set<int> s1; // 默认构造,使用 std::less<int> 比较器
3std::set<int, std::greater<int>> s2; // 使用 std::greater<int> 比较器,降序排列
4std::set<int> s3 = {3, 1, 4, 1, 5, 9, 2, 6}; // 初始化列表构造,重复元素会被忽略,并排序
5s3.insert(7); // 插入元素
6s3.insert(1); // 插入重复元素,set 会忽略
7s3.erase(4); // 删除元素 4
8s3.erase(s3.begin()); // 删除首元素
9s3.count(1); // 返回元素 1 的个数,set 中只能是 0 或 1
10s3.find(5); // 查找元素 5,返回迭代器,找不到返回 s3.end()
11s3.lower_bound(3); // 返回第一个 >= 3 的元素的迭代器
12s3.upper_bound(3); // 返回第一个 > 3 的元素的迭代器
13s3.equal_range(3); // 返回 [lower_bound(3), upper_bound(3)) 范围的迭代器对
14s3.size(); // 返回元素数量
15s3.empty(); // 判断是否为空
16s3.clear(); // 清空 set
17for (int x : s3) { // 范围 for 循环,按排序顺序遍历
18std::cout << x << " ";
19}

multiset (多重集合):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::multiset<int> ms1; // 默认构造,使用 std::less<int> 比较器
2std::multiset<int> ms2 = {3, 1, 4, 1, 5, 9, 2, 6, 1}; // 初始化列表构造,重复元素会保留,并排序
3ms2.insert(1); // 插入元素,multiset 允许重复元素
4ms2.erase(1); // 删除所有值为 1 的元素
5ms2.erase(ms2.find(1)); // 只删除找到的第一个值为 1 的元素
6ms2.count(1); // 返回元素 1 的个数,multiset 中可以 > 1
7ms2.find(5); // 查找元素 5,返回迭代器,找不到返回 ms2.end()
8ms2.lower_bound(1); // 返回第一个 >= 1 的元素的迭代器
9ms2.upper_bound(1); // 返回第一个 > 1 的元素的迭代器
10ms2.equal_range(1); // 返回 [lower_bound(1), upper_bound(1)) 范围的迭代器对
11// ... 其他操作与 set 类似

setmultiset 的对比
共同点
▮▮▮▮ⓐ 都基于红黑树实现,有序存储
▮▮▮▮ⓑ 查找、插入、删除操作的时间复杂度都是对数时间O(logn)
▮▮▮▮ⓒ 不支持随机访问,只能通过迭代器顺序访问。
▮▮▮▮ⓓ 可以自定义比较器,改变排序规则。
不同点
▮▮▮▮ⓐ 元素唯一性set 保证元素唯一multiset 允许重复元素
▮▮▮▮ⓑ count() 方法setcount(key) 返回 0 或 1;multisetcount(key) 返回 >= 0 的整数,表示元素个数。
▮▮▮▮ⓒ erase(key) 方法seterase(key) 删除所有值为 key 的元素(最多一个);multiseterase(key) 删除所有值为 key 的元素。如果只想删除一个,需要使用 ms.erase(ms.find(key))

适用场景
set (集合) 适用于以下场景:
▮▮▮▮ⓐ 需要存储一组唯一的、有序的元素:例如,关键词列表ID 集合不重复的统计数据等。
▮▮▮▮ⓑ 需要频繁进行查找操作:例如,判断元素是否存在范围查找等。
▮▮▮▮ⓒ 需要自动排序,并保持元素有序状态。
multiset (多重集合) 适用于以下场景:
▮▮▮▮ⓐ set 类似,但允许存储重复元素:例如,统计词频(单词可能重复出现)、记录事件发生次数等。
▮▮▮▮ⓑ 需要维护有序的、可重复的元素集合
▮▮▮▮ⓒ 需要统计元素的出现次数

2.2.2 map (映射) 与 multimap (多重映射): 有序唯一/可重复键值对 (Ordered Unique/Multiple Key-Value Pair)

深入剖析 mapmultimap 的键值对存储、基于键的查找、插入、删除操作,以及 operator[]at() 的区别。mapmultimap 用于存储键值对,并根据自动排序

内部实现
mapmultimap 的内部实现也通常基于红黑树 (Red-Black Tree),与 setmultiset 类似。
mapmultimap 存储的是 键值对 (key-value pairs),其中键 (key) 用于排序和查找,值 (value) 存储与键关联的数据。
▮ 排序规则基于键 (key) 的比较器,默认使用 std::less

性能特点
优点
▮▮▮▮ⓐ 有序存储mapmultimap 中的键值对根据键 (key) 有序排列。
▮▮▮▮ⓑ 快速查找:基于键 (key) 的查找、插入、删除操作的时间复杂度都是对数时间O(logn)
▮▮▮▮ⓒ 键唯一性 (map)map 保证键 (key) 的唯一性,重复插入相同键的键值对,会覆盖原有值或被忽略(取决于插入方式)。
▮▮▮▮ⓓ 键可重复性 (multimap)multimap 允许存储键相同的多个键值对
▮▮▮▮ⓔ 高效的键值对操作mapmultimap 提供了方便的键值对插入、查找、删除等操作。
▮▮▮▮ⓕ 迭代器稳定:与 setmultiset 类似,迭代器失效影响较小。
缺点
▮▮▮▮ⓐ 不支持随机访问 (按索引)mapmultimap 不支持按索引的随机访问,只能通过键 (key)迭代器访问。
▮▮▮▮ⓑ 额外内存开销:红黑树和键值对存储都需要额外的内存开销。
▮▮▮▮ⓒ 插入删除略慢于哈希表:与 setmultiset 类似,平均情况下,哈希表unordered_map 等)的插入删除效率更高O(1)

常用操作
map (映射):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <map> // 需要包含头文件 <map>
2std::map<std::string, int> m1; // 默认构造,键类型 std::string,值类型 int,使用 std::less<std::string> 比较器
3std::map<int, std::string, std::greater<int>> m2; // 键类型 int,值类型 std::string,使用 std::greater<int> 比较器,键降序排列
4std::map<std::string, int> m3 = { // 初始化列表构造
5{"apple", 1},
6{"banana", 2},
7{"cherry", 3}
8};
9m3["date"] = 4; // 使用 operator[] 插入或修改键值对,如果键不存在则插入,存在则修改
10m3["apple"] = 10; // 修改键 "apple" 的值
11m3.insert({"fig", 5}); // 使用 insert 插入键值对,如果键已存在,insert 不会覆盖原有值
12m3.insert(std::make_pair("grape", 6)); // 使用 insert 和 make_pair 插入键值对
13m3.erase("banana"); // 删除键为 "banana" 的键值对
14m3.erase(m3.find("cherry")); // 删除键为 "cherry" 的键值对(通过迭代器)
15m3.count("apple"); // 返回键 "apple" 的个数,map 中只能是 0 或 1
16m3.find("date"); // 查找键 "date",返回迭代器,找不到返回 m3.end()
17m3.at("apple"); // 访问键 "apple" 的值,进行边界检查,键不存在抛出 std::out_of_range 异常
18m3["apple"]; // 访问键 "apple" 的值,不进行边界检查,如果键不存在,则会插入一个默认值的新键值对
19m3.size(); // 返回键值对数量
20m3.empty(); // 判断是否为空
21m3.clear(); // 清空 map
22for (auto const& [key, val] : m3) { // C++17 结构化绑定,范围 for 循环遍历键值对
23std::cout << key << ": " << val << " ";
24}
25for (auto it = m3.begin(); it != m3.end(); ++it) { // 迭代器遍历键值对
26std::cout << it->first << ": " << it->second << " "; // it->first 获取键,it->second 获取值
27}

multimap (多重映射):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::multimap<std::string, int> mm1; // 默认构造,使用 std::less<std::string> 比较器
2std::multimap<std::string, int> mm2 = { // 初始化列表构造,键可以重复
3{"apple", 1},
4{"banana", 2},
5{"apple", 10},
6{"cherry", 3}
7};
8mm2.insert({"date", 4}); // 插入键值对,multimap 允许键重复
9mm2.insert({"apple", 20}); // 插入键相同的键值对
10mm2.erase("apple"); // 删除所有键为 "apple" 的键值对
11mm2.erase(mm2.find("apple")); // 只删除找到的第一个键为 "apple" 的键值对
12mm2.count("apple"); // 返回键 "apple" 的个数,multimap 中可以 > 1
13mm2.find("banana"); // 查找键 "banana",返回迭代器,找不到返回 mm2.end()
14mm2.lower_bound("apple"); // 返回第一个键 >= "apple" 的键值对的迭代器
15mm2.upper_bound("apple"); // 返回第一个键 > "apple" 的键值对的迭代器
16mm2.equal_range("apple"); // 返回 [lower_bound("apple"), upper_bound("apple")) 范围的迭代器对
17// ... 其他操作与 map 类似,但 multimap 没有 operator[] 和 at() 方法

注意multimap 没有 operator[]at() 方法,因为键不是唯一的,无法通过键来直接访问唯一的值。

mapmultimap 的对比
共同点
▮▮▮▮ⓐ 都基于红黑树实现,根据键 (key) 有序存储。
▮▮▮▮ⓑ 基于键 (key) 的查找、插入、删除操作的时间复杂度都是对数时间O(logn)
▮▮▮▮ⓒ 不支持按索引的随机访问,只能通过键或迭代器访问。
▮▮▮▮ⓓ 可以自定义键 (key) 的比较器,改变排序规则。
不同点
▮▮▮▮ⓐ 键 (key) 唯一性map 保证键 (key) 唯一multimap 允许键 (key) 重复
▮▮▮▮ⓑ operator[]at() 方法map 提供 operator[]at() 方法,用于访问和修改值;multimap 没有这些方法。
▮▮▮▮ⓒ insert() 方法mapinsert() 方法在键已存在时不覆盖原有值;multimapinsert() 方法总是插入新的键值对,即使键已存在。
▮▮▮▮ⓓ count(key) 方法mapcount(key) 返回 0 或 1;multimapcount(key) 返回 >= 0 的整数,表示键的个数。
▮▮▮▮ⓔ erase(key) 方法maperase(key) 删除所有键为 key 的键值对(最多一个);multimaperase(key) 删除所有键为 key 的键值对。如果只想删除一个,需要使用 mm.erase(mm.find(key))

operator[]at() 的区别 (仅 map 适用)
operator[]
▮▮▮▮ⓐ 访问:如果键存在,返回键对应值的引用,可以修改值。
▮▮▮▮ⓑ 插入:如果键不存在,会插入一个新的键值对,键为指定键,值为默认值(对于 int 类型,默认值为 0),然后返回新插入值的引用
▮▮▮▮ⓒ 不进行边界检查:如果键不存在,operator[]自动插入,不会抛出异常。
at()
▮▮▮▮ⓐ 访问:如果键存在,返回键对应值的引用,可以修改值。
▮▮▮▮ⓑ 不插入:如果键不存在,at() 不会插入新的键值对。
▮▮▮▮ⓒ 进行边界检查:如果键不存在,at() 会抛出 std::out_of_range 异常
总结
▮▮▮▮ⓐ 如果需要访问已存在的键,且不希望插入新键,应该使用 at() 并进行异常处理,或者先使用 find() 检查键是否存在。
▮▮▮▮ⓑ 如果需要访问或插入键值对,且允许插入新键,可以使用 operator[],代码更简洁。

适用场景
map (映射) 适用于以下场景:
▮▮▮▮ⓐ 需要存储键值对,并根据键进行快速查找:例如,字典配置信息索引等。
▮▮▮▮ⓑ 需要键唯一,且根据键自动排序
▮▮▮▮ⓒ 需要通过键来访问和修改值
multimap (多重映射) 适用于以下场景:
▮▮▮▮ⓐ map 类似,但允许键重复:例如,一个键对应多个值的场景(例如,一个作者对应多本书)、索引列表(关键词可能重复出现)等。
▮▮▮▮ⓑ 需要维护有序的、键可重复的键值对集合
▮▮▮▮ⓒ 需要查找特定键的所有值

2.3 容器适配器 (Container Adaptors)

介绍 stack (栈)、queue (队列)、priority_queue (优先队列) 等容器适配器的概念,以及它们如何基于其他容器实现特定的数据结构。容器适配器不是独立的容器,而是基于已有的容器(例如 dequevectorlist)进行封装,提供特定的接口和功能,使其表现为不同的数据结构。

2.3.1 stack (栈): 后进先出 (LIFO) 数据结构 (Last-In, First-Out Data Structure)

讲解 stack 的 LIFO 特性、push(), pop(), top() 等操作,以及其在函数调用栈、表达式求值等场景的应用。stack 模拟了这种数据结构,只允许在栈顶进行插入和删除操作。

特性
后进先出 (LIFO, Last-In, First-Out):最后进入栈的元素,最先被移除。
限制访问stack 只允许访问栈顶元素,不允许访问其他位置的元素。
常用操作push (入栈), pop (出栈), top (访问栈顶元素), empty (判空), size (获取大小)。

内部实现
stack 是一个容器适配器,默认情况下,它基于 deque 容器实现。也可以使用 vectorlist 作为底层容器。
stack 封装了底层容器的接口,只暴露栈相关的操作,隐藏了底层容器的其他操作。

常用操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <stack> // 需要包含头文件 <stack>
2std::stack<int> s1; // 默认构造,底层容器为 deque
3std::stack<int, std::vector<int>> s2; // 指定底层容器为 vector
4std::stack<int, std::list<int>> s3; // 指定底层容器为 list
5s1.push(10); // 入栈,将元素 10 压入栈顶
6s1.push(20);
7s1.top(); // 访问栈顶元素,返回栈顶元素的引用,但不移除元素
8s1.pop(); // 出栈,移除栈顶元素,但不返回值
9s1.empty(); // 判断栈是否为空
10s1.size(); // 返回栈中元素的数量
11// stack 没有迭代器,不能遍历

适用场景
stack 适用于需要 LIFO 数据结构的场景:
▮▮▮▮ⓐ 函数调用栈:程序运行时,函数调用和返回通常使用栈来管理,保存函数调用信息、局部变量等。
▮▮▮▮ⓑ 表达式求值:例如,中缀表达式转后缀表达式(逆波兰表达式),计算后缀表达式等。
▮▮▮▮ⓒ 括号匹配:检查代码或文本中的括号是否正确匹配。
▮▮▮▮ⓓ 浏览器的后退按钮:用户访问的网页历史记录可以使用栈来存储,后退操作相当于出栈。
▮▮▮▮ⓔ 深度优先搜索 (DFS):图的深度优先搜索可以使用栈来辅助实现。
▮▮▮▮ⓕ 撤销操作:例如,文本编辑器的撤销功能,可以使用栈来存储操作历史。

2.3.2 queue (队列): 先进先出 (FIFO) 数据结构 (First-In, First-Out Data Structure)

介绍 queue 的 FIFO 特性、push(), pop(), front(), back() 等操作,以及其在消息队列、广度优先搜索等场景的应用。queue 模拟了队列这种数据结构,元素从队尾进入,从队首移除。

特性
先进先出 (FIFO, First-In, First-Out):最先进入队列的元素,最先被移除。
限制访问queue 只允许访问队首元素队尾元素,不允许访问其他位置的元素。
常用操作push (入队,队尾插入), pop (出队,队首移除), front (访问队首元素), back (访问队尾元素), empty (判空), size (获取大小)。

内部实现
queue 也是一个容器适配器,默认情况下,它基于 deque 容器实现。也可以使用 list 作为底层容器(但不可以使用 vector,因为 vector 不支持高效的头部删除操作)。
queue 封装了底层容器的接口,只暴露队列相关的操作。

常用操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <queue> // 需要包含头文件 <queue>
2std::queue<int> q1; // 默认构造,底层容器为 deque
3std::queue<int, std::list<int>> q2; // 指定底层容器为 list
4q1.push(10); // 入队,将元素 10 加入队尾
5q1.push(20);
6q1.front(); // 访问队首元素,返回队首元素的引用,但不移除元素
7q1.back(); // 访问队尾元素,返回队尾元素的引用,但不移除元素
8q1.pop(); // 出队,移除队首元素,但不返回值
9q1.empty(); // 判断队列是否为空
10q1.size(); // 返回队列中元素的数量
11// queue 没有迭代器,不能遍历

适用场景
queue 适用于需要 FIFO 数据结构的场景:
▮▮▮▮ⓐ 消息队列:进程间通信、任务调度等场景,消息按照到达顺序处理。
▮▮▮▮ⓑ 广度优先搜索 (BFS):图的广度优先搜索使用队列来存储待访问的节点。
▮▮▮▮ⓒ 打印队列:打印任务按照提交顺序排队打印。
▮▮▮▮ⓓ 排队系统:例如,银行排队、餐厅排号等模拟。
▮▮▮▮ⓔ 网络数据包处理:网络数据包按照接收顺序处理。
▮▮▮▮ⓕ 多线程任务队列:多个线程共享的任务队列,保证任务按照提交顺序执行。

2.3.3 priority_queue (优先队列): 优先级队列 (Priority Queue)

深入剖析 priority_queue 的优先级排序特性、基于的实现,以及自定义比较函数的使用。priority_queue 是一种特殊的队列,元素出队顺序不是 FIFO,而是根据元素的优先级

特性
优先级排序priority_queue 中的元素不是按照插入顺序出队,而是按照元素的优先级出队。默认情况下,优先级最高的元素(通常是值最大的元素)先出队
基于堆 (Heap) 实现priority_queue 通常基于最大堆 (max-heap)最小堆 (min-heap) 实现,保证每次都能快速访问到优先级最高的元素。
常用操作push (入队), pop (出队), top (访问队顶元素,即优先级最高的元素), empty (判空), size (获取大小)。

内部实现
priority_queue 也是一个容器适配器,默认情况下,它基于 vector 容器实现,并使用堆算法(例如 std::make_heap, std::push_heap, std::pop_heap)来维护堆结构。
▮ 用户可以自定义比较函数 (comparison function)函数对象 (function object) 来定义元素的优先级规则。默认使用 std::less,构建最大堆;使用 std::greater,构建最小堆

常用操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <queue> // 需要包含头文件 <queue>
2#include <vector>
3#include <functional> // 需要包含头文件 <functional> 以使用 std::greater
4std::priority_queue<int> pq1; // 默认构造,最大堆,使用 std::less<int> 比较器
5std::priority_queue<int, std::vector<int>, std::greater<int>> pq2; // 最小堆,使用 std::greater<int> 比较器
6pq1.push(10); // 入队,元素会根据优先级插入到堆中合适的位置
7pq1.push(5);
8pq1.push(15);
9pq1.top(); // 访问优先级最高的元素(堆顶元素),但不移除元素
10pq1.pop(); // 出队,移除优先级最高的元素,并调整堆结构
11pq1.empty(); // 判断优先队列是否为空
12pq1.size(); // 返回优先队列中元素的数量
13// 使用 Lambda 表达式自定义比较函数 (最小堆)
14auto cmp = [](int left, int right) { return left > right; };
15std::priority_queue<int, std::vector<int>, decltype(cmp)> pq3(cmp);
16pq3.push(10);
17pq3.push(5);
18pq3.push(15);
19pq3.top(); // 此时 top() 返回 5,因为是最小堆
20// priority_queue 没有迭代器,不能遍历 (通常不需要遍历所有元素,只需要访问优先级最高的元素)

自定义比较函数
priority_queue 的第三个模板参数可以指定比较函数函数对象,用于定义元素的优先级。
▮ 比较函数需要是一个二元谓词 (binary predicate),即接受两个参数,返回 bool 值,表示第一个参数是否“小于”第二个参数(这里的“小于”是自定义的优先级规则)。
▮ 默认比较器 std::less<T> 创建最大堆std::greater<T> 创建最小堆
▮ 可以使用函数对象函数指针Lambda 表达式等作为比较函数。

适用场景
priority_queue 适用于需要 优先级队列 的场景:
▮▮▮▮ⓐ 任务调度:根据任务的优先级,优先执行优先级高的任务。
▮▮▮▮ⓑ 事件驱动模拟:例如,模拟交通流量、网络事件等,事件按照发生时间(优先级)顺序处理。
▮▮▮▮ⓒ 堆排序:可以使用优先队列实现堆排序算法。
▮▮▮▮ⓓ Dijkstra 算法Prim 算法:图算法中,可以使用优先队列来优化最短路径和最小生成树的查找过程。
▮▮▮▮ⓔ Top K 问题:查找一组数据中前 K 个最大或最小的元素,可以使用最小堆(找最大 K 个)或最大堆(找最小 K 个)的优先队列来高效解决。
▮▮▮▮ⓕ 实时系统:需要根据事件的紧急程度进行处理的系统。

2.4 无序容器 (Unordered Containers)

讲解 unordered_set (无序集合)、unordered_map (无序映射)、unordered_multiset (无序多重集合)、unordered_multimap (无序多重映射) 等基于哈希表 (Hash Table) 的无序容器。无序容器强调快速查找平均常数时间复杂度的插入、删除操作,但不保证元素的顺序

2.4.1 哈希表与哈希函数 (Hash Table and Hash Function)

介绍哈希表的原理、哈希冲突的解决策略,以及如何为自定义类型设计哈希函数。哈希表 是无序容器的底层数据结构,通过哈希函数将键映射到桶 (bucket) 中,实现快速查找。

哈希表原理
哈希函数 (Hash Function)
▮▮▮▮ⓐ 哈希函数是一个映射函数,将键 (key) 映射到一个整数值,称为哈希值 (hash value)哈希码 (hash code)
▮▮▮▮ⓑ 理想的哈希函数应具有以下特点:
▮▮▮▮▮▮▮▮❸ 高效性:计算哈希值应快速
▮▮▮▮▮▮▮▮❹ 均匀分布:哈希值应均匀分布在哈希表的桶范围内,减少哈希冲突。
▮▮▮▮▮▮▮▮❺ 确定性:对于相同的键,哈希函数应始终返回相同的哈希值
桶 (Bucket)
▮▮▮▮ⓐ 哈希表内部维护一个桶数组,每个桶可以存储零个或多个键值对(或元素)。
▮▮▮▮ⓑ 哈希函数计算出的哈希值通常需要对桶的数量取模,以确定键值对应该存储在哪个桶中。
哈希冲突 (Hash Collision)
▮▮▮▮ⓐ 哈希冲突 指的是不同的键被哈希函数映射到相同的哈希值,从而导致它们被分配到同一个桶中。
▮▮▮▮ⓑ 哈希冲突是不可避免的,好的哈希表设计应该尽量减少哈希冲突,并采用合适的冲突解决策略

哈希冲突解决策略
开放寻址法 (Open Addressing)
▮▮▮▮ⓐ 当发生哈希冲突时,在哈希表中寻找下一个可用的桶来存储冲突的键值对。
▮▮▮▮ⓑ 常见的开放寻址法包括:
▮▮▮▮▮▮▮▮❸ 线性探测 (Linear Probing):顺序查找下一个桶,直到找到空桶。容易产生聚集 (clustering) 现象,影响性能。
▮▮▮▮▮▮▮▮❹ 二次探测 (Quadratic Probing):以二次方步长查找下一个桶,可以缓解聚集现象。
▮▮▮▮▮▮▮▮❺ 双重哈希 (Double Hashing):使用多个哈希函数,发生冲突时使用第二个哈希函数计算新的探测位置。
链地址法 (Separate Chaining)
▮▮▮▮ⓐ 每个桶维护一个链表(或其他数据结构,例如红黑树),当发生哈希冲突时,将冲突的键值对添加到同一个桶的链表中。
▮▮▮▮ⓑ 链地址法是 unordered containers 常用的冲突解决策略。它实现简单,冲突处理灵活,但如果哈希冲突过多,单个桶的链表会变得很长,影响查找效率。
▮▮▮▮ⓒ 为了提高链地址法的性能,当单个桶的链表长度过长时,一些哈希表实现会将链表转换为红黑树,将链表查找O(n)优化为红黑树查找O(logn)

自定义类型的哈希函数
▮ 对于内置类型(例如 int, std::string),C++ 标准库已经提供了默认的哈希函数。
▮ 对于自定义类型,如果想将其作为无序容器的键,需要自定义哈希函数
▮ 自定义哈希函数需要满足以下要求:
▮▮▮▮ⓐ 必须是一个函数对象 (function object)函数,可以接受自定义类型的对象作为参数,并返回 std::size_t 类型的哈希值
▮▮▮▮ⓑ 必须满足相等性 (equality)哈希值一致性 (hash consistency)
▮▮▮▮▮▮▮▮❸ 如果两个对象相等(通过 operator== 比较返回 true),则它们的哈希值必须相等
▮▮▮▮▮▮▮▮❹ 如果两个对象不相等,它们的哈希值最好也应该不相等(但允许哈希冲突)。
实现自定义哈希函数的常用方法
▮▮▮▮ⓐ 组合成员变量的哈希值:对于自定义类型,可以将关键成员变量的哈希值进行组合,得到自定义类型的哈希值。可以使用 std::hash 模板类来获取内置类型和 std::string 的哈希函数。
▮▮▮▮ⓑ 使用 std::hash 模板特化:可以为自定义类型特化 (specialize) std::hash 模板类,提供自定义的哈希函数实现。
▮▮▮▮ⓒ 使用 Lambda 表达式或函数对象:可以定义一个 Lambda 表达式或函数对象作为哈希函数,传递给无序容器的构造函数或模板参数。

示例:自定义类型的哈希函数

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <unordered_set>
2#include <string>
3struct Person {
4std::string name;
5int age;
6bool operator==(const Person& other) const { // 重载 operator==,定义相等性
7return name == other.name && age == other.age;
8}
9};
10// 自定义哈希函数 (函数对象)
11struct PersonHash {
12std::size_t operator()(const Person& p) const {
13std::hash<std::string> name_hasher;
14std::hash<int> age_hasher;
15std::size_t hash_val = 0;
16// 组合 name 和 age 的哈希值,可以使用异或 ^ 或其他组合方式
17hash_val ^= name_hasher(p.name) + 0x9e3779b9 + (hash_val << 6) + (hash_val >> 2);
18hash_val ^= age_hasher(p.age) + 0x9e3779b9 + (hash_val << 6) + (hash_val >> 2);
19return hash_val;
20}
21};
22// 使用自定义哈希函数创建 unordered_set
23std::unordered_set<Person, PersonHash> personSet;
24personSet.insert({"Alice", 30});
25personSet.insert({"Bob", 25});
26personSet.insert({"Alice", 30}); // 重复元素,不会插入

2.4.2 unordered_set (无序集合) 与 unordered_multiset (无序多重集合): 无序唯一/可重复元素集合 (Unordered Unique/Multiple Element Set)

讲解 unordered_setunordered_multiset 的无序性、哈希查找的平均时间复杂度,以及适用场景。unordered_setunordered_multiset 基于哈希表实现,提供快速查找平均常数时间复杂度的插入、删除操作,但不保证元素的顺序。

特性
无序性unordered_setunordered_multiset 中的元素不保证有序,元素的顺序取决于哈希函数和哈希表内部的存储结构。迭代器遍历的顺序可能与元素的插入顺序不同,且每次运行程序顺序也可能不同
哈希查找:基于哈希表实现,查找、插入、删除操作的平均时间复杂度常数时间O(1)最坏情况下(哈希冲突严重),时间复杂度可能退化为线性时间O(n)
唯一性 (unordered_set)unordered_set 保证元素的唯一性,重复插入相同元素会被忽略。
可重复性 (unordered_multiset)unordered_multiset 允许存储重复元素
需要哈希函数和相等性比较:元素类型需要提供哈希函数(用于计算哈希值)和相等性比较运算符 operator==(用于处理哈希冲突和判断元素是否相等)。

性能特点
优点
▮▮▮▮ⓐ 平均常数时间复杂度:在平均情况下,查找、插入、删除操作的时间复杂度为 O(1),非常高效。
▮▮▮▮ⓑ 快速查找:哈希查找速度很快,尤其适用于大规模数据的查找操作。
▮▮▮▮ⓒ 唯一性 (unordered_set)可重复性 (unordered_multiset):提供两种选择,满足不同需求。
缺点
▮▮▮▮ⓐ 无序性:元素不保证有序,迭代器遍历顺序不确定。如果需要有序集合,应使用 setmultiset
▮▮▮▮ⓑ 最坏情况性能退化最坏情况下(哈希冲突严重,例如哈希函数设计不良或数据分布不均匀),查找、插入、删除操作的时间复杂度可能退化为 O(n)
▮▮▮▮ⓒ 额外内存开销:哈希表需要额外的桶数组链表(或红黑树)来处理哈希冲突,内存开销比序列容器略大,并且负载因子 (load factor) 会影响内存使用和性能。
▮▮▮▮ⓓ 需要哈希函数和相等性比较:自定义类型需要提供哈希函数和相等性比较运算符。

常用操作
unordered_set (无序集合):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <unordered_set> // 需要包含头文件 <unordered_set>
2std::unordered_set<int> us1; // 默认构造,使用默认哈希函数和相等性比较
3std::unordered_set<int> us2 = {3, 1, 4, 1, 5, 9, 2, 6}; // 初始化列表构造,重复元素会被忽略
4us2.insert(7); // 插入元素
5us2.insert(1); // 插入重复元素,unordered_set 会忽略
6us2.erase(4); // 删除元素 4
7us2.count(1); // 返回元素 1 的个数,unordered_set 中只能是 0 或 1
8us2.find(5); // 查找元素 5,返回迭代器,找不到返回 us2.end()
9us2.size(); // 返回元素数量
10us2.empty(); // 判断是否为空
11us2.clear(); // 清空 unordered_set
12// 无序遍历,顺序不确定
13for (int x : us2) {
14std::cout << x << " ";
15}
16us2.bucket_count(); // 返回哈希表的桶数量
17us2.load_factor(); // 返回负载因子 (平均每个桶的元素数量)
18us2.rehash(20); // 重新哈希,至少分配 20 个桶,提高性能

unordered_multiset (无序多重集合):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::unordered_multiset<int> ums1; // 默认构造,使用默认哈希函数和相等性比较
2std::unordered_multiset<int> ums2 = {3, 1, 4, 1, 5, 9, 2, 6, 1}; // 初始化列表构造,重复元素会保留
3ums2.insert(1); // 插入元素,unordered_multiset 允许重复元素
4ums2.erase(1); // 删除所有值为 1 的元素
5ums2.erase(ums2.find(1)); // 只删除找到的第一个值为 1 的元素
6ums2.count(1); // 返回元素 1 的个数,unordered_multiset 中可以 > 1
7ums2.find(5); // 查找元素 5,返回迭代器,找不到返回 ums2.end()
8// ... 其他操作与 unordered_set 类似

unordered_setunordered_multiset 的对比
共同点
▮▮▮▮ⓐ 都基于哈希表实现,无序存储
▮▮▮▮ⓑ 查找、插入、删除操作的平均时间复杂度O(1)
▮▮▮▮ⓒ 不保证元素有序,迭代器遍历顺序不确定。
▮▮▮▮ⓓ 需要提供哈希函数相等性比较运算符
▮▮▮▮ⓔ 提供了 bucket_count(), load_factor(), rehash() 等哈希表管理方法。
不同点
▮▮▮▮ⓐ 元素唯一性unordered_set 保证元素唯一unordered_multiset 允许重复元素
▮▮▮▮ⓑ count() 方法unordered_setcount(key) 返回 0 或 1;unordered_multisetcount(key) 返回 >= 0 的整数,表示元素个数。
▮▮▮▮ⓒ erase(key) 方法unordered_seterase(key) 删除所有值为 key 的元素(最多一个);unordered_multiseterase(key) 删除所有值为 key 的元素。如果只想删除一个,需要使用 ums.erase(ums.find(key))

适用场景
unordered_set (无序集合) 适用于以下场景:
▮▮▮▮ⓐ 需要存储一组唯一的元素,且不关心元素的顺序
▮▮▮▮ⓑ 需要频繁进行快速查找操作,例如去重成员资格检查等。
▮▮▮▮ⓒ 对性能要求高,但可以容忍最坏情况下的性能退化
▮▮▮▮ⓓ 哈希函数设计良好,哈希冲突较少 的场景。
unordered_multiset (无序多重集合) 适用于以下场景:
▮▮▮▮ⓐ unordered_set 类似,但允许存储重复元素
▮▮▮▮ⓑ 需要统计元素的频率或出现次数,但不关心元素的顺序
▮▮▮▮ⓒ 需要维护无序的、可重复的元素集合

2.4.3 unordered_map (无序映射) 与 unordered_multimap (无序多重映射): 无序唯一/可重复键值对 (Unordered Unique/Multiple Key-Value Pair)

深入剖析 unordered_mapunordered_multimap 的键值对存储、哈希查找、以及性能特点。unordered_mapunordered_multimap 基于哈希表实现,提供快速的键值对查找平均常数时间复杂度的插入、删除操作,但不保证键值对的顺序。

特性
无序性unordered_mapunordered_multimap 中的键值对不保证有序,顺序取决于哈希函数和哈希表内部的存储结构。
哈希查找:基于哈希表实现,基于键 (key) 的查找、插入、删除操作的平均时间复杂度常数时间O(1)最坏情况下可能退化为 O(n)
键唯一性 (unordered_map)unordered_map 保证键 (key) 的唯一性,重复插入相同键的键值对,会覆盖原有值或被忽略(取决于插入方式)。
键可重复性 (unordered_multimap)unordered_multimap 允许存储键相同的多个键值对
需要键的哈希函数和相等性比较:键类型需要提供哈希函数相等性比较运算符 operator==

性能特点
优点
▮▮▮▮ⓐ 平均常数时间复杂度:在平均情况下,基于键 (key) 的查找、插入、删除操作的时间复杂度为 O(1),非常高效。
▮▮▮▮ⓑ 快速查找:哈希查找速度很快,尤其适用于大规模键值对数据的查找操作。
▮▮▮▮ⓒ 键唯一性 (unordered_map)键可重复性 (unordered_multimap):提供两种选择,满足不同需求。
缺点
▮▮▮▮ⓐ 无序性:键值对不保证有序,迭代器遍历顺序不确定。如果需要有序映射,应使用 mapmultimap
▮▮▮▮ⓑ 最坏情况性能退化最坏情况下,查找、插入、删除操作的时间复杂度可能退化为 O(n)
▮▮▮▮ⓒ 额外内存开销:哈希表和键值对存储都需要额外的内存开销,负载因子会影响内存使用和性能。
▮▮▮▮ⓓ 需要键的哈希函数和相等性比较:键类型需要提供哈希函数和相等性比较运算符。

常用操作
unordered_map (无序映射):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <unordered_map> // 需要包含头文件 <unordered_map>
2std::unordered_map<std::string, int> um1; // 默认构造,使用默认哈希函数和相等性比较
3um1["apple"] = 1; // 使用 operator[] 插入或修改键值对
4um1["banana"] = 2;
5um1["apple"] = 10; // 修改键 "apple" 的值
6um1.insert({"cherry", 3}); // 使用 insert 插入键值对,如果键已存在,insert 不会覆盖原有值
7um1.insert(std::make_pair("date", 4)); // 使用 insert 和 make_pair 插入键值对
8um1.erase("banana"); // 删除键为 "banana" 的键值对
9um1.count("apple"); // 返回键 "apple" 的个数,unordered_map 中只能是 0 或 1
10um1.find("cherry"); // 查找键 "cherry",返回迭代器,找不到返回 um1.end()
11um1.at("apple"); // 访问键 "apple" 的值,进行边界检查,键不存在抛出 std::out_of_range 异常
12um1["apple"]; // 访问键 "apple" 的值,不进行边界检查,如果键不存在,则会插入一个默认值的新键值对
13um1.size(); // 返回键值对数量
14um1.empty(); // 判断是否为空
15um1.clear(); // 清空 unordered_map
16// 无序遍历,顺序不确定
17for (auto const& [key, val] : um1) { // C++17 结构化绑定,范围 for 循环遍历键值对
18std::cout << key << ": " << val << " ";
19}
20um1.bucket_count(); // 返回哈希表的桶数量
21um1.load_factor(); // 返回负载因子
22um1.rehash(20); // 重新哈希,至少分配 20 个桶

unordered_multimap (无序多重映射):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::unordered_multimap<std::string, int> umm1; // 默认构造,使用默认哈希函数和相等性比较
2std::unordered_multimap<std::string, int> umm2 = {
3{"apple", 1},
4{"banana", 2},
5{"apple", 10},
6{"cherry", 3}
7};
8umm2.insert({"date", 4}); // 插入键值对,unordered_multimap 允许键重复
9umm2.insert({"apple", 20}); // 插入键相同的键值对
10umm2.erase("apple"); // 删除所有键为 "apple" 的键值对
11umm2.erase(umm2.find("apple")); // 只删除找到的第一个键为 "apple" 的键值对
12umm2.count("apple"); // 返回键 "apple" 的个数,unordered_multimap 中可以 > 1
13umm2.find("banana"); // 查找键 "banana",返回迭代器,找不到返回 umm2.end()
14// ... 其他操作与 unordered_map 类似,但 unordered_multimap 没有 operator[] 和 at() 方法

注意unordered_multimap 没有 operator[]at() 方法

unordered_mapunordered_multimap 的对比
共同点
▮▮▮▮ⓐ 都基于哈希表实现,无序存储
▮▮▮▮ⓑ 基于键 (key) 的查找、插入、删除操作的平均时间复杂度O(1)
▮▮▮▮ⓒ 不保证键值对有序,迭代器遍历顺序不确定。
▮▮▮▮ⓓ 需要提供键 (key) 的哈希函数相等性比较运算符
▮▮▮▮ⓔ 提供了 bucket_count(), load_factor(), rehash() 等哈希表管理方法。
不同点
▮▮▮▮ⓐ 键 (key) 唯一性unordered_map 保证键 (key) 唯一unordered_multimap 允许键 (key) 重复
▮▮▮▮ⓑ operator[]at() 方法unordered_map 提供 operator[]at() 方法;unordered_multimap 没有这些方法。
▮▮▮▮ⓒ insert() 方法unordered_mapinsert() 方法在键已存在时不覆盖原有值;unordered_multimapinsert() 方法总是插入新的键值对,即使键已存在。
▮▮▮▮ⓓ count(key) 方法unordered_mapcount(key) 返回 0 或 1;unordered_multimapcount(key) 返回 >= 0 的整数,表示键的个数。
▮▮▮▮ⓔ erase(key) 方法unordered_maperase(key) 删除所有键为 key 的键值对(最多一个);unordered_multimaperase(key) 删除所有键为 key 的键值对。如果只想删除一个,需要使用 umm.erase(umm.find(key))

适用场景
unordered_map (无序映射) 适用于以下场景:
▮▮▮▮ⓐ 需要存储键值对,并根据键进行快速查找,且不关心键值对的顺序
▮▮▮▮ⓑ 需要键唯一,且平均查找、插入、删除操作时间为常数时间
▮▮▮▮ⓒ 对性能要求非常高,但可以容忍最坏情况下的性能退化
▮▮▮▮ⓓ 哈希函数设计良好,哈希冲突较少 的场景。
▮▮▮▮ⓔ 例如,缓存(快速查找缓存数据)、索引(快速查找索引信息)、频率统计(统计元素出现频率)等。
unordered_multimap (无序多重映射) 适用于以下场景:
▮▮▮▮ⓐ unordered_map 类似,但允许键重复
▮▮▮▮ⓑ 需要存储一个键对应多个值的情况,并进行快速查找,但不关心顺序
▮▮▮▮ⓒ 例如,反向索引(一个关键词对应多个文档)、日志分析(记录不同类型的事件,可能重复发生)等。

<END_OF_CHAPTER/>

3. 算法 (Algorithms)

3.1 算法基础与分类 (Algorithm Basics and Classification)

本节将深入探讨 C++ 标准库算法的基础概念和分类方法。理解这些基础知识对于有效利用标准库提供的强大算法工具至关重要。我们将介绍算法的核心概念、迭代器在算法中的作用,以及标准库算法的主要分类方式,为后续深入学习各种算法奠定坚实的基础。

3.1.1 迭代器与算法的结合 (Iterators and Algorithm Combination)

C++ 标准库算法的强大之处在于其泛型 (generic) 设计,这种泛型性很大程度上依赖于迭代器 (iterators)。迭代器是连接算法和容器的桥梁,它允许算法在不依赖特定容器类型的情况下,操作各种不同容器中的元素。可以将迭代器视为抽象指针 (abstract pointers),它提供了一种统一的方式来访问和遍历容器中的元素序列。

迭代器的核心作用

解耦算法与容器 (Decoupling algorithms and containers):算法不直接操作容器,而是通过迭代器间接访问容器中的元素。这意味着相同的算法可以应用于不同类型的容器,只要这些容器提供兼容的迭代器。例如,std::sort 算法可以用于 std::vector, std::deque, std::array 等多种容器,因为它们都提供了随机访问迭代器 (random access iterators)
定义算法的操作范围 (Defining the operation range of algorithms):大多数标准库算法接受一对迭代器作为参数,通常称为 [begin, end) 区间。begin 迭代器指向操作范围的起始位置,end 迭代器指向操作范围的结束位置的下一个位置(左闭右开区间 (left-closed and right-open interval))。这种区间表示法使得算法可以灵活地操作容器的全部或部分元素。
算法的泛型性基础 (Foundation of algorithm genericity):迭代器定义了算法操作元素的方式。例如,输入迭代器 (input iterators) 允许算法读取元素,输出迭代器 (output iterators) 允许算法写入元素,前向迭代器 (forward iterators) 允许算法单向遍历元素,双向迭代器 (bidirectional iterators) 允许算法双向遍历元素,随机访问迭代器 (random access iterators) 允许算法随机访问元素。算法根据其需要的迭代器类型进行设计,从而实现泛型性。

不同迭代器类型对算法适用性的影响

标准库定义了五种迭代器类别,它们之间具有增强 (refinement) 关系,即类别越靠后,功能越强大。算法对迭代器类别的要求决定了其可以操作的容器类型。

输入迭代器 (Input Iterators)
▮▮▮▮⚝ 功能:只能单向读取元素序列。支持自增 (++)、解引用 (*)、相等比较 (==, !=) 等操作。
▮▮▮▮⚝ 适用算法示例:std::find, std::accumulate (部分场景).
▮▮▮▮⚝ 容器示例:std::istream_iterator (输入流迭代器).
输出迭代器 (Output Iterators)
▮▮▮▮⚝ 功能:只能单向写入元素序列。支持自增 (++)、解引用赋值 (* = value) 等操作。
▮▮▮▮⚝ 适用算法示例:std::copy, std::transform (写入部分).
▮▮▮▮⚝ 容器示例:std::ostream_iterator (输出流迭代器), std::back_inserter, std::front_inserter, std::inserter (插入迭代器).
前向迭代器 (Forward Iterators)
▮▮▮▮⚝ 功能:兼具输入迭代器和输出迭代器的功能,并可以多次遍历同一序列。
▮▮▮▮⚝ 适用算法示例:std::replace_if, std::forward_list 的迭代器通常是前向迭代器。
▮▮▮▮⚝ 容器示例:std::forward_list (单向链表).
双向迭代器 (Bidirectional Iterators)
▮▮▮▮⚝ 功能:在前向迭代器的基础上,增加了反向遍历的能力。支持自减 (--) 操作。
▮▮▮▮⚝ 适用算法示例:std::reverse, std::liststd::set 等关联容器的迭代器通常是双向迭代器。
▮▮▮▮⚝ 容器示例:std::list (双向链表), std::set, std::map, std::multiset, std::multimap (关联容器).
随机访问迭代器 (Random Access Iterators)
▮▮▮▮⚝ 功能:功能最强大的迭代器,支持所有双向迭代器的操作,并增加了随机访问能力,例如通过下标访问 ([])、迭代器算术运算 (+, -, +=, -=)、关系比较 (<, >, <=, >=) 等。
▮▮▮▮⚝ 适用算法示例:std::sort, std::binary_search, std::vector, std::array, std::deque 的迭代器都是随机访问迭代器。
▮▮▮▮⚝ 容器示例:std::vector (向量), std::array (数组), std::deque (双端队列).

理解不同迭代器类别的特性和算法对迭代器类别的要求,有助于选择合适的容器和算法,并编写高效、泛型的 C++ 代码。在后续章节中,我们将更深入地探讨各种迭代器和算法的具体用法。

3.1.2 算法的分类:非修改性、修改性、排序、搜索、数值 (Algorithm Classification: Non-modifying, Modifying, Sorting, Searching, Numerical)

C++ 标准库算法提供了丰富的功能,为了更好地组织和理解这些算法,通常会对它们进行分类。一种常见的分类方式是根据算法的功能和操作类型进行划分。以下是标准库算法的主要分类,以及每一类算法的简要介绍和典型示例:

非修改性算法 (Non-modifying Algorithms)

功能:这类算法不改变被操作的元素序列的内容顺序。它们主要用于查询统计比较等操作,从序列中获取信息,但不修改序列本身。
特点:安全,不会对原始数据造成影响。可以应用于只读数据或需要在不改变数据的前提下进行分析的场景。
典型示例
▮▮▮▮⚝ std::for_each: 对序列中的每个元素执行指定操作 (函数或函数对象),但不修改元素本身。常用于遍历输出、统计等操作。
▮▮▮▮⚝ std::find: 在序列中查找第一个满足特定条件的元素。
▮▮▮▮⚝ std::count: 统计序列中满足特定条件的元素个数。
▮▮▮▮⚝ std::all_of, std::any_of, std::none_of: 检查序列中是否所有、任何或没有元素满足特定条件。
▮▮▮▮⚝ std::equal: 比较两个序列是否相等。
▮▮▮▮⚝ std::mismatch: 查找两个序列中第一个不匹配的元素对。

修改性算法 (Modifying Algorithms)

功能:这类算法会改变被操作的元素序列的内容,但通常不改变元素的顺序(除非特别指出)。它们用于转换复制填充替换移除等操作,对序列中的元素进行修改或重组。
特点:需要谨慎使用,因为会直接修改原始数据。适用于需要对数据进行变换或预处理的场景。
典型示例
▮▮▮▮⚝ std::transform: 将一个序列的元素通过指定操作转换为另一个序列。可以进行元素的值变换、类型转换等。
▮▮▮▮⚝ std::copy: 将一个序列的元素复制到另一个序列。
▮▮▮▮⚝ std::fill, std::generate: 用指定值或生成器函数填充序列。
▮▮▮▮⚝ std::remove, std::remove_if: 移除序列中满足特定条件的元素(实际上是将不移除的元素前移,并返回新逻辑结尾的迭代器,需要配合 erase 成员函数才能真正删除元素,对于 std::array 和 C 风格数组,removeremove_if 算法无法直接改变容器大小)。
▮▮▮▮⚝ std::replace, std::replace_if: 将序列中满足特定条件的元素替换为新值。
▮▮▮▮⚝ std::swap_ranges: 交换两个序列指定范围内的元素。

排序算法 (Sorting Algorithms)

功能:这类算法重新排列元素序列的顺序,使其按照特定的排序规则排列。排序是数据处理中非常常见的操作,用于数据检索、优化搜索效率等。
特点:排序算法通常对迭代器有随机访问迭代器 (random access iterators) 的要求,因为排序通常需要随机访问元素进行比较和交换。
典型示例
▮▮▮▮⚝ std::sort: 对序列进行全排序 (full sort),默认升序排列。可以自定义比较函数或函数对象来指定排序规则。
▮▮▮▮⚝ std::stable_sort: 稳定排序,保持相等元素的相对顺序不变。适用于需要保持原有顺序的排序场景。
▮▮▮▮⚝ std::partial_sort: 对序列进行部分排序 (partial sort),仅排序序列中前 n 个元素,使其为序列中最小的 n 个元素(或按指定规则排序的前 n 个元素)。
▮▮▮▮⚝ std::nth_element: 部分排序 (partition) 算法,将序列中第 n 个位置的元素放到其正确排序后的位置,并保证该位置之前的元素都小于等于它,之后的元素都大于等于它,但不保证其他元素的顺序。
▮▮▮▮⚝ std::is_sorted: 检查序列是否已排序。
▮▮▮▮⚝ std::merge: 合并两个已排序的序列到一个新的已排序序列。

搜索算法 (Searching Algorithms)

功能:这类算法在已排序未排序的序列中查找特定元素或元素范围。搜索是数据检索的关键操作,用于快速定位目标数据。
特点:对于已排序序列,可以使用二分搜索 (binary search) 算法,效率更高 (对数时间复杂度O(logn))。对于未排序序列,只能使用线性搜索 (linear search) 算法 (线性时间复杂度O(n)).
典型示例
▮▮▮▮⚝ std::find (线性搜索): 在未排序序列中查找第一个匹配的元素。
▮▮▮▮⚝ std::binary_search (二分搜索): 在已排序序列中判断是否存在特定元素。
▮▮▮▮⚝ std::lower_bound (二分搜索): 在已排序序列中查找第一个不小于 (not less than) 目标值的元素的位置。
▮▮▮▮⚝ std::upper_bound (二分搜索): 在已排序序列中查找第一个大于 (greater than) 目标值的元素的位置。
▮▮▮▮⚝ std::equal_range (二分搜索): 在已排序序列中查找与目标值相等的元素范围,返回一个 pair,包含 lower_boundupper_bound 迭代器。

数值算法 (Numerical Algorithms)

功能:这类算法主要用于执行数值计算操作,例如累加内积差分求和等。数值计算在科学计算、数据分析等领域非常重要。
特点:通常需要包含头文件 <numeric>
典型示例
▮▮▮▮⚝ std::accumulate: 对序列中的元素进行累加 (accumulation) 操作,计算总和。可以指定初始值和自定义的累加操作。
▮▮▮▮⚝ std::inner_product: 计算两个序列的内积 (inner product) (点积)。
▮▮▮▮⚝ std::adjacent_difference: 计算序列中相邻元素的差分 (adjacent difference)
▮▮▮▮⚝ std::partial_sum: 计算序列的部分和 (partial sum)
▮▮▮▮⚝ std::iota: 生成一个递增序列 (incrementing sequence),填充指定范围。

掌握标准库算法的分类,可以帮助我们快速定位需要的算法,并更好地理解算法的功能和适用场景。在后续章节中,我们将详细介绍每一类算法中的常用算法,并通过代码示例演示其用法。

3.2 常用算法详解 (Detailed Explanation of Common Algorithms)

本节将精选并详细讲解 C++ 标准库中一些最常用的算法,涵盖非修改性、修改性、排序、搜索和数值算法。对于每个算法,我们将深入剖析其功能、用法、应用场景,并提供代码示例,帮助读者理解和掌握这些算法的实际应用。

3.2.1 非修改性算法:for_each, find, count, all_of, any_of, none_of (Non-modifying Algorithms)

非修改性算法在不改变原始数据的前提下,对序列进行各种查询和统计操作。以下是几个常用的非修改性算法的详细讲解:

std::for_each: 遍历操作 (Traversal Operation)

功能:对指定范围 [begin, end) 内的每个元素应用一个函数对象 (function object)Lambda 表达式 (lambda expression)for_each 算法本身不修改元素,主要用于执行一些副作用 (side effects) 操作,例如输出元素、累积信息等。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<class InputIterator, class Function>
2Function for_each(InputIterator first, InputIterator last, Function f);

▮▮▮▮⚝ first, last: 定义要操作的元素范围的输入迭代器 (input iterators)
▮▮▮▮⚝ f: 要应用的函数对象函数指针 (function pointer)Lambda 表达式。它接受一个元素作为参数,返回类型会被忽略。
▮▮▮▮⚝ 返回值:返回传入的函数对象 f 的副本。在实际应用中,返回值通常被忽略,因为 for_each 的主要目的是执行函数对象 f 的副作用。

应用场景
▮▮▮▮⚝ 遍历输出容器元素:将容器中的元素打印到控制台或其他输出流。
▮▮▮▮⚝ 统计元素信息:例如,统计容器中正数、负数、偶数等的数量。
▮▮▮▮⚝ 执行其他副作用操作:例如,将元素写入日志文件、更新外部状态等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5};
6// 使用 Lambda 表达式输出每个元素
7std::cout << "输出容器元素: ";
8std::for_each(numbers.begin(), numbers.end(), [](int n){
9std::cout << n << " ";
10});
11std::cout << std::endl; // 输出:输出容器元素: 1 2 3 4 5
12// 使用函数对象统计偶数个数
13struct EvenCounter {
14int count = 0;
15void operator()(int n) {
16if (n % 2 == 0) {
17count++;
18}
19}
20};
21EvenCounter counter;
22std::for_each(numbers.begin(), numbers.end(), counter);
23std::cout << "偶数个数: " << counter.count << std::endl; // 输出:偶数个数: 2
24return 0;
25}

std::find: 查找元素 (Find Element)

功能:在指定范围 [first, last)查找第一个等于 (equal to) 指定值的元素。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<class InputIterator, class T>
2InputIterator find(InputIterator first, InputIterator last, const T& value);
3template<class InputIterator, class Predicate>
4InputIterator find_if(InputIterator first, InputIterator last, Predicate pred);
5template<class InputIterator, class Predicate>
6InputIterator find_if_not(InputIterator first, InputIterator last, Predicate pred);

▮▮▮▮⚝ find: 查找等于 value 的元素。
▮▮▮▮⚝ find_if: 查找第一个使谓词 (predicate) pred 返回 true 的元素。
▮▮▮▮⚝ find_if_not: 查找第一个使谓词 (predicate) pred 返回 false 的元素。
▮▮▮▮⚝ first, last: 定义搜索范围的输入迭代器 (input iterators)
▮▮▮▮⚝ value: 要查找的值。
▮▮▮▮⚝ pred: 谓词 (函数对象、函数指针或 Lambda 表达式),接受一个元素作为参数,返回 bool 值。
▮▮▮▮⚝ 返回值:
▮▮▮▮▮▮▮▮⚝ 如果找到匹配的元素,返回指向该元素的迭代器
▮▮▮▮▮▮▮▮⚝ 如果未找到匹配的元素,返回 last 迭代器。

应用场景
▮▮▮▮⚝ 检查容器中是否存在特定元素:例如,判断一个集合中是否包含某个值。
▮▮▮▮⚝ 查找满足特定条件的第一个元素:例如,在一个向量中查找第一个负数。
▮▮▮▮⚝ 在字符串中查找子字符串 (结合字符串迭代器)。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {10, 20, 30, 40, 50};
6// 查找值为 30 的元素
7auto it1 = std::find(numbers.begin(), numbers.end(), 30);
8if (it1 != numbers.end()) {
9std::cout << "找到元素 30, 位置: " << std::distance(numbers.begin(), it1) << std::endl; // 输出:找到元素 30, 位置: 2
10} else {
11std::cout << "未找到元素 30" << std::endl;
12}
13// 查找第一个大于 35 的元素 (使用 find_if 和 Lambda 表达式)
14auto it2 = std::find_if(numbers.begin(), numbers.end(), [](int n){
15return n > 35;
16});
17if (it2 != numbers.end()) {
18std::cout << "找到第一个大于 35 的元素: " << *it2 << ", 位置: " << std::distance(numbers.begin(), it2) << std::endl; // 输出:找到第一个大于 35 的元素: 40, 位置: 3
19} else {
20std::cout << "未找到大于 35 的元素" << std::endl;
21}
22return 0;
23}

std::count: 计数 (Count)

功能:统计指定范围 [first, last)等于 (equal to) 指定值的元素个数
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<class InputIterator, class T>
2typename iterator_traits<InputIterator>::difference_type
3count(InputIterator first, InputIterator last, const T& value);
4template<class InputIterator, class Predicate>
5typename iterator_traits<InputIterator>::difference_type
6count_if(InputIterator first, InputIterator last, Predicate pred);

▮▮▮▮⚝ count: 统计等于 value 的元素个数。
▮▮▮▮⚝ count_if: 统计使谓词 (predicate) pred 返回 true 的元素个数。
▮▮▮▮⚝ first, last: 定义计数范围的输入迭代器 (input iterators)
▮▮▮▮⚝ value: 要计数的值。
▮▮▮▮⚝ pred: 谓词 (函数对象、函数指针或 Lambda 表达式),接受一个元素作为参数,返回 bool 值。
▮▮▮▮⚝ 返回值:匹配元素的个数,类型通常为 std::iterator_traits<InputIterator>::difference_type (通常是整数类型,例如 std::ptrdiff_t).

应用场景
▮▮▮▮⚝ 统计容器中特定元素的出现次数:例如,统计一个向量中某个数值出现的频率。
▮▮▮▮⚝ 统计满足特定条件的元素数量:例如,统计一个列表中正数的个数。
▮▮▮▮⚝ 数据分析和统计

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 2, 3, 2, 4, 5, 2};
6// 统计值为 2 的元素个数
7int count1 = std::count(numbers.begin(), numbers.end(), 2);
8std::cout << "元素 2 的个数: " << count1 << std::endl; // 输出:元素 2 的个数: 4
9// 统计偶数个数 (使用 count_if 和 Lambda 表达式)
10int count2 = std::count_if(numbers.begin(), numbers.end(), [](int n){
11return n % 2 == 0;
12});
13std::cout << "偶数个数: " << count2 << std::endl; // 输出:偶数个数: 5
14return 0;
15}

std::all_of, std::any_of, std::none_of: 条件检查 (Condition Checking)

功能:检查指定范围 [first, last) 内的所有任何没有元素是否满足特定条件。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<class InputIterator, class Predicate>
2bool all_of(InputIterator first, InputIterator last, Predicate pred);
3template<class InputIterator, class Predicate>
4bool any_of(InputIterator first, InputIterator last, Predicate pred);
5template<class InputIterator, class Predicate>
6bool none_of(InputIterator first, InputIterator last, Predicate pred);

▮▮▮▮⚝ all_of: 所有元素都使谓词 (predicate) pred 返回 true 时,返回 true,否则返回 false
▮▮▮▮⚝ any_of: 至少有一个元素使谓词 (predicate) pred 返回 true 时,返回 true,否则返回 false
▮▮▮▮⚝ none_of: 所有元素都使谓词 (predicate) pred 返回 false 时,返回 true,否则返回 false (即 没有元素 使 pred 返回 true)。
▮▮▮▮⚝ first, last: 定义检查范围的输入迭代器 (input iterators)
▮▮▮▮⚝ pred: 谓词 (函数对象、函数指针或 Lambda 表达式),接受一个元素作为参数,返回 bool 值。
▮▮▮▮⚝ 返回值:bool 值,表示检查结果。

应用场景
▮▮▮▮⚝ 数据验证:例如,检查一个容器中的所有元素是否都符合某种规范。
▮▮▮▮⚝ 条件判断:例如,判断一个列表中是否存在满足特定条件的元素,从而决定后续操作。
▮▮▮▮⚝ 简化复杂的条件逻辑

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers1 = {2, 4, 6, 8, 10};
6std::vector<int> numbers2 = {1, 2, 3, 4, 5};
7// 检查 numbers1 中是否所有元素都是偶数
8bool all_even1 = std::all_of(numbers1.begin(), numbers1.end(), [](int n){
9return n % 2 == 0;
10});
11std::cout << "numbers1 中所有元素都是偶数: " << std::boolalpha << all_even1 << std::endl; // 输出:numbers1 中所有元素都是偶数: true
12// 检查 numbers2 中是否所有元素都是偶数
13bool all_even2 = std::all_of(numbers2.begin(), numbers2.end(), [](int n){
14return n % 2 == 0;
15});
16std::cout << "numbers2 中所有元素都是偶数: " << std::boolalpha << all_even2 << std::endl; // 输出:numbers2 中所有元素都是偶数: false
17// 检查 numbers2 中是否存在奇数
18bool any_odd = std::any_of(numbers2.begin(), numbers2.end(), [](int n){
19return n % 2 != 0;
20});
21std::cout << "numbers2 中存在奇数: " << std::boolalpha << any_odd << std::endl; // 输出:numbers2 中存在奇数: true
22// 检查 numbers1 中是否没有奇数
23bool none_odd = std::none_of(numbers1.begin(), numbers1.end(), [](int n){
24return n % 2 != 0;
25});
26std::cout << "numbers1 中没有奇数: " << std::boolalpha << none_odd << std::endl; // 输出:numbers1 中没有奇数: true
27return 0;
28}

这些非修改性算法提供了强大的数据查询和统计功能,它们是 C++ 标准库算法中非常基础且常用的工具。掌握它们可以帮助我们编写更简洁、更高效的代码,处理各种数据分析和验证任务。

3.2.2 修改性算法:transform, copy, fill, generate, remove, replace (Modifying Algorithms)

修改性算法会直接修改序列中的元素值。以下是一些常用的修改性算法的详细讲解:

std::transform: 元素变换 (Element Transformation)

功能:将指定范围 [first1, last1) 内的元素通过一个一元操作 (unary operation)两个范围 [first1, last1)[first2, last2) 的元素通过一个二元操作 (binary operation) 进行变换,并将结果存储目标范围 [result, result + (last1 - first1)) 中。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1// 一元操作
2template <class InputIterator, class OutputIterator, class UnaryOperation>
3OutputIterator transform(InputIterator first1, InputIterator last1,
4OutputIterator result, UnaryOperation op);
5// 二元操作
6template <class InputIterator1, class InputIterator2, class OutputIterator,
7class BinaryOperation>
8OutputIterator transform(InputIterator1 first1, InputIterator1 last1,
9InputIterator2 first2, OutputIterator result,
10BinaryOperation binary_op);

▮▮▮▮⚝ 一元操作版本
▮▮▮▮▮▮▮▮⚝ first1, last1: 定义输入范围输入迭代器 (input iterators)
▮▮▮▮▮▮▮▮⚝ result: 指向目标范围起始位置输出迭代器 (output iterator)。目标范围必须有足够的空间容纳变换后的元素。
▮▮▮▮▮▮▮▮⚝ op: 一元操作 (unary operation),函数对象、函数指针或 Lambda 表达式,接受一个输入范围的元素作为参数,返回变换后的值。
▮▮▮▮⚝ 二元操作版本
▮▮▮▮▮▮▮▮⚝ first1, last1: 定义第一个输入范围输入迭代器 (input iterators)
▮▮▮▮▮▮▮▮⚝ first2: 定义第二个输入范围起始位置的输入迭代器 (input iterator)。第二个输入范围至少要和第一个输入范围一样大。
▮▮▮▮▮▮▮▮⚝ result: 指向目标范围起始位置输出迭代器 (output iterator)。目标范围必须有足够的空间容纳变换后的元素。
▮▮▮▮▮▮▮▮⚝ binary_op: 二元操作 (binary operation),函数对象、函数指针或 Lambda 表达式,接受两个输入范围的对应元素作为参数,返回变换后的值。
▮▮▮▮⚝ 返回值:指向目标范围末尾输出迭代器,即 result + (last1 - first1)

应用场景
▮▮▮▮⚝ 元素值变换:例如,将容器中所有元素平方、取绝对值、转换为另一种类型。
▮▮▮▮⚝ 容器元素类型转换:将一个容器的元素类型转换为另一种类型,并存储到新的容器中。
▮▮▮▮⚝ 两个容器元素的组合操作:例如,将两个向量对应位置的元素相加、相乘等。
▮▮▮▮⚝ 原地 (in-place) 变换 (目标范围与源范围相同):虽然 transform 的目标范围通常是新的范围,但也可以将目标范围设置为与源范围相同,实现原地变换。需要注意迭代器不能重叠导致未定义行为。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <cmath> // std::abs
5#include <iterator> // std::back_inserter
6int main() {
7std::vector<int> numbers1 = {-1, 2, -3, 4, -5};
8std::vector<int> numbers2 = {10, 20, 30, 40, 50};
9std::vector<int> result1;
10std::vector<int> result2;
11std::vector<int> result3;
12// 一元操作:取绝对值
13std::transform(numbers1.begin(), numbers1.end(), std::back_inserter(result1), [](int n){
14return std::abs(n);
15});
16std::cout << "取绝对值后的结果: ";
17for (int n : result1) std::cout << n << " "; // 输出:取绝对值后的结果: 1 2 3 4 5
18std::cout << std::endl;
19// 一元操作:平方
20std::transform(numbers2.begin(), numbers2.end(), std::back_inserter(result2), [](int n){
21return n * n;
22});
23std::cout << "平方后的结果: ";
24for (int n : result2) std::cout << n << " "; // 输出:平方后的结果: 100 400 900 1600 2500
25std::cout << std::endl;
26// 二元操作:对应元素相加
27std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), std::back_inserter(result3), [](int n1, int n2){
28return n1 + n2;
29});
30std::cout << "对应元素相加的结果: ";
31for (int n : result3) std::cout << n << " "; // 输出:对应元素相加的结果: 9 22 27 44 45
32std::cout << std::endl;
33return 0;
34}

std::copy: 元素复制 (Element Copy)

功能:将指定范围 [first, last) 内的元素复制目标范围 [result, result + (last - first)) 中。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class InputIterator, class OutputIterator>
2OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result);
3template <class InputIterator, class OutputIterator>
4OutputIterator copy_backward(InputIterator first, InputIterator last, OutputIterator result);

▮▮▮▮⚝ copy: 从前向后复制元素。目标范围的起始位置 result 必须指向目标范围的开始位置
▮▮▮▮⚝ copy_backward: 从后向前复制元素。目标范围的起始位置 result 必须指向目标范围的结束位置的下一个位置copy_backward 通常用于原地复制 (in-place copy) 且源范围和目标范围可能重叠的情况,且目标范围的起始位置在源范围之内。
▮▮▮▮⚝ first, last: 定义源范围输入迭代器 (input iterators)
▮▮▮▮⚝ result: 指向目标范围起始位置输出迭代器 (output iterator)。目标范围必须有足够的空间容纳复制的元素。
▮▮▮▮⚝ 返回值:指向目标范围末尾输出迭代器,即 result + (last - first)

应用场景
▮▮▮▮⚝ 容器元素复制:例如,将一个向量的元素复制到另一个向量、数组或输出流。
▮▮▮▮⚝ 创建容器副本
▮▮▮▮⚝ 数据备份和迁移
▮▮▮▮⚝ 在容器内部移动元素 (使用 std::move 迭代器和 copycopy_backward 可以实现高效的元素移动)。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <iterator> // std::back_inserter
5int main() {
6std::vector<int> source = {1, 2, 3, 4, 5};
7std::vector<int> destination1;
8std::vector<int> destination2(5); // 预先分配空间
9// 复制到 destination1 (使用 back_inserter 动态扩展 destination1)
10std::copy(source.begin(), source.end(), std::back_inserter(destination1));
11std::cout << "复制到 destination1: ";
12for (int n : destination1) std::cout << n << " "; // 输出:复制到 destination1: 1 2 3 4 5
13std::cout << std::endl;
14// 复制到 destination2 (destination2 已预先分配空间)
15std::copy(source.begin(), source.end(), destination2.begin());
16std::cout << "复制到 destination2: ";
17for (int n : destination2) std::cout << n << " "; // 输出:复制到 destination2: 1 2 3 4 5
18std::cout << std::endl;
19// 原地复制 (使用 copy_backward, 目标范围起始位置在源范围之内)
20std::vector<int> source_in_place = {1, 2, 3, 4, 5};
21std::copy_backward(source_in_place.begin(), source_in_place.begin() + 3, source_in_place.end());
22std::cout << "原地复制后的 source_in_place: ";
23for (int n : source_in_place) std::cout << n << " "; // 输出:原地复制后的 source_in_place: 1 2 3 1 2
24std::cout << std::endl;
25return 0;
26}

std::fillstd::generate: 填充序列 (Fill Sequence)

功能:用指定值生成器函数****填充指定范围 [first, last) 内的元素。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class ForwardIterator, class T>
2void fill(ForwardIterator first, ForwardIterator last, const T& value);
3template <class ForwardIterator, class Generator>
4void generate(ForwardIterator first, ForwardIterator last, Generator gen);
5template <class ForwardIterator, class Size, class T>
6ForwardIterator fill_n(ForwardIterator first, Size n, const T& value);
7template <class ForwardIterator, class Size, class Generator>
8ForwardIterator generate_n(ForwardIterator first, Size n, Generator gen);

▮▮▮▮⚝ fill: 用 value 填充范围 [first, last) 内的所有元素。
▮▮▮▮⚝ generate: 使用 生成器函数 (generator function) gen 生成的值填充范围 [first, last) 内的所有元素。每次调用 gen 生成一个新值。
▮▮▮▮⚝ fill_n: 从 first 位置开始,用 value 填充 n 元素。
▮▮▮▮⚝ generate_n: 从 first 位置开始,使用 生成器函数 (generator function) gen 生成的值填充 n 元素。
▮▮▮▮⚝ first, last (或 n): 定义填充范围的前向迭代器 (forward iterators)
▮▮▮▮⚝ value: 要填充的值。
▮▮▮▮⚝ gen: 生成器函数 (generator function),函数对象、函数指针或 Lambda 表达式,不接受参数,返回要填充的值。
▮▮▮▮⚝ 返回值 (fill_n, generate_n): 返回指向填充范围末尾迭代器,即 first + nfillgenerate 返回 void

应用场景
▮▮▮▮⚝ 初始化容器元素:例如,将向量或数组的所有元素初始化为 0、-1 或其他默认值。
▮▮▮▮⚝ 生成测试数据:使用 generate 和随机数生成器生成随机测试数据。
▮▮▮▮⚝ 创建特定模式的序列:例如,使用 Lambda 表达式生成递增序列、斐波那契数列等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric> // std::iota
5int main() {
6std::vector<int> numbers1(5); // 大小为 5 的向量,元素未初始化
7std::vector<int> numbers2(5);
8std::vector<int> numbers3(5);
9// 使用 fill 用 10 填充 numbers1
10std::fill(numbers1.begin(), numbers1.end(), 10);
11std::cout << "使用 fill 填充后的 numbers1: ";
12for (int n : numbers1) std::cout << n << " "; // 输出:使用 fill 填充后的 numbers1: 10 10 10 10 10
13std::cout << std::endl;
14// 使用 generate 和 Lambda 表达式生成递增序列填充 numbers2
15int count = 1;
16std::generate(numbers2.begin(), numbers2.end(), [&count](){
17return count++;
18});
19std::cout << "使用 generate 填充后的 numbers2: ";
20for (int n : numbers2) std::cout << n << " "; // 输出:使用 generate 填充后的 numbers2: 1 2 3 4 5
21std::cout << std::endl;
22// 使用 generate_n 和 Lambda 表达式生成斐波那契数列的前 5 项填充 numbers3
23int a = 0, b = 1;
24std::generate_n(numbers3.begin(), 5, [&a, &b](){
25int next = a + b;
26a = b;
27b = next;
28return a;
29});
30std::cout << "使用 generate_n 填充后的 numbers3 (斐波那契数列): ";
31for (int n : numbers3) std::cout << n << " "; // 输出:使用 generate_n 填充后的 numbers3 (斐波那契数列): 1 2 3 5 8
32std::cout << std::endl;
33return 0;
34}

std::removestd::replace: 元素移除和替换 (Element Removal and Replacement)

功能
▮▮▮▮⚝ std::removestd::remove_if: 移除指定范围 [first, last)等于特定值或满足特定条件的元素。实际上removeremove_if 算法并不真正删除容器中的元素,而是将不移除的元素前移,覆盖要移除的元素,并返回指向新逻辑结尾迭代器。要真正删除元素,需要配合容器的 erase 成员函数
▮▮▮▮⚝ std::replacestd::replace_if: 将指定范围 [first, last)等于特定值或满足特定条件的元素替换新值

用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1// remove
2template <class ForwardIterator, class T>
3ForwardIterator remove(ForwardIterator first, ForwardIterator last, const T& value);
4template <class ForwardIterator, class Predicate>
5ForwardIterator remove_if(ForwardIterator first, ForwardIterator last, Predicate pred);
6// replace
7template <class ForwardIterator, class T>
8void replace(ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);
9template <class ForwardIterator, class Predicate, class T>
10void replace_if(ForwardIterator first, ForwardIterator last, Predicate pred, const T& new_value);

▮▮▮▮⚝ remove: 移除等于 value 的元素。
▮▮▮▮⚝ remove_if: 移除使谓词 (predicate) pred 返回 true 的元素。
▮▮▮▮⚝ replace: 将等于 old_value 的元素替换为 new_value
▮▮▮▮⚝ replace_if: 将使谓词 (predicate) pred 返回 true 的元素替换为 new_value
▮▮▮▮⚝ first, last: 定义操作范围的前向迭代器 (forward iterators)
▮▮▮▮⚝ value, old_value, new_value: 元素值。
▮▮▮▮⚝ pred: 谓词 (函数对象、函数指针或 Lambda 表达式),接受一个元素作为参数,返回 bool 值。
▮▮▮▮⚝ 返回值 (remove, remove_if): 返回指向新逻辑结尾前向迭代器

应用场景
▮▮▮▮⚝ 数据清理:移除容器中不需要的元素,例如删除空字符串、无效数据等。
▮▮▮▮⚝ 数据规范化:将容器中不符合规范的元素替换为规范值。
▮▮▮▮⚝ 字符串处理:移除字符串中的特定字符、替换子字符串等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers1 = {1, 2, 2, 3, 2, 4, 5, 2};
6std::vector<int> numbers2 = {1, -2, 3, -4, 5, -6};
7// 使用 remove 移除 numbers1 中的所有 2
8auto it1 = std::remove(numbers1.begin(), numbers1.end(), 2);
9std::cout << "remove 后的 numbers1 (逻辑删除): ";
10for (int n : numbers1) std::cout << n << " "; // 输出:remove 后的 numbers1 (逻辑删除): 1 3 4 5 2 4 5 2
11std::cout << std::endl;
12numbers1.erase(it1, numbers1.end()); // 真正删除元素
13std::cout << "erase 后 numbers1 (物理删除): ";
14for (int n : numbers1) std::cout << n << " "; // 输出:erase 后 numbers1 (物理删除): 1 3 4 5
15std::cout << std::endl;
16// 使用 remove_if 移除 numbers2 中的所有负数
17auto it2 = std::remove_if(numbers2.begin(), numbers2.end(), [](int n){
18return n < 0;
19});
20std::cout << "remove_if 后的 numbers2 (逻辑删除): ";
21for (int n : numbers2) std::cout << n << " "; // 输出:remove_if 后的 numbers2 (逻辑删除): 1 3 5 -4 5 -6
22std::cout << std::endl;
23numbers2.erase(it2, numbers2.end()); // 真正删除元素
24std::cout << "erase 后 numbers2 (物理删除): ";
25for (int n : numbers2) std::cout << n << " "; // 输出:erase 后 numbers2 (物理删除): 1 3 5
26std::cout << std::endl;
27std::vector<int> numbers3 = {1, 2, 2, 3, 2, 4, 5, 2};
28std::vector<int> numbers4 = {1, -2, 3, -4, 5, -6};
29// 使用 replace 将 numbers3 中的所有 2 替换为 20
30std::replace(numbers3.begin(), numbers3.end(), 2, 20);
31std::cout << "replace 后的 numbers3: ";
32for (int n : numbers3) std::cout << n << " "; // 输出:replace 后的 numbers3: 1 20 20 3 20 4 5 20
33std::cout << std::endl;
34// 使用 replace_if 将 numbers4 中的所有负数替换为 0
35std::replace_if(numbers4.begin(), numbers4.end(), [](int n){
36return n < 0;
37}, 0);
38std::cout << "replace_if 后的 numbers4: ";
39for (int n : numbers4) std::cout << n << " "; // 输出:replace_if 后的 numbers4: 1 0 3 0 5 0
40std::cout << std::endl;
41return 0;
42}

这些修改性算法提供了强大的数据变换和清理功能,它们是 C++ 标准库算法中非常实用且常用的工具。理解它们的功能和正确用法,可以帮助我们高效地处理各种数据修改任务。

3.2.3 排序算法:sort, stable_sort, partial_sort, nth_element (Sorting Algorithms)

排序算法用于重新排列序列中的元素顺序,使其按照特定的规则排列。以下是一些常用的排序算法的详细讲解:

std::sort: 全排序 (Full Sort)

功能:对指定范围 [first, last) 内的元素进行全排序 (full sort),默认升序排列。std::sort 使用快速排序 (Quicksort)内省排序 (Introsort) 等高效的排序算法实现,平均时间复杂度为O(NlogN)
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class RandomAccessIterator>
2void sort(RandomAccessIterator first, RandomAccessIterator last);
3template <class RandomAccessIterator, class Compare>
4void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

▮▮▮▮⚝ first, last: 定义排序范围的随机访问迭代器 (random access iterators)
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则。它接受两个元素作为参数,返回 bool 值,表示第一个元素是否应排在第二个元素之前。默认使用 operator< 进行比较 (升序排列)。
▮▮▮▮⚝ 返回值:voidstd::sort 直接在原序列上进行排序。

特点
▮▮▮▮⚝ 高效:平均时间复杂度O(NlogN),在大多数情况下性能良好。
▮▮▮▮⚝ 不稳定排序 (unstable sort):不保证相等元素的相对顺序在排序后保持不变。如果需要保持相等元素的相对顺序,应使用 std::stable_sort
▮▮▮▮⚝ 需要随机访问迭代器:只能用于提供随机访问迭代器的容器,例如 std::vector, std::array, std::deque

应用场景
▮▮▮▮⚝ 通用排序:对容器元素进行排序,满足大多数排序需求。
▮▮▮▮⚝ 为二分搜索等算法做准备std::binary_search, std::lower_bound, std::upper_bound, std::equal_range 等搜索算法要求序列已排序。
▮▮▮▮⚝ 数据分析和报表生成:对数据进行排序后进行统计分析或生成有序报表。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers1 = {5, 2, 8, 1, 9, 4, 7, 3, 6};
6std::vector<int> numbers2 = {5, 2, 8, 1, 9, 4, 7, 3, 6};
7// 默认升序排序
8std::sort(numbers1.begin(), numbers1.end());
9std::cout << "升序排序后的 numbers1: ";
10for (int n : numbers1) std::cout << n << " "; // 输出:升序排序后的 numbers1: 1 2 3 4 5 6 7 8 9
11std::cout << std::endl;
12// 使用 Lambda 表达式自定义降序排序
13std::sort(numbers2.begin(), numbers2.end(), [](int a, int b){
14return a > b; // 降序排列
15});
16std::cout << "降序排序后的 numbers2: ";
17for (int n : numbers2) std::cout << n << " "; // 输出:降序排序后的 numbers2: 9 8 7 6 5 4 3 2 1
18std::cout << std::endl;
19return 0;
20}

std::stable_sort: 稳定排序 (Stable Sort)

功能:与 std::sort 类似,对指定范围 [first, last) 内的元素进行全排序 (full sort),默认升序排列。std::stable_sort 的关键特点是稳定排序 (stable sort),即保证相等元素的相对顺序在排序后保持不变std::stable_sort 通常使用归并排序 (Merge Sort) 或其他稳定排序算法实现,时间复杂度通常略高于 std::sort,但仍然是O(NlogN)
用法:与 std::sort 的用法完全相同,只是算法实现和稳定性不同。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class RandomAccessIterator>
2void stable_sort(RandomAccessIterator first, RandomAccessIterator last);
3template <class RandomAccessIterator, class Compare>
4void stable_sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp);

特点
▮▮▮▮⚝ 稳定排序 (stable sort):这是与 std::sort 的主要区别。
▮▮▮▮⚝ 高效:时间复杂度O(NlogN),虽然可能略慢于 std::sort,但仍然是高效的排序算法。
▮▮▮▮⚝ 需要随机访问迭代器:与 std::sort 一样,需要随机访问迭代器。

应用场景
▮▮▮▮⚝ 需要保持相等元素相对顺序的排序:例如,对学生成绩列表进行排序,如果成绩相同,希望保持学生原有的顺序 (例如,按学号排序)。
▮▮▮▮⚝ 多级排序:先按一个键排序,再在相等键的范围内按另一个键排序。可以使用 stable_sort 多次排序来实现多级稳定排序。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4struct Student {
5std::string name;
6int score;
7int id; // 学号,用于记录原始顺序
8Student(std::string n, int s, int i) : name(n), score(s), id(i) {}
9};
10bool compareStudentByName(const Student& s1, const Student& s2) {
11return s1.name < s2.name;
12}
13bool compareStudentByScore(const Student& s1, const Student& s2) {
14return s1.score < s2.score;
15}
16int main() {
17std::vector<Student> students = {
18{"Alice", 85, 1},
19{"Bob", 90, 2},
20{"Charlie", 85, 3}, // 与 Alice 分数相同
21{"David", 95, 4}
22};
23// 按分数稳定排序 (分数相同,保持原有顺序)
24std::stable_sort(students.begin(), students.end(), compareStudentByScore);
25std::cout << "按分数稳定排序后的学生列表 (分数相同保持原有顺序):" << std::endl;
26for (const auto& student : students) {
27std::cout << "Name: " << student.name << ", Score: " << student.score << ", ID: " << student.id << std::endl;
28}
29/* 输出:
30按分数稳定排序后的学生列表 (分数相同保持原有顺序):
31Name: Alice, Score: 85, ID: 1
32Name: Charlie, Score: 85, ID: 3 // Charlie 在 Alice 后面,保持了原有顺序
33Name: Bob, Score: 90, ID: 2
34Name: David, Score: 95, ID: 4
35*/
36std::cout << std::endl;
37// 先按分数稳定排序,再按姓名稳定排序 (实现多级稳定排序)
38std::stable_sort(students.begin(), students.end(), compareStudentByName); // 按姓名稳定排序
39std::stable_sort(students.begin(), students.end(), compareStudentByScore); // 再次按分数稳定排序 (保证姓名顺序在分数相同时保持)
40std::cout << "先按分数稳定排序,再按姓名稳定排序后的学生列表 (多级稳定排序):" << std::endl;
41for (const auto& student : students) {
42std::cout << "Name: " << student.name << ", Score: " << student.score << ", ID: " << student.id << std::endl;
43}
44/* 输出:
45先按分数稳定排序,再按姓名稳定排序后的学生列表 (多级稳定排序):
46Name: Alice, Score: 85, ID: 1 // Alice 在 Charlie 前面,因为姓名首字母 A 在 C 前面
47Name: Charlie, Score: 85, ID: 3
48Name: Bob, Score: 90, ID: 2
49Name: David, Score: 95, ID: 4
50*/
51std::cout << std::endl;
52return 0;
53}

std::partial_sort: 部分排序 (Partial Sort)

功能:对指定范围 [first, last) 内的元素进行部分排序 (partial sort),将范围 [first, middle) 内的元素排序为序列中最小的 (或按自定义规则排序的) 前 middle - first 个元素,并将其余元素放置在 [middle, last) 范围内,但不保证 [middle, last) 范围内的元素有序。std::partial_sort 的时间复杂度为O(NlogM),其中N是序列长度,Mmiddle - first 的距离。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class RandomAccessIterator>
2void partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
3RandomAccessIterator last);
4template <class RandomAccessIterator, class Compare>
5void partial_sort(RandomAccessIterator first, RandomAccessIterator middle,
6RandomAccessIterator last, Compare comp);

▮▮▮▮⚝ first, middle, last: 定义排序范围的随机访问迭代器 (random access iterators)[first, middle) 是要排序的范围,[middle, last) 是剩余范围。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,与 std::sort 类似。默认使用 operator< 进行比较 (升序排列)。
▮▮▮▮⚝ 返回值:voidstd::partial_sort 直接在原序列上进行排序。

特点
▮▮▮▮⚝ 部分排序:只排序部分元素,对于只需要获取序列中最小或最大的前几个元素的场景,效率更高。
▮▮▮▮⚝ 高效:时间复杂度O(NlogM),当 middle - first 远小于 last - first 时,性能优势明显。
▮▮▮▮⚝ 不稳定排序 (unstable sort):不保证相等元素的相对顺序在排序后保持不变 (在 [first, middle) 范围内)。
▮▮▮▮⚝ 需要随机访问迭代器:与 std::sort 一样,需要随机访问迭代器。

应用场景
▮▮▮▮⚝ 查找序列中最小或最大的前 K 个元素:例如,在一个海量数据集中查找最小的 10 个值。
▮▮▮▮⚝ Top-N 问题:例如,找出销售额排名前 5 的商品。
▮▮▮▮⚝ 部分排序后进行其他操作:例如,先找到最小的几个元素,再对这些元素进行进一步处理。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 7, 3, 6};
6// 部分排序,将最小的 4 个元素排序到前面
7std::partial_sort(numbers.begin(), numbers.begin() + 4, numbers.end());
8std::cout << "部分排序后的 numbers (前 4 个最小元素已排序): ";
9for (int n : numbers) std::cout << n << " "; // 输出:部分排序后的 numbers (前 4 个最小元素已排序): 1 2 3 4 9 8 7 5 6 (前 4 个元素 1 2 3 4 已排序,其余元素顺序未定)
10std::cout << std::endl;
11std::vector<int> numbers2 = {5, 2, 8, 1, 9, 4, 7, 3, 6};
12// 使用 Lambda 表达式自定义降序部分排序,将最大的 3 个元素排序到前面
13std::partial_sort(numbers2.begin(), numbers2.begin() + 3, numbers2.end(), [](int a, int b){
14return a > b; // 降序排列
15});
16std::cout << "降序部分排序后的 numbers2 (前 3 个最大元素已排序): ";
17for (int n : numbers2) std::cout << n << " "; // 输出:降序部分排序后的 numbers2 (前 3 个最大元素已排序): 9 8 7 1 2 4 5 3 6 (前 3 个元素 9 8 7 已降序排序,其余元素顺序未定)
18std::cout << std::endl;
19return 0;
20}

std::nth_element: 部分排序 (Partition)

功能:对指定范围 [first, last) 内的元素进行部分排序 (partition),将范围中n 个位置 (由 nth 迭代器指定) 的元素放到其正确排序后的位置,并保证该位置之前的元素都小于等于它之后的元素都大于等于它,但不保证其他元素的顺序。std::nth_element 的平均时间复杂度为O(N),是线性时间复杂度的算法。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class RandomAccessIterator>
2void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
3RandomAccessIterator last);
4template <class RandomAccessIterator, class Compare>
5void nth_element(RandomAccessIterator first, RandomAccessIterator nth,
6RandomAccessIterator last, Compare comp);

▮▮▮▮⚝ first, nth, last: 定义排序范围的随机访问迭代器 (random access iterators)nth 指向要放置在正确排序位置的元素。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,与 std::sort 类似。默认使用 operator< 进行比较 (升序排列)。
▮▮▮▮⚝ 返回值:voidstd::nth_element 直接在原序列上进行排序 (partition)。

特点
▮▮▮▮⚝ 线性时间复杂度O(N):在所有排序算法中,std::nth_element 的平均时间复杂度最低,性能极高。
▮▮▮▮⚝ 部分排序 (partition):只保证第 n 个位置的元素正确,以及其前后元素的相对大小关系,不保证全排序。
▮▮▮▮⚝ 不稳定排序 (unstable sort):不保证相等元素的相对顺序在排序后保持不变。
▮▮▮▮⚝ 需要随机访问迭代器:与 std::sort 一样,需要随机访问迭代器。

应用场景
▮▮▮▮⚝ 查找序列中第 K 小 (或第 K 大) 的元素:例如,查找中位数 (median),即第N/2小的元素。
▮▮▮▮⚝ 快速选择 (Quickselect) 算法的核心std::nth_element 是快速选择算法的基础,用于高效地找到序列中第n个顺序统计量。
▮▮▮▮⚝ 只需要部分排序,不需要全排序的场景:例如,只需要找到中位数,或只需要将序列划分为小于等于某个值和大于等于某个值的两部分。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric> // std::accumulate
5int main() {
6std::vector<int> numbers = {5, 2, 8, 1, 9, 4, 7, 3, 6};
7// 查找第 4 小的元素 (索引为 3,即 numbers[3])
8std::nth_element(numbers.begin(), numbers.begin() + 3, numbers.end());
9std::cout << "nth_element 后的 numbers (第 4 小元素已就位): ";
10for (int n : numbers) std::cout << n << " "; // 输出:nth_element 后的 numbers (第 4 小元素已就位): 3 2 1 4 9 8 7 5 6 (第 4 小元素 4 已就位,其前元素 <= 4, 其后元素 >= 4)
11std::cout << std::endl;
12std::cout << "第 4 小的元素: " << numbers[3] << std::endl; // 输出:第 4 小的元素: 4
13std::vector<int> numbers2 = {5, 2, 8, 1, 9, 4, 7, 3, 6};
14// 使用 Lambda 表达式自定义比较函数,查找第 3 大的元素 (索引为 2)
15std::nth_element(numbers2.begin(), numbers2.begin() + 2, numbers2.end(), [](int a, int b){
16return a > b; // 降序比较,相当于查找第 3 大
17});
18std::cout << "降序 nth_element 后的 numbers2 (第 3 大元素已就位): ";
19for (int n : numbers2) std::cout << n << " "; // 输出:降序 nth_element 后的 numbers2 (第 3 大元素已就位): 9 8 7 1 2 4 5 3 6 (第 3 大元素 7 已就位,其前元素 >= 7, 其后元素 <= 7)
20std::cout << std::endl;
21std::cout << "第 3 大的元素: " << numbers2[2] << std::endl; // 输出:第 3 大的元素: 7
22return 0;
23}

这些排序算法提供了丰富的排序功能,从全排序到部分排序,从稳定排序到线性时间复杂度的部分排序,可以满足各种不同的排序需求。选择合适的排序算法,可以提高代码的效率和性能。

3.2.4 搜索算法:binary_search, lower_bound, upper_bound, equal_range (Searching Algorithms)

搜索算法用于在序列中查找特定元素或元素范围。对于已排序序列,可以使用高效的二分搜索算法。以下是一些常用的搜索算法的详细讲解:

std::binary_search: 二分搜索 (Binary Search)

功能:在已排序的指定范围 [first, last)判断是否存在与给定值相等的元素。std::binary_search 使用二分搜索算法 (binary search algorithm) 实现,时间复杂度为O(logN),效率极高。
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class ForwardIterator, class T>
2bool binary_search(ForwardIterator first, ForwardIterator last, const T& value);
3template <class ForwardIterator, class T, class Compare>
4bool binary_search(ForwardIterator first, ForwardIterator last, const T& value, Compare comp);

▮▮▮▮⚝ first, last: 定义搜索范围的前向迭代器 (forward iterators)必须是已排序的范围
▮▮▮▮⚝ value: 要搜索的值。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,必须与排序时使用的比较规则一致。默认使用 operator< 进行比较 (升序排序)。
▮▮▮▮⚝ 返回值:bool 值。如果找到与 value 相等的元素,返回 true,否则返回 false

特点
▮▮▮▮⚝ 高效:时间复杂度O(logN),对于大型有序序列,搜索速度非常快。
▮▮▮▮⚝ 二分搜索:基于二分搜索算法,要求序列已排序
▮▮▮▮⚝ 只判断存在性:只返回 truefalse,不返回元素的位置。如果需要获取元素的位置,应使用 std::lower_bound, std::upper_boundstd::equal_range

应用场景
▮▮▮▮⚝ 快速判断元素是否存在于有序序列中:例如,在一个已排序的列表中检查某个值是否存在。
▮▮▮▮⚝ 作为其他算法的基础:例如,std::set, std::map 等关联容器的 count, find 等成员函数内部通常使用二分搜索。
▮▮▮▮⚝ 需要高效查找的场景,例如字典查找、电话簿查找等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 已排序
6// 搜索是否存在元素 5
7bool found1 = std::binary_search(numbers.begin(), numbers.end(), 5);
8std::cout << "元素 5 是否存在: " << std::boolalpha << found1 << std::endl; // 输出:元素 5 是否存在: true
9// 搜索是否存在元素 10
10bool found2 = std::binary_search(numbers.begin(), numbers.end(), 10);
11std::cout << "元素 10 是否存在: " << std::boolalpha << found2 << std::endl; // 输出:元素 10 是否存在: false
12std::vector<int> numbers2 = {9, 8, 7, 6, 5, 4, 3, 2, 1}; // 降序排序
13// 使用 Lambda 表达式自定义比较函数,搜索是否存在元素 5 (降序序列)
14bool found3 = std::binary_search(numbers2.begin(), numbers2.end(), 5, [](int a, int b){
15return a > b; // 降序比较
16});
17std::cout << "降序序列中元素 5 是否存在: " << std::boolalpha << found3 << std::endl; // 输出:降序序列中元素 5 是否存在: true
18return 0;
19}

std::lower_bound: 下界 (Lower Bound)

功能:在已排序的指定范围 [first, last)查找第一个不小于 (not less than) 给定值的元素的位置 (迭代器)。如果序列中存在与给定值相等的元素,lower_bound 返回指向第一个相等元素的迭代器;如果序列中不存在与给定值相等的元素,lower_bound 返回指向第一个大于给定值的元素的迭代器;如果给定值大于序列中的所有元素,lower_bound 返回 last 迭代器。std::lower_bound 也使用二分搜索算法 (binary search algorithm) 实现,时间复杂度为O(logN)
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class ForwardIterator, class T>
2ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& value);
3template <class ForwardIterator, class T, class Compare>
4ForwardIterator lower_bound(ForwardIterator first, ForwardIterator last, const T& value, Compare comp);

▮▮▮▮⚝ first, last: 定义搜索范围的前向迭代器 (forward iterators)必须是已排序的范围
▮▮▮▮⚝ value: 要搜索的值。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,必须与排序时使用的比较规则一致。默认使用 operator< 进行比较 (升序排序)。
▮▮▮▮⚝ 返回值:前向迭代器。返回指向第一个不小于 value 的元素的迭代器,或 last 迭代器 (如果 value 大于所有元素)。

特点
▮▮▮▮⚝ 高效:时间复杂度O(logN)
▮▮▮▮⚝ 二分搜索:基于二分搜索算法,要求序列已排序
▮▮▮▮⚝ 查找下界:返回第一个不小于给定值的元素的位置。
▮▮▮▮⚝ 可用于插入位置:如果要在有序序列中插入元素并保持序列有序,lower_bound 返回的迭代器位置就是合适的插入位置。

应用场景
▮▮▮▮⚝ 查找有序序列中第一个不小于给定值的元素
▮▮▮▮⚝ 确定元素在有序序列中的插入位置
▮▮▮▮⚝ 范围查询 (配合 std::upper_bound 可以确定元素在有序序列中的范围)。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9}; // 已排序,包含重复元素 5
6// 查找值 5 的下界
7auto it1 = std::lower_bound(numbers.begin(), numbers.end(), 5);
8std::cout << "值 5 的下界位置: " << std::distance(numbers.begin(), it1) << ", 元素值: " << *it1 << std::endl; // 输出:值 5 的下界位置: 4, 元素值: 5 (指向第一个 5)
9// 查找值 5.5 的下界 (序列中不存在 5.5)
10auto it2 = std::lower_bound(numbers.begin(), numbers.end(), 5.5);
11std::cout << "值 5.5 的下界位置: " << std::distance(numbers.begin(), it2) << ", 元素值: " << *it2 << std::endl; // 输出:值 5.5 的下界位置: 7, 元素值: 6 (指向第一个大于 5.5 的元素 6)
12// 查找值 10 的下界 (大于所有元素)
13auto it3 = std::lower_bound(numbers.begin(), numbers.end(), 10);
14if (it3 == numbers.end()) {
15std::cout << "值 10 的下界位置: last (大于所有元素)" << std::endl; // 输出:值 10 的下界位置: last (大于所有元素)
16}
17return 0;
18}

std::upper_bound: 上界 (Upper Bound)

功能:在已排序的指定范围 [first, last)查找第一个大于 (greater than) 给定值的元素的位置 (迭代器)。std::upper_bound 也使用二分搜索算法 (binary search algorithm) 实现,时间复杂度为O(logN)
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class ForwardIterator, class T>
2ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& value);
3template <class ForwardIterator, class T, class Compare>
4ForwardIterator upper_bound(ForwardIterator first, ForwardIterator last, const T& value, Compare comp);

▮▮▮▮⚝ first, last: 定义搜索范围的前向迭代器 (forward iterators)必须是已排序的范围
▮▮▮▮⚝ value: 要搜索的值。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,必须与排序时使用的比较规则一致。默认使用 operator< 进行比较 (升序排序)。
▮▮▮▮⚝ 返回值:前向迭代器。返回指向第一个大于 value 的元素的迭代器,或 last 迭代器 (如果 value 大于等于所有元素)。

特点
▮▮▮▮⚝ 高效:时间复杂度O(logN)
▮▮▮▮⚝ 二分搜索:基于二分搜索算法,要求序列已排序
▮▮▮▮⚝ 查找上界:返回第一个大于给定值的元素的位置。
▮▮▮▮⚝ lower_bound 配合使用lower_boundupper_bound 通常一起使用,用于确定元素在有序序列中的范围。

应用场景
▮▮▮▮⚝ 查找有序序列中第一个大于给定值的元素
▮▮▮▮⚝ 范围查询 (配合 std::lower_bound 可以确定元素在有序序列中的范围)。
▮▮▮▮⚝ 统计有序序列中小于等于或大于某个值的元素个数

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9}; // 已排序,包含重复元素 5
6// 查找值 5 的上界
7auto it1 = std::upper_bound(numbers.begin(), numbers.end(), 5);
8std::cout << "值 5 的上界位置: " << std::distance(numbers.begin(), it1) << ", 元素值: " << *it1 << std::endl; // 输出:值 5 的上界位置: 7, 元素值: 6 (指向第一个大于 5 的元素 6)
9// 查找值 4.5 的上界 (序列中不存在 4.5)
10auto it2 = std::upper_bound(numbers.begin(), numbers.end(), 4.5);
11std::cout << "值 4.5 的上界位置: " << std::distance(numbers.begin(), it2) << ", 元素值: " << *it2 << std::endl; // 输出:值 4.5 的上界位置: 4, 元素值: 5 (指向第一个大于 4.5 的元素 5)
12// 查找值 9 的上界 (大于等于所有元素)
13auto it3 = std::upper_bound(numbers.begin(), numbers.end(), 9);
14if (it3 == numbers.end()) {
15std::cout << "值 9 的上界位置: last (大于等于所有元素)" << std::endl; // 输出:值 9 的上界位置: last (大于等于所有元素)
16}
17return 0;
18}

std::equal_range: 相等范围 (Equal Range)

功能:在已排序的指定范围 [first, last)查找与给定值相等的元素的范围std::equal_range 返回一个 std::pair,其 first 成员是 std::lower_bound 返回的迭代器 (下界),second 成员是 std::upper_bound 返回的迭代器 (上界)。如果序列中不存在与给定值相等的元素,则返回的 pair 的两个迭代器都指向第一个大于给定值的元素的位置 (或 last 迭代器)。std::equal_range 也使用二分搜索算法 (binary search algorithm) 实现,时间复杂度为O(logN)
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class ForwardIterator, class T>
2std::pair<ForwardIterator, ForwardIterator>
3equal_range(ForwardIterator first, ForwardIterator last, const T& value);
4template <class ForwardIterator, class T, class Compare>
5std::pair<ForwardIterator, ForwardIterator>
6equal_range(ForwardIterator first, ForwardIterator last, const T& value, Compare comp);

▮▮▮▮⚝ first, last: 定义搜索范围的前向迭代器 (forward iterators)必须是已排序的范围
▮▮▮▮⚝ value: 要搜索的值。
▮▮▮▮⚝ comp (可选):比较函数 (comparison function)函数对象 (function object),用于自定义排序规则,必须与排序时使用的比较规则一致。默认使用 operator< 进行比较 (升序排序)。
▮▮▮▮⚝ 返回值:std::pair<ForwardIterator, ForwardIterator>pair.first 是下界迭代器 (lower_bound),pair.second 是上界迭代器 (upper_bound)。

特点
▮▮▮▮⚝ 高效:时间复杂度O(logN)
▮▮▮▮⚝ 二分搜索:基于二分搜索算法,要求序列已排序
▮▮▮▮⚝ 返回相等范围:一次调用同时返回下界和上界,方便确定元素在有序序列中的完整范围。
▮▮▮▮⚝ 方便统计相等元素个数:通过计算 upper_bound - lower_bound 的距离,可以快速统计有序序列中与给定值相等的元素个数。

应用场景
▮▮▮▮⚝ 查找有序序列中与给定值相等的元素的范围
▮▮▮▮⚝ 统计有序序列中与给定值相等的元素个数
▮▮▮▮⚝ 范围查询

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9}; // 已排序,包含重复元素 5
6// 查找值 5 的相等范围
7auto range1 = std::equal_range(numbers.begin(), numbers.end(), 5);
8std::cout << "值 5 的相等范围: [" << std::distance(numbers.begin(), range1.first) << ", " << std::distance(numbers.begin(), range1.second) << ")" << std::endl; // 输出:值 5 的相等范围: [4, 7)
9std::cout << "相等范围内的元素: ";
10for (auto it = range1.first; it != range1.second; ++it) std::cout << *it << " "; // 输出:相等范围内的元素: 5 5 5
11std::cout << std::endl;
12std::cout << "值 5 的个数: " << std::distance(range1.first, range1.second) << std::endl; // 输出:值 5 的个数: 3
13// 查找值 10 的相等范围 (序列中不存在 10)
14auto range2 = std::equal_range(numbers.begin(), numbers.end(), 10);
15std::cout << "值 10 的相等范围: [" << std::distance(numbers.begin(), range2.first) << ", " << std::distance(numbers.begin(), range2.second) << ")" << std::endl; // 输出:值 10 的相等范围: [11, 11) (空范围,上下界相同)
16std::cout << "相等范围是否为空: " << std::boolalpha << (range2.first == range2.second) << std::endl; // 输出:相等范围是否为空: true
17return 0;
18}

这些二分搜索算法提供了在有序序列中高效查找元素和范围的功能。std::binary_search 用于判断元素是否存在,std::lower_boundstd::upper_bound 用于查找范围边界,std::equal_range 用于查找相等元素的完整范围。合理选择和使用这些搜索算法,可以显著提高代码的搜索效率。

3.2.5 数值算法:accumulate, inner_product, adjacent_difference, partial_sum (Numerical Algorithms)

数值算法主要用于执行数值计算操作。以下是一些常用的数值算法的详细讲解:

std::accumulate: 累加 (Accumulation)

功能:对指定范围 [first, last) 内的元素进行累加 (accumulation) 操作,计算总和。可以指定初始值自定义的累加操作
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class InputIterator, class T>
2T accumulate(InputIterator first, InputIterator last, T init);
3template <class InputIterator, class T, class BinaryOperation>
4T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation op);

▮▮▮▮⚝ first, last: 定义累加范围的输入迭代器 (input iterators)
▮▮▮▮⚝ init: 初始值。累加的起始值。
▮▮▮▮⚝ op (可选):二元操作 (binary operation),函数对象、函数指针或 Lambda 表达式,用于自定义累加操作。它接受当前累加值序列中的元素作为参数,返回新的累加值。默认使用 operator+ 进行累加 (求和)。
▮▮▮▮⚝ 返回值:累加结果,类型与初始值 init 相同。

特点
▮▮▮▮⚝ 通用累加:不仅可以求和,还可以通过自定义二元操作实现其他累加操作,例如乘积、最大值、最小值等。
▮▮▮▮⚝ 可指定初始值:可以灵活设置累加的起始值。
▮▮▮▮⚝ 可自定义操作:通过函数对象或 Lambda 表达式自定义累加操作,扩展算法的功能。

应用场景
▮▮▮▮⚝ 计算容器元素的总和
▮▮▮▮⚝ 计算容器元素的乘积
▮▮▮▮⚝ 查找容器元素的最大值或最小值 (通过自定义比较操作)。
▮▮▮▮⚝ 自定义累加逻辑,例如字符串拼接、集合合并等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <numeric> // std::accumulate
4#include <string> // std::string
5#include <functional> // std::multiplies
6int main() {
7std::vector<int> numbers = {1, 2, 3, 4, 5};
8std::vector<std::string> strings = {"Hello", ", ", "C++", " ", "World!"};
9// 计算 numbers 的元素总和 (默认求和)
10int sum = std::accumulate(numbers.begin(), numbers.end(), 0); // 初始值 0
11std::cout << "元素总和: " << sum << std::endl; // 输出:元素总和: 15
12// 计算 numbers 的元素乘积 (自定义乘法操作)
13int product = std::accumulate(numbers.begin(), numbers.end(), 1, std::multiplies<int>()); // 初始值 1,使用 std::multiplies<int>() 函数对象进行乘法
14std::cout << "元素乘积: " << product << std::endl; // 输出:元素乘积: 120
15// 连接 strings 中的字符串 (自定义字符串拼接操作)
16std::string concatenated_string = std::accumulate(strings.begin(), strings.end(), std::string("")); // 初始值空字符串
17std::cout << "字符串拼接结果: " << concatenated_string << std::endl; // 输出:字符串拼接结果: Hello, C++ World!
18return 0;
19}

std::inner_product: 内积 (Inner Product)

功能:计算两个指定范围 [first1, last1)[first2, last2)内积 (inner product),也称为点积 (dot product)。内积是将两个序列对应位置的元素相乘,然后将所有乘积求和。可以指定初始值自定义的乘法和加法操作
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class InputIterator1, class InputIterator2, class T>
2T inner_product(InputIterator1 first1, InputIterator1 last1,
3InputIterator2 first2, T init);
4template <class InputIterator1, class InputIterator2, class T,
5class BinaryOperation1, class BinaryOperation2>
6T inner_product(InputIterator1 first1, InputIterator1 last1,
7InputIterator2 first2, T init, BinaryOperation1 op1,
8BinaryOperation2 op2);

▮▮▮▮⚝ first1, last1: 定义第一个输入范围输入迭代器 (input iterators)
▮▮▮▮⚝ first2: 定义第二个输入范围起始位置的输入迭代器 (input iterator)。第二个输入范围至少要和第一个输入范围一样大。
▮▮▮▮⚝ init: 初始值。累加的起始值。
▮▮▮▮⚝ op1 (可选):二元操作 (binary operation),用于自定义乘法操作。默认使用 operator* 进行乘法。
▮▮▮▮⚝ op2 (可选):二元操作 (binary operation),用于自定义加法操作。默认使用 operator+ 进行加法 (求和)。
▮▮▮▮⚝ 返回值:内积结果,类型与初始值 init 相同。

特点
▮▮▮▮⚝ 计算内积:实现数学上的内积运算,广泛应用于线性代数、向量计算等领域。
▮▮▮▮⚝ 可自定义乘法和加法操作:扩展算法的功能,可以计算更广义的“内积”。
▮▮▮▮⚝ 需要两个输入范围:计算两个序列的对应元素操作。

应用场景
▮▮▮▮⚝ 向量点积计算
▮▮▮▮⚝ 信号处理图像处理等领域中的卷积运算 (可以看作广义的内积)。
▮▮▮▮⚝ 相似度计算 (例如,计算两个向量的余弦相似度)。
▮▮▮▮⚝ 自定义组合操作,例如计算加权和、多项式求值等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <numeric> // std::inner_product
4#include <functional> // std::plus, std::minus
5int main() {
6std::vector<int> vector1 = {1, 2, 3, 4, 5};
7std::vector<int> vector2 = {10, 20, 30, 40, 50};
8// 计算 vector1 和 vector2 的内积 (默认乘法和加法)
9int inner_product1 = std::inner_product(vector1.begin(), vector1.end(), vector2.begin(), 0); // 初始值 0
10std::cout << "内积结果 (默认乘法和加法): " << inner_product1 << std::endl; // 输出:内积结果 (默认乘法和加法): 550 (1*10 + 2*20 + 3*30 + 4*40 + 5*50 = 550)
11// 自定义乘法为减法,加法为加法 (计算 (v1[i]-v2[i]) 的和)
12int inner_product2 = std::inner_product(vector1.begin(), vector1.end(), vector2.begin(), 0, std::plus<int>(), std::minus<int>()); // 初始值 0, 加法 std::plus<int>(), 减法 std::minus<int>()
13std::cout << "内积结果 (自定义减法和加法): " << inner_product2 << std::endl; // 输出:内积结果 (自定义减法和加法): -135 ((1-10) + (2-20) + (3-30) + (4-40) + (5-50) = -135)
14return 0;
15}

std::adjacent_difference: 邻近差分 (Adjacent Difference)

功能:计算指定范围 [first, last) 内的邻近元素之间的差分 (adjacent difference),并将结果存储到目标范围 [result, result + (last - first)) 中。差分是指当前元素与前一个元素的差值。对于第一个元素,差分值通常为其本身 (或与初始值进行操作,如果指定了自定义操作)。可以指定自定义的差分操作
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class InputIterator, class OutputIterator>
2OutputIterator adjacent_difference(InputIterator first, InputIterator last,
3OutputIterator result);
4template <class InputIterator, class OutputIterator, class BinaryOperation>
5OutputIterator adjacent_difference(InputIterator first, InputIterator last,
6OutputIterator result, BinaryOperation op);

▮▮▮▮⚝ first, last: 定义输入范围输入迭代器 (input iterators)
▮▮▮▮⚝ result: 指向目标范围起始位置输出迭代器 (output iterator)。目标范围必须有足够的空间容纳差分结果。
▮▮▮▮⚝ op (可选):二元操作 (binary operation),用于自定义差分操作。默认使用 operator- 进行差分 (减法)。
▮▮▮▮⚝ 返回值:指向目标范围末尾输出迭代器,即 result + (last - first)

特点
▮▮▮▮⚝ 计算邻近差分:将序列转换为差分序列,可以用于数据压缩、趋势分析等。
▮▮▮▮⚝ 可自定义差分操作:扩展算法的功能,可以计算更广义的“差分”,例如比值、异或等。
▮▮▮▮⚝ 生成新的序列:差分结果存储到目标范围,生成新的序列。

应用场景
▮▮▮▮⚝ 数据压缩 (例如,差分编码)。
▮▮▮▮⚝ 时间序列分析趋势分析 (差分可以突出数据的变化趋势)。
▮▮▮▮⚝ 图像处理 (例如,边缘检测)。
▮▮▮▮⚝ 自定义差分逻辑,例如计算邻近元素的比值、异或值等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <numeric> // std::adjacent_difference
4#include <iterator> // std::back_inserter
5#include <functional> // std::divides
6int main() {
7std::vector<int> numbers = {1, 3, 6, 10, 15}; // 递增序列
8std::vector<int> diff_result1;
9std::vector<double> diff_result2;
10// 计算 numbers 的邻近差分 (默认减法)
11std::adjacent_difference(numbers.begin(), numbers.end(), std::back_inserter(diff_result1));
12std::cout << "邻近差分结果 (默认减法): ";
13for (int n : diff_result1) std::cout << n << " "; // 输出:邻近差分结果 (默认减法): 1 2 3 4 5 (1, 3-1=2, 6-3=3, 10-6=4, 15-10=5)
14std::cout << std::endl;
15std::vector<int> numbers2 = {10, 20, 40, 80, 160}; // 倍增序列
16// 计算 numbers2 的邻近比值 (自定义除法操作)
17std::adjacent_difference(numbers2.begin(), numbers2.end(), std::back_inserter(diff_result2), std::divides<double>());
18std::cout << "邻近比值结果 (自定义除法): ";
19for (double d : diff_result2) std::cout << d << " "; // 输出:邻近比值结果 (自定义除法): 10 2 2 2 2 (10, 20/10=2, 40/20=2, 80/40=2, 160/80=2)
20std::cout << std::endl;
21return 0;
22}

std::partial_sum: 部分和 (Partial Sum)

功能:计算指定范围 [first, last) 内的部分和 (partial sum),并将结果存储到目标范围 [result, result + (last - first)) 中。部分和是指序列中从第一个元素到当前元素的累加和。对于第一个元素,部分和就是其本身。可以指定自定义的加法操作
用法

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class InputIterator, class OutputIterator>
2OutputIterator partial_sum(InputIterator first, InputIterator last,
3OutputIterator result);
4template <class InputIterator, class OutputIterator, class BinaryOperation>
5OutputIterator partial_sum(InputIterator first, InputIterator last,
6OutputIterator result, BinaryOperation op);

▮▮▮▮⚝ first, last: 定义输入范围输入迭代器 (input iterators)
▮▮▮▮⚝ result: 指向目标范围起始位置输出迭代器 (output iterator)。目标范围必须有足够的空间容纳部分和结果。
▮▮▮▮⚝ op (可选):二元操作 (binary operation),用于自定义加法操作。默认使用 operator+ 进行加法 (求和)。
▮▮▮▮⚝ 返回值:指向目标范围末尾输出迭代器,即 result + (last - first)

特点
▮▮▮▮⚝ 计算部分和:将序列转换为部分和序列,可以用于前缀和计算、积分运算等。
▮▮▮▮⚝ 可自定义加法操作:扩展算法的功能,可以计算更广义的“部分和”,例如部分乘积、部分最大值等。
▮▮▮▮⚝ 生成新的序列:部分和结果存储到目标范围,生成新的序列。

应用场景
▮▮▮▮⚝ 前缀和计算:快速计算序列的前缀和,用于范围求和查询等。
▮▮▮▮⚝ 离散积分 (例如,数值积分)。
▮▮▮▮⚝ 在线算法流式数据处理 (部分和可以逐步累积计算结果)。
▮▮▮▮⚝ 自定义累加逻辑,例如计算部分乘积、部分最大值等。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <numeric> // std::partial_sum
4#include <iterator> // std::back_inserter
5#include <functional> // std::multiplies
6int main() {
7std::vector<int> numbers = {1, 2, 3, 4, 5};
8std::vector<int> partial_sum_result1;
9std::vector<int> partial_product_result;
10// 计算 numbers 的部分和 (默认加法)
11std::partial_sum(numbers.begin(), numbers.end(), std::back_inserter(partial_sum_result1));
12std::cout << "部分和结果 (默认加法): ";
13for (int n : partial_sum_result1) std::cout << n << " "; // 输出:部分和结果 (默认加法): 1 3 6 10 15 (1, 1+2=3, 1+2+3=6, 1+2+3+4=10, 1+2+3+4+5=15)
14std::cout << std::endl;
15// 计算 numbers 的部分乘积 (自定义乘法操作)
16std::partial_sum(numbers.begin(), numbers.end(), std::back_inserter(partial_product_result), std::multiplies<int>());
17std::cout << "部分乘积结果 (自定义乘法): ";
18for (int n : partial_product_result) std::cout << n << " "; // 输出:部分乘积结果 (自定义乘法): 1 2 6 24 120 (1, 1*2=2, 1*2*3=6, 1*2*3*4=24, 1*2*3*4*5=120)
19std::cout << std::endl;
20return 0;
21}

这些数值算法提供了丰富的数值计算功能,从简单的累加、内积到复杂的差分、部分和,可以满足各种不同的数值计算需求。掌握它们可以帮助我们高效地处理各种数值计算任务,并扩展算法的应用范围。

3.3 算法的自定义与扩展 (Algorithm Customization and Extension)

C++ 标准库算法的强大之处不仅在于其丰富的功能,还在于其高度的可定制性和可扩展性。通过函数对象 (function objects)Lambda 表达式 (lambda expressions) 等机制,我们可以灵活地定制算法的行为,使其满足特定的需求。此外,我们还可以根据需要自定义算法,扩展标准库的功能。

3.3.1 函数对象 (Functors) 在算法中的应用 (Application of Function Objects in Algorithms)

函数对象 (function object),也称为 Functor,是一个行为类似函数的对象,它可以像函数一样被调用,但本质上是一个类 (class) 的对象。函数对象通过重载函数调用运算符 operator() 来实现类似函数调用的行为。

函数对象作为算法的谓词 (Predicate)

很多标准库算法接受一个谓词 (predicate) 参数,用于指定算法的判断条件。谓词是一个返回 bool 值的函数或函数对象。函数对象可以作为谓词传递给算法,实现自定义的判断逻辑。

示例std::find_if, std::count_if, std::remove_if, std::replace_if, std::sort (自定义比较函数) 等算法都接受谓词参数。

优势
▮▮▮▮⚝ 状态保持 (Stateful):函数对象可以存储状态,例如计数器、累加器等。这使得函数对象可以实现比普通函数更复杂的操作,例如在多次调用之间保持状态。
▮▮▮▮⚝ 类型安全 (Type-safe):函数对象是对象,可以进行类型检查,避免类型错误。
▮▮▮▮⚝ 可以内联 (Inline):编译器更容易对函数对象进行内联优化,提高性能。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4// 自定义函数对象,判断是否为偶数
5struct IsEven {
6bool operator()(int n) const {
7return n % 2 == 0;
8}
9};
10// 自定义函数对象,判断是否大于阈值
11struct IsGreaterThan {
12int threshold;
13IsGreaterThan(int thresh) : threshold(thresh) {}
14bool operator()(int n) const {
15return n > threshold;
16}
17};
18int main() {
19std::vector<int> numbers = {10, 5, 20, 15, 25, 30};
20// 使用 IsEven 函数对象查找第一个偶数
21auto it1 = std::find_if(numbers.begin(), numbers.end(), IsEven());
22if (it1 != numbers.end()) {
23std::cout << "第一个偶数: " << *it1 << std::endl; // 输出:第一个偶数: 10
24}
25// 使用 IsGreaterThan 函数对象统计大于 15 的元素个数
26int count = std::count_if(numbers.begin(), numbers.end(), IsGreaterThan(15));
27std::cout << "大于 15 的元素个数: " << count << std::endl; // 输出:大于 15 的元素个数: 3
28return 0;
29}

函数对象作为算法的操作 (Operation)

一些标准库算法接受一个操作 (operation) 参数,用于指定算法要执行的具体操作。操作可以是一个函数或函数对象,它接受一个或多个元素作为参数,并执行相应的操作。

示例std::for_each, std::transform, std::accumulate, std::inner_product, std::adjacent_difference, std::partial_sum 等算法都接受操作参数。

优势
▮▮▮▮⚝ 灵活性 (Flexibility):函数对象可以实现各种复杂的操作逻辑,满足不同的算法需求。
▮▮▮▮⚝ 代码复用 (Code reuse):可以定义通用的函数对象,并在多个算法中复用。
▮▮▮▮⚝ 可组合性 (Composability):可以将多个函数对象组合起来,实现更复杂的操作流程。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric> // std::transform, std::accumulate
5#include <functional> // std::plus, std::multiplies
6// 自定义函数对象,将元素乘以系数
7struct MultiplyByFactor {
8int factor;
9MultiplyByFactor(int f) : factor(f) {}
10int operator()(int n) const {
11return n * factor;
12}
13};
14// 自定义函数对象,计算两个数的和的平方
15struct SquareOfSum {
16int operator()(int a, int b) const {
17int sum = a + b;
18return sum * sum;
19}
20};
21int main() {
22std::vector<int> numbers1 = {1, 2, 3, 4, 5};
23std::vector<int> numbers2 = {10, 20, 30, 40, 50};
24std::vector<int> result1;
25std::vector<int> result2;
26// 使用 MultiplyByFactor 函数对象将 numbers1 的每个元素乘以 2
27std::transform(numbers1.begin(), numbers1.end(), std::back_inserter(result1), MultiplyByFactor(2));
28std::cout << "乘以 2 后的结果: ";
29for (int n : result1) std::cout << n << " "; // 输出:乘以 2 后的结果: 2 4 6 8 10
30std::cout << std::endl;
31// 使用 SquareOfSum 函数对象计算 numbers1 和 numbers2 对应元素和的平方,并累加求和
32int sum_of_squares = std::inner_product(numbers1.begin(), numbers1.end(), numbers2.begin(), 0, std::plus<int>(), SquareOfSum());
33std::cout << "元素和的平方的累加和: " << sum_of_squares << std::endl; // 输出:元素和的平方的累加和: 15525 ((1+10)^2 + (2+20)^2 + (3+30)^2 + (4+40)^2 + (5+50)^2 = 15525)
34return 0;
35}

函数对象在标准库算法中扮演着重要的角色,它们提供了强大的定制能力,使得算法可以适应各种不同的应用场景。通过合理地设计和使用函数对象,可以编写出更灵活、更高效的 C++ 代码。

3.3.2 Lambda 表达式 (Lambda Expressions) 的简洁性与实用性 (Simplicity and Practicality of Lambda Expressions)

Lambda 表达式 (lambda expression) 是 C++11 引入的一种简洁的定义匿名函数对象的方式。Lambda 表达式可以直接在算法调用处定义函数对象,无需像传统函数对象那样需要先定义类或结构体,再创建对象,大大简化了代码,提高了可读性。

Lambda 表达式的语法

Lambda 表达式的基本语法形式如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1[capture list](parameter list) -> return type { function body }

[capture list] (捕获列表):用于捕获 Lambda 表达式所在作用域中的变量,供 Lambda 表达式的函数体使用。可以捕获值、引用或隐式捕获。
(parameter list) (参数列表):与普通函数的参数列表类似,定义 Lambda 表达式接受的参数。可以省略,如果不需要参数。
-> return type (返回类型)显式指定 Lambda 表达式的返回类型。可以省略,编译器可以进行返回类型推导 (return type deduction),尤其是在函数体只有一个 return 语句的情况下。
{ function body } (函数体):Lambda 表达式的具体实现代码,与普通函数的函数体类似。

Lambda 表达式在算法中的应用

Lambda 表达式可以像函数对象一样,作为谓词或操作传递给标准库算法,实现算法的定制。由于 Lambda 表达式的简洁性,在算法中使用 Lambda 表达式可以使代码更加清晰、易懂。

示例:几乎所有接受函数对象作为参数的标准库算法都可以使用 Lambda 表达式。例如,std::find_if, std::count_if, std::transform, std::sort, std::accumulate 等。

优势
▮▮▮▮⚝ 简洁性 (Conciseness):Lambda 表达式语法简洁,可以直接在算法调用处定义匿名函数对象,无需额外定义类或结构体。
▮▮▮▮⚝ 可读性 (Readability):Lambda 表达式将函数对象的定义和使用放在一起,使代码更易于理解和维护。
▮▮▮▮⚝ 内联友好 (Inline-friendly):编译器更容易对 Lambda 表达式进行内联优化,提高性能。
▮▮▮▮⚝ 捕获上下文 (Capture context):Lambda 表达式可以方便地捕获所在作用域中的变量,访问外部数据。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <numeric> // std::transform, std::accumulate
5#include <string> // std::string
6int main() {
7std::vector<int> numbers = {10, 5, 20, 15, 25, 30};
8std::vector<int> result;
9std::vector<std::string> strings = {"apple", "banana", "cherry"};
10// 使用 Lambda 表达式查找第一个大于 15 的元素
11auto it1 = std::find_if(numbers.begin(), numbers.end(), [](int n){
12return n > 15; // Lambda 表达式作为谓词
13});
14if (it1 != numbers.end()) {
15std::cout << "第一个大于 15 的元素: " << *it1 << std::endl; // 输出:第一个大于 15 的元素: 20
16}
17// 使用 Lambda 表达式将 numbers 的每个元素乘以 3,并存储到 result
18std::transform(numbers.begin(), numbers.end(), std::back_inserter(result), [](int n){
19return n * 3; // Lambda 表达式作为一元操作
20});
21std::cout << "乘以 3 后的结果: ";
22for (int n : result) std::cout << n << " "; // 输出:乘以 3 后的结果: 30 15 60 45 75 90
23std::cout << std::endl;
24// 使用 Lambda 表达式将 strings 中的字符串拼接成一个字符串,并统计总长度 (使用 accumulate)
25std::string concatenated_string = std::accumulate(strings.begin(), strings.end(), std::string(""), [](std::string acc, const std::string& s){
26return acc + s; // Lambda 表达式作为二元操作
27});
28int total_length = concatenated_string.length();
29std::cout << "拼接后的字符串: " << concatenated_string << ", 总长度: " << total_length << std::endl; // 输出:拼接后的字符串: applebananacherry, 总长度: 17
30return 0;
31}

Lambda 表达式的引入极大地简化了 C++ 中函数对象的使用,提高了代码的简洁性和可读性。在标准库算法中,Lambda 表达式已经成为定制算法行为的首选方式。

3.3.3 自定义算法的实现思路 (Implementation Ideas for Custom Algorithms)

除了使用标准库提供的算法外,有时我们需要根据特定的需求自定义算法,扩展标准库的功能。自定义算法可以更好地解决特定问题,提高代码的效率和可维护性。

自定义算法的设计原则

泛型性 (Genericity):尽可能地使自定义算法具有泛型性,使其可以应用于不同类型的数据和容器。可以使用模板 (templates)迭代器 (iterators) 来实现泛型性。
效率 (Efficiency):考虑算法的时间复杂度和空间复杂度,选择合适的算法策略和数据结构,提高算法的执行效率。
可读性 (Readability):编写清晰、易懂的代码,添加必要的注释,提高算法的可读性和可维护性。
可测试性 (Testability):编写单元测试用例,验证算法的正确性和鲁棒性。
与标准库算法风格一致 (Consistency with standard library algorithms):尽量遵循标准库算法的命名约定、接口设计和错误处理方式,使自定义算法更容易被理解和使用。

自定义算法的实现步骤

  1. 明确算法的功能和需求 (Define the algorithm's functionality and requirements):确定算法要解决的问题、输入输出、约束条件、性能要求等。
  2. 选择合适的算法策略和数据结构 (Choose appropriate algorithm strategy and data structure):根据需求选择合适的算法思想 (例如,分治、动态规划、贪心等) 和数据结构 (例如,数组、链表、树、哈希表等)。
  3. 设计算法接口 (Design algorithm interface):确定算法的函数签名,包括参数类型、返回值类型、迭代器类型、函数对象类型等。尽量与标准库算法的接口风格保持一致。
  4. 实现算法代码 (Implement algorithm code):根据算法策略编写算法的具体实现代码。注意代码的正确性、效率、可读性和可维护性。
  5. 编写单元测试 (Write unit tests):编写充分的单元测试用例,覆盖各种输入情况、边界条件和异常情况,验证算法的正确性和鲁棒性。
  6. 性能测试和优化 (Performance testing and optimization):进行性能测试,评估算法的性能是否满足需求。如果性能不足,进行代码优化或算法改进。
  7. 文档编写 (Write documentation):编写算法的文档,包括功能描述、用法示例、参数说明、返回值说明、时间复杂度、空间复杂度等,方便其他开发者使用和理解。

自定义算法示例:计算容器元素平均值

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <numeric> // std::accumulate
4#include <stdexcept> // std::domain_error
5// 自定义算法:计算容器元素的平均值 (泛型版本)
6template <typename InputIterator, typename SumType>
7SumType average(InputIterator first, InputIterator last) {
8// 使用类型推导,避免显式指定类型
9using DifferenceType = typename std::iterator_traits<InputIterator>::difference_type;
10DifferenceType count = std::distance(first, last);
11if (count == 0) {
12throw std::domain_error("Cannot calculate average of an empty range"); // 抛出异常处理空范围
13}
14SumType sum = std::accumulate(first, last, static_cast<SumType>(0)); // 累加求和,初始值 0 转换为 SumType 类型
15return sum / static_cast<SumType>(count); // 计算平均值,count 转换为 SumType 类型,避免类型不匹配
16}
17int main() {
18std::vector<int> numbers = {1, 2, 3, 4, 5};
19std::vector<double> empty_numbers;
20// 计算 int 向量的平均值 (double 类型结果)
21double avg1 = average<std::vector<int>::iterator, double>(numbers.begin(), numbers.end()); // 显式指定 SumType 为 double
22std::cout << "平均值 (int 向量, double 结果): " << avg1 << std::endl; // 输出:平均值 (int 向量, double 结果): 3
23// 计算 double 向量的平均值 (double 类型结果,类型推导)
24std::vector<double> double_numbers = {1.5, 2.5, 3.5, 4.5, 5.5};
25double avg2 = average<std::vector<double>::iterator, double>(double_numbers.begin(), double_numbers.end()); // 类型推导 SumType 为 double
26std::cout << "平均值 (double 向量, double 结果): " << avg2 << std::endl; // 输出:平均值 (double 向量, double 结果): 3.5
27// 计算空向量的平均值 (会抛出异常)
28try {
29double avg3 = average<std::vector<double>::iterator, double>(empty_numbers.begin(), empty_numbers.end());
30std::cout << "平均值 (空向量): " << avg3 << std::endl; // 不会执行到这里
31} catch (const std::domain_error& e) {
32std::cerr << "异常: " << e.what() << std::endl; // 输出:异常: Cannot calculate average of an empty range
33}
34return 0;
35}

自定义算法是 C++ 标准库算法的重要补充,它可以帮助我们解决更复杂、更专业的问题。通过遵循良好的设计原则和实现步骤,可以开发出高质量、可复用的自定义算法,扩展 C++ 标准库的功能,提升软件开发的效率和质量。

<END_OF_CHAPTER/>

4. 迭代器 (Iterators)

本章深入剖析 C++ 标准库迭代器 (iterator) 的概念、分类和使用方法,讲解不同迭代器类型的特性和应用场景,以及如何自定义迭代器。

4.1 迭代器概念与分类 (Iterator Concepts and Classification)

本节介绍迭代器的基本概念、作用,以及五种迭代器类别:输入迭代器 (input iterator)、输出迭代器 (output iterator)、前向迭代器 (forward iterator)、双向迭代器 (bidirectional iterator)、随机访问迭代器 (random access iterator)。

4.1.1 迭代器的基本概念:抽象指针 (Basic Concepts of Iterators: Abstract Pointers)

迭代器在 C++ 标准库中扮演着至关重要的角色,它们是连接容器 (containers) 和算法 (algorithms) 的桥梁 🌉。可以将迭代器视为抽象指针 (abstract pointers),但远比普通的指针更加通用和强大。

迭代器的作用

迭代器的主要作用是提供一种统一的方式来遍历 (traverse) 容器中的元素,而无需了解容器底层的具体实现细节。这意味着,无论你使用的是 vector (向量)、list (列表) 还是 map (映射) 等不同类型的容器,都可以使用迭代器以相似的方式访问容器内的元素。

迭代器的通用操作

尽管迭代器有不同的类别,但它们都支持一些通用的操作,使得算法可以以统一的方式处理不同类型的迭代器。这些通用操作包括:

解引用 (Dereference) *iter: 访问迭代器 iter 当前指向的元素的值。类似于指针的解引用操作。例如,如果 iter 指向容器中的一个整数元素,*iter 将返回该整数值。

自增 (Increment) ++iteriter++: 将迭代器 iter 移动到容器中的下一个元素。对于大多数迭代器,前缀自增 ++iter 通常比后缀自增 iter++ 效率更高,因为前缀自增返回的是迭代器自增后的引用,而后缀自增需要返回自增前的副本。

赋值 (Assignment) iter1 = iter2: 将一个迭代器 iter2 赋值给另一个迭代器 iter1,使得 iter1 指向 iter2 当前所指向的元素。

比较 (Comparison) iter1 == iter2iter1 != iter2: 比较两个迭代器 iter1iter2 是否相等或不等。通常,当两个迭代器指向容器中的相同位置时,它们被认为是相等的。

迭代器的有效性

在使用迭代器时,需要特别注意迭代器的有效性 (validity)。某些容器操作,例如插入 (insert) 或删除 (erase) 元素,可能会导致某些迭代器失效 (invalidate)。失效的迭代器不能再安全地使用,尝试解引用或自增失效的迭代器会导致未定义行为 (undefined behavior)。因此,在容器操作后,可能需要重新获取有效的迭代器。

代码示例

以下代码示例展示了如何使用迭代器遍历 std::vector (向量) 中的元素:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> numbers = {1, 2, 3, 4, 5};
5// 使用迭代器遍历 vector
6for (std::vector<int>::iterator iter = numbers.begin(); iter != numbers.end(); ++iter) {
7std::cout << *iter << " "; // 解引用迭代器以访问元素值
8}
9std::cout << std::endl;
10// 使用范围 for 循环,底层也是基于迭代器
11for (int number : numbers) {
12std::cout << number << " ";
13}
14std::cout << std::endl;
15return 0;
16}

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
11 2 3 4 5
21 2 3 4 5

在这个例子中,numbers.begin() 返回指向 vector 容器第一个元素的迭代器,numbers.end() 返回指向容器尾后位置 (past-the-end) 的迭代器,注意 end() 返回的迭代器并不指向任何元素,而是作为遍历结束的标志。循环条件 iter != numbers.end() 用于判断是否到达容器的末尾。++iter 将迭代器移动到下一个元素。*iter 解引用迭代器,访问当前迭代器指向的元素值。

4.1.2 迭代器类别:输入、输出、前向、双向、随机访问迭代器 (Iterator Categories: Input, Output, Forward, Bidirectional, Random Access)

C++ 标准库根据迭代器的功能强弱,将迭代器分为五个类别,形成了一个迭代器层次结构 (iterator hierarchy)。这种分类使得算法可以根据所需的迭代器功能选择合适的迭代器类别,并确保算法的通用性和效率。

迭代器类别从功能最弱到最强依次为:

输入迭代器 (Input Iterator)

输入迭代器是最基本的迭代器类别,它主要用于从容器中读取元素 (input)。输入迭代器支持以下操作:

解引用 (Dereference) *iter: 读取迭代器指向的元素值 (只读访问)。
自增 (Increment) ++iteriter++: 移动到容器中的下一个位置。
相等比较 (Equality comparison) iter1 == iter2 和 不等比较 (Inequality comparison) iter1 != iter2: 比较两个输入迭代器是否相等。

特点:

单向移动 (Single-pass): 输入迭代器通常只能向前移动一次,一旦迭代器自增后,就不能再次访问之前的元素。这意味着你只能单遍 (single pass) 遍历容器。
只读 (Read-only): 输入迭代器只能用于读取元素,不能用于修改元素。
输入流迭代器 (istream_iterator) 是典型的输入迭代器。

输出迭代器 (Output Iterator)

输出迭代器与输入迭代器相反,主要用于向容器中写入元素 (output)。输出迭代器支持以下操作:

解引用赋值 (Dereference assignment) *iter = value: 将 value 写入迭代器指向的位置 (只写访问)。
自增 (Increment) ++iteriter++: 移动到容器中的下一个位置。

特点:

单向移动 (Single-pass): 类似于输入迭代器,输出迭代器也通常只能向前移动一次。
只写 (Write-only): 输出迭代器只能用于写入元素,不能用于读取元素。
输出流迭代器 (ostream_iterator)插入迭代器 (insert iterators) (例如 back_inserter, front_inserter, inserter) 是典型的输出迭代器。

前向迭代器 (Forward Iterator)

前向迭代器继承了输入迭代器和输出迭代器的所有功能,并且可以多次遍历 (multi-pass) 容器,即可反复访问已经访问过的元素。前向迭代器支持输入迭代器和输出迭代器的所有操作,外加:

默认构造 (Default construction)拷贝构造 (Copy construction): 前向迭代器可以被默认构造和拷贝构造。
可以保存迭代器的状态: 可以保存迭代器的状态并在之后使用。

特点:

多遍 (Multi-pass): 可以多次遍历容器。
单向移动 (Forward movement): 只能向前移动。
forward_list (单向链表) 容器的迭代器是前向迭代器。

双向迭代器 (Bidirectional Iterator)

双向迭代器在前向迭代器的基础上增加了向后移动 (backward movement) 的能力。双向迭代器支持前向迭代器的所有操作,外加:

自减 (Decrement) --iteriter--: 将迭代器向后移动到容器中的前一个元素。

特点:

双向移动 (Bidirectional movement): 可以向前和向后移动。
多遍 (Multi-pass): 可以多次遍历容器。
list (列表), set (集合), multiset (多重集合), map (映射), multimap (多重映射) 等容器的迭代器都是双向迭代器。

随机访问迭代器 (Random Access Iterator)

随机访问迭代器是功能最强大的迭代器类别,它在双向迭代器的基础上增加了随机访问 (random access) 元素的能力,类似于指针的算术运算。随机访问迭代器支持双向迭代器的所有操作,外加:

迭代器算术运算 (Iterator arithmetic):
▮▮▮▮⚝ iter + n, iter - n: 将迭代器向前或向后移动 n 个位置。
▮▮▮▮⚝ iter += n, iter -= n: 迭代器自身向前或向后移动 n 个位置。
▮▮▮▮⚝ iter1 - iter2: 计算两个迭代器之间的距离。
关系运算符 (Relational operators): <, >, <=, >=: 比较两个迭代器在容器中的相对位置。
下标访问 (Subscript operator) iter[n]: 访问迭代器向前第 n 个位置的元素,等价于 *(iter + n)

特点:

随机访问 (Random access): 可以像数组下标一样直接访问任意位置的元素。
双向移动 (Bidirectional movement): 可以向前和向后移动。
多遍 (Multi-pass): 可以多次遍历容器。
vector (向量), deque (双端队列), array (数组) 容器的迭代器,以及普通指针 (pointer) 都属于随机访问迭代器。

迭代器类别之间的关系

迭代器类别之间存在着向上兼容 (upward compatibility) 的关系,功能更强的迭代器类别兼容功能较弱的迭代器类别。可以用一张图来表示迭代器类别的层次结构:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1Input Iterator
2^
3|
4Output Iterator
5^
6|
7Forward Iterator
8^
9|
10Bidirectional Iterator
11^
12|
13Random Access Iterator

这意味着,如果一个算法需要输入迭代器,你可以传入任何类别的迭代器(输入迭代器、前向迭代器、双向迭代器、随机访问迭代器)。如果一个算法需要随机访问迭代器,那么只能传入随机访问迭代器,而不能传入双向迭代器或更弱类别的迭代器。

理解迭代器类别对于正确使用 C++ 标准库算法至关重要。算法通常会指定其所需的迭代器类别,以确保算法能够正确高效地工作。

4.2 常用迭代器类型 (Common Iterator Types)

本节讲解常用容器的迭代器类型,以及迭代器适配器 (iterator adaptors) 的作用和使用方法,例如 reverse_iterator (反向迭代器), move_iterator (移动迭代器) 等。

4.2.1 容器的迭代器类型:begin(), end(), cbegin(), cend(), rbegin(), rend() (Iterator Types of Containers)

C++ 标准库容器都提供了成员函数来获取不同类型的迭代器,用于遍历容器中的元素。常用的迭代器获取函数包括:

begin()end()

begin(): 返回指向容器第一个元素 (first element) 的迭代器。
end(): 返回指向容器尾后位置 (past-the-end) 的迭代器,不指向任何元素,作为遍历结束的标志。

begin()end() 返回的迭代器类型取决于容器的类型,通常是容器默认的迭代器类型。对于可修改的容器,begin()end() 返回的迭代器允许修改容器中的元素。

cbegin()cend() (C++11)

cbegin(): 返回指向容器第一个元素的 const 迭代器 (const iterator)
cend(): 返回指向容器尾后位置的 const 迭代器 (const iterator)

cbegin()cend() 返回的总是 const 迭代器,即使容器本身是可修改的。const 迭代器只允许读取容器中的元素,不允许修改元素。在不需要修改容器元素时,应该优先使用 cbegin()cend(),以提高代码的安全性。

rbegin()rend() (Reverse Iterators)

rbegin(): 返回指向容器最后一个元素 (last element)反向迭代器 (reverse iterator)
rend(): 返回指向容器首前位置 (before-the-first)反向迭代器 (reverse iterator),用于反向遍历结束的标志。

rbegin()rend() 返回 反向迭代器,用于反向遍历 (reverse traversal) 容器中的元素。反向迭代器的自增操作 ++iter 会向前移动到容器中的前一个元素,自减操作 --iter 会向后移动到容器中的后一个元素,与普通迭代器的行为相反。

crbegin()crend() (Const Reverse Iterators) (C++11)

crbegin(): 返回指向容器最后一个元素的 const 反向迭代器 (const reverse iterator)
crend(): 返回指向容器首前位置的 const 反向迭代器 (const reverse iterator)

crbegin()crend() 返回 const 反向迭代器,结合了反向遍历和 const 迭代器的特性,既可以反向遍历容器,又保证了不能修改容器中的元素。

代码示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> numbers = {1, 2, 3, 4, 5};
5std::cout << "正向遍历 (Forward Iteration): ";
6for (auto iter = numbers.begin(); iter != numbers.end(); ++iter) {
7std::cout << *iter << " ";
8}
9std::cout << std::endl;
10std::cout << "Const 正向遍历 (Const Forward Iteration): ";
11for (auto iter = numbers.cbegin(); iter != numbers.cend(); ++iter) {
12std::cout << *iter << " ";
13}
14std::cout << std::endl;
15std::cout << "反向遍历 (Reverse Iteration): ";
16for (auto iter = numbers.rbegin(); iter != numbers.rend(); ++iter) {
17std::cout << *iter << " ";
18}
19std::cout << std::endl;
20std::cout << "Const 反向遍历 (Const Reverse Iteration): ";
21for (auto iter = numbers.crbegin(); iter != numbers.crend(); ++iter) {
22std::cout << *iter << " ";
23}
24std::cout << std::endl;
25return 0;
26}

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1正向遍历 (Forward Iteration): 1 2 3 4 5
2Const 正向遍历 (Const Forward Iteration): 1 2 3 4 5
3反向遍历 (Reverse Iteration): 5 4 3 2 1
4Const 反向遍历 (Const Reverse Iteration): 5 4 3 2 1

在这个例子中,我们分别使用了 begin(), cbegin(), rbegin(), crbegin() 和对应的 end() 系列函数来获取不同类型的迭代器,并展示了正向遍历、const 正向遍历、反向遍历和 const 反向遍历的效果。

4.2.2 迭代器适配器:reverse_iterator, move_iterator, istream_iterator, ostream_iterator (Iterator Adaptors)

迭代器适配器 (iterator adaptors) 是一种特殊的迭代器,它们基于已有的迭代器进行改造和增强,以提供新的迭代器行为。标准库提供了多种有用的迭代器适配器,常用的包括:

reverse_iterator (反向迭代器)

reverse_iterator 适配器将普通迭代器的方向反转,使其行为类似于 rbegin()rend() 返回的反向迭代器。可以使用普通迭代器来构造 reverse_iterator

示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <iterator> // 引入 iterator 头文件
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5};
6std::cout << "使用 reverse_iterator 反向遍历: ";
7// 使用 vector::end() 和 vector::begin() 构造 reverse_iterator
8for (std::reverse_iterator<std::vector<int>::iterator> riter(numbers.end()); riter != std::reverse_iterator<std::vector<int>::iterator>(numbers.begin()); ++riter) {
9std::cout << *riter << " ";
10}
11std::cout << std::endl;
12return 0;
13}

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1使用 reverse_iterator 反向遍历: 5 4 3 2 1

在这个例子中,我们使用 numbers.end()numbers.begin() 构造了 reverse_iterator,实现了与 rbegin()rend() 相同的反向遍历效果。

move_iterator (移动迭代器) (C++11)

move_iterator 适配器将迭代器的解引用操作转换为移动操作 (move operation)。当解引用 move_iterator 时,它会返回元素的 右值引用 (rvalue reference),从而可以移动元素而不是拷贝元素,提高性能。move_iterator 通常与移动语义 (move semantics) 结合使用,用于高效地转移资源所有权。

示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <iterator>
4int main() {
5std::vector<std::string> strings = {"hello", "world", "c++"};
6std::vector<std::string> moved_strings;
7std::cout << "移动前的 strings: ";
8for (const auto& str : strings) {
9std::cout << str << " ";
10}
11std::cout << std::endl;
12// 使用 move_iterator 将 strings 中的字符串移动到 moved_strings
13std::move(std::make_move_iterator(strings.begin()),
14std::make_move_iterator(strings.end()),
15std::back_inserter(moved_strings));
16std::cout << "移动后的 strings (可能为空): ";
17for (const auto& str : strings) {
18std::cout << str << " "; // 字符串可能已被移动走,可能为空
19}
20std::cout << std::endl;
21std::cout << "移动后的 moved_strings: ";
22for (const auto& str : moved_strings) {
23std::cout << str << " ";
24}
25std::cout << std::endl;
26return 0;
27}

输出 (strings 的内容可能为空字符串,取决于具体的编译器和标准库实现):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1移动前的 strings: hello world c++
2移动后的 strings (可能为空):
3移动后的 moved_strings: hello world c++

在这个例子中,std::move 算法结合 move_iteratorstrings 中的字符串移动到了 moved_strings 中。移动操作避免了深拷贝,提高了效率。移动后,strings 中的字符串可能变为空字符串,因为其资源已被移动走。

istream_iterator (输入流迭代器)

istream_iterator 适配器将输入流 (istream) 视为一个元素序列,可以从输入流中读取数据。它通过 >> 运算符从输入流中提取数据,每次自增迭代器都会尝试从输入流中读取下一个值。当到达输入流末尾或发生读取错误时,istream_iterator 会变为 末尾迭代器 (end-of-stream iterator)

示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <iterator>
4int main() {
5std::cout << "请输入一些整数,以空格分隔,以非数字输入结束: ";
6std::istream_iterator<int> input_iter(std::cin); // 关联到标准输入流 std::cin
7std::istream_iterator<int> end_iter; // 默认构造,表示末尾迭代器
8std::vector<int> input_numbers;
9// 使用输入流迭代器从标准输入读取整数
10std::copy(input_iter, end_iter, std::back_inserter(input_numbers));
11std::cout << "输入的整数为: ";
12for (int number : input_numbers) {
13std::cout << number << " ";
14}
15std::cout << std::endl;
16return 0;
17}

示例输入:

1.双击鼠标左键复制此行;2.单击复制所有代码。
11 2 3 4 5 abc

示例输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1请输入一些整数,以空格分隔,以非数字输入结束: 1 2 3 4 5 abc
2输入的整数为: 1 2 3 4 5

在这个例子中,istream_iterator<int> input_iter(std::cin) 创建了一个关联到标准输入流 std::cin 的输入流迭代器,istream_iterator<int> end_iter 创建了一个末尾迭代器。std::copy 算法使用输入流迭代器从标准输入读取整数,直到遇到非数字输入或输入流结束。

ostream_iterator (输出流迭代器)

ostream_iterator 适配器将输出流 (ostream) 视为一个元素序列的目标位置,可以向输出流中写入数据。每次向 ostream_iterator 赋值时,它会使用 << 运算符将值写入输出流,并可以指定一个分隔符在每个元素之间输出。

示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <iterator>
4int main() {
5std::vector<int> numbers = {1, 2, 3, 4, 5};
6std::cout << "使用 ostream_iterator 输出整数,以逗号分隔: ";
7std::ostream_iterator<int> output_iter(std::cout, ", "); // 关联到标准输出流 std::cout,分隔符为 ", "
8// 使用输出流迭代器将 vector 中的整数输出到标准输出
9std::copy(numbers.begin(), numbers.end(), output_iter);
10std::cout << std::endl;
11return 0;
12}

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1使用 ostream_iterator 输出整数,以逗号分隔: 1, 2, 3, 4, 5,

在这个例子中,ostream_iterator<int> output_iter(std::cout, ", ") 创建了一个关联到标准输出流 std::cout 的输出流迭代器,分隔符为 ", "。std::copy 算法使用输出流迭代器将 numbers 中的整数输出到标准输出,每个整数之间用逗号和空格分隔。注意,最后一个元素后面也会输出分隔符。

插入迭代器 (Insert Iterators)

插入迭代器是一类输出迭代器适配器,用于向容器中插入元素,而不是覆盖已有的元素。标准库提供了三种插入迭代器:

back_inserter(container): 创建一个在容器 尾部 (back) 插入元素的迭代器。容器需要支持 push_back() 操作,例如 vector, deque, list
front_inserter(container): 创建一个在容器 头部 (front) 插入元素的迭代器。容器需要支持 push_front() 操作,例如 deque, list
inserter(container, position): 创建一个在容器 指定位置 (position) 之前插入元素的迭代器。容器需要支持 insert(position, value) 操作,例如 vector, deque, list, set, map 等。

插入迭代器使得算法可以方便地将结果插入到容器中,而无需手动管理容器的插入操作。在前面的 move_iterator 示例和 istream_iterator 示例中,我们已经使用了 back_inserter

4.3 自定义迭代器 (Custom Iterators)

本节指导读者如何根据特定数据结构或遍历需求,设计和实现自定义迭代器,并使其符合标准库迭代器的规范。

4.3.1 自定义迭代器的设计原则 (Design Principles of Custom Iterators)

当标准库提供的迭代器不能满足特定需求时,例如需要遍历自定义数据结构,或者需要实现特殊的遍历逻辑时,就需要自定义迭代器 (custom iterators)。设计自定义迭代器需要遵循一些原则,以确保其能够与标准库算法和容器良好地协同工作:

确定迭代器类别 (Choose Iterator Category)

首先,需要根据自定义迭代器的功能需求,选择合适的迭代器类别:输入迭代器、输出迭代器、前向迭代器、双向迭代器或随机访问迭代器。选择合适的迭代器类别可以指导后续的操作符重载和功能实现。

定义迭代器类型别名 (Define Iterator Type Aliases)

为了符合标准库迭代器的规范,自定义迭代器类应该定义一些必要的类型别名 (type aliases),用于描述迭代器的相关类型。这些类型别名通常包括:

iterator_category: 迭代器类别标签,例如 std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, std::random_access_iterator_tag
value_type: 迭代器解引用操作返回的元素类型。
difference_type: 表示两个迭代器之间距离的类型,通常为 std::ptrdiff_t
pointer: 指向元素的指针类型,通常为 value_type*
reference: 元素的引用类型,通常为 value_type&

可以使用 std::iterator 基类模板来简化这些类型别名的定义。

重载必要的迭代器操作符 (Overload Necessary Iterator Operators)

根据选择的迭代器类别,需要重载相应的迭代器操作符,以实现迭代器的基本功能。常用的操作符包括:

解引用操作符 (Dereference operator) operator*(): 返回迭代器指向的元素的引用或值。
自增操作符 (Increment operator) operator++() (前缀和后缀): 移动迭代器到下一个位置。
自减操作符 (Decrement operator) operator--() (前缀和后缀) (双向和随机访问迭代器): 移动迭代器到前一个位置。
赋值操作符 (Assignment operator) operator=: 将一个迭代器赋值给另一个迭代器。
相等/不等比较操作符 (Equality/Inequality comparison operators) operator==()operator!=(): 比较两个迭代器是否相等。
迭代器算术运算符 (Iterator arithmetic operators) operator+(), operator-(), operator+=(), operator-=() (随机访问迭代器): 实现迭代器的算术运算。
关系运算符 (Relational operators) operator<(), operator>(), operator<=(), operator>=() (随机访问迭代器): 比较迭代器位置。
下标操作符 (Subscript operator) operator[]() (随机访问迭代器): 实现下标访问。

遵循迭代器语义 (Follow Iterator Semantics)

自定义迭代器在实现操作符时,需要遵循标准库迭代器的语义和约定。例如:

前缀自增 ++iter 应该返回自增后的迭代器引用,而后缀自增 iter++ 应该返回自增前的迭代器值
解引用操作符 *iter 应该返回迭代器当前指向元素的引用或值
相等比较操作符 operator==() 应该判断两个迭代器是否指向容器中的相同位置
迭代器操作不应该抛出异常 (no-throw),除非是不可避免的情况。
注意迭代器的失效问题,避免在迭代器失效后继续使用。

与算法的兼容性 (Compatibility with Algorithms)

自定义迭代器的设计目标之一是使其能够与标准库算法协同工作。为了实现兼容性,需要:

提供 begin()end() 函数 (或类似机制),用于获取自定义数据结构的起始和结束迭代器。
确保自定义迭代器满足算法所需的迭代器类别。例如,如果算法需要随机访问迭代器,自定义迭代器也必须是随机访问迭代器,或者至少是功能足够强的迭代器类别。

遵循这些设计原则,可以创建出功能完善、符合标准库规范、能够与标准库算法良好配合的自定义迭代器。

4.3.2 实现自定义迭代器的步骤与示例 (Steps and Examples for Implementing Custom Iterators)

实现自定义迭代器通常需要以下步骤:

定义迭代器类 (Define Iterator Class)

创建一个类,用于表示自定义迭代器。类名可以根据具体的数据结构和迭代器功能来命名,例如 MyVectorIterator, LinkedListIterator 等。

继承 std::iterator 基类 (Inherit from std::iterator)

std::iterator 基类模板继承,并指定迭代器类别、元素类型、距离类型等模板参数,简化类型别名的定义。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iterator> // 引入 iterator 头文件
2class MyIterator : public std::iterator<std::random_access_iterator_tag, int> {
3// ...
4};

这里我们选择了 std::random_access_iterator_tag 作为迭代器类别,int 作为元素类型。

添加必要的成员变量 (Add Necessary Member Variables)

根据自定义迭代器的功能,添加必要的成员变量。通常需要保存迭代器当前指向的位置信息,例如指向容器元素的指针、索引、节点指针等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1class MyIterator : public std::iterator<std::random_access_iterator_tag, int> {
2private:
3int* ptr; // 指向 int 元素的指针
4public:
5// ...
6};

实现构造函数 (Implement Constructors)

实现构造函数,用于初始化迭代器的成员变量。通常需要提供默认构造函数拷贝构造函数,以及接受位置信息的构造函数

1.双击鼠标左键复制此行;2.单击复制所有代码。
1class MyIterator : public std::iterator<std::random_access_iterator_tag, int> {
2private:
3int* ptr;
4public:
5MyIterator(int* p = nullptr) : ptr(p) {} // 构造函数
6MyIterator(const MyIterator& other) = default; // 拷贝构造函数
7// ...
8};

重载迭代器操作符 (Overload Iterator Operators)

根据选择的迭代器类别,重载必要的迭代器操作符,例如解引用、自增、比较等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1class MyIterator : public std::iterator<std::random_access_iterator_tag, int> {
2private:
3int* ptr;
4public:
5// 构造函数 ...
6int& operator*() const { return *ptr; } // 解引用操作符
7MyIterator& operator++() { ++ptr; return *this; } // 前缀自增
8MyIterator operator++(int) { MyIterator temp = *this; ++ptr; return temp; } // 后缀自增
9bool operator==(const MyIterator& other) const { return ptr == other.ptr; } // 相等比较
10bool operator!=(const MyIterator& other) const { return ptr != other.ptr; } // 不等比较
11// ... 其他操作符
12};

在自定义数据结构中提供 begin()end() 函数 (Provide begin() and end() in Custom Data Structure)

在自定义数据结构类中,提供 begin()end() 成员函数,返回自定义迭代器类型的对象,用于表示数据结构的起始和结束位置。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2class MyContainer {
3private:
4std::vector<int> data;
5public:
6// ...
7using iterator = MyIterator; // 定义迭代器类型别名
8iterator begin() { return iterator(&data[0]); } // 返回起始迭代器
9iterator end() { return iterator(&data[0] + data.size()); } // 返回结束迭代器
10const_iterator cbegin() const { return const_iterator(&data[0]); } // const 起始迭代器
11const_iterator cend() const { return const_iterator(&data[0] + data.size()); } // const 结束迭代器
12};

测试自定义迭代器 (Test Custom Iterator)

编写测试代码,验证自定义迭代器是否能够正确地遍历自定义数据结构,并能够与标准库算法协同工作。

完整示例代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <iterator>
4#include <algorithm>
5// 自定义迭代器类
6class MyIterator : public std::iterator<std::random_access_iterator_tag, int> {
7private:
8int* ptr;
9public:
10using iterator_category = std::random_access_iterator_tag;
11using value_type = int;
12using difference_type = std::ptrdiff_t;
13using pointer = int*;
14using reference = int&;
15MyIterator(int* p = nullptr) : ptr(p) {}
16MyIterator(const MyIterator& other) = default;
17int& operator*() const { return *ptr; }
18MyIterator& operator++() { ++ptr; return *this; }
19MyIterator operator++(int) { MyIterator temp = *this; ++ptr; return temp; }
20MyIterator& operator--() { --ptr; return *this; }
21MyIterator operator--(int) { MyIterator temp = *this; --ptr; return temp; }
22bool operator==(const MyIterator& other) const { return ptr == other.ptr; }
23bool operator!=(const MyIterator& other) const { return ptr != other.ptr; }
24difference_type operator-(const MyIterator& other) const { return ptr - other.ptr; }
25MyIterator operator+(difference_type n) const { return MyIterator(ptr + n); }
26MyIterator& operator+=(difference_type n) { ptr += n; return *this; }
27int& operator[](difference_type n) const { return *(ptr + n); }
28bool operator<(const MyIterator& other) const { return ptr < other.ptr; }
29bool operator>(const MyIterator& other) const { return ptr > other.ptr; }
30bool operator<=(const MyIterator& other) const { return ptr <= other.ptr; }
31bool operator>=(const MyIterator& other) const { return ptr >= other.ptr; }
32};
33// 自定义容器类
34class MyContainer {
35private:
36std::vector<int> data;
37public:
38using iterator = MyIterator;
39using const_iterator = MyIterator; // 简单起见,const_iterator 与 iterator 类型相同
40MyContainer(std::initializer_list<int> list) : data(list) {}
41iterator begin() { return iterator(&data[0]); }
42iterator end() { return iterator(&data[0] + data.size()); }
43const_iterator cbegin() const { return const_iterator(&data[0]); }
44const_iterator cend() const { return const_iterator(&data[0] + data.size()); }
45friend std::ostream& operator<<(std::ostream& os, const MyContainer& container) {
46os << "[";
47for (auto iter = container.cbegin(); iter != container.cend(); ++iter) {
48os << *iter << (iter + 1 == container.cend() ? "" : ", ");
49}
50os << "]";
51return os;
52}
53};
54int main() {
55MyContainer my_container = {1, 2, 3, 4, 5};
56std::cout << "自定义容器 my_container: " << my_container << std::endl;
57std::cout << "使用自定义迭代器遍历: ";
58for (auto iter = my_container.begin(); iter != my_container.end(); ++iter) {
59std::cout << *iter << " ";
60}
61std::cout << std::endl;
62std::cout << "使用 std::for_each 算法遍历: ";
63std::for_each(my_container.begin(), my_container.end(), [](int x){ std::cout << x << " "; });
64std::cout << std::endl;
65std::cout << "使用 std::sort 算法排序: ";
66std::sort(my_container.begin(), my_container.end(), std::greater<int>()); // 降序排序
67std::cout << my_container << std::endl;
68return 0;
69}

输出:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1自定义容器 my_container: [1, 2, 3, 4, 5]
2使用自定义迭代器遍历: 1 2 3 4 5
3使用 std::for_each 算法遍历: 1 2 3 4 5
4使用 std::sort 算法排序: [5, 4, 3, 2, 1]

这个例子展示了如何实现一个简单的随机访问迭代器 MyIterator,并将其应用到一个自定义容器 MyContainer 中。自定义迭代器可以与标准库算法 std::for_eachstd::sort 协同工作,验证了自定义迭代器的有效性。在实际应用中,自定义迭代器的实现会根据具体的数据结构和需求而更加复杂,但基本的设计原则和步骤是类似的。

<END_OF_CHAPTER/>

5. 函数对象 (Function Objects)

本章深入讲解 C++ 标准库函数对象 (Function Objects) 的概念、作用和使用方法,包括预定义的函数对象、函数适配器 (Function Adaptors) 以及 Lambda 表达式 (Lambda Expressions),揭示函数对象在泛型编程 (Generic Programming) 中的强大功能。

5.1 函数对象概念与优势 (Function Object Concepts and Advantages)

本节介绍函数对象的概念、与普通函数的区别,以及函数对象在泛型编程中的优势,例如状态保持、类型安全等。

5.1.1 函数对象与普通函数的区别 (Differences between Function Objects and Ordinary Functions)

函数对象,也常被称为仿函数 (Functor),本质上是行为类似函数的对象,即可以像函数一样被调用。在 C++ 中,任何重载了函数调用运算符 operator() 的类或结构体的对象,都可以被视为函数对象。

与普通函数 (Ordinary Functions) 相比,函数对象具有以下显著的区别和优势:

状态保持 (Stateful)
普通函数是无状态的,每次调用行为都是确定的,只依赖于输入参数。而函数对象可以存储状态,即在类中可以定义成员变量来保存状态信息。每次函数对象被调用时,可以利用或修改这些状态。这使得函数对象能够实现更复杂、更灵活的行为。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2// 普通函数,无状态
3int add(int a, int b) {
4return a + b;
5}
6// 函数对象,有状态
7class Adder {
8public:
9Adder(int base) : base_(base) {}
10int operator()(int num) const {
11return base_ + num; // 使用存储的状态 base_
12}
13private:
14int base_; // 状态
15};
16int main() {
17std::cout << "普通函数调用: " << add(3, 5) << std::endl; // 输出 8
18Adder adder5(5); // 创建函数对象,初始化状态 base_ 为 5
19std::cout << "函数对象调用1: " << adder5(3) << std::endl; // 输出 8 (5 + 3)
20std::cout << "函数对象调用2: " << adder5(7) << std::endl; // 输出 12 (5 + 7),状态 base_ 保持不变
21Adder adder10(10); // 创建另一个函数对象,状态 base_ 为 10
22std::cout << "函数对象调用3: " << adder10(2) << std::endl; // 输出 12 (10 + 2)
23return 0;
24}

在上述例子中,Adder 是一个函数对象类。它通过构造函数初始化状态 base_,并在 operator() 中使用这个状态进行加法运算。每次创建 Adder 对象时,可以设置不同的状态,从而实现不同的加法行为。而普通函数 add 无法做到这一点。

类型安全 (Type Safety) 与内联 (Inlining)
函数对象是对象,因此它们具有类型。在 C++ 的泛型编程中,类型信息对于编译期优化至关重要。编译器可以更好地理解函数对象的类型,从而进行内联优化 (Inlining Optimization)。内联可以消除函数调用的开销,提高程序执行效率。

普通函数指针 (Function Pointer) 虽然也可以用于泛型编程,但其类型信息不如函数对象丰富,内联优化的机会也相对较少。函数对象通常以模板参数 (Template Parameter) 的形式传递给算法,编译器在编译期就能确定函数对象的具体类型,从而更容易进行内联。

更丰富的接口 (Richer Interface)
函数对象可以是类的对象,这意味着它们可以拥有成员函数 (Member Functions)成员变量 (Member Variables)。除了 operator() 之外,还可以定义其他的成员函数来提供额外的功能。这使得函数对象比普通函数具有更强大的表达能力和更灵活的接口。

可以作为模板参数 (Template Argument)
函数对象可以作为模板参数传递给函数或类模板,从而实现高度的代码复用 (Code Reusability)定制化 (Customization)。标准库的算法 (Algorithms) 广泛使用了函数对象作为模板参数,允许用户自定义算法的行为。普通函数指针虽然也可以作为模板参数,但其灵活性和类型安全性不如函数对象。

总之,函数对象通过封装状态、提供类型安全、支持内联优化以及拥有更丰富的接口,在泛型编程中展现出比普通函数更强大的优势。尤其在需要定制算法行为保持状态或追求性能优化的场景下,函数对象是更佳的选择。

5.1.2 函数对象在泛型编程中的作用 (Role of Function Objects in Generic Programming)

函数对象在 C++ 泛型编程中扮演着至关重要的角色,主要体现在以下几个方面:

算法行为的定制 (Customization of Algorithm Behavior)
C++ 标准库的算法 (Algorithms) 设计为高度泛型,可以应用于各种容器 (Containers) 和数据类型。为了使算法能够适应不同的需求,标准库广泛使用函数对象来定制算法的行为

例如,std::sort 算法默认使用 operator< 进行升序排序。但如果需要进行降序排序,或者按照自定义的比较规则排序,就可以通过传递不同的函数对象来实现。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <functional> // 包含 std::greater
5int main() {
6std::vector<int> nums = {3, 1, 4, 1, 5, 9, 2, 6};
7// 默认升序排序 (使用 std::less,默认函数对象)
8std::sort(nums.begin(), nums.end());
9std::cout << "升序排序: ";
10for (int num : nums) {
11std::cout << num << " "; // 输出 1 1 2 3 4 5 6 9
12}
13std::cout << std::endl;
14// 降序排序 (使用 std::greater 函数对象)
15std::sort(nums.begin(), nums.end(), std::greater<int>());
16std::cout << "降序排序: ";
17for (int num : nums) {
18std::cout << num << " "; // 输出 9 6 5 4 3 2 1 1
19}
20std::cout << std::endl;
21// 使用 Lambda 表达式自定义排序规则 (也是一种函数对象)
22std::sort(nums.begin(), nums.end(), [](int a, int b) {
23return std::abs(a - 5) < std::abs(b - 5); // 按离 5 的距离升序排序
24});
25std::cout << "自定义排序: ";
26for (int num : nums) {
27std::cout << num << " "; // 输出 5 4 6 3 2 1 1 9
28}
29std::cout << std::endl;
30return 0;
31}

在这个例子中,我们使用 std::greater<int>() 函数对象实现了降序排序,并使用 Lambda 表达式定义了更复杂的自定义排序规则。这些都是通过函数对象来定制 std::sort 算法的行为。

策略模式 (Strategy Pattern) 的实现
函数对象可以用来实现策略模式。策略模式是一种设计模式,它允许在运行时选择算法或策略。通过将算法封装在函数对象中,可以将算法的选择权交给客户端,从而提高代码的灵活性和可扩展性。

例如,假设我们需要实现一个通用的数据处理框架,该框架可以接受不同的数据处理策略。我们可以定义一个抽象的策略接口(例如,一个包含 operator() 的抽象类或模板类),然后为每种具体的策略实现一个函数对象类。在运行时,根据用户的选择,框架可以动态地切换不同的策略。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <string>
4// 抽象策略接口 (使用模板函数对象)
5template <typename T>
6class ProcessingStrategy {
7public:
8virtual T process(const T& data) const = 0;
9virtual ~ProcessingStrategy() = default;
10};
11// 具体策略 1: 大写转换
12class UppercaseStrategy : public ProcessingStrategy<std::string> {
13public:
14std::string process(const std::string& data) const override {
15std::string result = data;
16for (char& c : result) {
17c = std::toupper(c);
18}
19return result;
20}
21};
22// 具体策略 2: 反转字符串
23class ReverseStrategy : public ProcessingStrategy<std::string> {
24public:
25std::string process(const std::string& data) const override {
26std::string result = data;
27std::reverse(result.begin(), result.end());
28return result;
29}
30};
31// 数据处理框架
32class DataProcessor {
33public:
34DataProcessor(ProcessingStrategy<std::string>* strategy) : strategy_(strategy) {}
35std::string processData(const std::string& data) const {
36return strategy_->process(data); // 调用策略的 process 方法
37}
38private:
39ProcessingStrategy<std::string>* strategy_;
40};
41int main() {
42std::string data = "hello world";
43// 使用大写转换策略
44UppercaseStrategy uppercaseStrategy;
45DataProcessor processor1(&uppercaseStrategy);
46std::cout << "大写转换策略: " << processor1.processData(data) << std::endl; // 输出 HELLO WORLD
47// 使用反转字符串策略
48ReverseStrategy reverseStrategy;
49DataProcessor processor2(&reverseStrategy);
50std::cout << "反转字符串策略: " << processor2.processData(data) << std::endl; // 输出 dlrow olleh
51return 0;
52}

在这个例子中,ProcessingStrategy 是抽象策略接口,UppercaseStrategyReverseStrategy 是具体的策略实现。DataProcessor 框架通过接受 ProcessingStrategy 指针,可以在运行时切换不同的策略。函数对象在这里充当了策略的角色。

回调函数 (Callback Functions) 的替代
在 C 语言中,回调函数通常使用函数指针来实现。但在 C++ 中,函数对象可以作为更类型安全、更灵活的回调机制。函数对象可以携带状态,并且可以利用模板实现泛型回调。

例如,GUI 框架中的事件处理机制通常使用回调函数来响应用户的操作(如按钮点击、鼠标移动等)。使用函数对象作为回调,可以方便地将事件处理逻辑封装在对象中,并传递必要的状态信息。

与其他泛型组件的协同工作
函数对象是 C++ 标准库泛型编程的重要组成部分,它们可以与迭代器 (Iterators)容器 (Containers) 和其他泛型算法协同工作,构建强大的泛型系统。例如,算法通常接受迭代器来指定操作范围,并接受函数对象来定制操作行为。这种组合方式使得 C++ 标准库具有高度的灵活性和可扩展性。

总结来说,函数对象在泛型编程中主要用于定制算法行为实现设计模式替代回调函数以及与其他泛型组件协同工作。它们是构建灵活、可复用、高性能 C++ 代码的重要工具。

5.2 预定义的函数对象与函数适配器 (Predefined Function Objects and Function Adaptors)

C++ 标准库 <functional> 头文件提供了一系列预定义的函数对象 (Predefined Function Objects)函数适配器 (Function Adaptors),方便开发者直接使用或组合,以满足常见的函数对象需求。

5.2.1 算术、比较、逻辑函数对象 (Arithmetic, Comparison, Logic Function Objects)

标准库预定义了许多常用的算术 (Arithmetic)比较 (Comparison)逻辑 (Logic) 函数对象,可以直接使用,无需自己编写简单的函数对象类。

算术函数对象 (Arithmetic Function Objects)

函数对象 功能描述 运算符 示例
std::plus<T> 加法 + std::plus<int>()(3, 5) 结果为 8
std::minus<T> 减法 - std::minus<int>()(8, 3) 结果为 5
std::multiplies<T> 乘法 * std::multiplies<int>()(2, 4) 结果为 8
std::divides<T> 除法 / std::divides<double>()(10.0, 2.0) 结果为 5.0
std::modulus<T> 取模 (求余) % std::modulus<int>()(10, 3) 结果为 1
std::negate<T> 取负数 (一元负号) - std::negate<int>()(5) 结果为 -5

这些函数对象都是模板类T 是操作数的类型。例如 std::plus<int> 表示对 int 类型进行加法操作的函数对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <functional>
3int main() {
4std::cout << "std::plus<int>(3, 5): " << std::plus<int>()(3, 5) << std::endl; // 输出 8
5std::cout << "std::minus<int>(8, 3): " << std::minus<int>()(8, 3) << std::endl; // 输出 5
6std::cout << "std::multiplies<int>(2, 4): " << std::multiplies<int>()(2, 4) << std::endl; // 输出 8
7std::cout << "std::divides<double>(10.0, 2.0): " << std::divides<double>()(10.0, 2.0) << std::endl; // 输出 5
8std::cout << "std::modulus<int>(10, 3): " << std::modulus<int>()(10, 3) << std::endl; // 输出 1
9std::cout << "std::negate<int>(5): " << std::negate<int>()(5) << std::endl; // 输出 -5
10return 0;
11}

比较函数对象 (Comparison Function Objects)

函数对象 功能描述 运算符 示例
std::equal_to<T> 等于 == std::equal_to<int>()(5, 5) 结果为 true
std::not_equal_to<T> 不等于 != std::not_equal_to<int>()(5, 3) 结果为 true
std::greater<T> 大于 > std::greater<int>()(5, 3) 结果为 true
std::less<T> 小于 < std::less<int>()(3, 5) 结果为 true
std::greater_equal<T> 大于等于 >= std::greater_equal<int>()(5, 5)结果为 true
std::less_equal<T> 小于等于 <= std::less_equal<int>()(3, 3) 结果为 true

这些函数对象常用于算法的比较操作,例如 std::sort 的排序规则、std::find_if 的条件判断等。std::less<T> 是默认的比较函数对象,用于升序排序。std::greater<T> 用于降序排序。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <functional>
3int main() {
4std::cout << "std::equal_to<int>(5, 5): " << std::equal_to<int>()(5, 5) << std::endl; // 输出 1 (true)
5std::cout << "std::not_equal_to<int>(5, 3): " << std::not_equal_to<int>()(5, 3) << std::endl; // 输出 1 (true)
6std::cout << "std::greater<int>(5, 3): " << std::greater<int>()(5, 3) << std::endl; // 输出 1 (true)
7std::cout << "std::less<int>(3, 5): " << std::less<int>()(3, 5) << std::endl; // 输出 1 (true)
8std::cout << "std::greater_equal<int>(5, 5): " << std::greater_equal<int>()(5, 5) << std::endl; // 输出 1 (true)
9std::cout << "std::less_equal<int>(3, 3): " << std::less_equal<int>()(3, 3) << std::endl; // 输出 1 (true)
10return 0;
11}

逻辑函数对象 (Logic Function Objects)

函数对象 功能描述 运算符 示例
std::logical_and<T> 逻辑与 (AND) && std::logical_and<bool>()(true, false) 结果为 false
std::logical_or<T> 逻辑或 (OR) \|\| std::logical_or<bool>()(true, false) 结果为 true
std::logical_not<T> 逻辑非 (NOT) ! std::logical_not<bool>()(false) 结果为 true

这些函数对象用于执行逻辑运算,常用于算法的条件判断。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <functional>
3int main() {
4std::cout << "std::logical_and<bool>(true, false): " << std::logical_and<bool>()(true, false) << std::endl; // 输出 0 (false)
5std::cout << "std::logical_or<bool>(true, false): " << std::logical_or<bool>()(true, false) << std::endl; // 输出 1 (true)
6std::cout << "std::logical_not<bool>(false): " << std::logical_not<bool>()(false) << std::endl; // 输出 1 (true)
7return 0;
8}

这些预定义的函数对象为常见的算术、比较和逻辑运算提供了方便的工具,可以直接在算法中使用,无需重复编写简单的函数对象类。

5.2.2 函数适配器:bind, not1, not2, mem_fn, ptr_fun (Function Adaptors)

函数适配器 (Function Adaptors) 是一类特殊的函数对象,它们可以修改或组合已有的函数对象或函数,生成新的函数对象。标准库提供了一些常用的函数适配器,用于实现更灵活的函数对象操作。

std::bind (绑定器)
std::bind 是一个非常强大的函数适配器,它可以绑定函数或函数对象的参数,生成一个新的函数对象。std::bind 可以实现以下功能:

绑定部分参数:将一个多参数函数的部分参数固定下来,生成一个参数较少的新函数对象。
调整参数顺序:调整函数参数的顺序。
绑定成员函数或成员变量:将成员函数或成员变量绑定到特定的对象实例上。

std::bind 的基本语法如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1auto new_function_object = std::bind(func, arg_list);

其中,func 可以是一个函数、函数指针、成员函数指针或函数对象。arg_list 是参数列表,用于绑定 func 的参数。参数列表中可以使用占位符 (Placeholders) std::placeholders::_1, std::placeholders::_2, ... 来表示新函数对象的参数位置。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <functional>
3// 一个简单的二元函数
4int subtract(int a, int b) {
5return a - b;
6}
7class Calculator {
8public:
9int multiply(int a, int b) {
10return a * b;
11}
12};
13int main() {
14// 绑定第一个参数为 10,生成一个一元函数对象,计算 10 - x
15auto subtract_from_10 = std::bind(subtract, 10, std::placeholders::_1);
16std::cout << "subtract_from_10(5): " << subtract_from_10(5) << std::endl; // 输出 5 (10 - 5)
17std::cout << "subtract_from_10(2): " << subtract_from_10(2) << std::endl; // 输出 8 (10 - 2)
18// 绑定第二个参数为 10,生成一个一元函数对象,计算 x - 10
19auto subtract_10 = std::bind(subtract, std::placeholders::_1, 10);
20std::cout << "subtract_10(5): " << subtract_10(5) << std::endl; // 输出 -5 (5 - 10)
21std::cout << "subtract_10(20): " << subtract_10(20) << std::endl; // 输出 10 (20 - 10)
22// 调整参数顺序,交换 subtract 函数的参数顺序,计算 b - a
23auto reversed_subtract = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);
24std::cout << "reversed_subtract(3, 8): " << reversed_subtract(3, 8) << std::endl; // 输出 5 (8 - 3)
25Calculator calculator;
26// 绑定成员函数 multiply 到 calculator 对象,生成一个二元函数对象,计算 calculator.multiply(x, y)
27auto multiply_calculator = std::bind(&Calculator::multiply, &calculator, std::placeholders::_1, std::placeholders::_2);
28std::cout << "multiply_calculator(3, 4): " << multiply_calculator(3, 4) << std::endl; // 输出 12 (3 * 4)
29return 0;
30}

std::bind 可以极大地提高函数对象的灵活性和复用性,尤其在需要将现有函数或函数对象应用于新的上下文时,std::bind 可以简化代码并提高效率。

std::not1std::not2 (逻辑取反适配器)
std::not1std::not2 是逻辑取反适配器,用于对一元或二元谓词 (Predicate) 函数对象的结果进行逻辑取反

std::not1(predicate):接受一个一元谓词函数对象 predicate,返回一个新的一元谓词函数对象,新谓词的结果是 !predicate(arg)
std::not2(predicate):接受一个二元谓词函数对象 predicate,返回一个新的一元谓词函数对象(注意,是一元),其行为可能不是直接的二元取反,使用较少,通常与 std::bind 结合使用以实现更复杂的逻辑。更常用的场景是使用 Lambda 表达式直接取反。

在 C++17 之后,std::not1std::not2 已经被 std::not_fn 取代,std::not_fn 更加通用和简洁,可以接受任意数量参数的谓词函数对象,并返回逻辑取反的结果。推荐使用 std::not_fn

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <functional>
5bool is_even(int num) {
6return num % 2 == 0;
7}
8int main() {
9std::vector<int> nums = {1, 2, 3, 4, 5, 6};
10// 查找第一个偶数
11auto it_even = std::find_if(nums.begin(), nums.end(), is_even);
12if (it_even != nums.end()) {
13std::cout << "第一个偶数: " << *it_even << std::endl; // 输出 2
14}
15// 查找第一个奇数 (使用 std::not_fn 对 is_even 取反)
16auto it_odd = std::find_if(nums.begin(), nums.end(), std::not_fn(is_even));
17if (it_odd != nums.end()) {
18std::cout << "第一个奇数: " << *it_odd << std::endl; // 输出 1
19}
20// 使用 Lambda 表达式直接取反,更简洁
21auto it_odd_lambda = std::find_if(nums.begin(), nums.end(), [](int num) {
22return !is_even(num);
23});
24if (it_odd_lambda != nums.end()) {
25std::cout << "第一个奇数 (Lambda): " << *it_odd_lambda << std::endl; // 输出 1
26}
27return 0;
28}

std::not_fn 可以方便地对谓词函数对象的结果进行取反,用于更灵活的条件判断。

std::mem_fn (成员函数适配器)
std::mem_fn成员函数适配器 (Member Function Adaptor),用于将成员函数指针 (Member Function Pointer) 转换为函数对象。这使得成员函数可以像普通函数对象一样,被用于标准库的算法中。

假设有一个类 MyClass,它有一个成员函数 getValue()。如果有一个 MyClass 对象指针的容器,例如 std::vector<MyClass*> objects,我们想获取每个对象的 getValue() 方法的结果,可以使用 std::transform 算法和 std::mem_fn 适配器。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <functional>
5class MyClass {
6public:
7MyClass(int value) : value_(value) {}
8int getValue() const {
9return value_;
10}
11private:
12int value_;
13};
14int main() {
15std::vector<MyClass*> objects;
16objects.push_back(new MyClass(10));
17objects.push_back(new MyClass(20));
18objects.push_back(new MyClass(30));
19std::vector<int> values;
20// 使用 std::mem_fn 将 MyClass::getValue 转换为函数对象,作用于 objects 容器中的每个指针
21std::transform(objects.begin(), objects.end(), std::back_inserter(values), std::mem_fn(&MyClass::getValue));
22std::cout << "Values: ";
23for (int value : values) {
24std::cout << value << " "; // 输出 10 20 30
25}
26std::cout << std::endl;
27// 清理内存
28for (MyClass* obj : objects) {
29delete obj;
30}
31return 0;
32}

std::mem_fn(&MyClass::getValue) 将成员函数指针 &MyClass::getValue 转换为一个函数对象。这个函数对象可以接受一个 MyClass* 指针作为参数,并调用 (*ptr)->getValue() 返回结果。这样,std::transform 算法就可以遍历 objects 容器中的每个指针,并调用 getValue() 方法,将结果存储到 values 容器中。

std::ptr_fun (函数指针适配器)
std::ptr_fun函数指针适配器 (Function Pointer Adaptor),用于将普通函数指针 (Ordinary Function Pointer) 转换为函数对象。在 C++11 之后,由于 Lambda 表达式和 std::function 的引入,std::ptr_fun 的使用场景已经大大减少,通常可以直接使用 Lambda 表达式或 std::function 来替代。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4#include <functional>
5int multiply_by_2(int num) {
6return num * 2;
7}
8int main() {
9std::vector<int> nums = {1, 2, 3, 4, 5};
10std::vector<int> doubled_nums;
11// 使用 std::ptr_fun 将函数指针 multiply_by_2 转换为函数对象
12std::transform(nums.begin(), nums.end(), std::back_inserter(doubled_nums), std::ptr_fun(multiply_by_2));
13std::cout << "Doubled numbers: ";
14for (int num : doubled_nums) {
15std::cout << num << " "; // 输出 2 4 6 8 10
16}
17std::cout << std::endl;
18// 使用 Lambda 表达式替代 std::ptr_fun,更简洁
19std::vector<int> doubled_nums_lambda;
20std::transform(nums.begin(), nums.end(), std::back_inserter(doubled_nums_lambda), [](int num) {
21return multiply_by_2(num);
22});
23std::cout << "Doubled numbers (Lambda): ";
24for (int num : doubled_nums_lambda) {
25std::cout << num << " "; // 输出 2 4 6 8 10
26}
27std::cout << std::endl;
28return 0;
29}

std::ptr_fun(multiply_by_2) 将函数指针 multiply_by_2 转换为函数对象,使其可以被 std::transform 算法接受。但使用 Lambda 表达式可以更简洁地实现相同的功能。

总结来说,函数适配器是一组强大的工具,可以修改和组合现有的函数或函数对象,生成新的、更灵活的函数对象。std::bind 用于参数绑定和调整,std::not_fn 用于逻辑取反,std::mem_fn 用于成员函数适配,std::ptr_fun 用于函数指针适配 (已逐渐被 Lambda 表达式取代)。合理使用函数适配器可以简化代码,提高代码的复用性和灵活性。

5.3 Lambda 表达式 (Lambda Expressions)

Lambda 表达式 (Lambda Expressions) 是 C++11 引入的一项重要特性,它提供了一种简洁、内联的方式来定义匿名函数对象 (Anonymous Function Objects)。Lambda 表达式可以看作是一种语法糖 (Syntactic Sugar),它简化了函数对象的创建和使用,尤其在需要短小、局部的函数对象时,Lambda 表达式非常方便。

5.3.1 Lambda 表达式的语法与构成 (Syntax and Components of Lambda Expressions)

Lambda 表达式的基本语法形式如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1[capture list](parameter list) -> return_type { function body }

一个 Lambda 表达式由以下几个部分构成:

捕获列表 (Capture List) [capture list]
捕获列表位于方括号 [] 中,用于指定 Lambda 表达式如何访问其所在作用域 (Scope) 内的变量。捕获方式有以下几种:

值捕获 (Capture by value):将外部变量的值拷贝到 Lambda 表达式内部。在 Lambda 表达式内部对捕获的变量进行修改不会影响外部变量。
▮▮▮▮⚝ [var]:捕获变量 var 的值。
▮▮▮▮⚝ [=]:默认值捕获,捕获所有外部变量的值。
引用捕获 (Capture by reference):将外部变量的引用传递给 Lambda 表达式内部。在 Lambda 表达式内部对捕获的变量进行修改会影响外部变量。
▮▮▮▮⚝ [&var]:捕获变量 var 的引用。
▮▮▮▮⚝ [&]:默认引用捕获,捕获所有外部变量的引用。
隐式捕获 (Implicit capture):让编译器自动推导捕获方式。
▮▮▮▮⚝ [=][&] 可以隐式捕获所有在 Lambda 函数体中使用的外部变量。
混合捕获 (Mixed capture):可以混合使用值捕获和引用捕获。
▮▮▮▮⚝ [=, &var1, &var2]:默认值捕获所有外部变量,但 var1var2 使用引用捕获。
▮▮▮▮⚝ [&, var1, var2]:默认引用捕获所有外部变量,但 var1var2 使用值捕获。
不捕获 (No capture):Lambda 表达式不捕获任何外部变量。
▮▮▮▮⚝ []:空的捕获列表,表示不捕获任何外部变量。

参数列表 (Parameter List) (parameter list)
参数列表位于圆括号 () 中,与普通函数的参数列表类似,用于指定 Lambda 表达式的输入参数。可以省略参数列表,如果省略,圆括号 () 也可以省略(如果 Lambda 函数体只有一条 return 语句,且返回类型可以被推导,则整个 () 也可以省略)。

返回类型 (Return Type) -> return_type
返回类型位于 -> 符号之后,用于显式指定 Lambda 表达式的返回类型。在很多情况下,编译器可以自动推导返回类型 (Return Type Deduction),此时可以省略返回类型部分。但如果 Lambda 函数体包含多条 return 语句,或者返回类型比较复杂,建议显式指定返回类型。

函数体 (Function Body) { function body }
函数体位于花括号 {} 中,包含 Lambda 表达式的具体实现代码,与普通函数的函数体类似。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5int factor = 3; // 外部变量
6// Lambda 表达式示例
7auto multiply_lambda = [factor](int num) -> int { // 值捕获 factor,参数 num,显式返回类型 int
8return num * factor;
9};
10std::vector<int> nums = {1, 2, 3, 4, 5};
11std::vector<int> multiplied_nums;
12// 使用 Lambda 表达式进行 transform 操作
13std::transform(nums.begin(), nums.end(), std::back_inserter(multiplied_nums), multiply_lambda);
14std::cout << "Multiplied numbers: ";
15for (int num : multiplied_nums) {
16std::cout << num << " "; // 输出 3 6 9 12 15
17}
18std::cout << std::endl;
19// 改变外部变量 factor 的值,值捕获的 Lambda 表达式不受影响
20factor = 5;
21std::cout << "multiply_lambda(2) after factor changed: " << multiply_lambda(2) << std::endl; // 仍然输出 6 (3 * 2)
22return 0;
23}

在这个例子中,multiply_lambda 是一个 Lambda 表达式。它值捕获了外部变量 factor,接受一个 int 参数 num,并显式指定返回类型为 int。Lambda 函数体实现了 num * factor 的乘法运算。即使在 Lambda 表达式定义后,外部变量 factor 的值被修改,Lambda 表达式的行为仍然保持不变,因为它捕获的是 factor 的值的拷贝。

5.3.2 捕获列表与 mutable 关键字 (Capture List and mutable Keyword)

捕获列表 (Capture List) 决定了 Lambda 表达式如何访问外部作用域的变量。mutable 关键字 则用于控制是否允许在 Lambda 表达式内部修改值捕获的变量的拷贝。

值捕获 (Capture by Value) 与 mutable
默认情况下,对于值捕获的变量,Lambda 表达式内部是只读的,不允许修改捕获的变量的拷贝。如果需要在 Lambda 表达式内部修改值捕获的变量,需要在参数列表后添加 mutable 关键字

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2int main() {
3int count = 0;
4// 值捕获 count,不使用 mutable,尝试修改 count 会导致编译错误
5// auto increment_lambda = [count]() {
6// count++; // 编译错误:assignment of read-only variable 'count'
7// std::cout << "Count in lambda: " << count << std::endl;
8// };
9// 值捕获 count,使用 mutable,允许修改 count 的拷贝
10auto mutable_increment_lambda = [count]() mutable {
11count++; // OK,修改的是拷贝
12std::cout << "Count in mutable lambda: " << count << std::endl;
13};
14mutable_increment_lambda(); // 调用 mutable_increment_lambda,修改的是 Lambda 内部 count 的拷贝
15mutable_increment_lambda();
16std::cout << "Count in main: " << count << std::endl; // 外部 count 的值仍然是 0,未被修改
17return 0;
18}

使用 mutable 关键字后,Lambda 表达式的函数调用运算符 operator() 默认变为 const 成员函数。这意味着 mutable 关键字实际上是在修改 Lambda 表达式对象的状态,而不是修改外部变量。值捕获的变量在 Lambda 表达式内部相当于成员变量,mutable 允许修改这些成员变量。

引用捕获 (Capture by Reference)
对于引用捕获的变量,Lambda 表达式内部访问的是外部变量的引用。因此,在 Lambda 表达式内部对捕获的变量进行修改会直接影响外部变量,无需使用 mutable 关键字。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2int main() {
3int count = 0;
4// 引用捕获 count
5auto increment_ref_lambda = [&count]() {
6count++; // OK,修改的是外部变量 count
7std::cout << "Count in ref lambda: " << count << std::endl;
8};
9increment_ref_lambda(); // 调用 increment_ref_lambda,修改的是外部 count
10increment_ref_lambda();
11std::cout << "Count in main: " << count << std::endl; // 外部 count 的值被修改为 2
12return 0;
13}

引用捕获可以直接修改外部变量,但需要注意生命周期 (Lifetime) 的问题。如果 Lambda 表达式的生命周期超过了被引用变量的生命周期,可能会导致悬空引用 (Dangling Reference),引发未定义行为 (Undefined Behavior)。因此,在使用引用捕获时,要确保被引用变量的生命周期足够长。

选择捕获方式的原则

只读访问外部变量:优先使用值捕获 [=]。值捕获更安全,避免了生命周期问题,且 Lambda 表达式的行为与外部变量的变化无关,更易于理解和维护。
需要修改外部变量:使用引用捕获 [&]。但要谨慎处理生命周期问题,确保被引用变量在 Lambda 表达式使用期间始终有效。
需要在 Lambda 表达式内部维护状态:使用值捕获 + mutable。但这通常意味着 Lambda 表达式具有了状态,可能更适合使用普通的函数对象类来替代,以提高代码的可读性和可维护性。

总之,选择合适的捕获方式需要根据具体的需求和场景进行权衡。在大多数情况下,值捕获是更安全、更推荐的选择。引用捕获mutable 关键字在特定场景下也很有用,但需要谨慎使用。

5.3.3 Lambda 表达式的应用场景与最佳实践 (Application Scenarios and Best Practices of Lambda Expressions)

Lambda 表达式以其简洁、内联的特性,在 C++ 编程中得到了广泛的应用。以下是一些常见的应用场景和最佳实践:

作为算法的谓词或操作 (Predicate or Operation for Algorithms)
这是 Lambda 表达式最常见的应用场景。标准库的算法 (Algorithms) 经常需要接受函数对象作为参数,用于定制算法的行为,例如 std::sort 的比较函数、std::find_if 的条件判断、std::transform 的元素变换等。Lambda 表达式可以方便地在算法调用处直接定义这些函数对象,提高代码的简洁性和可读性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> nums = {3, -1, 4, -2, 5, -3};
6// 使用 Lambda 表达式作为 std::sort 的比较函数,按绝对值升序排序
7std::sort(nums.begin(), nums.end(), [](int a, int b) {
8return std::abs(a) < std::abs(b);
9});
10std::cout << "按绝对值排序: ";
11for (int num : nums) {
12std::cout << num << " "; // 输出 -1 -2 -3 3 4 5
13}
14std::cout << std::endl;
15// 使用 Lambda 表达式作为 std::find_if 的谓词,查找第一个负数
16auto it_negative = std::find_if(nums.begin(), nums.end(), [](int num) {
17return num < 0;
18});
19if (it_negative != nums.end()) {
20std::cout << "第一个负数: " << *it_negative << std::endl; // 输出 -1
21}
22// 使用 Lambda 表达式作为 std::transform 的操作,将所有元素平方
23std::vector<int> squared_nums;
24std::transform(nums.begin(), nums.end(), std::back_inserter(squared_nums), [](int num) {
25return num * num;
26});
27std::cout << "元素平方: ";
28for (int num : squared_nums) {
29std::cout << num << " "; // 输出 1 4 9 9 16 25
30}
31std::cout << std::endl;
32return 0;
33}

作为回调函数 (Callback Function)
Lambda 表达式可以作为回调函数,用于事件处理、异步操作等场景。相比于函数指针,Lambda 表达式更加类型安全,并且可以方便地捕获上下文信息。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <functional>
3// 模拟一个事件处理函数,接受一个回调函数
4void process_event(std::function<void(int)> callback) {
5int event_data = 100; // 模拟事件数据
6callback(event_data); // 调用回调函数处理事件
7}
8int main() {
9int factor = 5;
10// 使用 Lambda 表达式作为回调函数,处理事件
11process_event([factor](int data) {
12std::cout << "Event data: " << data << std::endl;
13std::cout << "Multiplied data: " << data * factor << std::endl; // 捕获 factor 并使用
14});
15return 0;
16}

简化代码,提高可读性 (Simplify Code and Improve Readability)
对于一些简单的、局部的函数对象,使用 Lambda 表达式可以避免定义额外的命名函数或函数对象类,使代码更加简洁、紧凑,提高可读性。尤其在算法调用、回调函数等场景,Lambda 表达式可以使代码逻辑更加清晰。

内联优化 (Inlining Optimization)
Lambda 表达式通常是内联的,编译器更容易进行内联优化,消除函数调用的开销,提高程序执行效率。

最佳实践建议

保持 Lambda 表达式的简洁性:Lambda 表达式的主要优势在于简洁。对于复杂的逻辑,建议使用普通的命名函数或函数对象类来替代,以提高代码的可维护性。
谨慎使用引用捕获:引用捕获可能导致生命周期问题。优先使用值捕获,除非确实需要修改外部变量。
合理选择捕获方式:根据需求选择合适的捕获方式,避免过度捕获。只捕获 Lambda 表达式真正需要的外部变量。
显式指定返回类型 (可选):对于复杂的 Lambda 表达式,或者返回类型不易推导的情况,可以显式指定返回类型,提高代码的可读性和可维护性。
避免在 Lambda 表达式中进行复杂的控制流:如果 Lambda 函数体过于复杂(例如包含多层循环、条件分支等),可能意味着 Lambda 表达式承担了过多的责任,应该考虑将其拆分为更小的、更具模块化的函数或函数对象。

总而言之,Lambda 表达式是 C++ 中一项强大的特性,它简化了函数对象的创建和使用,提高了代码的简洁性和灵活性。合理使用 Lambda 表达式可以使 C++ 代码更加现代化、高效。

<END_OF_CHAPTER/>

6. 输入/输出 (Input/Output)

本章全面讲解 C++ 标准库的 I/O 流 (iostream) 库,包括流的概念、流类层次结构、格式化输入输出、文件 I/O 和字符串流,帮助读者掌握 C++ 的 I/O 操作。

6.1 I/O 流的概念与流类库 (I/O Stream Concepts and Stream Class Library)

本节介绍 I/O 流 (I/O Stream) 的基本概念、流的分类,以及 iostream 库的类层次结构,例如 istream (输入流), ostream (输出流), iostream (输入输出流), fstream (文件流), sstream (字符串流) 等。

6.1.1 流的概念:数据流与设备 (Stream Concepts: Data Streams and Devices)

在 C++ 中,I/O 操作是基于流 (stream) 的概念进行的。可以将流比作数据流动管道 🌊,它连接了数据源 (data source)数据目的地 (data destination)。这种抽象层使得程序可以以统一的方式处理来自不同来源或去往不同目的地的数据,而无需关心底层设备的具体细节。

数据流 (data stream)
数据流是程序与外部世界(如键盘、屏幕、文件、网络等)之间数据传输的抽象表示。数据以字节序列 (sequence of bytes) 的形式在流中流动。

▮▮▮▮ⓐ 输入流 (input stream):用于从数据源读取数据到程序中。例如,从键盘读取用户输入,或者从文件中读取数据。
▮▮▮▮ⓑ 输出流 (output stream):用于将程序中的数据写入到数据目的地。例如,将结果输出到屏幕,或者将数据写入到文件中。
▮▮▮▮ⓒ 输入/输出流 (input/output stream):同时支持输入和输出操作的流。

设备 (device)
设备是数据流的实际来源或目的地。C++ 标准库预定义了一些标准流对象 (standard stream objects),它们与特定的设备关联:

▮▮▮▮ⓐ std::cin标准输入流 (standard input stream),通常关联到键盘 (keyboard)。用于从键盘读取用户输入。
▮▮▮▮ⓑ std::cout标准输出流 (standard output stream),通常关联到终端 (terminal)控制台 (console)。用于向屏幕输出普通信息。
▮▮▮▮ⓒ std::cerr标准错误流 (standard error stream),通常关联到终端控制台。用于输出错误信息,通常不经过缓冲 (unbuffered),立即输出。
▮▮▮▮ⓓ std::clog标准日志流 (standard log stream),通常关联到终端控制台。用于输出日志信息,经过缓冲 (buffered)

除了标准流,还可以创建与文件 (file)字符串 (string) 等设备关联的流,以进行文件 I/O 和内存 I/O 操作。

流的状态 (stream state)
每个流对象都维护着一组状态标志 (state flags),用于表示流的当前状态,例如是否发生错误、是否到达文件末尾等。常用的状态标志包括:

▮▮▮▮ⓐ goodbit:无错误发生,流状态良好。
▮▮▮▮ⓑ eofbit:已到达文件末尾 (end-of-file)。
▮▮▮▮ⓒ failbit:发生逻辑错误,通常是格式错误或数据丢失,但流仍然可用。例如,尝试读取一个整数,但输入的是字符。
▮▮▮▮ⓓ badbit:发生严重错误,通常是 I/O 操作失败或流缓冲区损坏,流可能不可用。例如,磁盘错误。

可以使用流对象的成员函数(如 good(), eof(), fail(), bad())来检查流的状态,或者使用 rdstate() 函数获取所有状态标志的组合。可以使用 clear() 函数清除流的状态标志,使流恢复到良好状态。

6.1.2 iostream 库的类层次结构 (Class Hierarchy of iostream Library)

C++ 标准库的 iostream 库提供了一系列类,用于支持各种 I/O 操作。这些类通过继承 (inheritance) 形成了一个层次结构,提供了丰富的功能和灵活性。以下是 iostream 库的主要类及其关系:

基类 (Base Classes)

▮▮▮▮ⓐ ios_base:作为 iostream 库的最基类 (most base class),定义了与流状态、格式化标志、区域设置 (locale) 等国际化 (internationalization) 相关的功能。它不处理任何数据传输,只提供通用的接口和状态管理。

▮▮▮▮ⓑ ios:从 ios_base 派生而来,增加了缓冲区 (buffer) 和与字符流 (character stream) 相关的操作。它管理着流的缓冲区,并提供了格式化输入输出的基础功能。ios 类是一个抽象基类 (abstract base class),不能直接实例化,但作为 istreamostream 的基类。

输入/输出流类 (Input/Output Stream Classes)

▮▮▮▮ⓐ istream:从 ios 派生而来,提供了输入操作 (input operations) 的接口。例如,operator>> (提取运算符) 用于从输入流中提取数据。cin, cerr, clog, ifstream, istringstream, iostream, fstream, stringstream 等类都间接或直接继承自 istream

▮▮▮▮ⓑ ostream:从 ios 派生而来,提供了输出操作 (output operations) 的接口。例如,operator<< (插入运算符) 用于将数据插入到输出流中。cout, cerr, clog, ofstream, ostringstream, iostream, fstream, stringstream 等类都间接或直接继承自 ostream

▮▮▮▮ⓒ iostream多重继承 (multiple inheritance)istreamostream,提供了双向 I/O 操作 (bidirectional I/O operations) 的接口。这意味着 iostream 对象可以同时进行输入和输出操作。fstreamstringstream 等类继承自 iostream

文件流类 (File Stream Classes)

▮▮▮▮ⓐ ifstream:从 istream 派生而来,用于文件输入 (file input)。提供了打开文件进行读取操作的功能。

▮▮▮▮ⓑ ofstream:从 ostream 派生而来,用于文件输出 (file output)。提供了打开文件进行写入操作的功能。

▮▮▮▮ⓒ fstream:从 iostream 派生而来,用于文件输入/输出 (file input/output)。提供了打开文件进行双向读写操作的功能。

字符串流类 (String Stream Classes)

▮▮▮▮ⓐ istringstream:从 istream 派生而来,用于字符串输入 (string input)。可以将字符串作为输入流进行读取操作,常用于字符串解析。

▮▮▮▮ⓑ ostringstream:从 ostream 派生而来,用于字符串输出 (string output)。可以将数据输出到字符串流中,然后获取格式化后的字符串,常用于格式化字符串。

▮▮▮▮ⓒ stringstream:从 iostream 派生而来,用于字符串输入/输出 (string input/output)。可以同时进行字符串的读取和写入操作。

iostream 库类层次结构图 (Class Hierarchy Diagram):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1graph LR
2ios_base --> ios
3ios --> istream
4ios --> ostream
5istream --> ifstream
6ostream --> ofstream
7istream & ostream --> iostream
8iostream --> fstream
9istream --> istringstream
10ostream --> ostringstream
11iostream --> stringstream
12subgraph "文件流 (File Streams)"
13ifstream
14ofstream
15fstream
16end
17subgraph "字符串流 (String Streams)"
18istringstream
19ostringstream
20stringstream
21end
22subgraph "标准流 (Standard Streams)"
23cin
24cout
25cerr
26clog
27end
28cin -- 间接继承 --> istream
29cout -- 间接继承 --> ostream
30cerr -- 间接继承 --> ostream
31clog -- 间接继承 --> ostream

了解 iostream 库的类层次结构有助于理解各个流类的功能和关系,从而更好地选择和使用合适的流类进行 I/O 操作。

6.2 格式化输入输出 (Formatted Input/Output)

本节讲解如何使用格式控制符 (format control flags)操纵符 (manipulators) 来实现格式化输入输出,包括精度控制、宽度设置、进制转换等,使输出结果更符合需求,输入处理更灵活。

6.2.1 格式控制符与操纵符 (Format Control Flags and Manipulators)

C++ iostream 库提供了多种格式控制机制,可以控制输入输出的格式,例如数值的精度、宽度、进制,字符串的对齐方式等。这些控制机制主要通过格式控制符 (format control flags)操纵符 (manipulators) 实现。

格式控制符 (Format Control Flags)

格式控制符是 ios_base 类中定义的一组枚举常量 (enumeration constants),用于设置流的格式状态。可以使用 ios::setf() 函数设置格式标志,使用 ios::unsetf() 函数取消设置格式标志,使用 flags() 函数获取当前所有格式标志。

常用的格式控制符及其功能:

▮▮▮▮ⓐ 布尔值格式 (Boolean Format)
▮▮▮▮▮▮▮▮⚝ std::ios::boolalpha:以字母形式 (alphabetical) 输出布尔值 (truefalse)。
▮▮▮▮▮▮▮▮⚝ std::ios::noboolalpha:以数值形式 (numeric) 输出布尔值 (10),默认格式。

▮▮▮▮ⓑ 整数进制 (Integer Base)
▮▮▮▮▮▮▮▮⚝ std::ios::dec:以十进制 (decimal) 格式输出整数,默认格式。
▮▮▮▮▮▮▮▮⚝ std::ios::hex:以十六进制 (hexadecimal) 格式输出整数。
▮▮▮▮▮▮▮▮⚝ std::ios::oct:以八进制 (octal) 格式输出整数。
▮▮▮▮▮▮▮▮⚝ std::ios::showbase:在输出十六进制八进制数时,显示进制前缀 (base prefix) (0x0)。
▮▮▮▮▮▮▮▮⚝ std::ios::noshowbase:不显示进制前缀,默认格式。
▮▮▮▮▮▮▮▮⚝ std::ios::uppercase:在输出十六进制数时,使用大写字母 (uppercase letters) (A-F)。
▮▮▮▮▮▮▮▮⚝ std::ios::nouppercase:在输出十六进制数时,使用小写字母 (lowercase letters) (a-f),默认格式。

▮▮▮▮ⓒ 浮点数格式 (Floating-Point Format)
▮▮▮▮▮▮▮▮⚝ std::ios::fixed:以定点表示法 (fixed-point notation) 输出浮点数,即小数点固定,例如 3.14159
▮▮▮▮▮▮▮▮⚝ std::ios::scientific:以科学计数法 (scientific notation) 输出浮点数,例如 3.14159e+00
▮▮▮▮▮▮▮▮⚝ std::ios::defaultfloat:使用默认浮点数格式,由编译器决定,通常是标准表示法 (standard notation),根据数值大小自动选择定点或科学计数法。C++11 新增。
▮▮▮▮▮▮▮▮⚝ std::ios::showpoint:总是显示小数点 (decimal point),即使是整数部分。
▮▮▮▮▮▮▮▮⚝ std::ios::noshowpoint:只有在必要时才显示小数点,默认格式。
▮▮▮▮▮▮▮▮⚝ std::ios::uppercase:在输出科学计数法时,使用大写字母 (E) 表示指数。
▮▮▮▮▮▮▮▮⚝ std::ios::nouppercase:在输出科学计数法时,使用小写字母 (e) 表示指数,默认格式。

▮▮▮▮ⓓ 字段宽度与对齐 (Field Width and Alignment)
▮▮▮▮▮▮▮▮⚝ std::ios::internal内对齐 (internal alignment),符号或进制前缀左对齐,数值右对齐,中间填充空格。
▮▮▮▮▮▮▮▮⚝ std::ios::left左对齐 (left alignment),输出内容左对齐,右侧填充空格。
▮▮▮▮▮▮▮▮⚝ std::ios::right右对齐 (right alignment),输出内容右对齐,左侧填充空格,默认格式。

▮▮▮▮ⓔ 缓冲区刷新 (Buffer Flushing)
▮▮▮▮▮▮▮▮⚝ std::ios::unitbuf:在每次输出操作后刷新缓冲区 (flush buffer)
▮▮▮▮▮▮▮▮⚝ std::ios::nounitbuf:由系统决定何时刷新缓冲区,默认格式。
▮▮▮▮▮▮▮▮⚝ std::ios::skipws:在输入操作时,跳过空白字符 (skip whitespace characters),例如空格、制表符、换行符,默认格式。
▮▮▮▮▮▮▮▮⚝ std::ios::noskipws:在输入操作时,不跳过空白字符,将空白字符也作为输入的一部分。

操纵符 (Manipulators)

操纵符是一些预定义的函数 (predefined functions)对象 (objects),可以插入 (insert)提取 (extract) 到流中,用于修改流的格式状态或执行特定的 I/O 操作。操纵符通常比格式控制符更方便易用。

操纵符分为两类:

▮▮▮▮ⓐ 有参操纵符 (Parameterized Manipulators):需要参数 (parameters) 的操纵符,定义在 <iomanip> 头文件中。例如:
▮▮▮▮▮▮▮▮⚝ std::setw(int width):设置字段宽度 (field width)width 个字符。只对下一个输出项 (next output item) 有效。
▮▮▮▮▮▮▮▮⚝ std::setprecision(int precision):设置浮点数精度 (floating-point precision)precision 位有效数字(对于默认浮点数格式和科学计数法)或小数点后 precision 位(对于定点表示法)。
▮▮▮▮▮▮▮▮⚝ std::setfill(char fillchar):设置填充字符 (fill character)fillchar,默认为空格。用于填充字段宽度不足的部分。
▮▮▮▮▮▮▮▮⚝ std::setbase(int base):设置整数进制 (integer base)basebase 可以是 10 (十进制), 16 (十六进制), 8 (八进制) 或 0 (默认进制,由前缀决定)。

▮▮▮▮ⓑ 无参操纵符 (Non-parameterized Manipulators):不需要参数的操纵符,定义在 <iostream><iomanip> 头文件中。例如:
▮▮▮▮▮▮▮▮⚝ std::endl:插入换行符 (newline character)刷新缓冲区
▮▮▮▮▮▮▮▮⚝ std::ends:插入空字符 (null character),通常用于输出 C 风格字符串。
▮▮▮▮▮▮▮▮⚝ std::flush刷新缓冲区,将缓冲区中的内容立即输出。
▮▮▮▮▮▮▮▮⚝ std::ws:在输入流中提取并丢弃 (extract and discard) 所有前导空白字符 (leading whitespace characters)
▮▮▮▮▮▮▮▮⚝ std::boolalpha, std::noboolalpha, std::dec, std::hex, std::oct, std::showbase, std::noshowbase, std::uppercase, std::nouppercase, std::fixed, std::scientific, std::defaultfloat, std::showpoint, std::noshowpoint, std::left, std::right, std::internal, std::unitbuf, std::nounitbuf, std::skipws, std::noskipws:这些无参操纵符同名的格式控制符功能相同,但使用起来更简洁。

代码示例:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <iomanip>
3int main() {
4double pi = 3.141592653589793;
5int num = 255;
6bool flag = true;
7std::cout << "默认输出:" << std::endl;
8std::cout << pi << " " << num << " " << flag << std::endl; // 默认输出: 3.14159 255 1
9std::cout << "\n格式化输出:" << std::endl;
10std::cout << std::fixed << std::setprecision(2) << "pi = " << pi << std::endl; // 定点表示,精度2位: pi = 3.14
11std::cout << std::scientific << std::setprecision(3) << "pi = " << pi << std::endl; // 科学计数法,精度3位: pi = 3.142e+00
12std::cout << std::defaultfloat << std::setprecision(6) << "pi = " << pi << std::endl; // 默认浮点数格式,精度6位: pi = 3.14159
13std::cout << std::dec << "dec = " << num << std::endl; // 十进制: dec = 255
14std::cout << std::hex << std::showbase << std::uppercase << "hex = " << num << std::endl; // 十六进制,显示前缀,大写: hex = 0XFF
15std::cout << std::oct << std::showbase << "oct = " << num << std::endl; // 八进制,显示前缀: oct = 0377
16std::cout << std::boolalpha << "bool = " << flag << std::endl; // 布尔值字母形式: bool = true
17std::cout << std::noboolalpha << "bool = " << flag << std::endl; // 布尔值数值形式: bool = 1
18std::cout << std::setw(10) << std::left << "left" << std::setw(10) << std::right << "right" << std::endl; // 字段宽度10,左对齐,右对齐: left right
19return 0;
20}

6.2.2 自定义操纵符 (Custom Manipulators)

除了标准库提供的操纵符,还可以自定义操纵符 (custom manipulators),以满足特定的格式化输出需求。自定义操纵符可以是无参的 (parameterless)有参的 (parameterized)

无参自定义操纵符 (Parameterless Custom Manipulators)

无参自定义操纵符通常是一个函数 (function),它接受一个流对象作为引用参数 (reference parameter),并返回相同的流对象引用。函数内部可以执行任何流操作,例如设置格式状态、插入特定内容等。

示例:自定义一个输出分隔线的操纵符 separator

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <string>
3// 无参自定义操纵符:输出分隔线
4std::ostream& separator(std::ostream& os) {
5return os << "--------------------\n";
6}
7int main() {
8std::cout << "开始输出数据" << separator;
9std::cout << "数据 1" << std::endl;
10std::cout << "数据 2" << std::endl;
11std::cout << separator << "输出结束" << std::endl;
12return 0;
13}

有参自定义操纵符 (Parameterized Custom Manipulators)

有参自定义操纵符通常是一个类 (class)函数对象 (function object),它重载了 operator<<operator>> 运算符,接受流对象和参数,并返回一个辅助对象 (helper object)。这个辅助对象再重载 operator<<operator>> 运算符,接受流对象,并执行实际的流操作。这种方式比较复杂,但可以实现更灵活的参数传递。

更常见且简便的方法是使用Lambda 表达式 (Lambda expressions) 来创建有参自定义操纵符。

示例:自定义一个设置输出精度的操纵符 setprecision_custom(int p)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <iomanip>
3// 有参自定义操纵符:设置输出精度
4std::ostream& setprecision_custom(std::ostream& os, int p) {
5return os << std::setprecision(p);
6}
7// 使用 Lambda 表达式创建有参操纵符
8auto precision = [](int p) {
9return [&](std::ostream& os) -> std::ostream& {
10return setprecision_custom(os, p);
11};
12};
13int main() {
14double pi = 3.141592653589793;
15std::cout << "默认精度: " << pi << std::endl; // 默认精度: 3.14159
16std::cout << "自定义精度 3: " << precision(3) << pi << std::endl; // 自定义精度 3: 3.142
17std::cout << "自定义精度 5: " << precision(5) << pi << std::endl; // 自定义精度 5: 3.14159
18return 0;
19}

在实际应用中,自定义操纵符可以帮助封装 (encapsulate) 复杂的格式化逻辑,提高代码的可读性和可维护性。例如,可以自定义操纵符来输出特定格式的日期、时间、货币等。

6.3 文件 I/O 与字符串流 (File I/O and String Streams)

本节讲解如何使用 fstream 类进行文件读写操作 (file read and write operations),以及如何使用 stringstream 类进行内存中的字符串流操作 (string stream operations in memory)。文件 I/O 和字符串流是 C++ I/O 操作中非常重要的组成部分。

6.3.1 文件流:ifstream, ofstream, fstream (File Streams)

C++ 标准库提供了 ifstream (文件输入流), ofstream (文件输出流), fstream (文件输入/输出流) 三个类,用于文件 I/O 操作。这些类都继承自 iostream 库的基类,提供了文件操作的接口。

文件打开模式 (File Open Modes)

在打开文件时,需要指定文件打开模式 (file open mode),用于控制文件的打开方式和操作权限。文件打开模式通过 ios_base::openmode 枚举类型定义,可以组合使用。

常用的文件打开模式及其含义:

▮▮▮▮ⓐ std::ios::in输入模式 (input mode),打开文件用于读取 (read)ifstream 默认以 in 模式打开。
▮▮▮▮ⓑ std::ios::out输出模式 (output mode),打开文件用于写入 (write)ofstream 默认以 out 模式打开。如果文件已存在,默认截断 (truncate) 文件内容,即清空文件。
▮▮▮▮ⓒ std::ios::app追加模式 (append mode),打开文件用于追加写入 (append write)。每次写入操作都会在文件末尾添加内容,不会截断文件。
▮▮▮▮ⓓ std::ios::ate到达末尾模式 (at-end mode),打开文件后,文件指针 (file pointer) 移动到文件末尾。可以从文件任意位置开始读写。
▮▮▮▮ⓔ std::ios::trunc截断模式 (truncate mode),如果文件已存在,打开文件时清空 (clear) 文件内容。ofstream 默认以 out | trunc 模式打开。
▮▮▮▮ⓕ std::ios::binary二进制模式 (binary mode),以二进制 (binary) 方式读写文件,而不是文本 (text) 方式。在二进制模式下,不会进行字符编码转换和行尾转换。文本模式下,可能会进行行尾转换(例如,将 \r\n 转换为 \n)。

文件打开模式可以使用位或运算符 (|) 组合,例如 std::ios::in | std::ios::binary 表示以二进制输入模式打开文件。

文件流类的使用 (Usage of File Stream Classes)

▮▮▮▮ⓐ 创建文件流对象 (Create File Stream Objects)

▮▮▮▮▮▮▮▮⚝ ifstream fin;:创建 ifstream 对象 fin,用于文件输入。
▮▮▮▮▮▮▮▮⚝ ofstream fout;:创建 ofstream 对象 fout,用于文件输出。
▮▮▮▮▮▮▮▮⚝ fstream fio;:创建 fstream 对象 fio,用于文件输入/输出。

▮▮▮▮ⓑ 打开文件 (Open File)

▮▮▮▮▮▮▮▮⚝ 使用文件流对象的 open() 成员函数打开文件,需要指定文件路径 (file path)文件打开模式 (file open mode)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1fin.open("input.txt", std::ios::in); // 以输入模式打开 input.txt
2fout.open("output.txt", std::ios::out); // 以输出模式打开 output.txt
3fio.open("data.bin", std::ios::in | std::ios::out | std::ios::binary); // 以二进制输入/输出模式打开 data.bin

▮▮▮▮▮▮▮▮⚝ 可以在创建文件流对象时,直接在构造函数 (constructor) 中指定文件路径和打开模式,简化代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1ifstream fin("input.txt"); // 默认以输入模式打开
2ofstream fout("output.txt"); // 默认以输出模式打开
3fstream fio("data.bin", std::ios::in | std::ios::out | std::ios::binary);

▮▮▮▮ⓒ 检查文件是否成功打开 (Check if File Opened Successfully)

▮▮▮▮▮▮▮▮⚝ 使用文件流对象的 is_open() 成员函数检查文件是否成功打开。如果返回 true,表示文件已成功打开;如果返回 false,表示文件打开失败。文件打开失败可能是由于文件不存在、权限不足等原因。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1if (!fin.is_open()) {
2std::cerr << "Error opening input file!" << std::endl;
3return 1; // 返回错误代码
4}

▮▮▮▮▮▮▮▮⚝ 文件流对象在布尔上下文 (boolean context) 中会被隐式转换为 bool 类型,文件成功打开返回 true,否则返回 false。可以更简洁地检查文件打开状态:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1if (!fin) { // 等价于 !fin.is_open()
2std::cerr << "Error opening input file!" << std::endl;
3return 1;
4}

▮▮▮▮ⓓ 文件读写操作 (File Read and Write Operations)

▮▮▮▮▮▮▮▮⚝ 文件打开成功后,可以使用与 cincout 类似的 提取运算符 >> (extraction operator)插入运算符 << (insertion operator) 进行文件读写操作。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1// 从文件读取数据
2int num;
3fin >> num;
4std::string line;
5std::getline(fin, line); // 读取一行
6// 向文件写入数据
7fout << "The answer is: " << num << std::endl;
8fout << line << "\n";

▮▮▮▮▮▮▮▮⚝ 可以使用文件流对象的其他成员函数进行更精细的文件读写操作,例如:
▮▮▮▮▮▮▮▮⚝ get():读取单个字符 (single character)
▮▮▮▮▮▮▮▮⚝ getline():读取一行字符 (line of characters)
▮▮▮▮▮▮▮▮⚝ read():读取指定数量的字节 (specified number of bytes) (二进制读取)。
▮▮▮▮▮▮▮▮⚝ write():写入指定数量的字节 (二进制写入)。

▮▮▮▮ⓔ 关闭文件 (Close File)

▮▮▮▮▮▮▮▮⚝ 使用文件流对象的 close() 成员函数显式关闭 (explicitly close) 文件。文件流对象析构时 (upon destruction) 会自动关闭文件,但显式关闭是一个良好的编程习惯,可以立即释放文件资源,并确保缓冲区中的数据被刷新 (flushed) 到磁盘。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1fin.close();
2fout.close();
3fio.close();

代码示例:文件读写操作

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <fstream>
3#include <string>
4int main() {
5std::ofstream fout("example.txt"); // 创建 ofstream 对象并打开文件 example.txt
6if (!fout) {
7std::cerr << "Error opening output file!" << std::endl;
8return 1;
9}
10fout << "Hello, File I/O!" << std::endl;
11fout << "This is a test." << std::endl;
12fout.close(); // 显式关闭输出文件
13std::ifstream fin("example.txt"); // 创建 ifstream 对象并打开文件 example.txt
14if (!fin) {
15std::cerr << "Error opening input file!" << std::endl;
16return 1;
17}
18std::string line;
19std::cout << "File content:\n";
20while (std::getline(fin, line)) { // 逐行读取文件内容
21std::cout << line << std::endl;
22}
23fin.close(); // 显式关闭输入文件
24return 0;
25}

文件定位 (File Positioning)

文件流对象维护着一个文件指针 (file pointer),用于指示当前读写位置。可以使用文件流对象的成员函数来移动 (move) 文件指针,实现随机访问 (random access) 文件内容。

▮▮▮▮⚝ 获取当前文件指针位置 (Get Current File Pointer Position)
▮▮▮▮▮▮▮▮⚝ tellg() (for input streams): 返回读取 (get) 位置的文件指针。
▮▮▮▮▮▮▮▮⚝ tellp() (for output streams): 返回写入 (put) 位置的文件指针。

▮▮▮▮⚝ 设置文件指针位置 (Set File Pointer Position)
▮▮▮▮▮▮▮▮⚝ seekg(std::streampos pos) (for input streams): 设置绝对读取位置 (absolute get position)pos
▮▮▮▮▮▮▮▮⚝ seekp(std::streampos pos) (for output streams): 设置绝对写入位置 (absolute put position)pos
▮▮▮▮▮▮▮▮⚝ seekg(std::streamoff off, std::ios::seekdir dir) (for input streams): 设置相对读取位置 (relative get position),相对于 dir 偏移 off 字节。
▮▮▮▮▮▮▮▮⚝ seekp(std::streamoff off, std::ios::seekdir dir) (for output streams): 设置相对写入位置 (relative put position),相对于 dir 偏移 off 字节。

std::ios::seekdir 枚举类型定义了文件指针移动的参考位置 (reference positions)
▮▮▮▮▮▮▮▮⚝ std::ios::beg:文件开始位置 (beginning)
▮▮▮▮▮▮▮▮⚝ std::ios::cur:文件当前位置 (current position)
▮▮▮▮▮▮▮▮⚝ std::ios::end:文件末尾位置 (end)

代码示例:文件随机访问

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <fstream>
3int main() {
4std::ofstream fout("random_access.txt");
5if (!fout) {
6std::cerr << "Error opening output file!" << std::endl;
7return 1;
8}
9fout << "0123456789"; // 写入字符串
10fout.close();
11std::fstream fio("random_access.txt", std::ios::in | std::ios::out);
12if (!fio) {
13std::cerr << "Error opening file for read/write!" << std::endl;
14return 1;
15}
16char ch;
17fio.seekg(5, std::ios::beg); // 定位到文件开始位置偏移 5 字节处 (第 6 个字符 '5')
18fio >> ch;
19std::cout << "Character at position 5: " << ch << std::endl; // 输出 '5'
20fio.seekp(7, std::ios::beg); // 定位到文件开始位置偏移 7 字节处 (第 8 个字符 '7')
21fio << 'X'; // 将 '7' 替换为 'X'
22fio.seekg(0, std::ios::beg); // 定位到文件开始位置
23std::cout << "Modified file content: ";
24while (fio >> ch) {
25std::cout << ch; // 输出修改后的文件内容 "0123456X89"
26}
27std::cout << std::endl;
28fio.close();
29return 0;
30}

6.3.2 字符串流:istringstream, ostringstream, stringstream (String Streams)

C++ 标准库提供了 istringstream (字符串输入流), ostringstream (字符串输出流), stringstream (字符串输入/输出流) 三个类,用于在内存中进行字符串 I/O 操作 (string I/O operations in memory)。字符串流可以将字符串当作流来处理,实现字符串的格式化、解析、转换等功能。

字符串流类的使用 (Usage of String Stream Classes)

▮▮▮▮ⓐ 创建字符串流对象 (Create String Stream Objects)

▮▮▮▮▮▮▮▮⚝ istringstream iss(str);:创建 istringstream 对象 iss,并将字符串 str 初始化为输入源。
▮▮▮▮▮▮▮▮⚝ ostringstream oss;:创建 ostringstream 对象 oss,用于输出到字符串。
▮▮▮▮▮▮▮▮⚝ stringstream ss;:创建 stringstream 对象 ss,用于双向字符串 I/O。

▮▮▮▮ⓑ 从字符串流读取数据 (Read Data from String Stream)

▮▮▮▮▮▮▮▮⚝ 使用 istringstream 对象 iss提取运算符 >> 从字符串中解析 (parse) 数据。类似于从 cin 读取数据。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::string str = "123 3.14 hello";
2std::istringstream iss(str);
3int num;
4double pi;
5std::string word;
6iss >> num >> pi >> word; // 从字符串流中提取数据
7std::cout << "num = " << num << std::endl; // 输出 123
8std::cout << "pi = " << pi << std::endl; // 输出 3.14
9std::cout << "word = " << word << std::endl; // 输出 hello

▮▮▮▮ⓒ 向字符串流写入数据 (Write Data to String Stream)

▮▮▮▮▮▮▮▮⚝ 使用 ostringstream 对象 oss插入运算符 << 将数据格式化 (format) 输出到字符串流中。类似于向 cout 输出数据。可以使用操纵符 (manipulators) 进行格式控制。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::ostringstream oss;
2int age = 30;
3double height = 1.75;
4std::string name = "Alice";
5oss << "Name: " << name << ", Age: " << age << ", Height: " << std::fixed << std::setprecision(2) << height;
6std::string formatted_str = oss.str(); // 获取格式化后的字符串
7std::cout << formatted_str << std::endl; // 输出 "Name: Alice, Age: 30, Height: 1.75"

▮▮▮▮ⓓ 清空字符串流 (Clear String Stream)

▮▮▮▮▮▮▮▮⚝ 使用字符串流对象的 str("") 成员函数清空 (clear) 字符串流的内容。或者使用 clear() 函数清除流状态标志,并使用 str("") 设置空字符串。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::stringstream ss;
2ss << "Initial content";
3std::cout << "Initial string: " << ss.str() << std::endl; // 输出 "Initial string: Initial content"
4ss.str(""); // 清空字符串流内容
5ss << "New content";
6std::cout << "New string: " << ss.str() << std::endl; // 输出 "New string: New content"

代码示例:字符串流应用

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <sstream>
3#include <string>
4#include <iomanip>
5int main() {
6// ostringstream 示例:格式化字符串
7std::ostringstream oss;
8int width = 10;
9int value = 42;
10oss << std::setw(width) << std::right << value;
11std::string formatted_value = oss.str();
12std::cout << "Formatted value: \"" << formatted_value << "\"" << std::endl; // 输出 "Formatted value: " 42""
13// istringstream 示例:字符串解析
14std::string data = "name=Bob;age=25;city=NewYork";
15std::istringstream iss(data);
16std::string segment;
17std::cout << "\nParsed data segments:\n";
18while (std::getline(iss, segment, ';')) { // 以 ';' 为分隔符逐段读取字符串
19std::cout << "Segment: " << segment << std::endl;
20}
21// stringstream 示例:类型转换
22std::stringstream ss;
23int number = 123;
24ss << number; // 将整数写入字符串流
25std::string str_number = ss.str(); // 获取字符串
26std::cout << "\nInteger to string: " << str_number << std::endl; // 输出 "Integer to string: 123"
27ss.str(""); // 清空字符串流
28ss.clear(); // 清除流状态
29ss << "456";
30int another_number;
31ss >> another_number; // 从字符串流中读取整数
32std::cout << "String to integer: " << another_number << std::endl; // 输出 "String to integer: 456"
33return 0;
34}

字符串流在很多场景下都非常有用,例如:

格式化输出 (formatted output):将各种类型的数据格式化为字符串。
字符串解析 (string parsing):从字符串中提取数据,例如解析配置文件、日志文件等。
类型转换 (type conversion):在字符串和数值类型之间进行转换。
内存 I/O (memory I/O):在内存中进行数据缓冲和处理。

掌握文件流和字符串流的使用,可以有效地处理文件和字符串数据,提高程序的 I/O 处理能力。

<END_OF_CHAPTER/>

7. 内存管理 (Memory Management)

本章深入探讨 C++ 标准库提供的智能指针 (Smart Pointers) 和分配器 (Allocators),讲解 RAII (Resource Acquisition Is Initialization) 原则在内存管理中的应用,以及自定义分配器的实现。

7.1 智能指针 (Smart Pointers)

本节详细讲解 unique_ptr (独占指针), shared_ptr (共享指针), weak_ptr (弱指针) 的原理、使用场景和最佳实践,以及它们如何实现自动内存管理。

7.1.1 RAII 与智能指针 (RAII and Smart Pointers)

RAII (Resource Acquisition Is Initialization,资源获取即初始化) 是一种 C++ 编程技术,它将资源的生命周期与对象的生命周期绑定在一起。在 RAII 中,资源在对象创建时获取(初始化),并在对象销毁时释放。这种技术可以有效地防止资源泄漏,尤其是在异常处理的情况下。

智能指针是 RAII 原则的典型应用。它们是封装了原始指针的对象,并在其析构函数中自动释放所管理的内存。C++ 标准库提供了几种智能指针类型,每种类型都服务于不同的内存管理需求:

unique_ptr (独占指针): 提供独占所有权的语义,确保只有一个 unique_ptr 可以指向给定的动态分配对象。当 unique_ptr 销毁时,它所指向的对象也会被自动删除。
shared_ptr (共享指针): 提供共享所有权的语义,允许多个 shared_ptr 指向同一个动态分配对象。对象会在最后一个 shared_ptr 被销毁时自动删除,通过引用计数 (reference counting) 实现。
weak_ptr (弱指针): 与 shared_ptr 配合使用,提供对共享对象的非拥有性访问。weak_ptr 不会增加对象的引用计数,主要用于解决 shared_ptr 循环引用的问题。

智能指针的核心优势在于它们自动化了内存的释放,避免了手动 delete 带来的风险,例如忘记释放内存导致的内存泄漏,或者在异常抛出时未释放内存。通过 RAII,智能指针确保无论程序如何执行,动态分配的内存总会被及时回收,从而提高程序的健壮性和可靠性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3void demonstrateRAII() {
4// 使用 unique_ptr 管理动态分配的 int
5std::unique_ptr<int> ptr(new int(42));
6std::cout << "Value pointed to by ptr: " << *ptr << std::endl;
7// 当 ptr 超出作用域时,int 内存会被自动释放
8} // <-- ptr 析构函数在此处被调用,释放内存
9int main() {
10demonstrateRAII();
11std::cout << "Memory has been automatically released." << std::endl;
12return 0;
13}

在上述代码示例中,unique_ptr<int> ptr(new int(42)); 在栈上创建了一个 unique_ptr 对象 ptr,它管理着在堆上动态分配的 int 内存。当 demonstrateRAII() 函数执行完毕,ptr 超出作用域,其析构函数会被自动调用,析构函数内部会执行 delete ptr.get(); (简化描述),从而释放之前分配的 int 内存。这就是 RAII 的体现:资源(内存)的生命周期与对象 (ptr) 的生命周期绑定。

7.1.2 unique_ptr (独占指针): 独占所有权 (Exclusive Ownership)

unique_ptr 是 C++11 引入的一种智能指针,它提供了独占所有权 (exclusive ownership) 语义。这意味着一个 unique_ptr 对象独占地拥有它所指向的对象,不允许其他的智能指针共享所有权。当 unique_ptr 对象被销毁时,它所管理的对象也会被自动删除。

独占所有权特性:

不可复制 (Non-copyable): unique_ptr 对象不能被复制。试图复制 unique_ptr 会导致编译错误,因为复制会违反独占所有权的原则。
可移动 (Moveable): unique_ptr 对象可以被移动。移动操作允许所有权从一个 unique_ptr 对象转移到另一个 unique_ptr 对象。移动操作后,源 unique_ptr 将不再拥有任何对象(变为 nullptr)。

移动语义 (Move Semantics):

unique_ptr 的移动语义是其核心特性之一,它通过 std::move 函数来实现所有权的转移。移动操作避免了深拷贝的开销,提高了效率。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3std::unique_ptr<int> createUniquePtr() {
4std::unique_ptr<int> ptr(new int(100));
5return ptr; // 返回时发生移动,所有权转移到函数外部
6}
7int main() {
8std::unique_ptr<int> ptr1 = createUniquePtr(); // ptr1 获取所有权
9std::cout << "Value in ptr1: " << *ptr1 << std::endl;
10// std::unique_ptr<int> ptr2 = ptr1; // 错误!unique_ptr 不可复制
11std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确!移动 ptr1 的所有权到 ptr2
12std::cout << "Value in ptr2: " << *ptr2 << std::endl;
13// 移动后,ptr1 变为空指针
14if (ptr1 == nullptr) {
15std::cout << "ptr1 is now a nullptr." << std::endl;
16}
17return 0;
18}

适用场景:

管理动态分配的单个对象: unique_ptr 非常适合管理动态分配的单个对象,例如使用 new 创建的对象。
工厂函数返回动态分配对象: 工厂函数可以使用 unique_ptr 返回动态分配的对象,将内存管理的责任交给调用者,同时保证所有权的唯一性。
作为容器元素: unique_ptr 可以作为容器的元素,例如 std::vector<std::unique_ptr<MyClass>>,用于管理容器中动态分配的对象。

常用操作:

构造函数:
▮▮▮▮⚝ unique_ptr<T> ptr(new T(...)): 创建 unique_ptr 并管理 new 返回的指针。
▮▮▮▮⚝ unique_ptr<T> ptr(nullptr): 创建空的 unique_ptr
▮▮▮▮⚝ unique_ptr<T> ptr(std::move(other_unique_ptr)): 移动构造函数,转移所有权。
解引用操作:
▮▮▮▮⚝ *ptr: 访问所指向对象的值。
▮▮▮▮⚝ ptr->member: 访问所指向对象的成员。
get(): 返回原始指针,但不建议直接使用,因为它会绕过 unique_ptr 的管理。
release(): 释放 unique_ptr 的所有权,返回原始指针,unique_ptr 变为空。调用者需要负责释放返回的原始指针指向的内存。
reset():
▮▮▮▮⚝ ptr.reset(new T(...)): 替换 unique_ptr 管理的对象,旧对象会被删除。
▮▮▮▮⚝ ptr.reset(nullptr): 释放 unique_ptr 当前管理的对象,unique_ptr 变为空。
▮▮▮▮⚝ ptr.reset(): 与 ptr.reset(nullptr) 效果相同。
swap(): 交换两个 unique_ptr 对象管理的所有权。

自定义删除器 (Custom Deleters):

unique_ptr 允许指定自定义删除器,用于在对象销毁时执行特定的释放操作。默认情况下,unique_ptr 使用 delete 运算符释放内存。自定义删除器可以用于处理非 new/delete 分配的资源,例如文件句柄、网络连接等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3// 自定义删除器,用于释放文件句柄
4struct FileDeleter {
5void operator()(FILE* file) const {
6if (file) {
7std::cout << "Closing file handle." << std::endl;
8fclose(file);
9}
10}
11};
12int main() {
13// 使用 unique_ptr 管理文件句柄,并使用自定义删除器
14std::unique_ptr<FILE, FileDeleter> filePtr(fopen("example.txt", "r"), FileDeleter());
15if (filePtr) {
16std::cout << "File opened successfully." << std::endl;
17// ... 使用文件 ...
18} else {
19std::cerr << "Failed to open file." << std::endl;
20}
21// 当 filePtr 超出作用域时,FileDeleter 会被调用,关闭文件句柄
22return 0;
23} // <-- filePtr 析构函数在此处被调用,执行自定义删除器

7.1.3 shared_ptr (共享指针) 与 weak_ptr (弱指针): 共享所有权与循环引用 (Shared Ownership and Circular References)

shared_ptr 是 C++11 引入的另一种智能指针,它提供了共享所有权 (shared ownership) 语义。允许多个 shared_ptr 对象共享同一个动态分配对象的所有权。shared_ptr 通过引用计数 (reference counting) 来跟踪有多少个 shared_ptr 指向同一个对象。当最后一个指向该对象的 shared_ptr 被销毁时,对象才会被自动删除。

共享所有权特性:

可复制和可赋值 (Copyable and Assignable): shared_ptr 对象可以被复制和赋值。复制和赋值操作会增加被共享对象的引用计数。
引用计数 (Reference Counting): shared_ptr 内部维护一个引用计数器,记录当前有多少个 shared_ptr 指向同一个对象。
自动删除 (Automatic Deletion): 当引用计数降为零时,说明没有任何 shared_ptr 指向该对象,shared_ptr 会自动删除所管理的对象。

引用计数机制:

当创建一个新的 shared_ptr 指向一个对象时,或当一个 shared_ptr 被复制或赋值时,对象的引用计数会增加。当一个 shared_ptr 对象被销毁(例如超出作用域)时,引用计数会减少。当引用计数变为零时,shared_ptr 会负责删除所管理的对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3void demonstrateSharedPtr() {
4std::shared_ptr<int> ptr1(new int(50));
5std::cout << "Initial ref count: " << ptr1.use_count() << std::endl; // 输出:1
6std::shared_ptr<int> ptr2 = ptr1; // 复制 ptr1,ptr2 也指向同一对象
7std::cout << "After copy, ref count: " << ptr1.use_count() << std::endl; // 输出:2
8std::cout << "After copy, ref count: " << ptr2.use_count() << std::endl; // 输出:2
9{
10std::shared_ptr<int> ptr3 = ptr1; // ptr3 也指向同一对象,引用计数增加
11std::cout << "Inside block, ref count: " << ptr1.use_count() << std::endl; // 输出:3
12} // <-- ptr3 超出作用域,析构函数调用,引用计数减少
13std::cout << "After block, ref count: " << ptr1.use_count() << std::endl; // 输出:2
14} // <-- ptr1 和 ptr2 超出作用域,析构函数调用,引用计数减少,最终对象被删除
15int main() {
16demonstrateSharedPtr();
17std::cout << "Shared object has been automatically deleted." << std::endl;
18return 0;
19}

weak_ptr (弱指针):

weak_ptr 是一种与 shared_ptr 配合使用的智能指针,它提供对共享对象的非拥有性访问weak_ptr 不会增加对象的引用计数,因此不会影响对象的生命周期。weak_ptr 主要用于解决 shared_ptr 循环引用 (circular references) 的问题。

循环引用问题:

当两个或多个对象互相持有 shared_ptr 指向对方时,会形成循环引用。循环引用会导致对象的引用计数永远不会降为零,从而造成内存泄漏,即使这些对象已经不再被程序的其他部分使用。

weak_ptr 通过提供一种不增加引用计数的指针来打破循环引用。weak_ptr 可以从 shared_ptr 构造,但不会增加引用计数。要访问 weak_ptr 指向的对象,需要先调用 lock() 方法,lock() 方法会返回一个 shared_ptr。如果对象仍然存在(未被删除),lock() 会返回有效的 shared_ptr,否则返回空的 shared_ptr

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3class ClassB; // 前向声明
4class ClassA {
5public:
6std::shared_ptr<ClassB> bPtr;
7~ClassA() { std::cout << "ClassA destructor called." << std::endl; }
8};
9class ClassB {
10public:
11std::shared_ptr<ClassA> aPtr; // 使用 shared_ptr 导致循环引用
12// std::weak_ptr<ClassA> aPtr; // 使用 weak_ptr 解决循环引用
13~ClassB() { std::cout << "ClassB destructor called." << std::endl; }
14};
15int main() {
16std::shared_ptr<ClassA> a = std::make_shared<ClassA>();
17std::shared_ptr<ClassB> b = std::make_shared<ClassB>();
18a->bPtr = b;
19b->aPtr = a; // 循环引用形成
20std::cout << "shared_ptr count for a: " << a.use_count() << std::endl; // 输出:2
21std::cout << "shared_ptr count for b: " << b.use_count() << std::endl; // 输出:2
22// a 和 b 超出作用域,但由于循环引用,它们的引用计数永远不会降为 0,导致内存泄漏 (如果使用 shared_ptr<ClassA> aPtr;)
23// 如果 ClassB 中使用 weak_ptr<ClassA> aPtr;,则不会发生内存泄漏,ClassA 和 ClassB 的析构函数会被调用
24return 0;
25} // <-- a 和 b 超出作用域

在上述代码示例中,如果 ClassB 中使用 std::shared_ptr<ClassA> aPtr;,则会形成循环引用,ClassAClassB 的析构函数不会被调用,导致内存泄漏。如果将 ClassB 中的 std::shared_ptr<ClassA> aPtr; 改为 std::weak_ptr<ClassA> aPtr;,则可以解决循环引用问题,ClassAClassB 的析构函数会被正常调用,内存得到释放。

适用场景:

共享所有权的对象: 当多个对象需要共享同一个动态分配对象的所有权时,例如在缓存、观察者模式等场景。
缓存: 多个组件可能需要访问同一个缓存数据,shared_ptr 可以方便地共享缓存对象。
观察者模式: 主题对象可以使用 shared_ptr 管理观察者列表,多个观察者可以共享对同一个主题的引用。
解决循环引用: 在需要表示对象之间相互引用,但又不能形成循环引用时,可以使用 weak_ptr 打破循环。

常用操作:

构造函数:
▮▮▮▮⚝ shared_ptr<T> ptr(new T(...)): 创建 shared_ptr 并管理 new 返回的指针。
▮▮▮▮⚝ shared_ptr<T> ptr(nullptr): 创建空的 shared_ptr
▮▮▮▮⚝ shared_ptr<T> ptr(other_shared_ptr): 复制构造函数,增加引用计数。
▮▮▮▮⚝ shared_ptr<T> ptr(std::move(other_shared_ptr)): 移动构造函数,转移所有权(引用计数不变)。
▮▮▮▮⚝ shared_ptr<T> ptr(weak_ptr): 从 weak_ptr 构造 shared_ptr,如果 weak_ptr 指向的对象仍然存在,则成功构造,否则抛出异常 (std::bad_weak_ptr)。
解引用操作:
▮▮▮▮⚝ *ptr: 访问所指向对象的值。
▮▮▮▮⚝ ptr->member: 访问所指向对象的成员。
get(): 返回原始指针,不建议直接使用。
reset():
▮▮▮▮⚝ ptr.reset(new T(...)): 替换 shared_ptr 管理的对象,旧对象的引用计数减少,可能被删除。
▮▮▮▮⚝ ptr.reset(nullptr): 释放 shared_ptr 当前管理的对象,引用计数减少,可能被删除,shared_ptr 变为空。
▮▮▮▮⚝ ptr.reset(): 与 ptr.reset(nullptr) 效果相同。
swap(): 交换两个 shared_ptr 对象管理的所有权。
use_count(): 返回当前对象的引用计数。
unique(): 判断是否是唯一所有者(引用计数是否为 1)。

weak_ptr 的常用操作:

构造函数:
▮▮▮▮⚝ weak_ptr<T> ptr(): 创建空的 weak_ptr
▮▮▮▮⚝ weak_ptr<T> ptr(shared_ptr): 从 shared_ptr 构造 weak_ptr
▮▮▮▮⚝ weak_ptr<T> ptr(other_weak_ptr): 复制构造函数。
lock(): 尝试获取 weak_ptr 指向的对象的 shared_ptr。如果对象仍然存在,返回有效的 shared_ptr,否则返回空的 shared_ptr
expired(): 判断 weak_ptr 指向的对象是否已经被删除(过期)。
reset(): 释放 weak_ptr,使其不再指向任何对象。
swap(): 交换两个 weak_ptr

7.1.4 make_unique 与 make_shared 的优势 (Advantages of make_unique and make_shared)

make_uniquemake_shared 是 C++11 (对于 make_shared) 和 C++14 (对于 make_unique) 引入的辅助函数,用于创建 unique_ptrshared_ptr。它们相比于直接使用 new 创建智能指针,具有以下优势:

异常安全性 (Exception Safety):

使用 make_uniquemake_shared 可以提高异常安全性。考虑以下使用 new 创建 shared_ptr 的代码:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::shared_ptr<MyClass> ptr = std::shared_ptr<MyClass>(new MyClass(arg1, arg2));

在上述代码中,new MyClass(arg1, arg2)std::shared_ptr<MyClass>(...) 是两个独立的操作。在极端情况下,如果 new MyClass(arg1, arg2) 成功执行,但 std::shared_ptr<MyClass> 的构造函数抛出异常(例如内存不足),那么 new 分配的内存将无法被 shared_ptr 管理,导致内存泄漏。

而使用 make_shared 可以避免这个问题:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(arg1, arg2);

make_shared一次内存分配中同时完成对象的内存分配和 shared_ptr 的控制块 (control block) 的内存分配。如果 MyClass 的构造函数或 make_shared 内部抛出异常,由于内存分配过程是原子的,不会发生内存泄漏。make_unique 也有类似的异常安全优势。

效率 (Efficiency):

make_shared 通常比 std::shared_ptr<T>(new T(...)) 更高效,尤其是在频繁创建和销毁 shared_ptr 的场景下。

减少内存分配次数: make_shared 在一次内存分配中同时分配对象本身和 shared_ptr 的控制块。而 std::shared_ptr<T>(new T(...)) 需要两次内存分配:一次分配对象本身,一次分配控制块。减少内存分配次数可以提高性能。
控制块和对象内存相邻: make_shared 将控制块和对象内存分配在相邻的内存区域,可以提高缓存局部性 (cache locality),从而提高性能。

总结:

推荐使用 make_uniquemake_shared 来创建 unique_ptrshared_ptr,以获得更好的异常安全性和潜在的性能提升。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3class MyClass {
4public:
5MyClass(int value) : value_(value) {
6std::cout << "MyClass constructor called, value = " << value_ << std::endl;
7}
8~MyClass() {
9std::cout << "MyClass destructor called, value = " << value_ << std::endl;
10}
11int getValue() const { return value_; }
12private:
13int value_;
14};
15int main() {
16// 使用 make_unique 创建 unique_ptr
17std::unique_ptr<MyClass> uniquePtr = std::make_unique<MyClass>(10);
18std::cout << "Unique ptr value: " << uniquePtr->getValue() << std::endl;
19// 使用 make_shared 创建 shared_ptr
20std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>(20);
21std::cout << "Shared ptr 1 value: " << sharedPtr1->getValue() << std::endl;
22std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1; // 共享所有权
23std::cout << "Shared ptr 2 value: " << sharedPtr2->getValue() << std::endl;
24std::cout << "Shared ptr 1 use count: " << sharedPtr1.use_count() << std::endl; // 输出:2
25std::cout << "Shared ptr 2 use count: " << sharedPtr2.use_count() << std::endl; // 输出:2
26return 0;
27}

7.2 分配器 (Allocators)

本节介绍分配器 (Allocators) 的概念、标准库提供的默认分配器 std::allocator,以及自定义分配器的实现思路和应用场景。

7.2.1 分配器的作用与接口 (Role and Interface of Allocators)

在 C++ 标准库中,分配器 (allocator) 是一个模板类,用于封装内存的分配和释放策略。容器 (containers) 通过分配器来管理其元素的内存。默认情况下,标准库容器使用 std::allocator 作为其分配器。

分配器的作用:

内存分配: 分配器负责为容器的元素分配内存。
内存释放: 分配器负责释放容器元素所占用的内存。
对象构造与析构: 分配器还负责在已分配的内存上构造 (construct) 对象,以及在对象不再需要时析构 (destroy) 对象。

分配器的标准接口:

分配器需要满足一定的接口规范,以便容器能够正确地使用它。分配器的主要成员函数包括:

类型定义 (Type Definitions):

allocator::value_type: 分配器分配的对象的类型。
allocator::pointer: 指向 value_type 的指针类型。
allocator::const_pointer: 指向 const value_type 的指针类型。
allocator::reference: value_type 的引用类型。
allocator::const_reference: const value_type 的引用类型。
allocator::size_type: 表示大小的无符号整型类型。
allocator::difference_type: 表示指针差值的有符号整型类型。

成员函数 (Member Functions):

pointer allocate(size_type n, allocator::const_pointer hint = 0): 分配能够容纳 nvalue_type 对象的内存。hint 参数是可选的,可以提供内存分配的建议(通常被忽略)。如果分配失败,抛出 std::bad_alloc 异常。
void deallocate(pointer p, size_type n): 释放之前通过 allocate 分配的,起始地址为 p,大小为 nvalue_type 对象的内存。
size_type max_size() const: 返回分配器可以成功分配的最大对象数量。
void construct(pointer p, const_reference val): 在已分配的内存地址 p 上构造一个对象,使用值 val 初始化。
void destroy(pointer p): 析构位于内存地址 p 的对象,但不释放内存。

静态成员函数 (Static Member Functions):

template <class U, class... Args> void construct(U* p, Args&&... args) (C++11 起): 在已分配的内存地址 p 上构造一个类型为 U 的对象,使用参数 args... 进行构造。
template <class U> void destroy(U* p) (C++11 起): 析构位于内存地址 p 的类型为 U 的对象,但不释放内存。

默认分配器 std::allocator 的简单接口:

实际上,std::allocator 的接口比上述描述的更复杂一些,包含了一些用于支持 placement new 和其他高级特性的成员,但对于理解分配器的基本作用,上述接口已经足够。

7.2.2 默认分配器 std::allocator (Default Allocator)

std::allocator 是 C++ 标准库提供的默认分配器。当创建标准库容器时,如果没有显式指定分配器,容器会默认使用 std::allocator<T>

std::allocator 的基本功能:

使用 ::operator new::operator delete 进行内存分配和释放: std::allocator 内部使用全局的 ::operator new 来分配内存,使用全局的 ::operator delete 来释放内存。
简单的对象构造和析构: std::allocatorconstructdestroy 成员函数使用 placement new 和直接析构函数调用来构造和析构对象。
通用性: std::allocator 适用于大多数通用的内存分配场景,满足标准库容器的默认内存管理需求。

std::allocator 的使用:

在大多数情况下,不需要显式使用或修改 std::allocator。标准库容器的默认行为已经足够高效和通用。只有在需要定制化内存管理策略时,才需要考虑自定义分配器。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <memory>
4int main() {
5// 使用默认分配器 std::allocator<int>
6std::vector<int> vec;
7for (int i = 0; i < 5; ++i) {
8vec.push_back(i); // 内部使用 std::allocator<int> 分配内存
9}
10std::cout << "Vector elements: ";
11for (int val : vec) {
12std::cout << val << " ";
13}
14std::cout << std::endl;
15// 显式指定分配器 (通常不需要)
16std::vector<int, std::allocator<int>> vec2;
17for (int i = 0; i < 5; ++i) {
18vec2.push_back(i);
19}
20std::cout << "Vector 2 elements: ";
21for (int val : vec2) {
22std::cout << val << " ";
23}
24std::cout << std::endl;
25return 0;
26}

std::allocator 的特点:

基于全局 newdelete: 内存分配和释放的性能取决于全局 newdelete 的实现。
无状态 (Stateless): std::allocator 对象通常是无状态的,多个 std::allocator 对象可以互相替换使用。
默认行为: 提供标准的、通用的内存管理行为,适用于大多数应用场景。

7.2.3 自定义分配器的实现与应用 (Implementation and Application of Custom Allocators)

在某些特定场景下,默认的 std::allocator 可能无法满足性能或功能需求。例如:

性能优化: 在高频内存分配和释放的场景下,自定义分配器可以采用更高效的内存管理策略,例如内存池 (memory pool)、固定大小块分配等,来提高性能。
内存限制: 在嵌入式系统或资源受限的环境中,自定义分配器可以实现更精细的内存控制,例如限制内存使用量、使用特定的内存区域等。
调试与监控: 自定义分配器可以添加额外的调试信息或监控功能,例如跟踪内存分配和释放、检测内存泄漏等。

自定义分配器的实现思路:

要实现自定义分配器,需要创建一个类,并满足分配器的接口规范。以下是一些实现自定义分配器的关键步骤和思路:

定义分配器类:

创建一个类,例如 MyAllocator<T>,它需要是一个模板类,以支持不同类型的对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <typename T>
2class MyAllocator {
3public:
4using value_type = T;
5// ... 其他类型定义 ...
6MyAllocator() noexcept = default;
7template <typename U> MyAllocator(const MyAllocator<U>&) noexcept {} // 转换构造函数
8pointer allocate(size_type n, const_pointer hint = 0);
9void deallocate(pointer p, size_type n);
10template <typename U, typename... Args>
11void construct(U* p, Args&&... args);
12void destroy(pointer p);
13size_type max_size() const noexcept;
14bool operator==(const MyAllocator&) const noexcept { return true; }
15bool operator!=(const MyAllocator&) const noexcept { return false; }
16};

实现 allocatedeallocate:

这是自定义分配器的核心部分,需要实现内存的分配和释放逻辑。可以使用 ::operator new::operator delete,也可以使用更高级的内存管理技术,例如内存池。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <typename T>
2typename MyAllocator<T>::pointer MyAllocator<T>::allocate(size_type n, const_pointer /*hint*/) {
3if (n > max_size()) {
4throw std::bad_alloc();
5}
6pointer p = static_cast<pointer>(::operator new(n * sizeof(value_type)));
7if (!p) {
8throw std::bad_alloc();
9}
10return p;
11}
12template <typename T>
13void MyAllocator<T>::deallocate(pointer p, size_type /*n*/) {
14::operator delete(p);
15}

实现 constructdestroy:

使用 placement new 和析构函数调用来构造和析构对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <typename T>
2template <typename U, typename... Args>
3void MyAllocator<T>::construct(U* p, Args&&... args) {
4::new (p) U(std::forward<Args>(args)...); // placement new
5}
6template <typename T>
7void MyAllocator<T>::destroy(pointer p) {
8p->~value_type(); // 显式调用析构函数
9}

使用自定义分配器:

在创建标准库容器时,可以将自定义分配器作为模板参数传入。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <memory>
4// ... MyAllocator 的实现 ...
5int main() {
6// 使用自定义分配器 MyAllocator<int>
7std::vector<int, MyAllocator<int>> myVec;
8for (int i = 0; i < 5; ++i) {
9myVec.push_back(i); // 使用 MyAllocator<int> 分配内存
10}
11std::cout << "Vector elements using MyAllocator: ";
12for (int val : myVec) {
13std::cout << val << " ";
14}
15std::cout << std::endl;
16return 0;
17}

内存池 (Memory Pool) 分配器示例:

内存池是一种常用的内存管理技术,它可以预先分配一大块内存,然后从这块内存中分配小块内存,避免频繁的系统调用,提高性能。以下是一个简单的内存池分配器示例(简化版,仅供演示思路):

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <memory>
4#include <cstddef> // std::byte
5template <typename T>
6class PoolAllocator {
7private:
8std::byte* pool_;
9size_t poolSize_;
10std::byte* currentPos_;
11public:
12using value_type = T;
13using pointer = T*;
14using const_pointer = const T*;
15using size_type = std::size_t;
16using difference_type = std::ptrdiff_t;
17PoolAllocator(size_t poolSize) : poolSize_(poolSize), pool_(new std::byte[poolSize]), currentPos_(pool_) {}
18~PoolAllocator() { delete[] pool_; }
19PoolAllocator(const PoolAllocator&) = delete;
20PoolAllocator& operator=(const PoolAllocator&) = delete;
21pointer allocate(size_type n, const_pointer hint = nullptr) {
22size_t bytesNeeded = n * sizeof(value_type);
23if (currentPos_ + bytesNeeded > pool_ + poolSize_) {
24throw std::bad_alloc(); // 内存池耗尽
25}
26pointer p = reinterpret_cast<pointer>(currentPos_);
27currentPos_ += bytesNeeded;
28return p;
29}
30void deallocate(pointer p, size_type n) {
31// 内存池分配器通常不真正释放内存,而是在析构时一次性释放整个内存池
32// 这里为了简化,不实现 deallocate 的具体操作
33}
34template <typename U, typename... Args>
35void construct(U* p, Args&&... args) {
36::new (p) U(std::forward<Args>(args)...);
37}
38void destroy(pointer p) {
39p->~value_type();
40}
41size_type max_size() const noexcept {
42return poolSize_ / sizeof(value_type);
43}
44};
45int main() {
46// 使用内存池分配器 PoolAllocator<int>,内存池大小为 1024 字节
47std::vector<int, PoolAllocator<int>> poolVec(PoolAllocator<int>(1024));
48for (int i = 0; i < 100; ++i) {
49poolVec.push_back(i); // 使用 PoolAllocator<int> 分配内存,从内存池中分配
50}
51std::cout << "Vector elements using PoolAllocator: ";
52for (int val : poolVec) {
53std::cout << val << " ";
54}
55std::cout << std::endl;
56return 0;
57}

自定义分配器的注意事项:

性能测试: 自定义分配器是否真的能提高性能,需要进行实际的性能测试和分析,根据具体应用场景进行优化。
复杂性: 自定义分配器的实现可能比较复杂,需要仔细考虑内存管理策略、线程安全等问题。
适用性: 自定义分配器并非在所有场景下都适用,对于简单的应用,默认的 std::allocator 通常已经足够。

<END_OF_CHAPTER/>

8. 并发与多线程 (Concurrency and Multithreading)

本章深入讲解 C++ 标准库提供的并发编程工具,包括线程 (Threads), 互斥量 (Mutexes), 条件变量 (Condition Variables), 期物 (Futures) 和原子操作 (Atomic Operations),帮助读者编写安全高效的并发程序。

8.1 线程与线程管理 (Threads and Thread Management)

本节介绍 std::thread 类的使用方法,包括线程的创建、启动、join()detach() 操作,以及线程局部存储 (Thread-Local Storage)。

8.1.1 std::thread 类的使用 (Usage of std::thread Class)

std::thread 类是 C++ 标准库中用于创建和管理线程的核心组件。它允许程序在同一个进程中并行执行多个线程,从而提高程序的并发性和响应速度。

线程的创建和启动
创建 std::thread 对象即启动了一个新的线程。构造函数的第一个参数是要在新线程中执行的函数(或可调用对象),后续参数将作为该函数的参数传递。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3void task_function(int id) {
4std::cout << "线程 " << id << " 开始执行" << std::endl;
5// 模拟一些耗时操作
6std::this_thread::sleep_for(std::chrono::seconds(2));
7std::cout << "线程 " << id << " 执行完毕" << std::endl;
8}
9int main() {
10std::thread thread1(task_function, 1); // 创建并启动线程 1,执行 task_function(1)
11std::thread thread2(task_function, 2); // 创建并启动线程 2,执行 task_function(2)
12std::cout << "主线程继续执行" << std::endl;
13thread1.join(); // 等待线程 1 执行结束
14thread2.join(); // 等待线程 2 执行结束
15std::cout << "所有线程执行完毕,程序结束" << std::endl;
16return 0;
17}

在这个例子中,我们创建了两个线程 thread1thread2,它们都执行 task_function 函数,并传递了不同的 id 参数。thread1.join()thread2.join() 调用会阻塞主线程,直到 thread1thread2 执行完毕。

向线程函数传递参数
std::thread 构造函数可以接受任意数量的参数,这些参数会被传递给线程函数。需要注意的是,默认情况下,传递给线程函数的参数是按值复制的。如果需要传递引用或指针,需要使用 std::refstd::cref 进行包装。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3void modify_value(int& value) {
4value = 100;
5std::cout << "线程中修改后的值: " << value << std::endl;
6}
7int main() {
8int num = 0;
9std::cout << "初始值: " << num << std::endl;
10std::thread modifier_thread(modify_value, std::ref(num)); // 传递 num 的引用
11modifier_thread.join();
12std::cout << "主线程中修改后的值: " << num << std::endl; // num 的值在线程中被修改
13return 0;
14}

在这个例子中,我们使用 std::ref(num)num 的引用传递给 modify_value 函数,因此线程中对 value 的修改会反映到主线程中的 num 变量。

使用 Lambda 表达式创建线程
Lambda 表达式可以方便地创建简单的线程函数,尤其是在线程函数逻辑不复杂的情况下。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3int main() {
4std::thread lambda_thread([](const std::string& message) {
5std::cout << "Lambda 线程执行: " << message << std::endl;
6}, "Hello from Lambda Thread!"); // 使用 Lambda 表达式作为线程函数
7lambda_thread.join();
8return 0;
9}

这个例子展示了如何使用 Lambda 表达式创建一个简单的线程,并向 Lambda 表达式传递一个字符串参数。

8.1.2 join() 与 detach(): 线程同步与分离 (Thread Synchronization and Detachment)

线程创建后,需要决定如何管理线程的生命周期,join()detach() 是两种主要的线程管理方式。

join():线程同步
调用 thread.join() 会阻塞当前线程(通常是主线程),直到被 join() 的线程执行完毕。join() 确保了线程的同步,主线程会等待子线程完成后再继续执行。

特点
▮▮▮▮⚝ 主线程等待子线程完成。
▮▮▮▮⚝ 保证线程同步。
▮▮▮▮⚝ 线程对象必须是 joinable 的(thread.joinable() == true)。
▮▮▮▮⚝ 在线程对象析构前,必须调用 join()detach(),否则程序会 std::terminate 终止。

适用场景
▮▮▮▮⚝ 需要等待子线程完成才能继续执行后续操作的场景。
▮▮▮▮⚝ 需要获取子线程执行结果的场景(可以通过引用传递参数或使用 future)。

detach():线程分离
调用 thread.detach() 会将线程从创建它的线程中分离出来。分离后的线程会在后台独立运行,不再受创建它的线程控制。当分离的线程执行完毕后,其资源由系统自动回收。

特点
▮▮▮▮⚝ 线程在后台独立运行。
▮▮▮▮⚝ 创建线程的线程不会等待分离的线程完成。
▮▮▮▮⚝ 线程资源在线程结束后由系统自动回收。
▮▮▮▮⚝ 线程对象必须是 joinable 的(thread.joinable() == true)。
▮▮▮▮⚝ 分离后的线程无法再被 join()

适用场景
▮▮▮▮⚝ 线程执行的任务与主线程的后续执行逻辑无关,例如日志记录、事件通知等后台任务。
▮▮▮▮⚝ 线程的生命周期需要独立于创建线程的场景。

选择 join() 还是 detach()
选择 join() 还是 detach() 取决于具体的应用场景和线程管理需求。

使用 join() 的情况
▮▮▮▮⚝ 当主线程需要等待子线程的结果或子线程的完成状态时,应该使用 join()
▮▮▮▮⚝ 保证程序逻辑的顺序执行和线程间的同步。

使用 detach() 的情况
▮▮▮▮⚝ 当子线程的任务是独立的、后台运行的,并且不需要主线程等待其完成时,可以使用 detach()
▮▮▮▮⚝ 适用于长时间运行且不影响主线程的任务,例如监控线程、服务线程等。

错误处理
无论是 join() 还是 detach(),都应该注意异常处理。如果线程函数抛出异常而没有被捕获,程序可能会 std::terminate 终止。可以使用 try-catch 块在线程函数内部捕获异常,或者使用 future 来获取线程的异常信息。

8.1.3 线程局部存储 (Thread-Local Storage)

线程局部存储 (Thread-Local Storage, TLS) 允许每个线程拥有独立的变量副本。即使多个线程访问同一个线程局部变量,它们操作的也是各自线程私有的副本,避免了数据竞争和同步问题。

thread_local 关键字
C++11 引入了 thread_local 存储说明符,用于声明线程局部变量。使用 thread_local 声明的变量,其生命周期与线程的生命周期相同,每个线程都拥有该变量的独立实例。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3thread_local int thread_local_var = 0; // 声明线程局部变量
4void thread_task(int thread_id) {
5thread_local_var = thread_id * 10;
6std::cout << "线程 " << thread_id << ", thread_local_var = " << thread_local_var << std::endl;
7// 模拟其他操作
8std::this_thread::sleep_for(std::chrono::seconds(1));
9std::cout << "线程 " << thread_id << ", thread_local_var after sleep = " << thread_local_var << std::endl;
10}
11int main() {
12std::thread thread1(thread_task, 1);
13std::thread thread2(thread_task, 2);
14thread1.join();
15thread2.join();
16std::cout << "主线程, thread_local_var = " << thread_local_var << std::endl; // 主线程访问的是自己的副本,初始值为 0
17return 0;
18}

在这个例子中,thread_local_var 是一个线程局部变量。thread1thread2 修改的是各自线程的 thread_local_var 副本,互不影响。主线程访问的 thread_local_var 是主线程自己的副本,初始值为全局定义的 0。

线程局部存储的用途
避免数据竞争:TLS 可以有效地避免多线程环境下的数据竞争问题,因为每个线程操作的是独立的变量副本,不需要额外的同步机制。
提高性能:由于不需要同步,访问线程局部变量通常比访问共享变量更快,可以提高并发程序的性能。
简化并发编程:TLS 可以简化某些并发编程场景,例如在 Web 服务器中,每个请求处理线程可以使用 TLS 存储请求上下文信息,而无需显式地传递和管理这些信息。

注意事项
初始化:线程局部变量的初始化发生在线程启动时。如果线程局部变量有初始化器,则每个线程在创建时都会执行初始化器。
生命周期:线程局部变量的生命周期与线程的生命周期相同。当线程结束时,线程局部变量会被销毁。
开销:虽然 TLS 可以提高某些场景的性能,但过度使用 TLS 可能会增加内存开销,因为每个线程都需要分配独立的存储空间。

8.2 互斥量与锁 (Mutexes and Locks)

本节讲解 std::mutex, std::recursive_mutex, std::timed_mutex 等互斥量类型,以及 std::lock_guard, std::unique_lock 等锁管理器的使用,保障多线程环境下的数据安全。

8.2.1 互斥量类型:mutex, recursive_mutex, timed_mutex (Mutex Types)

互斥量 (Mutex, Mutual Exclusion) 是一种同步原语,用于保护共享资源,防止多个线程同时访问,从而避免数据竞争和保证线程安全。C++ 标准库提供了多种互斥量类型,以满足不同的并发需求。

std::mutex (互斥量)
std::mutex 是最基本的互斥量类型,提供独占的互斥访问。一个线程获得互斥锁后,其他线程必须等待锁被释放才能访问被保护的共享资源。

主要操作
▮▮▮▮⚝ lock(): 尝试获取互斥锁。如果锁已被其他线程持有,则当前线程阻塞,直到锁被释放并成功获取锁。
▮▮▮▮⚝ unlock(): 释放互斥锁,允许其他等待线程尝试获取锁。
▮▮▮▮⚝ try_lock(): 尝试非阻塞地获取互斥锁。如果锁可用,则获取锁并返回 true;如果锁已被持有,则立即返回 false,不会阻塞。

适用场景
▮▮▮▮⚝ 保护临界区 (Critical Section),即需要互斥访问的共享资源或代码段。
▮▮▮▮⚝ 实现线程间的互斥同步。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::mutex mtx; // 定义互斥量
5int shared_counter = 0;
6void increment_counter() {
7for (int i = 0; i < 100000; ++i) {
8mtx.lock(); // 获取互斥锁
9shared_counter++; // 临界区:访问共享计数器
10mtx.unlock(); // 释放互斥锁
11}
12}
13int main() {
14std::thread thread1(increment_counter);
15std::thread thread2(increment_counter);
16thread1.join();
17thread2.join();
18std::cout << "共享计数器最终值: " << shared_counter << std::endl; // 结果接近 200000
19return 0;
20}

在这个例子中,mtx 用于保护 shared_counter 变量,确保每次只有一个线程可以修改 shared_counter,避免数据竞争。

std::recursive_mutex (递归互斥量)
std::recursive_mutex 允许同一个线程多次获取同一个互斥锁,而不会造成死锁。这在递归函数或嵌套函数中需要多次获取同一互斥锁的情况下非常有用。

特点
▮▮▮▮⚝ 允许同一个线程递归地获取锁。
▮▮▮▮⚝ 维护一个计数器,记录锁被同一个线程获取的次数。
▮▮▮▮⚝ 只有当计数器变为 0 时,锁才真正被释放。

适用场景
▮▮▮▮⚝ 递归函数或嵌套函数需要多次获取同一个互斥锁的场景。
▮▮▮▮⚝ 复杂的类成员函数调用链中,可能需要在多个成员函数中获取同一个互斥锁。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::recursive_mutex recursive_mtx;
5void recursive_function(int count) {
6recursive_mtx.lock();
7std::cout << "线程 " << std::this_thread::get_id() << ", 递归深度: " << count << std::endl;
8if (count > 0) {
9recursive_function(count - 1); // 递归调用,再次尝试获取锁
10}
11recursive_mtx.unlock();
12}
13int main() {
14std::thread thread1(recursive_function, 3);
15thread1.join();
16return 0;
17}

在这个例子中,recursive_function 递归调用自身,每次调用都会尝试获取 recursive_mtx 锁。由于 recursive_mtx 是递归互斥量,同一个线程可以多次成功获取锁。

std::timed_mutex (定时互斥量)
std::timed_mutex 除了具备 std::mutex 的所有功能外,还增加了超时获取锁的功能。线程可以尝试在指定时间内获取锁,如果超时仍未获取到锁,则返回失败,不会无限期阻塞。

新增操作
▮▮▮▮⚝ try_lock_for(duration): 尝试在指定 duration 时间内获取互斥锁。如果在超时时间内成功获取锁,则返回 true;否则返回 false
▮▮▮▮⚝ try_lock_until(time_point): 尝试在指定 time_point 时间点之前获取互斥锁。如果在指定时间点之前成功获取锁,则返回 true;否则返回 false

适用场景
▮▮▮▮⚝ 需要避免线程无限期阻塞等待锁的场景,例如需要设置超时机制的资源访问。
▮▮▮▮⚝ 某些实时性要求较高的系统中,需要限制锁等待时间。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <chrono>
5std::timed_mutex timed_mtx;
6void timed_lock_task() {
7std::chrono::milliseconds timeout(100);
8if (timed_mtx.try_lock_for(timeout)) { // 尝试在 100ms 内获取锁
9std::cout << "线程 " << std::this_thread::get_id() << " 成功获取定时互斥锁" << std::endl;
10std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟持有锁的时间
11timed_mtx.unlock();
12} else {
13std::cout << "线程 " << std::this_thread::get_id() << " 获取定时互斥锁超时" << std::endl;
14}
15}
16int main() {
17std::thread thread1(timed_lock_task);
18std::thread thread2(timed_lock_task);
19thread1.join();
20thread2.join();
21return 0;
22}

在这个例子中,thread2 尝试获取 timed_mtx 锁时,如果 thread1 已经持有锁且持有时间超过 100ms,thread2try_lock_for 会超时返回 false,从而避免无限期阻塞。

8.2.2 锁管理器:lock_guard, unique_lock (Lock Managers)

为了更好地管理互斥锁的生命周期,并确保在异常情况下锁能够正确释放,C++ 标准库提供了锁管理器 std::lock_guardstd::unique_lock。它们都遵循 RAII (Resource Acquisition Is Initialization) 原则,在构造时获取锁,在析构时自动释放锁。

std::lock_guard (锁卫士)
std::lock_guard 是一个轻量级的锁管理器,独占式地管理互斥锁。它在构造函数中获取互斥锁,在析构函数中自动释放互斥锁,确保了互斥锁的自动释放,即使在临界区代码抛出异常也能保证锁被释放,从而避免死锁。

特点
▮▮▮▮⚝ RAII 风格的锁管理,自动获取和释放锁。
▮▮▮▮⚝ 构造函数中获取锁,析构函数中释放锁。
▮▮▮▮⚝ 简单高效,开销小。
▮▮▮▮⚝ 不支持延迟锁定、条件锁定、所有权转移等高级操作。

使用方式
▮▮▮▮⚝ 创建 std::lock_guard 对象时,需要传入一个互斥量对象作为参数。
▮▮▮▮⚝ std::lock_guard 对象的作用域结束后,会自动释放锁。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::mutex mtx;
5int shared_data = 0;
6void access_shared_data() {
7std::lock_guard<std::mutex> lock(mtx); // RAII 锁管理,构造时获取锁
8// 临界区:访问共享数据
9shared_data++;
10std::cout << "线程 " << std::this_thread::get_id() << ", shared_data = " << shared_data << std::endl;
11// lock_guard 对象作用域结束,析构时自动释放锁
12}
13int main() {
14std::thread thread1(access_shared_data);
15std::thread thread2(access_shared_data);
16thread1.join();
17thread2.join();
18return 0;
19}

在这个例子中,std::lock_guard<std::mutex> lock(mtx);lock 对象构造时获取了 mtx 锁,当 access_shared_data 函数执行完毕,lock 对象析构时,会自动释放 mtx 锁,确保了临界区的互斥访问和异常安全。

std::unique_lock (唯一锁)
std::unique_lock 是一个更通用、更灵活的锁管理器,也遵循 RAII 原则。它提供了比 std::lock_guard 更多的功能,例如延迟锁定条件锁定定时锁定所有权转移等。

特点
▮▮▮▮⚝ RAII 风格的锁管理,自动获取和释放锁。
▮▮▮▮⚝ 构造函数中可以延迟获取锁(通过 std::defer_lock 参数)。
▮▮▮▮⚝ 可以尝试获取锁(通过 try_lock() 成员函数)。
▮▮▮▮⚝ 可以定时获取锁(通过 try_lock_for()try_lock_until() 成员函数)。
▮▮▮▮⚝ 可以与条件变量结合使用wait(), notify_one(), notify_all())。
▮▮▮▮⚝ 锁的所有权可以转移(通过移动语义)。

构造函数参数
▮▮▮▮⚝ 默认构造函数:创建一个 std::unique_lock 对象,不关联任何互斥量,也不获取锁
▮▮▮▮⚝ 接受互斥量参数:创建一个 std::unique_lock 对象,关联指定的互斥量,并立即尝试获取锁
▮▮▮▮⚝ std::defer_lock:创建一个 std::unique_lock 对象,关联指定的互斥量,但不立即获取锁,需要显式调用 lock() 成员函数获取锁(延迟锁定)。
▮▮▮▮⚝ std::try_to_lock:创建一个 std::unique_lock 对象,关联指定的互斥量,并尝试非阻塞地获取锁
▮▮▮▮⚝ std::adopt_lock:创建一个 std::unique_lock 对象,关联指定的互斥量,并假定当前线程已经持有该互斥锁接管已持有的锁)。

常用成员函数
▮▮▮▮⚝ lock(): 获取互斥锁(如果构造时使用了 std::defer_lock,则需要显式调用 lock() 获取锁)。
▮▮▮▮⚝ unlock(): 释放互斥锁。
▮▮▮▮⚝ try_lock(): 尝试非阻塞地获取互斥锁。
▮▮▮▮⚝ try_lock_for(duration): 尝试在指定时间内获取互斥锁。
▮▮▮▮⚝ try_lock_until(time_point): 尝试在指定时间点之前获取互斥锁。
▮▮▮▮⚝ release(): 释放 std::unique_lock 对象对互斥锁的所有权,但不释放互斥锁。调用 release() 后,std::unique_lock 对象不再管理互斥锁的生命周期,需要手动管理互斥锁的释放。
▮▮▮▮⚝ swap(unique_lock): 交换两个 std::unique_lock 对象管理的互斥锁的所有权。
▮▮▮▮⚝ owns_lock(): 检查 std::unique_lock 对象是否持有互斥锁。
▮▮▮▮⚝ operator bool(): 检查 std::unique_lock 对象是否持有互斥锁(与 owns_lock() 相同)。

适用场景
▮▮▮▮⚝ 需要更灵活的锁管理方式的场景,例如需要延迟锁定、条件锁定、定时锁定等。
▮▮▮▮⚝ 需要与条件变量结合使用进行线程同步的场景。
▮▮▮▮⚝ 需要转移锁所有权的场景。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::mutex mtx;
5void deferred_locking() {
6std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟锁定,构造时不获取锁
7std::cout << "线程 " << std::this_thread::get_id() << " 尝试获取锁..." << std::endl;
8lock.lock(); // 显式获取锁
9std::cout << "线程 " << std::this_thread::get_id() << " 成功获取锁" << std::endl;
10// 临界区
11std::this_thread::sleep_for(std::chrono::seconds(1));
12lock.unlock(); // 显式释放锁 (虽然析构时也会自动释放,但这里演示显式释放)
13}
14int main() {
15std::thread thread1(deferred_locking);
16thread1.join();
17return 0;
18}

这个例子演示了 std::unique_lock 的延迟锁定功能,构造 lock 对象时使用了 std::defer_lock 参数,因此在构造时不会立即获取锁,需要显式调用 lock.lock() 才能获取锁。

选择 lock_guard 还是 unique_lock
优先使用 lock_guard:在大多数情况下,std::lock_guard 已经足够满足互斥锁管理的需求。它简单高效,开销小,是首选的锁管理器。
在需要更多功能时使用 unique_lock:当需要延迟锁定、条件锁定、定时锁定、所有权转移等高级功能时,或者需要与条件变量结合使用时,才考虑使用 std::unique_lockstd::unique_lock 提供了更多的灵活性,但也会带来一定的性能开销。

8.2.3 死锁与避免策略 (Deadlock and Avoidance Strategies)

死锁 (Deadlock) 是指两个或多个线程因互相竞争资源而造成的一种僵局,若无外力作用,所有参与死锁的线程都将永远处于互相等待的状态而无法继续执行。死锁是并发编程中常见且严重的问题,需要理解死锁的产生原因和条件,并采取相应的避免策略。

死锁的产生条件
死锁的发生通常需要同时满足以下四个必要条件,即 Coffman 条件

互斥条件 (Mutual Exclusion):至少有一个资源必须处于独占模式,即一次只有一个线程可以使用该资源。其他线程如果请求该资源,必须等待,直到持有资源的线程释放之。例如,互斥锁就是一种互斥资源。
占有并等待条件 (Hold and Wait):一个线程至少持有一个资源,并且还在请求其他线程占有的资源。也就是说,线程在等待新资源的同时,保持着对已占有资源的所有权。
不可剥夺条件 (No Preemption):线程已获得的资源在未使用完之前,不能被剥夺,只能由持有该资源的线程主动释放。
循环等待条件 (Circular Wait):发生死锁时,必然存在一个线程-资源的循环等待链,例如,线程 A 等待线程 B 占有的资源,线程 B 又等待线程 C 占有的资源,...,线程 Z 等待线程 A 占有的资源,形成一个环路。

只要这四个条件同时满足,就可能发生死锁。反之,要避免死锁,只需要破坏其中一个或多个条件即可。

常见的死锁避免策略

破坏互斥条件:这通常是不现实的。互斥条件是保证资源互斥访问的必要条件,很多资源本质上就是互斥的,例如互斥锁、打印机等。如果取消互斥,可能会导致数据不一致或其他并发问题。

破坏占有并等待条件
▮▮▮▮⚝ 一次性申请所有资源:线程在开始执行前,一次性申请它所需要的所有资源。只有当所有资源都可用时,才允许线程继续执行。如果部分资源被占用,则线程必须等待,并释放已占有的资源,之后重新申请所有资源。
▮▮▮▮⚝ 缺点:资源利用率低,可能导致线程饥饿。

破坏不可剥夺条件
▮▮▮▮⚝ 资源剥夺:当一个线程占有某些资源,并且请求新的资源而无法立即满足时,可以剥夺(暂时释放)该线程已占有的资源,将这些资源分配给其他线程。当需要被剥夺资源的线程再次需要这些资源时,再重新分配给它。
▮▮▮▮⚝ 实现复杂,开销较大,可能会导致数据不一致。

破坏循环等待条件
▮▮▮▮⚝ 资源排序:对系统中所有资源进行线性排序,例如编号。线程在申请资源时,必须按照资源的编号顺序依次申请。也就是说,线程只能先申请编号小的资源,再申请编号大的资源,不允许逆序申请
▮▮▮▮⚝ 优点:简单有效,资源利用率较高。
▮▮▮▮⚝ 缺点:需要预先对资源进行排序,可能不适用于所有场景。

更实用的死锁避免建议

避免嵌套锁:尽量避免在一个锁的临界区内再获取另一个锁。如果必须使用嵌套锁,要确保锁的获取顺序一致,即所有线程都按照相同的顺序获取锁。
使用定时锁:使用 std::timed_mutexstd::unique_lock 的定时获取锁功能,设置超时时间,避免线程因等待锁而无限期阻塞。
细粒度锁:尽量使用细粒度锁来保护共享资源,减少锁的竞争范围,降低死锁发生的概率。
锁的释放要及时:在临界区代码执行完毕后,要及时释放锁,避免长时间持有锁,减少其他线程等待锁的时间。
死锁检测与恢复:在某些复杂的系统中,可以实现死锁检测机制,定期检测系统是否发生死锁。如果检测到死锁,可以采取一些恢复措施,例如回滚事务重启线程等。但死锁检测和恢复机制的实现通常比较复杂,开销也较大。

最佳实践
在实际并发编程中,最有效的死锁避免策略是预防。通过良好的设计和编码习惯,尽量避免死锁的发生。例如,保持锁的获取顺序一致避免嵌套锁使用细粒度锁及时释放锁等。

8.3 条件变量与期物 (Condition Variables and Futures)

本节讲解 std::condition_variable 条件变量的使用,以及 std::future, std::promise, std::packaged_task 等期物类型,实现线程间的同步和数据传递。

8.3.1 std::condition_variable 条件变量 (Condition Variables)

条件变量 (Condition Variable) 是一种同步原语,用于线程间的条件同步。条件变量通常与互斥量 (Mutex) 结合使用,允许线程在某个条件不满足时等待,当条件满足时被其他线程唤醒。条件变量可以有效地实现线程间的协作和通信。

条件变量的基本操作
wait(lock)
▮▮▮▮⚝ 释放互斥锁 lock:线程调用 wait() 时,必须先持有与条件变量关联的互斥锁 lockwait() 操作会原子地释放互斥锁,并使当前线程进入阻塞等待状态,等待被其他线程唤醒。
▮▮▮▮⚝ 等待唤醒:线程进入等待状态后,会一直阻塞,直到被其他线程通过 notify_one()notify_all() 唤醒。
▮▮▮▮⚝ 重新获取互斥锁:当线程被唤醒后,wait() 操作会尝试重新获取互斥锁 lock。只有成功获取互斥锁后,wait() 才会返回,线程才能继续执行后续代码。
▮▮▮▮⚝ 虚假唤醒 (Spurious Wakeup):条件变量可能会发生虚假唤醒,即线程在条件不满足的情况下被意外唤醒。因此,必须在一个循环中检查条件,以确保条件真的满足才继续执行。

notify_one()
▮▮▮▮⚝ 唤醒一个等待线程:调用 notify_one() 会唤醒一个等待在同一个条件变量上的线程(如果有等待线程)。如果有多个线程在等待,具体唤醒哪个线程由系统调度策略决定。
▮▮▮▮⚝ 不释放互斥锁notify_one() 操作本身不释放互斥锁。通常需要在 notify_one() 调用之后,手动释放互斥锁,以便被唤醒的线程能够成功获取互斥锁并继续执行。

notify_all()
▮▮▮▮⚝ 唤醒所有等待线程:调用 notify_all() 会唤醒所有等待在同一个条件变量上的线程。所有被唤醒的线程都会尝试重新获取互斥锁,并检查条件是否满足。
▮▮▮▮⚝ 不释放互斥锁notify_all() 操作本身也不释放互斥锁

条件变量的使用模式
条件变量通常与互斥锁和一个条件谓词 (Condition Predicate) 结合使用。条件谓词是一个布尔表达式,用于表示线程需要等待的条件是否满足。

等待线程 (Waiter)

  1. 获取互斥锁
  2. 循环检查条件谓词while (!condition_predicate)
  3. 如果条件不满足,调用 condition_variable.wait(lock) 进入等待状态(原子地释放互斥锁并等待唤醒)。
  4. 当被唤醒后,wait() 返回,线程重新获取互斥锁。
  5. 再次循环检查条件谓词。
  6. 如果条件满足,退出循环,执行后续操作(临界区代码)。
  7. 释放互斥锁

通知线程 (Notifier)

  1. 获取互斥锁
  2. 修改共享状态,使条件谓词变为真
  3. 调用 condition_variable.notify_one()condition_variable.notify_all() 唤醒等待线程。
  4. 释放互斥锁

条件变量的示例:生产者-消费者模型

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <condition_variable>
5#include <queue>
6std::mutex mtx;
7std::condition_variable cv;
8std::queue<int> data_queue;
9bool production_finished = false;
10void producer() {
11for (int i = 1; i <= 10; ++i) {
12std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产数据
13{
14std::lock_guard<std::mutex> lock(mtx);
15data_queue.push(i);
16std::cout << "生产者生产数据: " << i << std::endl;
17}
18cv.notify_one(); // 通知消费者有新数据
19}
20{
21std::lock_guard<std::mutex> lock(mtx);
22production_finished = true; // 标记生产完成
23}
24cv.notify_all(); // 通知所有消费者生产已完成
25}
26void consumer() {
27while (true) {
28int data;
29{
30std::unique_lock<std::mutex> lock(mtx);
31cv.wait(lock, []{ return !data_queue.empty() || production_finished; }); // 等待条件:队列非空或生产完成
32if (data_queue.empty() && production_finished) {
33break; // 队列为空且生产完成,消费者退出
34}
35data = data_queue.front();
36data_queue.pop();
37std::cout << "消费者消费数据: " << data << std::endl;
38}
39// 模拟消费数据
40std::this_thread::sleep_for(std::chrono::milliseconds(200));
41}
42std::cout << "消费者退出" << std::endl;
43}
44int main() {
45std::thread producer_thread(producer);
46std::thread consumer_thread(consumer);
47producer_thread.join();
48consumer_thread.join();
49std::cout << "程序结束" << std::endl;
50return 0;
51}

在这个生产者-消费者模型的例子中,条件变量 cv 用于在生产者和消费者线程之间进行同步。消费者线程在队列为空时等待,生产者线程在生产数据后通知消费者线程。条件谓词 []{ return !data_queue.empty() || production_finished; } 确保消费者线程只有在队列非空或生产完成时才被唤醒。

8.3.2 期物:future, promise, packaged_task (Futures)

期物 (Futures) 是一种同步机制,用于获取异步操作的结果。C++ 标准库提供了 std::future, std::promise, std::packaged_task 等期物相关的类,用于支持异步操作的结果传递和异常处理。

std::future (期物)
std::future 表示一个异步操作的未来结果。线程可以通过 std::future 对象来获取异步操作的结果,或者等待异步操作完成。

主要操作
▮▮▮▮⚝ get(): 获取异步操作的结果。如果结果尚未就绪,get()阻塞当前线程,直到结果就绪。get() 只能被调用一次,多次调用会抛出异常 std::future_error
▮▮▮▮⚝ valid(): 检查 std::future 对象是否有效,即是否关联到一个异步操作。
▮▮▮▮⚝ wait(): 等待异步操作完成,不返回结果
▮▮▮▮⚝ wait_for(duration): 等待最多 duration 时间,看异步操作是否完成。返回等待状态 std::future_status,表示超时、就绪或延迟启动。
▮▮▮▮⚝ wait_until(time_point): 等待到指定时间点 time_point,看异步操作是否完成。返回等待状态 std::future_status

获取 std::future 对象的方式
▮▮▮▮⚝ std::async() 返回的 std::future 对象。
▮▮▮▮⚝ std::promise::get_future() 返回的 std::future 对象。
▮▮▮▮⚝ std::packaged_task::get_future() 返回的 std::future 对象。

std::promise (承诺)
std::promise 用于设置异步操作的结果。一个 std::promise 对象通常与一个 std::future 对象关联,std::promise 用于设置结果,std::future 用于获取结果。

主要操作
▮▮▮▮⚝ get_future(): 返回与 std::promise 对象关联的 std::future 对象。
▮▮▮▮⚝ set_value(value): 设置异步操作的正常结果 value。设置结果后,与该 std::promise 对象关联的 std::future 对象将变为就绪状态,等待在 future.get() 上的线程将被唤醒并获取结果。
▮▮▮▮⚝ set_exception(exception_ptr): 设置异步操作的异常结果 exception_ptr。设置异常后,与该 std::promise 对象关联的 std::future 对象将变为就绪状态,等待在 future.get() 上的线程将被唤醒并抛出异常。
▮▮▮▮⚝ set_value_at_thread_exit(value): 在设置结果的线程退出时设置异步操作的正常结果 value
▮▮▮▮⚝ set_exception_at_thread_exit(exception_ptr): 在设置异常的线程退出时设置异步操作的异常结果 exception_ptr

std::packaged_task (打包任务)
std::packaged_task 用于将一个函数或可调用对象包装成一个异步任务std::packaged_task 内部包含一个 std::promise 对象,当任务执行完成后,会将任务的返回值或异常设置为 std::promise 的结果。

主要操作
▮▮▮▮⚝ 构造函数:接受一个函数或可调用对象作为参数,将其包装成异步任务。
▮▮▮▮⚝ get_future(): 返回与 std::packaged_task 对象关联的 std::future 对象。
▮▮▮▮⚝ operator() (或 call()): 执行包装的任务。执行任务时,会将任务的返回值或异常设置为 std::promise 的结果,并使关联的 std::future 对象变为就绪状态。
▮▮▮▮⚝ reset(): 重置 std::packaged_task 对象,使其可以重新执行任务。

期物的使用示例
使用 std::promisestd::future 手动传递结果

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <future>
4int calculate_sum(int a, int b) {
5std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算
6return a + b;
7}
8int main() {
9std::promise<int> promise; // 创建 promise 对象
10std::future<int> future = promise.get_future(); // 获取 future 对象
11std::thread calculation_thread([&promise]() {
12int result = calculate_sum(5, 10);
13promise.set_value(result); // 设置 promise 的结果
14});
15std::cout << "等待计算结果..." << std::endl;
16int sum = future.get(); // 获取 future 的结果,阻塞等待直到结果就绪
17std::cout << "计算结果: " << sum << std::endl;
18calculation_thread.join();
19return 0;
20}

使用 std::packaged_task 自动传递结果

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <future>
4int calculate_product(int a, int b) {
5std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算
6return a * b;
7}
8int main() {
9std::packaged_task<int(int, int)> task(calculate_product); // 创建 packaged_task
10std::future<int> future = task.get_future(); // 获取 future 对象
11std::thread task_thread(std::move(task), 6, 7); // 启动线程执行 packaged_task
12std::cout << "等待任务结果..." << std::endl;
13int product = future.get(); // 获取 future 的结果,阻塞等待直到任务完成
14std::cout << "任务结果: " << product << std::endl;
15task_thread.join();
16return 0;
17}

使用 std::async 启动异步任务并获取结果(下一节详细介绍)。

8.3.3 异步操作与 std::async (Asynchronous Operations and std::async)

std::async 是 C++ 标准库提供的一个高层次的异步操作接口,用于启动异步任务并获取其结果std::async 简化了异步编程的复杂性,可以自动管理线程的创建和销毁,并返回一个 std::future 对象,用于获取异步任务的结果。

std::async 的使用
std::async 函数接受一个函数或可调用对象作为参数,并在新的执行线程延迟执行的方式下异步执行该任务。std::async 返回一个 std::future 对象,用于获取异步任务的结果。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <future>
3int async_task(const std::string& message) {
4std::cout << "异步任务执行: " << message << std::endl;
5std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
6return 42;
7}
8int main() {
9std::future<int> result_future = std::async(std::launch::async, async_task, "Hello from async!"); // 启动异步任务,立即在新线程中执行
10std::cout << "主线程继续执行..." << std::endl;
11int result = result_future.get(); // 获取异步任务的结果,阻塞等待直到结果就绪
12std::cout << "异步任务结果: " << result << std::endl;
13return 0;
14}

在这个例子中,std::async(std::launch::async, async_task, "Hello from async!") 启动了一个异步任务 async_task,并指定启动策略为 std::launch::async必须在新线程中执行)。std::async 返回的 result_future 对象可以用于获取异步任务的结果。

std::launch 启动策略
std::async 函数的第一个参数是启动策略 std::launch,用于控制异步任务的执行方式。std::launch 有两种取值:

std::launch::async (异步启动)
▮▮▮▮⚝ 强制在新线程中执行异步任务。
▮▮▮▮⚝ std::async 会创建一个新的线程来执行任务,任务会立即开始执行(与调用 std::async 的线程并发执行)。
▮▮▮▮⚝ 如果系统资源不足,无法创建新线程,std::async 可能会抛出 std::system_error 异常。

std::launch::deferred (延迟启动)
▮▮▮▮⚝ 延迟执行异步任务。
▮▮▮▮⚝ std::async 不立即创建新线程,也不立即执行任务。任务会在 future.get()future.wait() 被调用时,在调用 get()wait() 的线程同步执行。
▮▮▮▮⚝ 如果 future.get()future.wait() 从未被调用,则异步任务永远不会执行

std::launch::any (自动选择)
▮▮▮▮⚝ 允许系统自动选择 std::launch::asyncstd::launch::deferred 策略。
▮▮▮▮⚝ 具体使用哪种策略由系统资源和调度策略决定,不保证一定在新线程中执行,也不保证一定延迟执行。
▮▮▮▮⚝ 是 std::async默认启动策略

std::async 的选择建议
明确需要并发执行的任务,使用 std::launch::async:如果明确需要异步任务与主线程并发执行,并且希望任务立即开始执行,应该使用 std::launch::async 策略。但需要注意,std::launch::async 可能会创建新线程,增加系统开销,并可能在资源不足时抛出异常。
允许延迟执行的任务,使用 std::launch::deferred:如果异步任务可以延迟到需要结果时再执行,或者只是为了方便地获取任务结果,可以使用 std::launch::deferred 策略。std::launch::deferred 不会创建新线程,开销较小,但任务是同步执行的,没有真正的并发性。
让系统自动选择,使用 std::launch::any (默认策略):在大多数情况下,可以使用 std::launch::any 策略,让系统根据资源和调度情况自动选择合适的执行方式。这通常是最简单、最方便的选择。

std::future 的结果获取与异常处理
std::async 返回的 std::future 对象可以用于获取异步任务的结果或异常。

获取结果:调用 future.get() 可以获取异步任务的返回值。如果任务正常完成,get() 返回任务的返回值;如果任务抛出异常,get() 会重新抛出该异常。get() 会阻塞当前线程,直到结果就绪。
异常处理:如果异步任务抛出异常,future.get() 会捕获该异常,并在 get() 调用时重新抛出。可以使用 try-catch 块捕获 future.get() 可能抛出的异常,进行异常处理。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <future>
3#include <stdexcept>
4int throwing_task() {
5std::cout << "异步任务抛出异常..." << std::endl;
6throw std::runtime_error("异步任务执行失败");
7}
8int main() {
9std::future<int> future = std::async(std::launch::async, throwing_task);
10try {
11int result = future.get(); // 尝试获取结果,可能会抛出异常
12std::cout << "异步任务结果: " << result << std::endl; // 不会执行到这里
13} catch (const std::runtime_error& e) {
14std::cerr << "捕获到异常: " << e.what() << std::endl; // 捕获并处理异常
15}
16return 0;
17}

在这个例子中,throwing_task 函数会抛出一个 std::runtime_error 异常。当主线程调用 future.get() 获取结果时,会捕获到这个异常,并在 catch 块中进行处理。

8.4 原子操作 (Atomic Operations)

本节介绍 std::atomic 原子类型和原子操作,以及原子操作在无锁编程中的作用,提高并发程序的性能和效率。

8.4.1 std::atomic 原子类型 (Atomic Types)

原子操作 (Atomic Operations) 是指不可被中断的操作。在一个原子操作执行过程中,不会被其他线程或中断打断。原子操作通常用于对共享变量进行同步,保证在多线程环境下对共享变量的访问是线程安全的,且无需使用互斥锁

C++11 引入了 <atomic> 头文件,提供了 std::atomic 模板类和一系列原子操作函数,用于支持原子操作。

std::atomic 模板类
std::atomic<T> 是一个模板类,用于包装基本数据类型 T,使其具有原子操作的特性。std::atomic<T> 可以包装整型、浮点型、指针类型等,但并非所有类型都支持原子操作,具体取决于硬件平台和编译器实现。

原子性std::atomic<T> 提供的操作都是原子的,例如读取写入递增递减比较并交换 (Compare-and-Swap, CAS) 等。这些操作在执行过程中不会被其他线程打断,保证了操作的完整性和线程安全性。
内存序 (Memory Order):原子操作还涉及内存序的概念,用于控制原子操作的内存访问顺序,以及对其他非原子操作的可见性。内存序是原子操作的重要组成部分,需要根据具体的并发场景选择合适的内存序,以保证程序的正确性和性能。

常用的原子操作
加载 (Load):原子地读取 std::atomic<T> 对象的值。例如 load() 成员函数。
存储 (Store):原子地将一个新值写入 std::atomic<T> 对象。例如 store() 成员函数。
交换 (Exchange):原子地将一个新值写入 std::atomic<T> 对象,并返回旧值。例如 exchange() 成员函数。
比较并交换 (Compare-and-Swap, CAS):原子地比较 std::atomic<T> 对象的值与一个期望值 expected,如果相等,则将对象的值更新为新值 desired,并返回 true;否则,不更新对象的值,并将 expected 更新为对象的当前值,并返回 false。CAS 操作是实现无锁编程的重要基石。例如 compare_exchange_weak()compare_exchange_strong() 成员函数。
递增/递减 (Increment/Decrement):原子地对 std::atomic<T> 对象的值进行递增或递减操作。例如 fetch_add(), fetch_sub(), operator++(), operator--() 等。
位运算 (Bitwise Operations):原子地对 std::atomic<T> 对象的值进行位运算,例如按位与、按位或、按位异或等。例如 fetch_and(), fetch_or(), fetch_xor() 等。

内存序 (Memory Order)
内存序用于控制原子操作的内存访问顺序,以及对其他非原子操作的可见性。C++11 提供了六种内存序选项,作为原子操作函数的可选参数:

std::memory_order_relaxed (宽松序)
▮▮▮▮⚝ 最宽松的内存序,只保证原子性,不保证顺序性和可见性。
▮▮▮▮⚝ 原子操作之间可以任意重排序,与其他非原子操作之间也可能重排序。
▮▮▮▮⚝ 性能最高,但适用场景有限,通常只用于计数器等对顺序性和可见性要求不高的场景。

std::memory_order_consume (消费序)
▮▮▮▮⚝ 用于控制依赖关系的内存序。
▮▮▮▮⚝ 当一个线程通过原子加载操作(load())消费某个原子变量的值时,memory_order_consume 保证在该原子加载操作之后,所有依赖于该原子加载结果非原子操作,都将在该原子加载操作之后执行。
▮▮▮▮⚝ 主要用于读多写少,且需要保证依赖关系的场景。

std::memory_order_acquire (获取序)
▮▮▮▮⚝ 用于同步线程的内存序。
▮▮▮▮⚝ 当一个线程执行原子加载操作(load()exchange() 等)并使用 memory_order_acquire 内存序时,memory_order_acquire 保证在该原子加载操作之后,所有后续的内存访问,都将在该原子加载操作之后执行。
▮▮▮▮⚝ 并且,memory_order_acquire 保证之前执行了 memory_order_release 或更强内存序的原子存储操作写入结果,对当前线程可见。
▮▮▮▮⚝ 常用于锁的获取操作。

std::memory_order_release (释放序)
▮▮▮▮⚝ 用于同步线程的内存序。
▮▮▮▮⚝ 当一个线程执行原子存储操作(store()exchange() 等)并使用 memory_order_release 内存序时,memory_order_release 保证在该原子存储操作之前,所有之前的内存访问,都将在该原子存储操作之前执行。
▮▮▮▮⚝ 并且,memory_order_release 保证之后执行了 memory_order_acquire 或更强内存序的原子加载操作的线程,能够看到当前线程的写入结果
▮▮▮▮⚝ 常用于锁的释放操作。

std::memory_order_acq_rel (获取-释放序)
▮▮▮▮⚝ 结合了 memory_order_acquirememory_order_release 的特性
▮▮▮▮⚝ 用于同时进行原子加载和原子存储操作(例如 exchange(), fetch_add() 等)时。
▮▮▮▮⚝ 保证了 获取序释放序 的所有特性,即既保证了操作之前的内存访问顺序,又保证了操作之后的内存访问顺序,并实现了线程间的同步可见性

std::memory_order_seq_cst (顺序一致性序)
▮▮▮▮⚝ 最强的内存序,也是默认的内存序。
▮▮▮▮⚝ 保证所有原子操作都是顺序一致的,即所有线程看到的原子操作顺序都是一致的,如同在一个全局唯一的顺序中执行。
▮▮▮▮⚝ 提供了最强的顺序性和可见性保证,但性能最低
▮▮▮▮⚝ 适用于对顺序性和可见性要求最高的场景,例如复杂的同步算法。

选择内存序的原则
性能优先:如果对顺序性和可见性要求不高,可以优先考虑 std::memory_order_relaxed
同步需求:如果需要同步线程,保证线程间的可见性,应该使用 std::memory_order_acquirestd::memory_order_release(或 std::memory_order_acq_rel)。
顺序一致性:如果对顺序一致性有严格要求,可以使用 std::memory_order_seq_cst,但需要注意性能开销。
默认选择:在不确定如何选择内存序时,优先使用默认的 std::memory_order_seq_cst,以保证程序的正确性。之后再根据性能测试和分析,逐步优化内存序。

原子类型的使用示例
原子计数器

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <atomic>
4std::atomic<int> counter = 0; // 原子计数器
5void increment_counter() {
6for (int i = 0; i < 100000; ++i) {
7counter++; // 原子递增操作,默认内存序为 std::memory_order_seq_cst
8}
9}
10int main() {
11std::thread thread1(increment_counter);
12std::thread thread2(increment_counter);
13thread1.join();
14thread2.join();
15std::cout << "原子计数器最终值: " << counter << std::endl; // 结果接近 200000
16return 0;
17}

在这个例子中,counter 是一个原子计数器,使用 std::atomic<int> 声明。counter++ 操作是原子递增操作,保证了多线程环境下的线程安全,无需使用互斥锁。

8.4.2 原子操作的应用场景 (Application Scenarios of Atomic Operations)

原子操作在并发编程中有很多应用场景,主要用于实现无锁数据结构高性能并发算法

计数器 (Counters)
原子计数器是最常见的原子操作应用场景。可以使用原子计数器来实现线程安全的计数功能,例如请求计数任务计数资源引用计数等。

标志位 (Flags)
原子标志位用于在多线程之间传递状态信息,例如线程同步标志事件通知标志取消标志等。可以使用原子布尔类型 std::atomic<bool> 或原子整型类型作为标志位。

无锁数据结构 (Lock-Free Data Structures)
原子操作是实现无锁数据结构(例如无锁队列、无锁栈、无锁哈希表等)的关键技术。无锁数据结构使用原子操作来保证数据结构的线程安全性,避免了互斥锁的开销,可以提高并发程序的性能和效率。

无锁队列 (Lock-Free Queue):可以使用原子操作来实现高效的无锁队列,例如使用 CAS 操作来实现入队和出队操作。无锁队列常用于生产者-消费者模型消息队列等场景。
无锁栈 (Lock-Free Stack):可以使用原子操作来实现无锁栈,例如使用 CAS 操作来实现入栈和出栈操作。无锁栈常用于任务调度函数调用栈等场景。

自旋锁 (Spin Locks)
自旋锁是一种忙等待的锁,当线程尝试获取锁时,如果锁已被其他线程持有,则线程会循环检测锁是否释放,而不是进入阻塞状态。自旋锁的实现通常基于原子操作,例如使用原子 CAS 操作来实现锁的获取和释放。

适用场景临界区代码执行时间非常短,且锁竞争不激烈的场景。自旋锁的优势在于避免了线程切换的开销,但如果临界区代码执行时间较长或锁竞争激烈,自旋锁会浪费 CPU 资源。

引用计数 (Reference Counting)
原子引用计数用于实现线程安全的引用计数功能,例如智能指针 std::shared_ptr 的引用计数就是使用原子操作实现的。原子引用计数可以保证在多线程环境下,对引用计数的递增和递减操作是原子安全的。

8.4.3 无锁编程的挑战与权衡 (Challenges and Trade-offs in Lock-Free Programming)

无锁编程 (Lock-Free Programming) 是一种并发编程技术,使用原子操作代替互斥锁来实现线程安全的数据结构和算法。无锁编程的目标是提高并发程序的性能和效率避免互斥锁带来的开销,例如锁竞争、线程阻塞、上下文切换等。

无锁编程的优势
更高的性能:无锁编程避免了互斥锁的开销,例如锁竞争、线程阻塞、上下文切换等,可以显著提高并发程序的性能和效率。
更好的可伸缩性:无锁程序通常具有更好的可伸缩性,可以更好地利用多核处理器的性能,随着处理器核心数的增加,性能提升更明显。
避免死锁:无锁编程不需要使用互斥锁,因此可以天然地避免死锁问题。
更低的延迟:无锁程序通常具有更低的延迟,因为线程不需要等待锁,可以更快地响应请求。

无锁编程的挑战
更高的复杂性:无锁编程比基于锁的编程更复杂,需要深入理解原子操作、内存序、数据结构和算法等并发编程知识。
更难调试:无锁程序的错误通常更难调试,因为错误可能发生在细微的竞争条件中,难以复现和定位。
ABA 问题:无锁编程中可能会遇到 ABA 问题,即一个变量的值从 A 变为 B,又变回 A,但在某些场景下,这仍然可能导致错误。需要使用版本号双 CAS 操作等技术来解决 ABA 问题。
活锁和饥饿:无锁程序可能会出现 活锁 (Livelock)饥饿 (Starvation) 问题。活锁是指多个线程不断地重试某个操作,但始终无法成功,导致程序空转。饥饿是指某些线程长时间无法获得资源,导致程序性能下降或功能异常。

无锁编程的权衡
性能与复杂性的权衡:无锁编程可以提高性能,但也会增加程序的复杂性和开发难度。需要在性能提升和开发成本之间进行权衡。
适用场景:无锁编程并非适用于所有并发场景。对于临界区代码执行时间较短锁竞争激烈性能要求高的场景,无锁编程可能更适合。对于临界区代码执行时间较长锁竞争不激烈代码可维护性要求高的场景,基于锁的编程可能更合适。
开发成本:无锁编程需要更高的开发成本,需要更资深的并发编程专家来设计、实现和维护无锁程序。

最佳实践
谨慎使用无锁编程:无锁编程虽然具有性能优势,但也存在挑战和权衡。在决定使用无锁编程之前,需要仔细评估其适用性和必要性。
循序渐进:可以先从简单的无锁数据结构和算法开始尝试,例如原子计数器、原子标志位、无锁队列等,逐步积累无锁编程经验。
充分测试:无锁程序需要进行充分的测试,包括单元测试、集成测试、性能测试、压力测试等,以确保程序的正确性和稳定性。
持续学习:无锁编程是一个不断发展的领域,需要持续学习新的技术和方法,关注最新的研究成果和最佳实践。

<END_OF_CHAPTER/>

9. 实用工具 (Utilities)

本章将深入探讨 C++ 标准库中提供的各种实用工具 (Utilities),这些工具如同工具箱中的各类精巧器械,虽不直接构成程序的核心骨架,却能在日常开发中发挥至关重要的作用,提升代码的效率、安全性与可读性。我们将逐一剖析 pair (对组)、tuple (元组)、optional (可选值)、variant (变体类型) 等通用工具,以及时间工具 (Time Utilities)、类型 traits (Type Traits)、错误处理 (Error Handling) 和本地化 (Localization) 等重要组成部分,旨在帮助读者全面掌握这些工具的使用方法和应用场景,从而编写出更加健壮、灵活且易于维护的 C++ 代码。

9.1 通用工具:pair, tuple, optional, variant (General Utilities)

本节将聚焦于 C++ 标准库中提供的通用工具,它们如同编程工具箱中的瑞士军刀,功能多样且用途广泛。我们将深入讲解 pair (对组)、tuple (元组)、optional (可选值) 和 variant (变体类型) 这四种核心工具,剖析其设计理念、使用方法以及在提升代码表达能力和灵活性方面的独特优势。掌握这些工具,能让开发者在面对各种编程场景时更加游刃有余,写出更优雅、更高效的代码。

9.1.1 pair (对组) 与 tuple (元组): 组合数据 (Combined Data)

pair (对组) 和 tuple (元组) 是 C++ 标准库中用于组合数据的基本工具,它们允许将多个不同类型或相同类型的值组合成一个单一的实体。虽然它们都用于数据组合,但在灵活性和适用场景上有所区别。

pair (对组)

pair 是一个模板类,用于将两个可能类型不同的值组合在一起。它常用于函数需要返回两个值的情况,或者需要将两个相关联的值作为一个单元处理的场景。

用途 (Purpose)

⚝ 将两个相关的值捆绑在一起,例如键值对 (key-value pairs)。
⚝ 作为函数的返回值,返回两个相关联的结果。
⚝ 简化数据结构,例如表示点的坐标 std::pair<int, int>

创建 (Creation)

⚝ 使用构造函数直接创建:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::pair<int, std::string> p1(1, "hello");
2std::pair<double, char> p2{3.14, 'a'};

⚝ 使用 std::make_pair() 辅助函数,类型推导更简洁:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1auto p3 = std::make_pair(true, 100); // p3 的类型会被推导为 std::pair<bool, int>

访问元素 (Element Access)

⚝ 使用 .first 访问第一个元素。
⚝ 使用 .second 访问第二个元素。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::pair<int, std::string> p(42, "answer");
2int number = p.first; // number 的值为 42
3std::string text = p.second; // text 的值为 "answer"

结构化绑定 (Structured Bindings)

C++17 引入的结构化绑定 (Structured Bindings) 可以更方便地访问 pair 的元素:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::pair<int, std::string> p(10, "example");
2auto [index, message] = p; // 使用结构化绑定
3// index 的值为 10, message 的值为 "example"
4std::cout << "Index: " << index << ", Message: " << message << std::endl;

tuple (元组)

tuple 是一个更通用的模板类,可以组合任意数量不同类型的值。它扩展了 pair 的概念,提供了更强大的数据组合能力。

用途 (Purpose)

⚝ 组合多个相关但不一定相同类型的数据。
⚝ 作为函数的返回值,返回多个结果。
⚝ 构建复杂的数据结构,例如数据库记录、配置信息等。

创建 (Creation)

⚝ 使用构造函数直接创建:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::tuple<int, std::string, double> t1(1, "world", 2.718);
2std::tuple<char, bool, long long> t2{'b', false, 1234567890LL};

⚝ 使用 std::make_tuple() 辅助函数:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1auto t3 = std::make_tuple('X', 99, "end"); // t3 的类型会被推导为 std::tuple<char, int, const char*>

访问元素 (Element Access)

⚝ 使用 std::get<index>(tuple_object) 模板函数访问指定索引的元素,索引从 0 开始

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::tuple<int, std::string, double> t(7, "tuple_data", 1.618);
2int id = std::get<0>(t); // id 的值为 7
3std::string name = std::get<1>(t); // name 的值为 "tuple_data"
4double value = std::get<2>(t); // value 的值为 1.618

结构化绑定 (Structured Bindings):同样适用于 tuple,更方便地访问元素:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::tuple<int, std::string, double> t(2023, "year", 3.14159);
2auto [year, label, pi] = t; // 使用结构化绑定
3// year 的值为 2023, label 的值为 "year", pi 的值为 3.14159
4std::cout << "Year: " << year << ", Label: " << label << ", Pi: " << pi << std::endl;

应用场景对比 (Comparison of Application Scenarios)

pair 适用于简单地组合两个值,例如表示键值对或点的坐标。它的使用场景更加具体和常见于需要返回两个值的函数。
tuple 更为通用,适用于组合任意数量的值,特别是在需要返回多个值或构建复杂数据结构时。当需要组合的数据数量超过两个,或者类型更加多样化时,tuple 是更合适的选择。

代码示例 (Code Example)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <string>
3#include <tuple>
4#include <utility> // std::pair, std::make_pair
5int main() {
6// pair 示例
7std::pair<int, std::string> student_id_name = std::make_pair(1001, "Alice");
8std::cout << "Student ID: " << student_id_name.first << ", Name: " << student_id_name.second << std::endl;
9// tuple 示例
10std::tuple<int, std::string, double> product_info = std::make_tuple(123, "Laptop", 999.99);
11std::cout << "Product ID: " << std::get<0>(product_info)
12<< ", Name: " << std::get<1>(product_info)
13<< ", Price: " << std::get<2>(product_info) << std::endl;
14// 结构化绑定示例
15auto [id, name, price] = product_info;
16std::cout << "Product ID (structured binding): " << id
17<< ", Name (structured binding): " << name
18<< ", Price (structured binding): " << price << std::endl;
19return 0;
20}

总结 (Summary)

pairtuple 是 C++ 标准库中强大的数据组合工具。pair 专注于两个值的组合,简洁易用;tuple 则更加通用,能够处理任意数量的值的组合。结构化绑定进一步简化了对 pairtuple 元素的访问,提高了代码的可读性和简洁性。在实际开发中,根据数据组合的复杂度和数量选择合适的工具,能够有效地组织和管理数据。

9.1.2 optional (可选值): 表示可能缺失的值 (Representing Potentially Missing Values)

optional (可选值) 是 C++17 引入的一个模板类,用于显式地表示一个值可能存在,也可能不存在的情况。它优雅地解决了传统上使用空指针 (null pointer) 或特殊值来表示“值不存在”的弊端,提高了代码的安全性和可读性。

概念 (Concept)

std::optional<T> 可以看作是一个可以容纳类型 T 的值的容器,但这个容器可以是空的 (empty)。它有两种状态:

有值 (engaged)optional 对象包含一个类型为 T 的值。
无值 (disengaged)optional 对象不包含任何值。

与使用指针来表示可选值不同,optional 明确地表达了“值可能缺失”的语义,避免了空指针解引用的风险,并提供了更清晰的接口来检查和访问值。

状态检查 (Status Check)

optional 提供了多种方法来检查其状态(是否有值):

bool operator bool() const noexcept;explicit operator bool() const noexcept;:隐式或显式转换为 bool 类型,当 optional 有值时返回 true,否则返回 false

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::optional<int> opt_value = std::optional<int>(42);
2if (opt_value) { // 或者 if (opt_value.has_value())
3std::cout << "Optional has value: " << *opt_value << std::endl;
4} else {
5std::cout << "Optional has no value." << std::endl;
6}

bool has_value() const noexcept;:显式检查是否包含值,有值返回 true,否则返回 false

值访问 (Value Access)

访问 optional 中包含的值有以下几种方式:

T& operator*() &;T const& operator*() const&;:解引用运算符,返回对包含值的引用。如果 optional 无值,调用此操作符将导致未定义行为。因此,在使用解引用运算符前,务必先检查 optional 是否有值

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::optional<std::string> opt_str = "hello";
2if (opt_str.has_value()) {
3std::string& str_ref = *opt_str; // 获取值的引用
4std::cout << "Value: " << str_ref << std::endl;
5}

T* operator->() &;T const* operator->() const&;:指针运算符,返回指向包含值的指针。如果 optional 无值,调用此操作符将导致未定义行为。同样,使用前需要检查 optional 状态。
T value() &;T value() const&;T value() &&;T value() const&&;:返回包含的值的拷贝或移动。如果 optional 无值,调用 value() 将抛出 std::bad_optional_access 异常

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::optional<int> opt_num;
2try {
3int num = opt_num.value(); // 尝试访问无值的 optional
4} catch (const std::bad_optional_access& e) {
5std::cerr << "Exception caught: " << e.what() << std::endl; // 输出错误信息
6}

T value_or(T default_value) const&;T value_or(T default_value) &&;:如果 optional 有值,则返回该值,否则返回提供的 default_value不会抛出异常

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::optional<int> opt_age;
2int age = opt_age.value_or(0); // 如果 opt_age 无值,age 默认为 0
3std::cout << "Age: " << age << std::endl;

template<class F> auto and_then(F f) &;template<class F> auto and_then(F f) const&;template<class F> auto and_then(F f) &&;template<class F> auto and_then(F f) const&&; (C++23):链式操作,如果 optional 有值,则将值传递给函数 f 并返回 f 的结果,否则返回一个空的 optional
template<class F> auto or_else(F f) &;template<class F> auto or_else(F f) const&;template<class F> auto or_else(F f) &&;template<class F> auto or_else(F f) const&&; (C++23):链式操作,如果 optional 有值,则返回 optional 本身,否则调用函数 f 并返回 f 的结果(f 的结果也应该是 optional)。

避免空指针异常的应用 (Avoiding Null Pointer Exceptions)

optional 最重要的应用之一是替代指针来表示可能为空的值,从而避免空指针解引用导致的程序崩溃。在函数可能返回空值的情况下,使用 optional 作为返回类型,能够清晰地表达这种可能性,并迫使调用者显式地处理值可能不存在的情况。

代码示例 (Code Example)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <optional>
3#include <string>
4std::optional<int> divide(int numerator, int denominator) {
5if (denominator == 0) {
6return std::nullopt; // 除数为 0,返回无值 optional
7}
8return numerator / denominator; // 正常返回结果
9}
10std::optional<std::string> get_username(int user_id) {
11if (user_id > 1000) {
12return "ValidUser";
13} else {
14return std::nullopt; // user_id 无效,返回无值 optional
15}
16}
17int main() {
18std::optional<int> result1 = divide(10, 2);
19if (result1.has_value()) {
20std::cout << "Division result: " << result1.value() << std::endl;
21} else {
22std::cout << "Division by zero!" << std::endl;
23}
24std::optional<int> result2 = divide(5, 0);
25if (result2.has_value()) {
26std::cout << "Division result: " << result2.value() << std::endl;
27} else {
28std::cout << "Division by zero!" << std::endl;
29}
30std::optional<std::string> username1 = get_username(1001);
31if (username1) {
32std::cout << "Username: " << *username1 << std::endl;
33} else {
34std::cout << "Invalid user ID." << std::endl;
35}
36std::optional<std::string> username2 = get_username(999);
37if (username2) {
38std::cout << "Username: " << *username2 << std::endl;
39} else {
40std::cout << "Invalid user ID." << std::endl;
41}
42return 0;
43}

总结 (Summary)

optional 是 C++ 标准库中用于处理可选值的强大工具。它通过类型系统显式地表达了值可能缺失的情况,避免了空指针的潜在风险,并提供了丰富的接口来检查和访问值。在函数返回可能为空的结果、处理可能不存在的数据或者需要清晰表达“值缺失”语义的场景下,optional 都是一个非常优秀的选择,能够提高代码的健壮性和可读性。

9.1.3 variant (变体类型): 类型安全的联合体 (Type-Safe Union)

variant (变体类型) 是 C++17 引入的一个模板类,它提供了一种类型安全的联合体 (union) 实现。variant 允许一个变量在运行时存储多种预先定义的类型中的一种,但同一时刻只能存储一个类型的值。与传统的 union 相比,variant 提供了更强的类型安全性和更好的错误处理机制。

概念 (Concept)

std::variant<T1, T2, T3, ...> 定义了一个可以存储类型 T1, T2, T3, ... 中任何一个值的变量。variant 会跟踪当前存储值的类型,并在访问时进行类型检查,从而避免了传统 union 中可能出现的类型混淆和数据损坏问题。

类型安全特性 (Type-Safe Features)

类型跟踪 (Type Tracking)variant 内部会记录当前存储值的类型索引 (index)。可以使用 index() 方法获取当前存储类型的索引(从 0 开始)。
类型检查 (Type Checking):在访问 variant 中存储的值时,variant 会进行类型检查,确保以正确的类型进行访问。如果尝试以错误的类型访问,将会抛出异常或导致编译错误。
构造与赋值 (Construction and Assignment)variant 的构造和赋值操作都是类型安全的。它会根据传入的值自动选择合适的类型进行存储,并进行必要的类型转换(如果可能)。

访问器 (Visitor) 模式的应用 (Application of Visitor Pattern)

访问 variant 中存储的值,最安全和推荐的方式是使用访问器 (Visitor) 模式std::visit() 函数接受一个访问器对象 (通常是一个 Lambda 表达式或函数对象) 和一个 variant 对象作为参数,然后根据 variant 当前存储的类型,调用访问器对象中对应类型的重载版本。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <variant>
3#include <string>
4int main() {
5std::variant<int, std::string, double> var;
6var = 10; // 存储 int 类型
7std::cout << "Index: " << var.index() << std::endl; // 输出索引 0
8var = "hello"; // 存储 std::string 类型
9std::cout << "Index: " << var.index() << std::endl; // 输出索引 1
10var = 3.14; // 存储 double 类型
11std::cout << "Index: " << var.index() << std::endl; // 输出索引 2
12// 使用 std::visit 访问 variant 的值
13std::visit([](auto&& arg){
14using T = std::decay_t<decltype(arg)>;
15if constexpr (std::is_same_v<T, int>) {
16std::cout << "Value is int: " << arg << std::endl;
17} else if constexpr (std::is_same_v<T, std::string>) {
18std::cout << "Value is string: " << arg << std::endl;
19} else if constexpr (std::is_same_v<T, double>) {
20std::cout << "Value is double: " << arg << std::endl;
21}
22}, var);
23return 0;
24}

在上面的例子中,Lambda 表达式作为访问器,根据 variant var 当前存储的类型,执行不同的操作。std::decay_t 用于去除类型修饰,std::is_same_v 用于编译期类型比较,if constexpr 用于编译期条件判断,确保代码的类型安全和效率。

替代传统 union 的优势 (Advantages over Traditional union)

类型安全 (Type Safety)variant 提供了严格的类型检查,避免了传统 union 的类型混淆和内存安全问题。
析构函数 (Destructor)variant 能够正确地调用当前存储类型的析构函数,即使存储的是复杂类型(如 std::string),也能保证资源的正确释放。传统 union 则无法做到这一点。
异常安全 (Exception Safety)variant 的操作通常是异常安全的,即使在操作过程中抛出异常,也能保证 variant 对象的状态一致性。
更清晰的语义 (Clearer Semantics)variant 明确地表达了“变量可以存储多种类型中的一种”的语义,提高了代码的可读性和可维护性。

代码示例 (Code Example)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <variant>
3#include <string>
4#include <vector>
5// 定义一个简单的访问器结构体
6struct VariantVisitor {
7void operator()(int i) const {
8std::cout << "Integer: " << i << std::endl;
9}
10void operator()(const std::string& s) const {
11std::cout << "String: " << s << std::endl;
12}
13void operator()(double d) const {
14std::cout << "Double: " << d << std::endl;
15}
16};
17int main() {
18std::vector<std::variant<int, std::string, double>> data;
19data.push_back(123);
20data.push_back("variant example");
21data.push_back(3.14159);
22VariantVisitor visitor; // 创建访问器对象
23for (const auto& v : data) {
24std::visit(visitor, v); // 使用 std::visit 和访问器访问 variant
25}
26return 0;
27}

总结 (Summary)

variant 是 C++ 标准库提供的类型安全联合体,它克服了传统 union 的诸多缺陷,提供了更安全、更易用、更强大的类型选择能力。通过 variant,开发者可以创建能够存储多种类型值的变量,并使用访问器模式安全地访问和操作这些值。在需要表示多种可能类型的数据,并且需要保证类型安全和资源管理正确性的场景下,variant 是比传统 union 更优秀、更现代的选择。

9.2 时间工具 (Time Utilities)

C++ 标准库的 <chrono> 头文件提供了强大的时间工具 (Time Utilities),用于处理时间相关的各种操作,例如时间点的表示、时间段的计算、时钟的获取以及时间的格式化输出等。这部分工具是编写需要精确计时、时间间隔计算、超时控制等程序的基石。

9.2.1 duration (持续时间), time_point (时间点), clock (时钟)

<chrono> 库的核心概念包括 duration (持续时间)、time_point (时间点) 和 clock (时钟)。理解这三个概念及其相互关系是掌握 <chrono> 库的关键。

duration (持续时间)

duration 表示时间间隔,例如 5 秒、100 毫秒、1 小时等。duration 是一个模板类,其定义形式为 std::chrono::duration<Rep, Period>,其中:

Rep (Representation):表示时间间隔的数值类型,通常是算术类型,例如 int, long long, double 等。
Period (Period):表示时间单位的比例,是一个 std::ratio 类型,定义了 Rep 类型数值所代表的时间单位。例如 std::ratio<1, 1> 表示秒,std::ratio<1, 1000> 表示毫秒。

<chrono> 预定义了一些常用的 duration 类型,例如:

std::chrono::nanoseconds:纳秒
std::chrono::microseconds:微秒
std::chrono::milliseconds:毫秒
std::chrono::seconds:秒
std::chrono::minutes:分钟
std::chrono::hours:小时

单位转换 (Unit Conversion)

duration 对象之间可以进行单位转换,但需要显式地进行类型转换或使用 duration_cast 函数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3int main() {
4using namespace std::chrono;
5seconds sec(5); // 5 秒
6milliseconds ms = duration_cast<milliseconds>(sec); // 秒转换为毫秒
7microseconds us = duration_cast<microseconds>(sec); // 秒转换为微秒
8std::cout << "5 seconds is " << ms.count() << " milliseconds" << std::endl;
9std::cout << "5 seconds is " << us.count() << " microseconds" << std::endl;
10return 0;
11}

时间运算 (Time Operations)

duration 对象支持算术运算,例如加法、减法、乘法、除法等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3int main() {
4using namespace std::chrono;
5seconds d1(10);
6seconds d2(3);
7seconds sum = d1 + d2; // 加法
8seconds diff = d1 - d2; // 减法
9seconds product = d1 * 2; // 乘法
10seconds quotient = d1 / 2; // 除法
11std::cout << "Sum: " << sum.count() << " seconds" << std::endl;
12std::cout << "Difference: " << diff.count() << " seconds" << std::endl;
13std::cout << "Product: " << product.count() << " seconds" << std::endl;
14std::cout << "Quotient: " << quotient.count() << " seconds" << std::endl;
15return 0;
16}

time_point (时间点)

time_point 表示时间轴上的一个特定时刻,例如 2023年10月27日 10:00:00 UTC。time_point 也是一个模板类,其定义形式为 std::chrono::time_point<Clock, Duration>,其中:

Clock (时钟):指定时间点所使用的时钟类型,例如 system_clock, steady_clock, high_resolution_clock 等。
Duration (持续时间):表示时间点相对于时钟起点的偏移量,通常是一个 duration 类型。

不同时钟类型 (Different Clock Types) 的区别

<chrono> 库提供了几种预定义的时钟类型,它们的主要区别在于精度、稳定性和与系统时间的关系:

std::chrono::system_clock:表示系统时钟,通常基于系统时间,可以被用户或系统管理员修改。它的时间可能不单调 (non-monotonic),即时间可能会向前或向后跳跃。适用于需要与系统时间同步的场景,例如显示当前时间、记录日志时间戳等。
std::chrono::steady_clock:表示稳定时钟,保证时间单调递增 (monotonic),不会被系统时间调整影响。但它的起点和当前系统时间可能没有明确关系。适用于测量时间间隔、性能测试等对时间稳定性要求高的场景。
std::chrono::high_resolution_clock:表示高精度时钟,提供系统中可用的最高时间精度。它可能是 system_clocksteady_clock 的别名,取决于平台的实现。

时间点运算 (Time Point Operations)

time_point 对象可以与 duration 对象进行加减运算,得到新的 time_point 对象。也可以计算两个 time_point 对象之间的时间间隔,得到一个 duration 对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3int main() {
4using namespace std::chrono;
5time_point<system_clock> now = system_clock::now(); // 获取当前系统时间点
6time_point<system_clock> future = now + hours(2); // 2 小时后的时间点
7time_point<system_clock> past = now - minutes(30); // 30 分钟前的时间点
8duration<double> elapsed_time = future - now; // 计算时间间隔
9std::cout << "Current time: " << system_clock::to_time_t(now) << std::endl;
10std::cout << "Time in 2 hours: " << system_clock::to_time_t(future) << std::endl;
11std::cout << "Time 30 minutes ago: " << system_clock::to_time_t(past) << std::endl;
12std::cout << "Elapsed time: " << elapsed_time.count() << " seconds" << std::endl;
13return 0;
14}

clock (时钟)

clock 是时间点的来源,它定义了时间的度量标准和精度。<chrono> 库提供的时钟类型主要有 system_clock, steady_clock, high_resolution_clock。每个时钟类型都提供了静态方法 now() 来获取当前时间点。

代码示例 (Code Example)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3#include <thread> // std::this_thread::sleep_for
4int main() {
5using namespace std::chrono;
6auto start_time = steady_clock::now(); // 使用 steady_clock 记录开始时间
7std::cout << "Start operation..." << std::endl;
8std::this_thread::sleep_for(milliseconds(1500)); // 模拟耗时操作
9std::cout << "Operation completed." << std::endl;
10auto end_time = steady_clock::now(); // 记录结束时间
11auto elapsed = end_time - start_time; // 计算时间间隔
12std::cout << "Elapsed time: " << duration_cast<milliseconds>(elapsed).count() << " milliseconds" << std::endl;
13return 0;
14}

总结 (Summary)

duration, time_point, clock<chrono> 库的核心组件。duration 表示时间间隔,time_point 表示时间点,clock 提供时间点来源。理解它们的概念和关系,能够灵活地进行时间计算、测量和控制。在需要精确时间处理的场景下,<chrono> 库提供了强大而类型安全的工具。

9.2.2 时间格式化与输出 (Time Formatting and Output)

<chrono> 库本身主要关注时间的计算和表示,时间的格式化输出和输入则通常与 I/O 流库结合使用。C++11 引入了 <iomanip> 头文件中的 std::put_timestd::get_time 操纵符 (manipulators),用于实现时间的格式化输出和输入。

std::put_time (格式化输出)

std::put_time 是一个 I/O 操纵符,用于将 time_point 对象按照指定的格式字符串 (format string) 输出到输出流。它的原型如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class charT>
2/*unspecified*/ put_time(const std::tm* tmb, const charT* fmt);

tmb:指向 std::tm 结构体的指针,通常通过 std::localtimestd::gmtimetime_point 转换为 std::tm
fmt:格式字符串,遵循 strftime 函数的格式规则。

常用的格式控制符包括:

%Y:年份(四位数)
%m:月份(01-12)
%d:日(01-31)
%H:小时(24小时制,00-23)
%M:分钟(00-59)
%S:秒(00-59)
%F:ISO 8601 格式日期 (%Y-%m-%d)
%T:ISO 8601 格式时间 (%H:%M:%S)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3#include <iomanip> // std::put_time
4int main() {
5using namespace std::chrono;
6time_point<system_clock> now = system_clock::now();
7std::time_t t = system_clock::to_time_t(now); // 转换为 std::time_t
8std::tm tm_val;
9localtime_r(&t, &tm_val); // 转换为本地时间 std::tm (线程安全版本)
10std::cout << "Formatted time: " << std::put_time(&tm_val, "%Y-%m-%d %H:%M:%S") << std::endl;
11std::cout << "Formatted date (ISO 8601): " << std::put_time(&tm_val, "%F") << std::endl;
12std::cout << "Formatted time (ISO 8601): " << std::put_time(&tm_val, "%T") << std::endl;
13return 0;
14}

std::get_time (格式化输入)

std::get_time 是一个 I/O 操纵符,用于从输入流按照指定的格式字符串解析时间,并将结果存储到 std::tm 结构体中。它的原型如下:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <class charT>
2/*unspecified*/ get_time(std::tm* tmb, const charT* fmt);

tmb:指向 std::tm 结构体的指针,用于存储解析后的时间信息。
fmt:格式字符串,与 std::put_time 的格式字符串规则相同。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <chrono>
3#include <iomanip> // std::get_time
4#include <sstream> // std::istringstream
5int main() {
6std::istringstream ss("2023-10-27 14:30:00");
7std::tm tm_val;
8ss >> std::get_time(&tm_val, "%Y-%m-%d %H:%M:%S");
9if (ss.fail()) {
10std::cerr << "Parse failed\n";
11} else {
12std::cout << "Year: " << tm_val.tm_year + 1900 << std::endl; // tm_year 从 1900 年算起
13std::cout << "Month: " << tm_val.tm_mon + 1 << std::endl; // tm_mon 从 0 开始
14std::cout << "Day: " << tm_val.tm_mday << std::endl;
15std::cout << "Hour: " << tm_val.tm_hour << std::endl;
16std::cout << "Minute: " << tm_val.tm_min << std::endl;
17std::cout << "Second: " << tm_val.tm_sec << std::endl;
18}
19return 0;
20}

总结 (Summary)

std::put_timestd::get_time 操纵符提供了 C++ 中时间格式化输出和输入的能力。结合 <chrono> 库的时间类型和格式字符串,可以灵活地控制时间的显示格式和解析方式。在需要与用户交互、数据持久化或与其他系统交换时间数据的场景下,时间格式化至关重要。

9.3 类型 Traits (Type Traits)

类型 traits (Type Traits) 是 C++ 标准库中一组强大的编译期工具,用于在编译时获取和查询类型的各种属性和特征。类型 traits 本身是模板类,通过模板特化 (template specialization) 和静态常量 (static constant) 或静态成员函数 (static member function) 来对外提供类型信息。类型 traits 是 C++ 元编程 (Metaprogramming) 的重要组成部分,广泛应用于泛型编程、编译期优化和代码生成等领域。

9.3.1 类型 Traits 的概念与作用 (Concepts and Role of Type Traits)

类型 traits 的核心思想是在编译时对类型进行 introspect (内省),获取类型的各种信息,例如:

⚝ 类型是否是算术类型 (arithmetic type)?
⚝ 类型是否是指针类型 (pointer type)?
⚝ 类型是否是类类型 (class type)?
⚝ 类型是否可以被默认构造 (default constructible)?
⚝ 类型是否可以被拷贝构造 (copy constructible)?

类型 traits 的结果通常以静态常量 value 的形式提供,或者通过静态成员函数 is_xxx() 返回布尔值。这些结果都可以在编译期计算出来,不会产生运行时的开销。

类型 Traits 的元编程特性 (Metaprogramming Features)

类型 traits 的元编程特性主要体现在以下几个方面:

编译期计算 (Compile-time Computation):类型 traits 的结果在编译期计算,不产生运行时开销,有助于提高程序性能。
条件编译 (Conditional Compilation):可以根据类型 traits 的结果进行条件编译,选择不同的代码路径,实现编译期多态 (compile-time polymorphism) 和代码优化。
泛型编程 (Generic Programming):类型 traits 可以用于约束模板参数的类型,或者根据类型特征选择不同的算法实现,提高泛型代码的灵活性和效率。
代码生成 (Code Generation):类型 traits 可以用于在编译期生成代码,例如根据类型特征生成不同的数据结构或算法实现。

类型 Traits 在泛型编程和编译期优化中的作用 (Role in Generic Programming and Compile-time Optimization)

类型约束 (Type Constraints):在泛型编程中,可以使用类型 traits 来约束模板参数的类型,例如使用 std::is_integral 约束模板参数必须是整型类型。这可以提高模板代码的类型安全性,并在编译期发现类型错误。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <type_traits>
3template <typename T>
4typename std::enable_if<std::is_integral<T>::value, T>::type // 使用 enable_if 和 is_integral 进行类型约束
5process_integral(T value) {
6std::cout << "Processing integral value: " << value << std::endl;
7return value * 2;
8}
9template <typename T>
10typename std::enable_if<!std::is_integral<T>::value, void>::type // 使用 enable_if 和 !is_integral 处理非整型类型
11process_integral(T value) {
12std::cout << "Error: Input must be an integral type." << std::endl;
13// 对于非整型类型,不进行处理
14}
15int main() {
16process_integral(10); // OK,整型
17process_integral(3.14); // 编译错误,因为 double 不是整型 (实际代码为了演示 enable_if 的用法,使用了重载来处理非整型情况,但更常见的用法是使用 static_assert 或 concepts 来进行编译期断言或约束)
18return 0;
19}

算法选择 (Algorithm Selection):可以根据类型 traits 的结果,在编译期选择不同的算法实现,以达到最佳性能。例如,对于 std::vector,如果元素类型是可平凡拷贝的 (trivially copyable),则可以使用 memcpy 等高效的内存拷贝算法,否则需要使用拷贝构造函数逐个元素拷贝。

编译期优化 (Compile-time Optimization):类型 traits 可以帮助编译器进行更多的编译期优化。例如,如果类型 traits 告诉编译器某个类型是空类型 (empty type),则编译器可以进行空基类优化 (Empty Base Optimization, EBO),减小对象的大小。

总结 (Summary)

类型 traits 是 C++ 元编程的重要工具,它提供了在编译期获取和查询类型信息的能力。类型 traits 在泛型编程、编译期优化和代码生成等方面发挥着重要作用,能够提高代码的类型安全性、性能和灵活性。

9.3.2 常用类型 Traits:is_integral, is_class, is_pointer, enable_if (Common Type Traits)

C++ 标准库 <type_traits> 头文件提供了丰富的类型 traits,涵盖了类型的各种属性和特征。以下介绍几个常用的类型 traits:

std::is_integral (整型判断)

std::is_integral<T> 用于判断类型 T 是否是整型类型 (包括 bool, char, short, int, long, long long 及其 unsigned 版本)。它继承自 std::integral_constant<bool, value>,其中静态常量 valuetrue 如果 T 是整型,否则为 false

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <type_traits>
3int main() {
4std::cout << std::boolalpha; // 输出 bool 类型为 true/false
5std::cout << "is_integral<int>: " << std::is_integral<int>::value << std::endl; // true
6std::cout << "is_integral<bool>: " << std::is_integral<bool>::value << std::endl; // true
7std::cout << "is_integral<char>: " << std::is_integral<char>::value << std::endl; // true
8std::cout << "is_integral<double>: " << std::is_integral<double>::value << std::endl; // false
9std::cout << "is_integral<std::string>: " << std::is_integral<std::string>::value << std::endl; // false
10return 0;
11}

std::is_class (类类型判断)

std::is_class<T> 用于判断类型 T 是否是类类型 (包括 class, struct, union)。它继承自 std::integral_constant<bool, value>,静态常量 valuetrue 如果 T 是类类型,否则为 false

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <type_traits>
3#include <string>
4class MyClass {};
5struct MyStruct {};
6union MyUnion {};
7int main() {
8std::cout << std::boolalpha;
9std::cout << "is_class<MyClass>: " << std::is_class<MyClass>::value << std::endl; // true
10std::cout << "is_class<MyStruct>: " << std::is_class<MyStruct>::value << std::endl; // true
11std::cout << "is_class<MyUnion>: " << std::is_class<MyUnion>::value << std::endl; // true
12std::cout << "is_class<int>: " << std::is_class<int>::value << std::endl; // false
13std::cout << "is_class<std::string>: " << std::is_class<std::string>::value << std::endl; // true
14return 0;
15}

std::is_pointer (指针类型判断)

std::is_pointer<T> 用于判断类型 T 是否是指针类型 (包括普通指针、函数指针、成员指针)。它继承自 std::integral_constant<bool, value>,静态常量 valuetrue 如果 T 是指针类型,否则为 false

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <type_traits>
3int main() {
4std::cout << std::boolalpha;
5int n = 10;
6int* ptr = &n;
7int (*func_ptr)(int) = nullptr; // 函数指针
8std::cout << "is_pointer<int*>: " << std::is_pointer<int*>::value << std::endl; // true
9std::cout << "is_pointer<decltype(ptr)>: " << std::is_pointer<decltype(ptr)>::value << std::endl; // true (使用 decltype 推导类型)
10std::cout << "is_pointer<decltype(func_ptr)>: " << std::is_pointer<decltype(func_ptr)>::value << std::endl; // true (函数指针)
11std::cout << "is_pointer<int>: " << std::is_pointer<int>::value << std::endl; // false
12return 0;
13}

std::enable_if (条件编译)

std::enable_if<Condition, T> 不是一个类型判断 traits,而是一个条件编译工具。它继承自 std::conditional_t<Condition, T, void>。当 Conditiontrue 时,std::enable_if 定义了一个类型 type,其类型为 T;当 Conditionfalse 时,std::enable_if 不定义类型 type,从而导致编译错误(在 SFINAE 上下文中,会使得模板重载决议失败,而不是编译错误)。std::enable_if 常用于在模板编程中实现条件编译和函数重载。

使用 std::enable_if 实现条件编译示例

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <type_traits>
3template <typename T>
4typename std::enable_if<std::is_arithmetic<T>::value, void>::type // 约束 T 必须是算术类型
5process_value(T value) {
6std::cout << "Processing arithmetic value: " << value << std::endl;
7}
8template <typename T>
9typename std::enable_if<!std::is_arithmetic<T>::value, void>::type // 约束 T 不能是算术类型
10process_value(T value) {
11std::cout << "Processing non-arithmetic value (as generic type)." << std::endl;
12}
13int main() {
14process_value(10); // 调用第一个重载版本 (算术类型)
15process_value("hello"); // 调用第二个重载版本 (非算术类型)
16return 0;
17}

在上面的例子中,process_value 函数有两个重载版本,通过 std::enable_ifstd::is_arithmetic 类型 traits,实现了根据模板参数 T 是否为算术类型选择不同的重载版本。

总结 (Summary)

std::is_integral, std::is_class, std::is_pointer, std::enable_if 等类型 traits 是 C++ 标准库中常用的编译期工具。它们提供了类型判断和条件编译的能力,在泛型编程、编译期优化和代码生成等领域具有广泛的应用价值。掌握这些类型 traits,能够编写出更灵活、更高效、更类型安全的 C++ 代码。

9.3.3 自定义类型 Traits 的实现思路 (Implementation Ideas for Custom Type Traits)

除了使用标准库提供的类型 traits,开发者还可以根据自身需求定义自定义的类型 traits。自定义类型 traits 的实现思路通常基于模板特化 (template specialization)SFINAE (Substitution Failure Is Not An Error) 原则。

自定义类型 Traits 的设计原则 (Design Principles of Custom Type Traits)

编译期计算 (Compile-time Computation):自定义类型 traits 的结果应该在编译期计算出来,避免运行时开销。
易于使用 (Easy to Use):自定义类型 traits 应该提供简洁易用的接口,通常是静态常量 value 或静态成员函数。
可扩展性 (Extensibility):自定义类型 traits 应该易于扩展,可以方便地添加新的类型或属性判断。
遵循标准库风格 (Follow Standard Library Style):自定义类型 traits 的命名和接口风格应该尽量与标准库的类型 traits 保持一致,提高代码的可读性和一致性。

实现自定义类型 Traits 的步骤与示例 (Steps and Examples for Implementing Custom Type Traits)

定义通用模板 (Define Generic Template)

首先,定义一个通用的模板类,作为类型 traits 的基本框架。通常,这个模板类会继承自 std::false_typestd::true_type,并提供一个静态常量 value 来表示类型属性的布尔值结果。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template <typename T>
2struct is_container : std::false_type {}; // 默认情况下,不是容器类型,继承自 false_type

模板特化 (Template Specialization)

针对需要特殊处理的类型,进行模板特化。在特化版本中,将基类修改为 std::true_type,或者修改静态常量 value 的值。

例如,要特化 std::vectorstd::list 为容器类型:

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <list>
3#include <type_traits>
4template <typename T>
5struct is_container : std::false_type {}; // 通用模板
6template <typename T>
7struct is_container<std::vector<T>> : std::true_type {}; // vector 特化版本,是容器类型
8template <typename T>
9struct is_container<std::list<T>> : std::true_type {}; // list 特化版本,是容器类型

使用类型 Traits (Use Type Traits)

使用自定义类型 traits 与使用标准库类型 traits 的方式类似,通过访问静态常量 value 获取类型属性的布尔值结果。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <list>
4// ... (is_container 的定义,见上述代码) ...
5int main() {
6std::cout << std::boolalpha;
7std::cout << "is_container<int>: " << is_container<int>::value << std::endl; // false
8std::cout << "is_container<std::vector<int>>: " << is_container<std::vector<int>>::value << std::endl; // true
9std::cout << "is_container<std::list<double>>: " << is_container<std::list<double>>::value << std::endl; // true
10return 0;
11}

更复杂的自定义类型 Traits 示例:判断类型是否可默认构造 (DefaultConstructible)

标准库已经提供了 std::is_default_constructible 类型 trait,这里仅作为自定义类型 traits 的示例。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <type_traits>
2template <typename T>
3struct is_default_constructible_custom : std::false_type {}; // 通用模板,默认不可默认构造
4template <typename T>
5struct is_default_constructible_custom<T> : std::is_default_constructible<T> {}; // 使用 std::is_default_constructible 的特化版本,如果 std::is_default_constructible<T> 为真,则自定义的 trait 也为真

SFINAE (Substitution Failure Is Not An Error) 原则的应用

在更复杂的类型 traits 实现中,可能会用到 SFINAE 原则。SFINAE 指的是在模板参数替换 (substitution) 过程中,如果发生错误,不应该立即导致编译失败,而是应该将当前的模板从重载决议的候选集中移除,尝试其他的重载版本。SFINAE 常用于实现更精确的类型判断和条件编译。

总结 (Summary)

自定义类型 traits 的实现主要依赖于模板特化和 SFINAE 原则。通过定义通用模板和针对特定类型的特化版本,可以实现各种自定义的类型属性判断。自定义类型 traits 可以扩展类型 traits 的应用范围,满足更复杂的类型判断需求。

9.4 错误处理与本地化 (Error Handling and Localization)

C++ 标准库还提供了一些工具用于错误处理 (Error Handling) 和本地化 (Localization),虽然在 <utilities> 头文件中没有直接定义,但它们是构建健壮、国际化应用程序的重要组成部分。本节将简要介绍 C++ 标准库的异常处理机制和本地化功能。

9.4.1 异常处理 (Exceptions): try, catch, throw

C++ 的异常处理 (Exceptions) 机制是一种结构化的错误处理方式,允许程序在运行时抛出 (throw) 异常,并在程序的其他地方捕获 (catch) 和处理这些异常。异常处理机制提高了程序的健壮性和可维护性,使得错误处理代码与正常业务逻辑代码分离,代码结构更清晰。

try, catch, throw 关键字

try 块try 块用于包裹可能抛出异常的代码。如果在 try 块中的代码执行过程中抛出了异常,程序会跳转到与之匹配的 catch 块进行处理。
catch 块catch 块用于捕获和处理特定类型的异常。一个 try 块可以跟随多个 catch 块,每个 catch 块处理不同类型的异常。catch 块的参数声明了它能够捕获的异常类型。
throw 表达式throw 表达式用于抛出异常。可以抛出任何类型的值作为异常对象,但通常抛出继承自 std::exception 或其派生类的异常对象,以便于统一处理和获取错误信息。

标准库异常类 (Standard Library Exception Classes)

C++ 标准库定义了一系列异常类,都继承自基类 std::exception。这些异常类覆盖了常见的运行时错误情况,例如:

std::logic_error:逻辑错误,通常是程序设计错误,例如参数错误、越界访问等。其派生类包括 std::invalid_argument, std::domain_error, std::length_error, std::out_of_range 等。
std::runtime_error:运行时错误,通常是程序运行环境错误,例如内存不足、文件打开失败等。其派生类包括 std::bad_alloc, std::system_error, std::overflow_error, std::underflow_error 等。
std::bad_cast:类型转换错误,例如 dynamic_cast 失败时抛出。
std::bad_typeidtypeid 运算符应用于空指针时抛出。
std::bad_optional_access:访问空的 std::optional 对象时抛出。

异常处理的基本流程

  1. 在可能抛出异常的代码块前加上 try 关键字,形成 try 块。
  2. try 块后跟随一个或多个 catch 块,每个 catch 块指定要捕获的异常类型。
  3. try 块中的代码抛出异常时,系统会查找与异常类型匹配的 catch 块。
  4. 如果找到匹配的 catch 块,程序会跳转到 catch 块中执行异常处理代码。
  5. 如果找不到匹配的 catch 块,异常会继续向上一层调用栈传播,直到被找到的 catch 块捕获,或者最终导致程序终止 (如果一直没有被捕获)。
  6. catch 块中,可以访问异常对象,获取错误信息,并进行相应的错误处理,例如记录日志、提示用户、恢复程序状态等。
  7. catch 块执行完毕后,程序会继续从 try-catch 块之后的位置继续执行(除非在 catch 块中再次抛出异常)。

代码示例 (Code Example)

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <stdexcept> // std::runtime_error, std::out_of_range
3#include <vector>
4int main() {
5std::vector<int> data = {1, 2, 3};
6try {
7// 尝试访问越界元素,可能抛出 std::out_of_range 异常
8int value = data.at(10); // 使用 at() 方法进行安全访问,越界会抛出异常
9std::cout << "Value: " << value << std::endl; // 这行代码不会被执行,因为上一行会抛出异常
10} catch (const std::out_of_range& e) {
11std::cerr << "Out of range exception caught: " << e.what() << std::endl; // 捕获 std::out_of_range 异常
12} catch (const std::exception& e) {
13std::cerr << "Generic exception caught: " << e.what() << std::endl; // 捕获其他 std::exception 类型的异常
14} catch (...) {
15std::cerr << "Unknown exception caught!" << std::endl; // 捕获所有类型的异常 (catch-all)
16}
17std::cout << "Program continues after exception handling." << std::endl; // 程序继续执行
18return 0;
19}

总结 (Summary)

C++ 的异常处理机制提供了一种结构化的错误处理方式,通过 try-catch-throw 关键字和标准库异常类,可以有效地处理运行时错误,提高程序的健壮性和可维护性。合理地使用异常处理,能够使代码更清晰、更易于理解和维护。

9.4.2 本地化 (Localization): 国际化与区域设置 (Internationalization and Locale Settings)

本地化 (Localization) 是指使软件适应特定地区或语言的过程,也称为 L10N (因为 "localization" 中间有 10 个字母)。本地化通常包括以下方面:

文本翻译 (Text Translation):将用户界面文本、错误消息、帮助文档等翻译成目标语言。
日期、时间、数字、货币格式 (Date, Time, Number, Currency Formats):根据目标地区的习惯,调整日期、时间、数字、货币的显示格式。
字符编码 (Character Encoding):支持目标语言的字符编码,例如 UTF-8, GBK 等。
排序规则 (Collation Rules):根据目标语言的规则进行字符串排序。

C++ 标准库的 <locale> 头文件提供了本地化 (Localization) 的支持,主要通过 std::locale 类和相关的 facets (locale facets) 来实现。

std::locale 类

std::locale 类表示一个本地化环境,包含了特定地区或语言的文化习惯信息。std::locale 对象可以用于控制程序的本地化行为,例如日期时间格式、数字格式、货币格式、字符排序等。

创建 std::locale 对象

⚝ 默认 locale:std::locale::classic() 返回一个 "C" locale,这是最基本的 locale,通常使用 POSIX 标准。
⚝ 全局 locale:std::locale("") 返回一个与系统当前 locale 设置相关的 locale。可以使用 std::locale::global() 设置全局 locale。
⚝ 指定名称的 locale:std::locale("locale_name") 创建一个指定名称的 locale,例如 "en_US.UTF-8" (美国英语,UTF-8 编码), "zh_CN.UTF-8" (简体中文,UTF-8 编码) 等。

使用 std::locale 进行本地化

std::locale 对象本身并不直接执行本地化操作,而是作为参数传递给 I/O 流对象,或者用于创建 locale facets 对象,从而影响 I/O 流的行为或提供本地化信息。

示例:设置全局 locale 并格式化输出数字

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <locale> // std::locale, std::numpunct_byname, std::use_facet
3#include <iomanip> // std::fixed, std::setprecision
4int main() {
5try {
6// 设置全局 locale 为 German (Germany)
7std::locale::global(std::locale("de_DE.UTF-8"));
8} catch (const std::runtime_error& e) {
9std::cerr << "Failed to set locale: " << e.what() << std::endl;
10return 1;
11}
12double value = 1234567.89;
13// 使用全局 locale 格式化输出数字
14std::cout.imbue(std::locale()); // 将全局 locale 赋予 std::cout
15std::cout << std::fixed << std::setprecision(2) << value << std::endl; // 输出格式化的数字
16return 0;
17}

在上面的例子中,程序尝试将全局 locale 设置为德国 (de_DE.UTF-8),然后使用全局 locale 格式化输出一个浮点数。在德国的 locale 设置下,小数点会使用逗号 ",",千位分隔符会使用点 ".".。

locale facets (本地化方面)

locale facets 是 std::locale 对象的组成部分,提供了具体的本地化服务。常用的 locale facets 包括:

std::ctype<charT>:字符分类和转换,例如判断字符是否是数字、字母、空白字符,以及字符的大小写转换。
std::numpunct<charT>:数字格式化,例如小数点、千位分隔符的设置。
std::moneypunct<charT>:货币格式化,例如货币符号、货币单位的设置。
std::timepunct<charT>:时间日期格式化,例如日期和时间的显示格式。
std::collate<charT>:字符串排序规则,例如字符串的比较和排序。
std::messages<charT>:消息国际化,用于加载和显示本地化的消息文本(通常用于 GUI 程序)。

可以使用 std::use_facet<Facet>(locale) 函数从 std::locale 对象中获取指定的 facet 对象,然后调用 facet 对象的方法进行本地化操作。

总结 (Summary)

C++ 标准库的 <locale> 头文件提供了本地化 (Localization) 的支持。通过 std::locale 类和 locale facets,可以实现程序在不同地区和语言环境下的自适应,包括文本显示、日期时间格式、数字货币格式、字符排序等方面。本地化是国际化 (Internationalization, I18N) 的重要组成部分,对于开发面向全球用户的软件至关重要。

<END_OF_CHAPTER/>

Appendix A: 标准库头文件概览 (Overview of Standard Library Headers)

附录 A 提供 C++ 标准库常用头文件的列表,并简要描述每个头文件的主要内容,方便读者快速查找和引用。

Appendix A.1: 容器 (Containers) 相关头文件 📦

本节列出与 C++ 标准库容器 (Containers) 相关的头文件,容器是用于存储和组织数据的类模板。

<vector>
▮▮▮▮描述:提供了 std::vector 向量(动态数组)容器模板。vector 是一种序列容器,支持快速随机访问,在尾部进行快速插入和删除操作。
<deque>
▮▮▮▮描述:提供了 std::deque 双端队列 (double-ended queue) 容器模板。deque 也是一种序列容器,支持在头部和尾部进行快速插入和删除操作。
<list>
▮▮▮▮描述:提供了 std::list 列表 (list) 容器模板。list 是一种序列容器,以双向链表实现,擅长在任意位置进行快速插入和删除操作。
<forward_list>
▮▮▮▮描述:提供了 std::forward_list 前向列表 (forward list) 容器模板。forward_list 是一种序列容器,以单向链表实现,相比 list 更节省空间,但只能向前迭代。
<array>
▮▮▮▮描述:提供了 std::array 数组 (array) 容器模板。array 是一种序列容器,封装了固定大小的数组,提供了边界检查和容器接口。
<set>
▮▮▮▮描述:提供了 std::set 集合 (set) 和 std::multiset 多重集合 (multiset) 容器模板。set 存储有序唯一元素,multiset 存储有序可重复元素,底层通常基于红黑树实现,支持高效的查找、插入和删除操作。
<map>
▮▮▮▮描述:提供了 std::map 映射 (map) 和 std::multimap 多重映射 (multimap) 容器模板。map 存储有序唯一键值对,multimap 存储有序可重复键值对,同样基于红黑树实现,支持高效的键查找、插入和删除操作。
<unordered_set>
▮▮▮▮描述:提供了 std::unordered_set 无序集合 (unordered set) 和 std::unordered_multiset 无序多重集合 (unordered multiset) 容器模板。基于哈希表实现,提供平均常数时间复杂度的查找、插入和删除操作,但不保证元素顺序。
<unordered_map>
▮▮▮▮描述:提供了 std::unordered_map 无序映射 (unordered map) 和 std::unordered_multimap 无序多重映射 (unordered multimap) 容器模板。同样基于哈希表实现,提供平均常数时间复杂度的键查找、插入和删除操作,但不保证键值对顺序。
<stack>
▮▮▮▮描述:提供了 std::stack 栈 (stack) 容器适配器 (container adaptor)。基于其他序列容器(如 dequevector)实现后进先出 (LIFO) 的数据结构。
<queue>
▮▮▮▮描述:提供了 std::queue 队列 (queue) 和 std::priority_queue 优先队列 (priority queue) 容器适配器。queue 基于其他序列容器实现先进先出 (FIFO) 的数据结构,priority_queue 提供优先级访问的队列。

Appendix A.2: 算法 (Algorithms) 相关头文件 <0xF0><0x9F><0xAA><0x99>

本节列出与 C++ 标准库算法 (Algorithms) 相关的头文件,算法是对容器中的元素进行各种操作的函数模板。

<algorithm>
▮▮▮▮描述:提供了大量的通用算法,包括查找、排序、计数、变换、复制、删除、替换、合并等。这些算法通常通过迭代器 (iterators) 操作容器中的元素。例如:std::sort, std::find, std::copy, std::transform 等。
<numeric>
▮▮▮▮描述:提供了数值算法,用于执行数值计算,例如累加 (accumulate)、内积 (inner product)、部分和 (partial sum)、邻近差分 (adjacent difference) 等。例如:std::accumulate, std::inner_product
<cmath> (或 <math.h>)
▮▮▮▮描述:虽然主要用于数学函数,但某些数值算法可能间接使用到 <cmath> 中的函数,例如三角函数、指数函数、对数函数等。
<cstdlib> (或 <stdlib.h>)
▮▮▮▮描述:包含一些通用工具函数,例如 std::abs, std::rand, std::srand, std::qsort, std::bsearch 等,在某些算法实现或辅助功能中可能会用到。

Appendix A.3: 迭代器 (Iterators) 相关头文件 <0xF0><0x9F><0xAA><0xB1>

本节列出与 C++ 标准库迭代器 (Iterators) 相关的头文件,迭代器是用于遍历容器元素的通用接口。

<iterator>
▮▮▮▮描述:提供了迭代器相关的类和函数,包括迭代器标签 (iterator tags)、迭代器适配器 (iterator adaptors)、迭代器辅助函数 (iterator helper functions) 等。例如:std::iterator_traits, std::reverse_iterator, std::begin, std::end

Appendix A.4: 函数对象 (Function Objects) 相关头文件 🎭

本节列出与 C++ 标准库函数对象 (Function Objects) (也称为 functor) 相关的头文件,函数对象是行为类似函数的对象,可以作为算法的参数,提供更灵活的操作。

<functional>
▮▮▮▮描述:提供了预定义的函数对象(例如算术、比较、逻辑运算的函数对象)、函数适配器(例如 std::bind, std::not1, std::not2)、以及函数对象相关的工具类和模板。例如:std::plus, std::less, std::bind, std::function

Appendix A.5: 输入/输出 (Input/Output) 相关头文件 <0xF0><0x9F><0x93><0x81>

本节列出与 C++ 标准库输入/输出 (Input/Output) 相关的头文件,用于进行数据输入和输出操作。

<iostream>
▮▮▮▮描述:提供了标准输入输出流对象,例如 std::cin (标准输入), std::cout (标准输出), std::cerr (标准错误), std::clog (标准日志)。以及相关的流操纵符 (manipulators),例如 std::endl, std::flush
<fstream>
▮▮▮▮描述:提供了文件输入输出流类,用于文件操作,例如 std::ifstream (文件输入流), std::ofstream (文件输出流), std::fstream (文件输入输出流)。
<sstream>
▮▮▮▮描述:提供了字符串流类,用于在内存中进行字符串的输入输出操作,例如 std::istringstream (字符串输入流), std::ostringstream (字符串输出流), std::stringstream (字符串输入输出流)。
<iomanip>
▮▮▮▮描述:提供了额外的 I/O 操纵符,用于格式化输入输出,例如设置精度、宽度、进制等。例如:std::setprecision, std::setw, std::setfill, std::hex, std::dec
<cstdio> (或 <stdio.h>)
▮▮▮▮描述:提供了 C 风格的输入输出函数,例如 printf, scanf, fopen, fclose 等。虽然 C++ 推荐使用 iostream 库,但在某些情况下,cstdio 中的函数仍然可以使用。

Appendix A.6: 内存管理 (Memory Management) 相关头文件 <0xF0><0x9F><0x97><0xBF>

本节列出与 C++ 标准库内存管理 (Memory Management) 相关的头文件,用于管理程序的内存分配和释放。

<memory>
▮▮▮▮描述:提供了智能指针 (smart pointers) 类模板,例如 std::unique_ptr, std::shared_ptr, std::weak_ptr,用于自动管理动态分配的内存,防止内存泄漏。同时包含分配器 (allocators) 相关的类和函数,用于自定义内存分配策略。例如:std::allocator, std::make_shared, std::make_unique
<new>
▮▮▮▮描述:提供了与内存分配和释放相关的运算符和函数,例如 new, delete 运算符,以及异常处理相关的 std::bad_alloc 异常类。

Appendix A.7: 并发与多线程 (Concurrency and Multithreading) 相关头文件 <0xF0><0x9F><0xAA><0xAE>

本节列出与 C++ 标准库并发与多线程 (Concurrency and Multithreading) 相关的头文件,用于编写并发程序。

<thread>
▮▮▮▮描述:提供了线程 (threads) 相关的类和函数,用于创建和管理线程,例如 std::thread 类。
<mutex>
▮▮▮▮描述:提供了互斥量 (mutexes) 相关的类,用于保护共享资源,防止数据竞争,例如 std::mutex, std::recursive_mutex, std::timed_mutex。以及锁管理器 (lock managers),例如 std::lock_guard, std::unique_lock
<condition_variable>
▮▮▮▮描述:提供了条件变量 (condition variables) 相关的类,用于线程间的同步和通信,例如 std::condition_variable, std::condition_variable_any
<future>
▮▮▮▮描述:提供了期物 (futures) 相关的类和函数,用于异步操作的结果获取和状态管理,例如 std::future, std::promise, std::packaged_task, std::async
<atomic>
▮▮▮▮描述:提供了原子操作 (atomic operations) 相关的类和函数,用于实现无锁 (lock-free) 并发编程,例如 std::atomic 类模板。

Appendix A.8: 实用工具 (Utilities) 相关头文件 🛠️

本节列出 C++ 标准库中各种实用工具 (Utilities) 相关的头文件,这些工具提供了各种常用的功能和类型。

<utility>
▮▮▮▮描述:提供了通用的工具类和函数,例如 std::pair (对组), std::move (移动语义), std::forward (完美转发), 关系运算符 (例如 std::rel_ops 命名空间)。
<tuple>
▮▮▮▮描述:提供了元组 (tuple) 相关的类模板 std::tuple,用于组合多个不同类型的值。
<optional>
▮▮▮▮描述:提供了 std::optional 类模板,用于表示可能存在或不存在的值。
<variant>
▮▮▮▮描述:提供了 std::variant 类模板,用于表示可以存储多种类型之一的值,类型安全的联合体 (union) 。
<chrono>
▮▮▮▮描述:提供了时间 (time) 相关的类和函数,用于处理时间点 (time points)、持续时间 (durations)、时钟 (clocks) 等,例如 std::chrono::system_clock, std::chrono::steady_clock, std::chrono::duration, std::chrono::time_point
<type_traits>
▮▮▮▮描述:提供了类型 traits (type traits) 工具,用于在编译期查询和判断类型的属性,例如 std::is_integral, std::is_class, std::is_pointer, std::enable_if
<cstddef> (或 <stddef.h>)
▮▮▮▮描述:定义了一些通用的类型,例如 std::size_t, std::ptrdiff_t, std::nullptr_t,以及宏 offsetof
<cstdint> (或 <stdint.h>)
▮▮▮▮描述:定义了固定宽度的整数类型,例如 std::int32_t, std::uint64_t 等。
<limits>
▮▮▮▮描述:提供了数值类型的极限信息,例如 std::numeric_limits 类模板,可以查询各种数值类型的最大值、最小值、精度等。
<cassert> (或 <assert.h>)
▮▮▮▮描述:提供了断言宏 assert,用于在程序中进行运行时检查。
<stdexcept>
▮▮▮▮描述:定义了标准异常类,例如 std::exception, std::runtime_error, std::logic_error, std::out_of_range, std::domain_error 等。用于抛出和捕获标准库异常。
<system_error>
▮▮▮▮描述:提供了与系统错误相关的类和函数,例如 std::error_code, std::error_category, std::system_error
<ctime> (或 <time.h>)
▮▮▮▮描述:提供了 C 风格的时间和日期函数,例如 std::time, std::localtime, std::asctime 等。 与 <chrono> 库相比,<ctime> 提供的功能较为基础。
<clocale> (或 <locale.h>) and <locale>
▮▮▮▮描述:提供了本地化 (localization) 支持,用于处理不同文化和地区的语言、数字、日期、货币等格式。 <clocale> 提供 C 风格的本地化功能, <locale> 提供 C++ 风格的本地化功能和类 std::locale

请注意,这只是 C++ 标准库常用头文件的一个概览,C++ 标准库包含的内容非常丰富,还有其他头文件可能未在此处列出。 建议查阅 C++ 标准文档或 cppreference.com 等在线资源获取更全面和详细的信息。

<END_OF_CHAPTER/>

Appendix B: 常见错误与解决方案 (Common Errors and Solutions)

附录 B 总结使用 C++ 标准库时常见的错误,并提供相应的解决方案和最佳实践,帮助读者避免陷阱。

使用 C++ 标准库容器 (Containers) 时,常见的错误包括迭代器失效 (Iterator Invalidation)、越界访问 (Out-of-bounds Access)、以及不恰当的容器选择 (Inappropriate Container Choice)。理解这些错误的原因和解决方案,能够帮助开发者编写更健壮和高效的代码。

Appendix B1.1: 迭代器失效 (Iterator Invalidation)

迭代器失效是使用容器时最常见的错误之一。当容器的内部结构发生变化时,之前获得的迭代器可能会变得无效,继续使用失效的迭代器会导致未定义行为 (Undefined Behavior),例如程序崩溃或数据损坏。

错误类型:
▮▮▮▮ⓑ 插入或删除元素导致 vector (向量) 或 deque (双端队列) 迭代器失效:
当向 vectordeque 插入元素导致容量 (capacity) 重新分配时,或者在非尾部位置插入或删除元素时,原有迭代器、指针和引用可能会失效。对于 vector,插入元素若引起重新分配,所有迭代器都会失效;在非尾部删除元素,删除点之后的所有迭代器也会失效。对于 deque,在除首尾位置之外的任何位置插入或删除元素,所有迭代器都会失效。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> vec = {1, 2, 3, 4, 5};
5auto it = vec.begin();
6++it; // 指向 2
7vec.insert(vec.begin(), 0); // 在头部插入元素,可能导致 vector 重新分配内存
8// 错误:it 可能已经失效,解引用可能导致未定义行为
9// std::cout << *it << std::endl; // 取消注释可能会导致错误
10return 0;
11}

▮▮▮▮ⓑ 删除元素导致 list (列表) 或 forward_list (单向链表) 迭代器失效:
对于 listforward_list,删除元素只会使指向被删除元素的迭代器失效,其他迭代器仍然有效。但是,需要特别注意在循环删除元素时,迭代器的正确更新。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <list>
3int main() {
4std::list<int> lst = {1, 2, 3, 4, 5};
5auto it = lst.begin();
6++it; // 指向 2
7for (auto current = lst.begin(); current != lst.end(); ) {
8if (*current % 2 == 0) {
9current = lst.erase(current); // erase 返回指向删除元素之后元素的有效迭代器
10} else {
11++current;
12}
13}
14// 正确:it 仍然可能指向有效元素(如果 2 没有被删除且在删除操作后仍然有效),但不推荐依赖这种行为
15// std::cout << *it << std::endl; // 取消注释可能仍然有效,如果 'it' 原先指向的元素没有被删除
16return 0;
17}

▮▮▮▮ⓒ 关联容器 (Associative Containers) 和无序容器 (Unordered Containers) 的迭代器失效:
对于 set (集合), map (映射), unordered_set (无序集合), unordered_map (无序映射) 等容器,插入元素不会导致任何迭代器失效(除非引起重新哈希,对于无序容器而言)。删除元素只会使指向被删除元素的迭代器失效。

解决方案与最佳实践:
▮▮▮▮ⓑ 谨慎使用迭代器进行插入和删除操作:
vectordeque 上进行插入和删除操作时,要特别注意迭代器失效问题。尽量使用容器提供的成员函数,并根据返回值更新迭代器。

▮▮▮▮ⓑ 使用基于范围的 for 循环 (Range-based for loop) 或算法 (Algorithms):
在许多情况下,基于范围的 for 循环和标准库算法可以避免直接操作迭代器,从而减少迭代器失效的风险。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> vec = {1, 2, 3, 4, 5};
6// 使用算法,避免手动迭代器操作
7vec.erase(std::remove_if(vec.begin(), vec.end(), [](int x){ return x % 2 == 0; }), vec.end());
8for (int val : vec) { // 基于范围的 for 循环
9std::cout << val << " ";
10}
11std::cout << std::endl;
12return 0;
13}

▮▮▮▮ⓒ 始终检查迭代器的有效性:
虽然通常难以直接检查迭代器是否失效,但应尽量避免在可能导致迭代器失效的操作后继续使用旧迭代器。

Appendix B1.2: 越界访问 (Out-of-bounds Access)

越界访问是指访问容器中不存在的元素位置。这通常发生在序列容器 (Sequence Containers) 上,例如 vector, deque, array (数组),尝试使用索引访问超出容器大小 (size) 或容量 (capacity) 的位置。

错误类型:
▮▮▮▮ⓑ 使用 operator[] 访问 vector, deque, array 越界:
operator[] 不进行边界检查,如果索引越界,会导致未定义行为。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> vec = {1, 2, 3};
5// 错误:索引 3 越界,vec 的有效索引是 0, 1, 2
6// std::cout << vec[3] << std::endl; // 取消注释会导致未定义行为
7return 0;
8}

▮▮▮▮ⓑ 使用 at() 成员函数但仍然越界:
at() 成员函数会进行边界检查,越界时会抛出 std::out_of_range 异常。虽然 at() 提供了安全访问,但如果未捕获异常,程序仍然会终止。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <stdexcept>
4int main() {
5std::vector<int> vec = {1, 2, 3};
6try {
7// 错误:索引 3 越界
8std::cout << vec.at(3) << std::endl;
9} catch (const std::out_of_range& e) {
10std::cerr << "越界访问异常: " << e.what() << std::endl;
11}
12return 0;
13}

▮▮▮▮ⓒ 迭代器越界:
尝试解引用 end() 迭代器,或者在迭代器递增或递减操作中超出有效范围。end() 迭代器指向容器最后一个元素的下一个位置,不指向任何实际元素,解引用 end() 迭代器是错误的。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> vec = {1, 2, 3};
5// 错误:解引用 end() 迭代器
6// std::cout << *vec.end() << std::endl; // 取消注释会导致未定义行为
7auto it = vec.begin();
8for (int i = 0; i <= vec.size(); ++i) { // 循环条件错误,当 i 等于 size() 时,it 将变为 end() 迭代器
9if (i < vec.size()) {
10std::cout << *it << " ";
11++it;
12}
13}
14std::cout << std::endl;
15return 0;
16}

解决方案与最佳实践:
▮▮▮▮ⓑ 使用 at() 进行安全访问:
当需要边界检查时,使用 at() 成员函数代替 operator[]

▮▮▮▮ⓑ 在访问前检查容器大小 (size):
在使用索引访问容器元素前,确保索引在有效范围内,即小于容器的 size()

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3int main() {
4std::vector<int> vec = {1, 2, 3};
5int index = 3;
6if (index < vec.size()) {
7std::cout << vec[index] << std::endl;
8} else {
9std::cerr << "索引越界,索引值为: " << index << ", 容器大小为: " << vec.size() << std::endl;
10}
11return 0;
12}

▮▮▮▮ⓒ 正确使用迭代器范围:
在循环中使用迭代器时,确保迭代器在 begin()end() 之间有效。通常循环条件应为 it != container.end(),而不是 it < container.end() (除非是随机访问迭代器)。

Appendix B1.3: 不恰当的容器选择 (Inappropriate Container Choice)

选择合适的容器对于程序的性能和效率至关重要。不恰当的容器选择可能导致性能瓶颈或代码复杂性增加。

错误类型:
▮▮▮▮ⓑ 频繁在 vector 头部插入或删除元素:
vector 在尾部插入和删除元素效率很高 (均摊O(1)),但在头部或中部插入或删除元素效率较低 (O(n)),因为需要移动大量元素。如果需要频繁在头部插入或删除,dequelist 是更合适的选择。

▮▮▮▮ⓑ 频繁查找元素但使用 vectorlist:
vectorlist 的查找操作 (例如使用 std::find) 的时间复杂度为O(n)。如果需要频繁进行查找操作,set, map, unordered_set, unordered_map 等关联容器或无序容器通常更高效,它们的查找时间复杂度为O(logn)或均摊O(1)

▮▮▮▮ⓒ 需要有序元素但使用 unordered_setunordered_map:
unordered_setunordered_map 提供快速的平均查找速度,但不保证元素的有序性。如果需要保持元素的有序性,应使用 setmap

解决方案与最佳实践:
▮▮▮▮ⓑ 根据操作特点选择容器:
▮▮▮▮▮▮▮▮❸ 序列容器:
▮▮▮▮▮▮▮▮▮▮▮▮⚝ vector: 动态数组,随机访问快,尾部插入/删除快,中部/头部插入/删除慢。适用于需要频繁随机访问,但插入删除操作较少的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ deque: 双端队列,头部和尾部插入/删除快,随机访问相对 vector 稍慢,但仍为O(1)。适用于需要在两端频繁插入/删除的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ list: 双向链表,插入/删除快 (O(1)),但随机访问慢 (O(n))。适用于频繁插入/删除,但很少随机访问的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ forward_list: 单向链表,与 list 类似,但只能单向遍历,空间效率更高。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ array: 固定大小数组,与 C 风格数组类似,但更安全,提供容器接口。适用于大小固定,且需要容器接口的场景。

▮▮▮▮▮▮▮▮❷ 关联容器:
▮▮▮▮▮▮▮▮▮▮▮▮⚝ set: 有序唯一元素集合,基于红黑树实现,查找、插入、删除操作时间复杂度为O(logn),元素自动排序。适用于需要存储唯一有序元素的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ multiset: 有序可重复元素集合,与 set 类似,但允许重复元素。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ map: 有序唯一键值对集合,基于红黑树实现,按键排序,查找、插入、删除操作时间复杂度为O(logn)。适用于需要存储有序键值对的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ multimap: 有序可重复键值对集合,与 map 类似,但允许键重复。

▮▮▮▮▮▮▮▮❸ 无序容器:
▮▮▮▮▮▮▮▮▮▮▮▮⚝ unordered_set: 无序唯一元素集合,基于哈希表实现,平均查找、插入、删除操作时间复杂度为均摊O(1),但不保证元素顺序。适用于需要快速查找,但不关心元素顺序的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ unordered_multiset: 无序可重复元素集合,与 unordered_set 类似,但允许重复元素。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ unordered_map: 无序唯一键值对集合,基于哈希表实现,平均查找、插入、删除操作时间复杂度为均摊O(1),但不保证键值对顺序。适用于需要快速查找键值对,但不关心顺序的场景。
▮▮▮▮▮▮▮▮▮▮▮▮⚝ unordered_multimap: 无序可重复键值对集合,与 unordered_map 类似,但允许键重复。

▮▮▮▮ⓑ 性能测试和分析:
在关键性能路径上,对不同容器进行性能测试,选择最适合具体应用场景的容器。使用性能分析工具 (Performance Analysis Tools) 帮助识别性能瓶颈。

C++ 标准库算法 (Algorithms) 提供了丰富的通用算法,但使用不当同样会导致错误。常见的算法相关错误包括迭代器范围错误 (Iterator Range Errors)、谓词错误 (Predicate Errors)、以及算法误用 (Algorithm Misuse)。

Appendix B2.1: 迭代器范围错误 (Iterator Range Errors)

许多标准库算法接受迭代器范围作为输入,错误的迭代器范围会导致算法行为异常,甚至程序崩溃。

错误类型:
▮▮▮▮ⓑ 起始迭代器超过结束迭代器:
算法通常要求起始迭代器指向的元素位置在结束迭代器之前或相同。如果起始迭代器超过结束迭代器,例如 algorithm(vec.end(), vec.begin(), ...),会导致未定义行为。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> vec = {1, 2, 3};
6// 错误:起始迭代器 vec.end() 超过了结束迭代器 vec.begin()
7// std::sort(vec.end(), vec.begin()); // 取消注释会导致未定义行为
8return 0;
9}

▮▮▮▮ⓑ 迭代器来自不同的容器:
传递给算法的迭代器必须来自同一个容器,或者至少在逻辑上是连续的内存区域。如果迭代器来自不同的容器,算法的行为是未定义的。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> vec1 = {1, 2, 3};
6std::vector<int> vec2 = {4, 5, 6};
7// 错误:迭代器来自不同的容器 vec1 和 vec2
8// std::copy(vec1.begin(), vec1.end(), vec2.begin()); // 取消注释会导致未定义行为
9return 0;
10}

▮▮▮▮ⓒ 输出迭代器空间不足:
对于修改容器或生成新元素的算法 (例如 std::copy, std::transform),如果输出迭代器指向的目标容器空间不足,会导致写入越界。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> src = {1, 2, 3};
6std::vector<int> dest; // dest 初始为空,空间不足
7// 错误:dest 空间不足,copy 会导致写入越界
8// std::copy(src.begin(), src.end(), dest.begin()); // 取消注释会导致未定义行为
9// 正确的做法是预先分配足够的空间或使用插入迭代器
10dest.resize(src.size());
11std::copy(src.begin(), src.end(), dest.begin()); // 现在 dest 有足够的空间
12return 0;
13}

解决方案与最佳实践:
▮▮▮▮ⓑ 确保迭代器范围有效:
始终确保算法的起始迭代器不晚于结束迭代器,且迭代器来自同一个容器或连续内存区域。

▮▮▮▮ⓑ 预分配输出容器空间或使用插入迭代器:
对于输出型算法,确保目标容器有足够的空间接收输出结果。可以使用 resize() 预先分配空间,或者使用插入迭代器 (例如 std::back_inserter, std::front_inserter, std::inserter) 自动扩展容器。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> src = {1, 2, 3};
6std::vector<int> dest;
7// 使用 back_inserter 自动扩展 dest
8std::copy(src.begin(), src.end(), std::back_inserter(dest));
9for (int val : dest) {
10std::cout << val << " ";
11}
12std::cout << std::endl;
13return 0;
14}

Appendix B2.2: 谓词错误 (Predicate Errors)

许多算法接受谓词 (Predicate),即返回布尔值的函数对象或 Lambda 表达式,用于自定义算法的行为。错误的谓词可能导致算法逻辑错误或运行时异常。

错误类型:
▮▮▮▮ⓑ 谓词不满足严格弱序关系 (Strict Weak Ordering):
例如 std::sort, std::lower_bound 等排序和搜索算法,要求谓词满足严格弱序关系。如果谓词不满足此要求,会导致排序结果错误或未定义行为。严格弱序关系需要满足以下条件 (对于比较函数 comp(a, b)):
▮▮▮▮▮▮▮▮❶ 非自反性 (Irreflexivity): comp(a, a) 必须为 false
▮▮▮▮▮▮▮▮❷ 反对称性 (Antisymmetry): 如果 comp(a, b)true,则 comp(b, a) 必须为 false
▮▮▮▮▮▮▮▮❸ 传递性 (Transitivity): 如果 comp(a, b)truecomp(b, c)true,则 comp(a, c) 必须为 true
▮▮▮▮▮▮▮▮❹ 不可比性的传递性 (Transitivity of Incomparability): 如果 comp(a, b)comp(b, a) 均为 false,且 comp(b, c)comp(c, b) 均为 false,则 comp(a, c)comp(c, a) 必须均为 false

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4struct Point {
5int x, y;
6};
7// 错误的比较函数,不满足严格弱序关系 (例如,当两点 x 坐标相等时,比较函数不明确)
8bool comparePointsBad(const Point& a, const Point& b) {
9return a.x <= b.x; // 错误:应该使用 < 而不是 <=
10}
11bool comparePointsGood(const Point& a, const Point& b) {
12return a.x < b.x; // 正确:使用 < 满足严格弱序关系
13}
14int main() {
15std::vector<Point> points = {{2, 1}, {1, 2}, {2, 3}, {1, 1}};
16// 错误:使用不满足严格弱序关系的比较函数
17// std::sort(points.begin(), points.end(), comparePointsBad); // 取消注释可能导致排序错误或未定义行为
18// 正确:使用满足严格弱序关系的比较函数
19std::sort(points.begin(), points.end(), comparePointsGood);
20for (const auto& p : points) {
21std::cout << "(" << p.x << ", " << p.y << ") ";
22}
23std::cout << std::endl;
24return 0;
25}

▮▮▮▮ⓑ 谓词函数有副作用 (Side Effects):
谓词函数应只进行比较或判断操作,不应修改输入对象或其他状态。如果谓词函数有副作用,可能会导致算法行为不可预测。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int counter = 0;
5// 错误的谓词,带有副作用,修改了全局变量 counter
6bool hasSideEffect(int x) {
7counter++;
8return x % 2 == 0;
9}
10int main() {
11std::vector<int> vec = {1, 2, 3, 4, 5};
12// 错误:谓词 hasSideEffect 有副作用,count_if 的行为可能不符合预期
13std::count_if(vec.begin(), vec.end(), hasSideEffect);
14std::cout << "counter = " << counter << std::endl; // counter 的值取决于算法实现细节,不可预测
15return 0;
16}

解决方案与最佳实践:
▮▮▮▮ⓑ 确保谓词满足严格弱序关系:
对于排序和搜索算法,仔细设计比较函数或谓词,确保满足严格弱序关系。使用 < 而不是 <= 进行比较通常是一个好的起点。

▮▮▮▮ⓑ 避免谓词函数产生副作用:
谓词函数应保持纯粹性,只进行判断和比较,不修改任何状态。

▮▮▮▮ⓒ 使用 Lambda 表达式简化谓词:
对于简单的谓词逻辑,使用 Lambda 表达式可以提高代码的简洁性和可读性,并减少错误的可能性。

Appendix B2.3: 算法误用 (Algorithm Misuse)

误用算法指的是选择了不适合当前任务的算法,或者错误地理解了算法的功能和适用场景。

错误类型:
▮▮▮▮ⓑ 使用 std::sortlist 进行排序:
std::sort 要求随机访问迭代器,list 只提供双向迭代器,因此不能直接使用 std::sortlist 进行排序。虽然有些编译器可能允许编译通过,但效率会非常低,因为 std::sort 会退化成O(n2)复杂度的算法。list 提供了成员函数 sort(),专门用于链表排序,效率更高 (O(nlogn))。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <list>
3#include <algorithm>
4int main() {
5std::list<int> lst = {3, 1, 4, 2};
6// 错误:对 list 使用 std::sort,效率低或编译错误
7// std::sort(lst.begin(), lst.end()); // 取消注释可能编译错误或效率极低
8// 正确:使用 list 的成员函数 sort()
9lst.sort();
10for (int val : lst) {
11std::cout << val << " ";
12}
13std::cout << std::endl;
14return 0;
15}

▮▮▮▮ⓑ 使用 std::binary_search 对未排序的序列进行搜索:
std::binary_search, std::lower_bound, std::upper_bound, std::equal_range 等二分搜索算法,要求输入序列必须是有序的。对未排序的序列使用二分搜索算法,结果是未定义的。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> vec = {3, 1, 4, 2}; // 未排序的 vector
6// 错误:对未排序的 vec 使用 binary_search,结果不确定
7bool found = std::binary_search(vec.begin(), vec.end(), 3); // 结果不保证正确
8if (found) {
9std::cout << "找到 3" << std::endl;
10} else {
11std::cout << "未找到 3" << std::endl;
12}
13// 正确:先排序再使用 binary_search
14std::sort(vec.begin(), vec.end());
15found = std::binary_search(vec.begin(), vec.end(), 3); // 结果正确
16if (found) {
17std::cout << "找到 3 (排序后)" << std::endl;
18} else {
19std::cout << "未找到 3 (排序后)" << std::endl;
20}
21return 0;
22}

▮▮▮▮ⓒ 误解算法的功能:
例如,混淆 std::removestd::erase 的作用。std::remove 只是将指定元素移动到容器尾部,并返回指向被移除元素首位置的迭代器,但并没有真正删除元素,容器的大小 (size) 并没有改变。需要配合容器的 erase() 成员函数才能真正删除元素。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <vector>
3#include <algorithm>
4int main() {
5std::vector<int> vec = {1, 2, 2, 3, 2, 4};
6// 错误:只使用 remove,元素并没有真正删除,容器大小不变
7std::remove(vec.begin(), vec.end(), 2); // 只是将 2 移动到尾部
8std::cout << "remove 后的大小: " << vec.size() << std::endl; // 大小仍然是 6
9std::cout << "remove 后的内容: ";
10for (int val : vec) {
11std::cout << val << " "; // 内容可能为 1 3 4 2 2 2,尾部的值是不确定的
12}
13std::cout << std::endl;
14// 正确:remove 与 erase 结合使用,真正删除元素
15vec = {1, 2, 2, 3, 2, 4}; // 重新初始化 vec
16auto it = std::remove(vec.begin(), vec.end(), 2);
17vec.erase(it, vec.end()); // erase 真正删除元素
18std::cout << "remove + erase 后的大小: " << vec.size() << std::endl; // 大小变为 3
19std::cout << "remove + erase 后的内容: ";
20for (int val : vec) {
21std::cout << val << " "; // 内容为 1 3 4
22}
23std::cout << std::endl;
24return 0;
25}

解决方案与最佳实践:
▮▮▮▮ⓑ 选择合适的算法:
仔细阅读算法文档,理解算法的功能、要求和适用场景,选择最符合当前任务的算法。

▮▮▮▮ⓑ 理解算法的前提条件:
例如,二分搜索算法要求序列有序,排序算法对迭代器类型有要求。确保输入满足算法的前提条件。

▮▮▮▮ⓒ 仔细阅读算法文档:
参考 cppreference.com 等权威文档,深入理解每个算法的细节和用法。

▮▮▮▮ⓓ 多实践和测试:
通过编写代码和进行充分的测试,加深对算法的理解,并验证算法的正确性。

C++ 内存管理 (Memory Management) 是一个复杂但至关重要的领域。使用 C++ 标准库提供的智能指针 (Smart Pointers) 可以显著减少内存泄漏 (Memory Leaks) 和悬 dangling 指针 (Dangling Pointers) 等错误。

Appendix B3.1: 内存泄漏 (Memory Leaks)

内存泄漏发生在动态分配的内存 (Dynamic Memory) 没有被正确释放时。长期运行的程序如果存在内存泄漏,会导致可用内存逐渐减少,最终可能耗尽系统资源。

错误类型:
▮▮▮▮ⓑ 使用 new 分配内存后,忘记使用 delete 释放:
这是最经典的内存泄漏错误。如果使用 new 动态分配内存后,在所有路径上都没有对应的 delete 操作,就会发生内存泄漏。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2void allocateMemory() {
3int* ptr = new int[100]; // 分配内存
4// ... 忘记释放内存
5// delete[] ptr; // 缺少 delete[] 导致内存泄漏
6} // 函数结束,ptr 指针变量消失,但分配的内存没有释放
7int main() {
8allocateMemory(); // 调用 allocateMemory 会发生内存泄漏
9// ...
10return 0;
11}

▮▮▮▮ⓑ 异常安全问题:
如果在 newdelete 之间抛出异常,且没有适当的异常处理机制,delete 操作可能不会被执行,导致内存泄漏。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <stdexcept>
3void processData() {
4int* ptr = new int[100]; // 分配内存
5if (true) {
6throw std::runtime_error("处理数据时发生错误"); // 抛出异常
7}
8delete[] ptr; // 如果异常抛出,这行代码不会执行,导致内存泄漏
9}
10int main() {
11try {
12processData();
13} catch (const std::exception& e) {
14std::cerr << "异常: " << e.what() << std::endl;
15}
16return 0; // allocateMemory 中分配的内存没有释放,发生内存泄漏
17}

解决方案与最佳实践:
▮▮▮▮ⓑ 使用智能指针 (Smart Pointers):
使用 std::unique_ptrstd::shared_ptr 等智能指针可以自动管理动态分配的内存,避免手动 newdelete,从而有效防止内存泄漏。智能指针会在其生命周期结束时自动释放所管理的内存,即使在发生异常的情况下也能确保内存被释放 (RAII 原则)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <memory>
3#include <stdexcept>
4void processDataWithSmartPtr() {
5std::unique_ptr<int[]> ptr(new int[100]); // 使用 unique_ptr 管理内存
6if (true) {
7throw std::runtime_error("处理数据时发生错误"); // 抛出异常
8}
9// 不需要手动 delete[],unique_ptr 会自动释放内存
10} // 函数结束,unique_ptr 对象 ptr 销毁,自动释放内存
11int main() {
12try {
13processDataWithSmartPtr();
14} catch (const std::exception& e) {
15std::cerr << "异常: " << e.what() << std::endl;
16}
17return 0; // 没有内存泄漏,unique_ptr 自动释放内存
18}

▮▮▮▮ⓑ RAII (Resource Acquisition Is Initialization) 原则:
遵循 RAII 原则,将资源 (例如内存、文件句柄、锁等) 的管理与对象的生命周期绑定。在构造函数中获取资源,在析构函数中释放资源。智能指针是 RAII 原则的典型应用。

▮▮▮▮ⓒ 代码审查和内存泄漏检测工具 (Memory Leak Detection Tools):
进行代码审查,检查是否存在潜在的内存泄漏风险。使用 Valgrind, AddressSanitizer 等内存泄漏检测工具,帮助发现和定位内存泄漏问题。

Appendix B3.2: 悬 dangling 指针 (Dangling Pointers)

悬 dangling 指针是指指向已经被释放或无效内存区域的指针。解引用悬 dangling 指针会导致未定义行为,例如程序崩溃或数据损坏。

错误类型:
▮▮▮▮ⓑ 删除 (delete) 内存后,仍然使用原指针:
当使用 delete 释放了动态分配的内存后,原来的指针变量本身仍然存在,但它指向的内存已经无效。继续使用这个指针就是悬 dangling 指针错误。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2int main() {
3int* ptr = new int(10);
4delete ptr; // 释放内存
5// 错误:ptr 成为悬 dangling 指针,解引用会导致未定义行为
6// std::cout << *ptr << std::endl; // 取消注释会导致未定义行为
7// 最佳实践:释放内存后,立即将指针置为 nullptr
8ptr = nullptr;
9// 此时 ptr 为空指针,解引用空指针是明确的错误,更容易调试
10// std::cout << *ptr << std::endl; // 取消注释会导致程序崩溃,但错误更明显
11return 0;
12}

▮▮▮▮ⓑ 返回局部变量的指针或引用:
函数返回局部变量的指针或引用,当函数结束时,局部变量的生命周期结束,其内存被释放,返回的指针或引用就变成了悬 dangling 指针或引用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2int* getLocalPointer() {
3int localValue = 20;
4return &localValue; // 返回局部变量的地址
5} // 函数结束,localValue 销毁
6int main() {
7int* danglingPtr = getLocalPointer(); // danglingPtr 指向已销毁的局部变量
8// 错误:danglingPtr 是悬 dangling 指针,解引用会导致未定义行为
9// std::cout << *danglingPtr << std::endl; // 取消注释会导致未定义行为
10return 0;
11}

解决方案与最佳实践:
▮▮▮▮ⓑ 释放内存后,立即将指针置为 nullptr:
当使用 delete 释放内存后,立即将指针变量设置为 nullptr。这样可以避免误用悬 dangling 指针,并且解引用空指针会更容易被检测到。

▮▮▮▮ⓑ 避免返回局部变量的指针或引用:
不要返回局部变量的指针或引用。如果需要返回动态分配的对象,应使用智能指针管理其生命周期。

▮▮▮▮ⓒ 使用智能指针 (Smart Pointers):
智能指针可以有效地避免悬 dangling 指针问题。例如,std::unique_ptr 保证独占所有权,std::shared_ptr 通过引用计数管理对象生命周期,当最后一个 shared_ptr 销毁时,对象才会被释放。

▮▮▮▮ⓓ 生命周期管理 (Lifetime Management):
仔细考虑对象的生命周期和指针的作用域。确保指针的生命周期不超过其指向对象的生命周期。

在并发 (Concurrency) 和多线程 (Multithreading) 编程中,常见的错误包括数据竞争 (Data Races)、死锁 (Deadlocks)、以及活锁 (Livelocks)。理解和避免这些错误,是编写安全高效并发程序的关键。

Appendix B4.1: 数据竞争 (Data Races)

数据竞争发生在多个线程并发访问同一内存位置,并且至少有一个线程进行写操作,同时没有使用任何同步机制 (Synchronization Mechanism) 来保护数据访问时。数据竞争会导致程序行为不确定,结果难以预测。

错误类型:
▮▮▮▮ⓑ 多个线程同时读写共享变量,没有互斥保护:
多个线程同时访问和修改共享变量,例如全局变量或静态变量,如果没有使用互斥锁 (Mutex) 等同步机制保护,就会发生数据竞争。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <vector>
4int sharedCounter = 0; // 共享变量
5void incrementCounter() {
6for (int i = 0; i < 100000; ++i) {
7sharedCounter++; // 数据竞争:多个线程同时写 sharedCounter
8}
9}
10int main() {
11std::vector<std::thread> threads;
12for (int i = 0; i < 4; ++i) {
13threads.emplace_back(incrementCounter);
14}
15for (auto& thread : threads) {
16thread.join();
17}
18std::cout << "Counter value: " << sharedCounter << std::endl; // 预期结果是 400000,但实际结果可能小于 400000,因为数据竞争
19return 0;
20}

解决方案与最佳实践:
▮▮▮▮ⓑ 使用互斥锁 (Mutexes) 保护共享数据:
使用 std::mutex 互斥锁保护对共享变量的访问,确保同一时刻只有一个线程可以访问共享变量,避免数据竞争。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <vector>
4#include <mutex>
5int sharedCounter = 0;
6std::mutex counterMutex; // 互斥锁
7void incrementCounterSafe() {
8for (int i = 0; i < 100000; ++i) {
9std::lock_guard<std::mutex> lock(counterMutex); // 加锁
10sharedCounter++; // 线程安全地访问 sharedCounter
11} // lock_guard 析构时自动解锁
12}
13int main() {
14std::vector<std::thread> threads;
15for (int i = 0; i < 4; ++i) {
16threads.emplace_back(incrementCounterSafe);
17}
18for (auto& thread : threads) {
19thread.join();
20}
21std::cout << "Counter value: " << sharedCounter << std::endl; // 结果保证是 400000,没有数据竞争
22return 0;
23}

▮▮▮▮ⓑ 使用原子操作 (Atomic Operations):
对于简单的共享变量操作 (例如计数器递增、标志位设置),可以使用 std::atomic 原子类型和原子操作,提供更轻量级的同步机制,性能通常比互斥锁更好。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <vector>
4#include <atomic>
5std::atomic<int> sharedCounterAtomic(0); // 原子计数器
6void incrementCounterAtomic() {
7for (int i = 0; i < 100000; ++i) {
8sharedCounterAtomic++; // 原子操作,线程安全
9}
10}
11int main() {
12std::vector<std::thread> threads;
13for (int i = 0; i < 4; ++i) {
14threads.emplace_back(incrementCounterAtomic);
15}
16for (auto& thread : threads) {
17thread.join();
18}
19std::cout << "Counter value: " << sharedCounterAtomic << std::endl; // 结果保证是 400000,没有数据竞争,且性能通常更好
20return 0;
21}

▮▮▮▮ⓒ 避免共享状态 (Minimize Shared State):
尽量减少线程之间的共享状态。尽可能使用线程局部存储 (Thread-Local Storage) 或消息传递 (Message Passing) 等方式,降低数据竞争的风险。

Appendix B4.2: 死锁 (Deadlocks)

死锁是指两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行的僵局状态。死锁通常发生在多个线程竞争多个互斥锁时。

错误类型:
▮▮▮▮ⓑ 循环等待 (Circular Wait):
当线程 A 持有锁 L1,并尝试获取锁 L2;同时线程 B 持有锁 L2,并尝试获取锁 L1 时,就会发生循环等待,导致死锁。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::mutex mutex1;
5std::mutex mutex2;
6void threadFunction1() {
7std::lock(mutex1, mutex2); // 同时获取两个锁,可能导致死锁
8std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
9std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
10std::cout << "Thread 1: 获取到两个锁" << std::endl;
11}
12void threadFunction2() {
13std::lock(mutex2, mutex1); // 获取锁的顺序与 threadFunction1 相反,可能导致死锁
14std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
15std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
16std::cout << "Thread 2: 获取到两个锁" << std::endl;
17}
18int main() {
19std::thread t1(threadFunction1);
20std::thread t2(threadFunction2);
21t1.join();
22t2.join();
23std::cout << "程序结束" << std::endl; // 程序可能永远无法到达这里,因为死锁
24return 0;
25}

解决方案与最佳实践:
▮▮▮▮ⓑ 避免循环等待:
确保所有线程以相同的顺序获取锁。如果所有线程都按照锁 L1, L2, L3, ... 的顺序获取锁,就可以避免循环等待。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4std::mutex mutex1;
5std::mutex mutex2;
6void threadFunction1NoDeadlock() {
7std::lock_guard<std::mutex> lock1(mutex1); // 先获取 mutex1
8std::lock_guard<std::mutex> lock2(mutex2); // 再获取 mutex2,顺序一致
9std::cout << "Thread 1: 获取到两个锁 (无死锁)" << std::endl;
10}
11void threadFunction2NoDeadlock() {
12std::lock_guard<std::mutex> lock1(mutex1); // 先获取 mutex1,顺序一致
13std::lock_guard<std::mutex> lock2(mutex2); // 再获取 mutex2,顺序一致
14std::cout << "Thread 2: 获取到两个锁 (无死锁)" << std::endl;
15}
16int main() {
17std::thread t1(threadFunction1NoDeadlock);
18std::thread t2(threadFunction2NoDeadlock);
19t1.join();
20t2.join();
21std::cout << "程序结束 (无死锁)" << std::endl; // 程序正常结束,没有死锁
22return 0;
23}

▮▮▮▮ⓑ 超时机制 (Timeout Mechanism):
使用 std::timed_mutex 尝试限时获取锁,如果超过一定时间仍未获取到锁,则放弃等待,避免永久阻塞。

▮▮▮▮ⓒ 锁的层次结构 (Lock Hierarchy):
建立锁的层次结构,规定线程只能按照层次顺序获取锁。高层锁的获取必须在低层锁释放之后。

▮▮▮▮ⓓ 死锁检测与恢复 (Deadlock Detection and Recovery):
在复杂的系统中,可以实现死锁检测机制,当检测到死锁时,采取措施 (例如回滚事务、重启线程) 进行恢复。但这通常比较复杂,应尽量在设计阶段避免死锁。

Appendix B4.3: 活锁 (Livelocks)

活锁类似于死锁,但线程不是被阻塞,而是不断地重试相同的操作,但始终无法成功。例如,两个线程为了避免冲突,都尝试退让,但退让的策略导致它们不断地互相退让,永远无法前进。

错误类型:
▮▮▮▮ⓑ 线程不断重试,但条件始终无法满足:
例如,两个线程都需要同时获取两个资源才能继续执行,如果它们发现对方已经占用了自己需要的资源,就都选择释放已占有的资源并重试,但重试的时机和策略导致它们总是无法同时获取到所需资源。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <chrono>
5std::mutex mutex1;
6std::mutex mutex2;
7void threadFunctionLivelock1() {
8while (true) {
9std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
10std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
11std::lock(lock1, lock2); // 尝试同时获取两个锁
12if (lock1.owns_lock() && lock2.owns_lock()) {
13std::cout << "Thread 1: 获取到两个锁 (活锁避免)" << std::endl;
14break; // 获取成功,退出循环
15} else {
16std::cout << "Thread 1: 活锁退让,释放锁并重试" << std::endl;
17std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暂休眠后重试
18}
19}
20}
21void threadFunctionLivelock2() {
22while (true) {
23std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
24std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
25std::lock(lock1, lock2); // 尝试同时获取两个锁,顺序相同
26if (lock1.owns_lock() && lock2.owns_lock()) {
27std::cout << "Thread 2: 获取到两个锁 (活锁避免)" << std::endl;
28break; // 获取成功,退出循环
29} else {
30std::cout << "Thread 2: 活锁退让,释放锁并重试" << std::endl;
31std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 短暂休眠后重试
32}
33}
34}
35int main() {
36std::thread t1(threadFunctionLivelock1);
37std::thread t2(threadFunctionLivelock2);
38t1.join();
39t2.join();
40std::cout << "程序结束 (活锁)" << std::endl; // 程序可能陷入活锁,不断重试但无法前进
41return 0;
42}

解决方案与最佳实践:
▮▮▮▮ⓑ 随机退避 (Random Backoff):
在重试之前引入随机的退避时间。例如,线程在发现资源冲突后,等待一段随机时间再重试,减少多个线程同时重试导致活锁的概率。

▮▮▮▮ⓑ 优先级 (Priority):
为线程或操作设置优先级。高优先级的线程可以优先获取资源,避免低优先级线程一直被饿死 (Starvation) 或陷入活锁。

▮▮▮▮ⓒ 避免不必要的退让:
重新审视退让策略,确保退让策略确实有助于解决冲突,而不是加剧冲突。在很多情况下,简单的等待或排队机制比复杂的退让策略更有效。

▮▮▮▮ⓓ 监控与干预 (Monitoring and Intervention):
在复杂的系统中,可以监控线程的运行状态,检测是否发生活锁。当检测到活锁时,可以采取干预措施,例如调整线程优先级、强制释放资源等。

本附录总结了 C++ 标准库使用中常见的错误类型和相应的解决方案,希望能够帮助读者在实际开发中避免这些陷阱,编写更健壮、高效和可靠的 C++ 代码。深入理解这些错误背后的原理,并掌握正确的编程实践,是成为一名优秀的 C++ 开发者的重要一步。

<END_OF_CHAPTER/>

Appendix C: 性能考量与优化建议 (Performance Considerations and Optimization Tips)

附录 C 讨论 C++ 标准库的性能特点,并提供一些性能优化建议,帮助读者编写更高效的代码。

C.1 容器性能考量 (Container Performance Considerations)

本节深入探讨 C++ 标准库容器的性能特性,并提供选择和使用容器以优化性能的实用建议。

C.1.1 选择合适的容器 (Choosing the Right Container)

选择正确的容器是优化程序性能的关键第一步。不同的容器在内存布局、元素访问、插入和删除操作等方面具有不同的性能特点。理解这些特点可以帮助开发者根据具体应用场景做出最佳选择。

序列容器 (Sequence Containers)
std::vector (向量):
▮▮▮▮⚝ 描述:动态数组,元素在内存中连续存储。
▮▮▮▮⚝ 优点:随机访问速度快O(1),尾部插入和删除速度快(均摊O(1))。
▮▮▮▮⚝ 缺点:头部或中部插入和删除效率低O(n),可能涉及内存重新分配和元素移动。
▮▮▮▮⚝ 适用场景:频繁随机访问元素,尾部插入和删除操作较多的场景。例如,存储和处理大量数据,需要快速访问任意位置元素的情况。
▮▮▮▮⚝ 优化建议:如果预知元素数量,可以使用 reserve() 预先分配足够的容量,减少内存重新分配的次数。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3int main() {
4std::vector<int> vec;
5vec.reserve(100); // 预分配 100 个元素的空间
6for (int i = 0; i < 100; ++i) {
7vec.push_back(i); // 尾部插入
8}
9std::cout << "Vector size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;
10return 0;
11}

std::deque (双端队列):
▮▮▮▮⚝ 描述:双端动态数组,允许快速在头部和尾部进行插入和删除操作。内部通常采用分段连续存储结构。
▮▮▮▮⚝ 优点:头部和尾部插入和删除速度快(均摊O(1)),随机访问速度接近 vector
▮▮▮▮⚝ 缺点:随机访问速度略慢于 vector,中部插入和删除效率低O(n)
▮▮▮▮⚝ 适用场景:需要在头部和尾部频繁插入和删除元素的场景,例如消息队列、任务调度等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <deque>
2#include <iostream>
3int main() {
4std::deque<int> deq;
5deq.push_back(1); // 尾部插入
6deq.push_front(0); // 头部插入
7std::cout << "Deque front: " << deq.front() << ", back: " << deq.back() << std::endl;
8return 0;
9}

std::list (列表) 和 std::forward_list (单向链表):
▮▮▮▮⚝ 描述:链式存储结构,list 是双向链表,forward_list 是单向链表。
▮▮▮▮⚝ 优点:任意位置插入和删除速度快O(1),无需内存连续。forward_list 内存开销更小,操作略快于 list
▮▮▮▮⚝ 缺点:随机访问速度慢O(n),需要遍历链表;forward_list 不支持反向迭代。
▮▮▮▮⚝ 适用场景:频繁在任意位置插入和删除元素的场景,例如文本编辑、链表操作等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <list>
2#include <forward_list>
3#include <iostream>
4int main() {
5std::list<int> lst = {1, 2, 3};
6auto it = lst.begin();
7std::advance(it, 1); // 移动迭代器到第二个元素
8lst.insert(it, 4); // 在第二个元素前插入 4
9std::cout << "List elements: ";
10for (int val : lst) {
11std::cout << val << " ";
12}
13std::cout << std::endl;
14std::forward_list<int> flst = {1, 2, 3};
15flst.push_front(0);
16std::cout << "Forward list front: " << flst.front() << std::endl;
17return 0;
18}

std::array (数组):
▮▮▮▮⚝ 描述:固定大小的数组,大小在编译时确定,存储在栈上或静态存储区。
▮▮▮▮⚝ 优点:随机访问速度快O(1),与 C 风格数组性能相当,但更安全(提供边界检查)。
▮▮▮▮⚝ 缺点:大小固定,不能动态扩展。
▮▮▮▮⚝ 适用场景:元素数量固定且较小的场景,例如固定大小的缓冲区、矩阵表示等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <array>
2#include <iostream>
3int main() {
4std::array<int, 5> arr = {1, 2, 3, 4, 5};
5std::cout << "Array element at index 2: " << arr[2] << std::endl;
6return 0;
7}

关联容器 (Associative Containers)
std::set (集合) 和 std::multiset (多重集合):
▮▮▮▮⚝ 描述:基于红黑树实现的有序集合,元素自动排序。set 存储唯一元素,multiset 允许重复元素。
▮▮▮▮⚝ 优点:元素自动有序,查找、插入和删除操作平均时间复杂度为O(logn)
▮▮▮▮⚝ 缺点:维护有序性有额外开销,空间开销相对较大。
▮▮▮▮⚝ 适用场景:需要存储有序且唯一(或可重复)元素的集合,例如字典、索引等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <set>
2#include <iostream>
3int main() {
4std::set<int> s;
5s.insert(3);
6s.insert(1);
7s.insert(2); // 元素自动排序
8std::cout << "Set elements: ";
9for (int val : s) {
10std::cout << val << " ";
11}
12std::cout << std::endl;
13return 0;
14}

std::map (映射) 和 std::multimap (多重映射):
▮▮▮▮⚝ 描述:基于红黑树实现的有序键值对容器,键自动排序。map 键唯一,multimap 键可重复。
▮▮▮▮⚝ 优点:键自动有序,基于键的查找、插入和删除操作平均时间复杂度为O(logn)
▮▮▮▮⚝ 缺点:维护键的有序性有额外开销,空间开销较大。
▮▮▮▮⚝ 适用场景:需要存储有序键值对的场景,例如配置管理、索引映射等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <map>
2#include <iostream>
3int main() {
4std::map<std::string, int> m;
5m["apple"] = 1;
6m["banana"] = 2;
7m["orange"] = 3; // 键自动排序
8std::cout << "Map value for key 'banana': " << m["banana"] << std::endl;
9return 0;
10}

无序容器 (Unordered Containers)
std::unordered_set (无序集合) 和 std::unordered_multiset (无序多重集合):
▮▮▮▮⚝ 描述:基于哈希表实现的无序集合,元素无序存储。unordered_set 存储唯一元素,unordered_multiset 允许重复元素。
▮▮▮▮⚝ 优点:平均情况下,查找、插入和删除操作时间复杂度为O(1)
▮▮▮▮⚝ 缺点:最坏情况下可能退化为O(n);元素无序;哈希计算和维护哈希表有额外开销。
▮▮▮▮⚝ 适用场景:对元素顺序没有要求,需要快速查找、插入和删除元素的集合,例如缓存、快速查找表等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <unordered_set>
2#include <iostream>
3int main() {
4std::unordered_set<int> uset;
5uset.insert(3);
6uset.insert(1);
7uset.insert(2); // 元素无序
8std::cout << "Unordered set elements: ";
9for (int val : uset) {
10std::cout << val << " ";
11}
12std::cout << std::endl;
13return 0;
14}

std::unordered_map (无序映射) 和 std::unordered_multimap (无序多重映射):
▮▮▮▮⚝ 描述:基于哈希表实现的无序键值对容器,键无序存储。unordered_map 键唯一,unordered_multimap 键可重复。
▮▮▮▮⚝ 优点:平均情况下,基于键的查找、插入和删除操作时间复杂度为O(1)
▮▮▮▮⚝ 缺点:最坏情况下可能退化为O(n);键无序;哈希计算和维护哈希表有额外开销。
▮▮▮▮⚝ 适用场景:对键的顺序没有要求,需要快速通过键查找值的场景,例如配置查找、数据索引等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <unordered_map>
2#include <iostream>
3int main() {
4std::unordered_map<std::string, int> umap;
5umap["apple"] = 1;
6umap["banana"] = 2;
7umap["orange"] = 3; // 键无序
8std::cout << "Unordered map value for key 'banana': " << umap["banana"] << std::endl;
9return 0;
10}

C.1.2 预分配容器容量 (Pre-allocating Container Capacity)

对于动态数组类型的容器,如 vectordeque,预先分配足够的容量可以显著提升性能,尤其是在需要存储大量元素时。当容器的容量不足以容纳新元素时,会触发内存重新分配,这通常是一个耗时的操作,包括分配新的内存块、将现有元素复制或移动到新内存块、以及释放旧的内存块。通过 reserve() 方法预先分配容量,可以减少甚至避免这种重新分配的发生。

vector::reserve() 的使用
▮▮▮▮⚝ reserve(n) 方法可以请求容器预留至少能容纳 n 个元素的空间。这并不会改变容器的大小(size()),只是预先分配了内存。
▮▮▮▮⚝ 适用场景:在循环中向 vector 批量添加元素,且预先知道大致或最大元素数量时。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3int main() {
4std::vector<int> vec;
5vec.reserve(1000); // 预留 1000 个元素的空间
6for (int i = 0; i < 1000; ++i) {
7vec.push_back(i); // 避免多次内存重新分配
8}
9std::cout << "Vector size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;
10return 0;
11}

deque 的分段分配
▮▮▮▮⚝ deque 通常采用分段连续的存储方式,内存重新分配的开销相对 vector 较小,但预先分配一定数量的段仍然可以提升性能。
▮▮▮▮⚝ 尽管 deque 没有 reserve() 方法,但其内存分配策略使其在动态增长时更为高效。

注意事项
▮▮▮▮⚝ 过度预分配可能会导致内存浪费,需要根据实际情况权衡。
▮▮▮▮⚝ 对于 list, forward_list, set, map, unordered_set, unordered_map 等基于节点或哈希表的容器,预分配容量的概念不适用,因为它们是按需分配内存的。

C.1.3 理解容器操作的复杂度 (Understanding Container Operation Complexity)

了解各种容器操作的时间复杂度是进行性能优化的基础。标准库文档通常会明确指出每个容器操作的时间复杂度,例如插入、删除、查找等。选择容器时,应重点关注程序中最频繁操作的复杂度。

常见容器操作复杂度总结

容器类型 随机访问 插入/删除(头部/尾部) 插入/删除(中部) 查找(有序) 查找(无序)
std::vector O(1) 尾部O(1) O(n) O(logn)* N/A
std::deque O(1) 头部/尾部O(1) O(n) O(logn)* N/A
std::list O(n) O(1) O(1) O(n) N/A
std::forward_list O(n) 头部O(1) O(1) O(n) N/A
std::array O(1) N/A (固定大小) N/A O(logn)* N/A
std::set O(logn) O(logn) O(logn) O(logn) N/A
std::multiset O(logn) O(logn) O(logn) O(logn) N/A
std::map O(logn) O(logn) O(logn) O(logn) N/A
std::multimap O(logn) O(logn) O(logn) O(logn) N/A
std::unordered_set 平均O(1) 平均O(1) 平均O(1) N/A 平均O(1)
std::unordered_multiset 平均O(1) 平均O(1) 平均O(1) N/A 平均O(1)
std::unordered_map 平均O(1) 平均O(1) 平均O(1) N/A 平均O(1)
std::unordered_multimap 平均O(1) 平均O(1) 平均O(1) N/A 平均O(1)

> \* 对于 vector, deque, array,在已排序的情况下可以使用二分查找等对数时间复杂度的算法。

根据操作选择容器示例
▮▮▮▮⚝ 如果程序中频繁进行随机访问,且元素数量固定或尾部增删为主,vectorarray 是较好的选择。
▮▮▮▮⚝ 如果程序中需要在头部和尾部频繁增删元素,deque 是更合适的选择。
▮▮▮▮⚝ 如果程序中需要在任意位置频繁增删元素,且对顺序没有要求,listforward_list 更高效。
▮▮▮▮⚝ 如果程序需要存储有序键值对并进行查找,mapmultimap 是合适的选择。
▮▮▮▮⚝ 如果程序需要快速查找元素,且对顺序没有要求,unordered_setunordered_map 通常能提供更好的性能。

C.1.4 避免不必要的拷贝和移动 (Avoiding Unnecessary Copies and Moves)

C++ 中对象的拷贝和移动操作可能会带来显著的性能开销,尤其是在处理大型对象时。标准库容器操作,如插入、删除、排序等,都可能涉及到元素的拷贝或移动。优化策略包括使用移动语义、emplace 操作和引用。

移动语义 (Move Semantics)
▮▮▮▮⚝ C++11 引入了移动语义,允许资源(如动态分配的内存)的所有权从一个对象转移到另一个对象,而无需深拷贝。
▮▮▮▮⚝ 容器的插入操作,如 push_back()insert(),通常有接受右值引用 (rvalue reference) 的重载版本,可以触发移动语义。
▮▮▮▮⚝ 确保自定义类型实现了移动构造函数 (move constructor)移动赋值运算符 (move assignment operator),以便在容器操作中充分利用移动语义。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3#include <string>
4class MyString {
5public:
6std::string data;
7MyString(std::string s) : data(s) {
8std::cout << "Constructor: " << data << std::endl;
9}
10MyString(const MyString& other) : data(other.data) {
11std::cout << "Copy Constructor: " << data << std::endl;
12}
13MyString(MyString&& other) noexcept : data(std::move(other.data)) {
14std::cout << "Move Constructor: " << data << std::endl;
15}
16MyString& operator=(const MyString& other) {
17data = other.data;
18std::cout << "Copy Assignment: " << data << std::endl;
19return *this;
20}
21MyString& operator=(MyString&& other) noexcept {
22data = std::move(other.data);
23std::cout << "Move Assignment: " << data << std::endl;
24return *this;
25}
26~MyString() {
27std::cout << "Destructor: " << data << std::endl;
28}
29};
30int main() {
31std::vector<MyString> vec;
32std::string s = "Hello";
33vec.push_back(MyString(s)); // 使用临时对象,触发移动构造
34MyString ms1("World");
35vec.push_back(ms1); // 插入现有对象,触发拷贝构造
36vec.push_back(std::move(ms1)); // 显式移动,触发移动构造,ms1 变为有效但未指定状态
37return 0;
38}

emplace 操作 (Emplace Operations)
▮▮▮▮⚝ 容器提供了 emplace_back(), emplace_front(), emplace()emplace 系列方法,允许直接在容器内部构造元素,避免了临时对象的创建和拷贝或移动。
▮▮▮▮⚝ emplace 操作接受构造函数参数,并在容器的内存空间中直接调用构造函数创建对象。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3class MyObject {
4public:
5int value;
6MyObject(int v) : value(v) {
7std::cout << "Object constructed with value: " << value << std::endl;
8}
9MyObject(const MyObject& other) : value(other.value) {
10std::cout << "Object copied with value: " << value << std::endl;
11}
12MyObject(MyObject&& other) noexcept : value(other.value) {
13std::cout << "Object moved with value: " << value << std::endl;
14}
15};
16int main() {
17std::vector<MyObject> vec;
18vec.emplace_back(10); // 直接在 vector 内部构造 MyObject,避免拷贝或移动
19return 0;
20}

使用引用 (References)
▮▮▮▮⚝ 在不需要修改元素的情况下,使用常量引用 (const reference) 传递容器元素,可以避免不必要的拷贝。
▮▮▮▮⚝ 在算法中,如果可能,尽量使用移动迭代器 (move iterator),结合移动语义优化元素操作。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3#include <algorithm>
4void print_element(const int& element) { // 使用常量引用,避免拷贝
5std::cout << element << " ";
6}
7int main() {
8std::vector<int> numbers = {1, 2, 3, 4, 5};
9std::for_each(numbers.begin(), numbers.end(), print_element); // 遍历时使用常量引用
10std::cout << std::endl;
11return 0;
12}

C.2 算法性能考量 (Algorithm Performance Considerations)

标准库算法提供了丰富的功能,但不同的算法在性能上存在差异。选择合适的算法、理解算法复杂度以及优化自定义操作是提升算法性能的关键。

C.2.1 选择合适的算法 (Choosing the Right Algorithm)

标准库提供了多种算法来完成相似的任务,例如查找元素可以使用 std::find, std::binary_search, std::lower_bound 等。选择算法时,需要考虑数据特性(如是否排序)、操作需求和性能要求。

查找算法的选择
▮▮▮▮⚝ std::find (线性查找):
▮▮▮▮▮▮▮▮⚝ 描述:在无序序列中查找指定元素,从头到尾逐个比较。
▮▮▮▮▮▮▮▮⚝ 时间复杂度:O(n)
▮▮▮▮▮▮▮▮⚝ 适用场景:在无序序列中查找元素,或者序列较小。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4int main() {
5std::vector<int> data = {5, 2, 8, 1, 9};
6auto it = std::find(data.begin(), data.end(), 8); // 线性查找元素 8
7if (it != data.end()) {
8std::cout << "Found element: " << *it << std::endl;
9}
10return 0;
11}

▮▮▮▮⚝ std::binary_search (二分查找):
▮▮▮▮▮▮▮▮⚝ 描述:在有序序列中查找指定元素,采用二分查找算法。
▮▮▮▮▮▮▮▮⚝ 时间复杂度:O(logn)
▮▮▮▮▮▮▮▮⚝ 前提条件:序列必须已排序
▮▮▮▮▮▮▮▮⚝ 适用场景:在有序序列中快速判断元素是否存在。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4int main() {
5std::vector<int> sorted_data = {1, 2, 5, 8, 9}; // 有序序列
6bool found = std::binary_search(sorted_data.begin(), sorted_data.end(), 5); // 二分查找元素 5
7if (found) {
8std::cout << "Element found" << std::endl;
9}
10return 0;
11}

▮▮▮▮⚝ std::lower_boundstd::upper_bound (下界和上界):
▮▮▮▮▮▮▮▮⚝ 描述:在有序序列中查找第一个不小于lower_bound)或大于upper_bound)指定值的元素的位置。
▮▮▮▮▮▮▮▮⚝ 时间复杂度:O(logn)
▮▮▮▮▮▮▮▮⚝ 前提条件:序列必须已排序
▮▮▮▮▮▮▮▮⚝ 适用场景:在有序序列中查找元素的插入位置,或查找某个范围的元素。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4int main() {
5std::vector<int> sorted_data = {1, 2, 2, 5, 5, 5, 8, 9}; // 有序序列,包含重复元素
6auto lower = std::lower_bound(sorted_data.begin(), sorted_data.end(), 5); // 查找第一个不小于 5 的位置
7auto upper = std::upper_bound(sorted_data.begin(), sorted_data.end(), 5); // 查找第一个大于 5 的位置
8std::cout << "Lower bound index for 5: " << std::distance(sorted_data.begin(), lower) << std::endl;
9std::cout << "Upper bound index for 5: " << std::distance(sorted_data.begin(), upper) << std::endl;
10std::cout << "Range of 5: [" << std::distance(sorted_data.begin(), lower) << ", " << std::distance(sorted_data.begin(), upper) << ")" << std::endl;
11return 0;
12}

排序算法的选择
▮▮▮▮⚝ std::sort (快速排序或堆排序):
▮▮▮▮▮▮▮▮⚝ 描述:通用排序算法,通常采用内省式排序 (Introsort),结合快速排序、堆排序和插入排序的优点。
▮▮▮▮▮▮▮▮⚝ 平均时间复杂度:O(nlogn)
▮▮▮▮▮▮▮▮⚝ 适用场景:通用排序需求,性能良好。

▮▮▮▮⚝ std::stable_sort (归并排序):
▮▮▮▮▮▮▮▮⚝ 描述:稳定排序算法,保持相等元素的相对顺序。通常采用归并排序 (Merge Sort)
▮▮▮▮▮▮▮▮⚝ 时间复杂度:O(nlogn)
▮▮▮▮▮▮▮▮⚝ 适用场景:需要保持相等元素相对顺序的排序,例如多条件排序的第二条件。

▮▮▮▮⚝ std::partial_sort (堆排序):
▮▮▮▮▮▮▮▮⚝ 描述:部分排序算法,只排序序列中前k个最小(或最大)的元素。
▮▮▮▮▮▮▮▮⚝ 时间复杂度:O(nlogk)
▮▮▮▮▮▮▮▮⚝ 适用场景:只需要序列中部分有序的场景,例如查找前k个最大/小元素。

▮▮▮▮⚝ std::nth_element (快速选择):
▮▮▮▮▮▮▮▮⚝ 描述:选择算法,将序列中第n个位置的元素放到正确的位置,使得该位置之前的元素都不大于它,之后的元素都不小于它,但不保证完全排序。
▮▮▮▮▮▮▮▮⚝ 平均时间复杂度:O(n)
▮▮▮▮▮▮▮▮⚝ 适用场景:只需要找到第n个元素,不需要完全排序的场景,例如中位数查找。

C.2.2 算法复杂度与数据结构 (Algorithm Complexity and Data Structures)

算法的性能与所操作的数据结构密切相关。选择算法时,需要考虑容器的特性,例如随机访问性能、是否排序等。例如,对 vector 使用 std::sort 很高效,但对 list 使用 std::sort 效率较低,因为 list 的迭代器是双向迭代器,std::sort 的随机访问迭代器要求不满足,虽然 list 提供了成员函数 sort(),但是效率不如 vectorstd::sort

排序算法与容器
▮▮▮▮⚝ std::sort 适用于随机访问迭代器的容器,如 vector, deque, array
▮▮▮▮⚝ 对于 listforward_list,应使用其成员函数 sort(),这些成员函数针对链表结构进行了优化。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <list>
3#include <algorithm>
4#include <iostream>
5#include <chrono>
6int main() {
7int n = 100000;
8std::vector<int> vec(n);
9std::list<int> lst(n);
10for (int i = 0; i < n; ++i) {
11vec[i] = rand() % n;
12lst.push_back(rand() % n);
13}
14// 排序 vector
15auto start_vec = std::chrono::high_resolution_clock::now();
16std::sort(vec.begin(), vec.end());
17auto end_vec = std::chrono::high_resolution_clock::now();
18std::chrono::duration<double> duration_vec = end_vec - start_vec;
19std::cout << "Time for vector sort: " << duration_vec.count() << " seconds" << std::endl;
20// 排序 list
21auto start_lst = std::chrono::high_resolution_clock::now();
22lst.sort(); // 使用 list 的成员函数 sort
23auto end_lst = std::chrono::high_resolution_clock::now();
24std::chrono::duration<double> duration_lst = end_lst - start_lst;
25std::cout << "Time for list sort: " << duration_lst.count() << " seconds" << std::endl;
26return 0;
27}

查找算法与容器
▮▮▮▮⚝ std::binary_search, std::lower_bound, std::upper_bound 等二分查找算法要求容器已排序,且适用于随机访问迭代器的容器。
▮▮▮▮⚝ 对于未排序的容器,只能使用线性查找算法,如 std::find
▮▮▮▮⚝ 对于 set, map有序关联容器,其成员函数 find(), lower_bound(), upper_bound() 等具有更高的查找效率O(logn),因为它们利用了容器内部的有序结构。
▮▮▮▮⚝ 对于 unordered_set, unordered_map无序关联容器,其成员函数 find() 等具有平均O(1)的查找效率。

C.2.3 自定义比较函数与谓词的效率 (Efficiency of Custom Comparison Functions and Predicates)

许多标准库算法允许用户自定义比较函数或谓词,以实现特定的排序或判断逻辑。自定义操作的效率直接影响算法的整体性能。优化建议包括:

避免复杂计算
▮▮▮▮⚝ 比较函数和谓词应尽可能简单,避免在其中进行复杂的计算或 I/O 操作。
▮▮▮▮⚝ 复杂的比较逻辑可能会成为性能瓶颈,尤其是在大规模数据排序或查找时。

使用 const 引用
▮▮▮▮⚝ 比较函数和谓词的参数应使用 const 引用,避免不必要的拷贝

内联 (Inline)
▮▮▮▮⚝ 简单的比较函数和谓词可以声明为 inline减少函数调用开销,但现代编译器通常能自动内联简单的函数,显式 inline 可能不是必须的。

Lambda 表达式的优化
▮▮▮▮⚝ 对于简单的谓词,Lambda 表达式通常比函数对象更简洁,且编译器更容易优化。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4struct Point {
5int x, y;
6};
7// 比较函数,按 x 坐标排序
8bool comparePoints(const Point& a, const Point& b) {
9return a.x < b.x; // 简单的比较逻辑
10}
11int main() {
12std::vector<Point> points = {{3, 2}, {1, 5}, {2, 3}};
13std::sort(points.begin(), points.end(), comparePoints); // 使用自定义比较函数排序
14// 使用 Lambda 表达式
15std::sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
16return a.y < b.y; // 按 y 坐标排序
17});
18for (const auto& p : points) {
19std::cout << "(" << p.x << ", " << p.y << ") ";
20}
21std::cout << std::endl;
22return 0;
23}

C.2.4 利用移动语义优化算法 (Optimizing Algorithms with Move Semantics)

在算法操作中,如果元素类型支持移动语义,可以利用 std::move 和移动迭代器 (move iterator) 来优化性能,尤其是在元素拷贝代价较高时。

std::move 的应用
▮▮▮▮⚝ 在 std::transform, std::copy 等算法中,可以使用 std::move 将元素移动到目标位置,而不是拷贝。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4#include <string>
5int main() {
6std::vector<std::string> source = {"apple", "banana", "orange"};
7std::vector<std::string> destination(source.size());
8// 使用 std::move 进行移动
9std::transform(source.begin(), source.end(), destination.begin(), [](std::string& s){
10return std::move(s);
11});
12std::cout << "Source after move:" << std::endl;
13for (const auto& s : source) {
14std::cout << "\"" << s << "\" "; // 源字符串可能为空或处于有效但未指定状态
15}
16std::cout << std::endl;
17std::cout << "Destination:" << std::endl;
18for (const auto& s : destination) {
19std::cout << "\"" << s << "\" ";
20}
21std::cout << std::endl;
22return 0;
23}

移动迭代器 std::move_iterator
▮▮▮▮⚝ std::move_iterator 可以将迭代器转换为移动迭代器,使得算法在遍历序列时,对元素执行移动操作而不是拷贝操作。
▮▮▮▮⚝ 适用于 std::copy, std::transform 等算法。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <algorithm>
3#include <iostream>
4#include <string>
5int main() {
6std::vector<std::string> source = {"apple", "banana", "orange"};
7std::vector<std::string> destination(source.size());
8// 使用 std::move_iterator 进行移动复制
9std::copy(std::make_move_iterator(source.begin()), std::make_move_iterator(source.end()), destination.begin());
10std::cout << "Source after move_iterator:" << std::endl;
11for (const auto& s : source) {
12std::cout << "\"" << s << "\" "; // 源字符串可能为空或处于有效但未指定状态
13}
14std::cout << std::endl;
15std::cout << "Destination:" << std::endl;
16for (const auto& s : destination) {
17std::cout << "\"" << s << "\" ";
18}
19std::cout << std::endl;
20return 0;
21}

C.3 内存管理性能考量 (Memory Management Performance Considerations)

C++ 程序的性能与内存管理密切相关。频繁的动态内存分配和释放、内存碎片、以及不合理的内存使用模式都会影响程序性能。标准库提供了智能指针和分配器等工具来辅助内存管理。

C.3.1 减少动态内存分配 (Reducing Dynamic Memory Allocation)

动态内存分配(使用 newdelete)通常比栈内存分配开销更大。频繁的小块内存分配和释放会导致内存碎片,降低内存利用率和程序性能。优化策略包括:

栈内存优先
▮▮▮▮⚝ 尽可能使用栈内存分配对象,例如局部变量、固定大小的 std::array
▮▮▮▮⚝ 栈内存分配和释放速度快,且由编译器自动管理,避免内存泄漏和碎片问题。

对象池 (Object Pool)
▮▮▮▮⚝ 对于频繁创建和销毁的小型对象,可以使用对象池技术。
▮▮▮▮⚝ 对象池预先分配一块大的内存区域,用于存储多个对象。创建对象时从对象池中分配,销毁对象时放回对象池,而不是直接释放内存。
▮▮▮▮⚝ 减少了动态内存分配和释放的次数,提高了内存分配效率,减少了内存碎片。

使用容器的 reserve()shrink_to_fit()
▮▮▮▮⚝ 对于 vectordeque,可以使用 reserve() 预先分配容量,减少内存重新分配。
▮▮▮▮⚝ 当容器不再需要额外容量时,可以使用 shrink_to_fit() 释放多余的内存,但 shrink_to_fit() 只是一个请求,实际效果取决于具体实现。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <iostream>
3int main() {
4std::vector<int> vec;
5vec.reserve(1000); // 预留空间
6for (int i = 0; i < 100; ++i) {
7vec.push_back(i);
8}
9std::cout << "Vector size: " << vec.size() << ", capacity: " << vec.capacity() << std::endl;
10vec.shrink_to_fit(); // 尝试释放多余容量
11std::cout << "Vector size: " << vec.size() << ", capacity after shrink_to_fit: " << vec.capacity() << std::endl;
12return 0;
13}

C.3.2 智能指针的开销 (Overhead of Smart Pointers)

智能指针(如 std::shared_ptrstd::unique_ptr)通过自动管理动态分配的内存,避免了手动 newdelete 带来的内存泄漏和悬挂指针问题。然而,智能指针也引入了一些性能开销。

std::shared_ptr 的引用计数开销
▮▮▮▮⚝ std::shared_ptr 使用引用计数 (reference counting) 来跟踪资源的所有者数量。
▮▮▮▮⚝ 每次拷贝、赋值或销毁 std::shared_ptr 时,都需要原子操作 (atomic operation) 来增加或减少引用计数,这会带来一定的性能开销,尤其是在多线程环境下。
▮▮▮▮⚝ 循环引用 (circular references) 可能导致内存泄漏,需要使用 std::weak_ptr 打破循环引用。

std::unique_ptr 的零开销抽象
▮▮▮▮⚝ std::unique_ptr 提供了独占所有权 (exclusive ownership),没有引用计数开销。
▮▮▮▮⚝ 对于不需要共享所有权的场景,std::unique_ptr 通常是更高效的选择。
▮▮▮▮⚝ std::unique_ptr 的开销接近于原始指针,但提供了更好的安全性。

make_sharedmake_unique 的优势
▮▮▮▮⚝ 使用 std::make_shared<T>(...)std::make_unique<T>(...) 创建智能指针,可以提高性能和异常安全性。
▮▮▮▮⚝ make_shared控制块 (control block)(用于存储引用计数等信息)和对象本身分配在同一块内存中,减少了内存分配次数,并提高了缓存局部性 (cache locality)。
▮▮▮▮⚝ make_unique 提供了创建 std::unique_ptr 的安全且高效的方式,避免了手动 new 可能导致的异常安全问题。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <memory>
2#include <iostream>
3int main() {
4// 使用 make_shared 创建 shared_ptr
5std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(42);
6std::shared_ptr<int> sharedPtr2 = sharedPtr1; // 共享所有权
7// 使用 make_unique 创建 unique_ptr
8std::unique_ptr<int> uniquePtr1 = std::make_unique<int>(100);
9// std::unique_ptr<int> uniquePtr2 = uniquePtr1; // 错误,unique_ptr 不支持拷贝
10std::unique_ptr<int> uniquePtr2 = std::move(uniquePtr1); // 移动所有权
11return 0;
12}

C.3.3 自定义分配器的使用场景 (Use Cases for Custom Allocators)

标准库容器允许指定自定义分配器 (allocator),以控制容器的内存分配行为。自定义分配器可以用于优化特定场景下的内存管理,例如:

内存池 (Memory Pool) 分配器
▮▮▮▮⚝ 适用于频繁分配和释放固定大小对象的场景。
▮▮▮▮⚝ 预先分配一块大的内存区域,从中分配和释放对象,避免了系统默认分配器的开销和内存碎片。

单调分配器 (Monotonic Allocator)
▮▮▮▮⚝ 适用于内存分配是单调递增,不需要释放已分配内存的场景,例如临时数据结构、日志记录等。
▮▮▮▮⚝ 分配速度非常快,只需维护一个指向可用内存起始位置的指针。

共享内存分配器 (Shared Memory Allocator)
▮▮▮▮⚝ 适用于进程间共享内存的场景。
▮▮▮▮⚝ 允许在多个进程之间共享容器的内存,实现进程间数据共享。

调试分配器 (Debug Allocator)
▮▮▮▮⚝ 用于内存泄漏检测和调试。
▮▮▮▮⚝ 记录每次内存分配和释放操作,检查是否存在内存泄漏或重复释放等错误。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <vector>
2#include <memory>
3#include <iostream>
4// 简单的自定义分配器示例 (仅供演示,不完整)
5template <typename T>
6class SimpleAllocator {
7public:
8using value_type = T;
9SimpleAllocator() = default;
10template <typename U> SimpleAllocator(const SimpleAllocator<U>&) noexcept {}
11T* allocate(std::size_t n) {
12std::cout << "Allocating " << n * sizeof(T) << " bytes" << std::endl;
13return static_cast<T*>(std::malloc(n * sizeof(T)));
14}
15void deallocate(T* p, std::size_t n) {
16std::cout << "Deallocating " << n * sizeof(T) << " bytes" << std::endl;
17std::free(p);
18}
19};
20template <typename T, typename Alloc>
21bool operator==(const SimpleAllocator<T>&, const SimpleAllocator<T>&) { return true; }
22template <typename T, typename Alloc>
23bool operator!=(const SimpleAllocator<T>&, const SimpleAllocator<T>&) { return false; }
24int main() {
25std::vector<int, SimpleAllocator<int>> vec({1, 2, 3}); // 使用自定义分配器
26vec.push_back(4);
27return 0;
28}

> 注意:自定义分配器的实现较为复杂,需要仔细考虑内存对齐、构造/析构、状态管理等问题。在大多数情况下,默认分配器 std::allocator 已经足够高效。自定义分配器通常只在特定性能瓶颈特殊内存管理需求时才考虑使用。

C.4 并发性能考量 (Concurrency Performance Considerations)

并发编程可以提高程序的吞吐量和响应速度,但同时也引入了新的性能挑战,如线程同步开销、锁竞争、数据竞争等。标准库提供了线程、互斥量、条件变量、原子操作等工具来支持并发编程。

C.4.1 避免锁竞争 (Avoiding Lock Contention)

锁 (mutex) 是实现线程同步的重要工具,但过度使用锁或锁的粒度过大会导致锁竞争 (lock contention),降低并发程序的性能。当多个线程竞争同一个锁时,线程会被阻塞和唤醒,导致上下文切换开销,并降低程序的并行度。

减小锁的粒度 (Reduce Lock Granularity)
▮▮▮▮⚝ 将全局锁 (global lock) 替换为细粒度锁 (fine-grained lock),例如分段锁 (segmented lock)哈希锁 (hash lock)
▮▮▮▮⚝ 细粒度锁可以减少锁的竞争范围,提高并发度。

读写锁 (Read-Write Lock)
▮▮▮▮⚝ 对于读多写少的场景,可以使用读写锁 std::shared_mutex
▮▮▮▮⚝ 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入。

无锁数据结构 (Lock-Free Data Structures)
▮▮▮▮⚝ 在某些情况下,可以使用无锁数据结构(如原子操作实现的队列、栈等)来避免锁的开销。
▮▮▮▮⚝ 无锁编程复杂性较高,需要仔细考虑内存序 (memory order)ABA 问题

减少锁的持有时间 (Reduce Lock Holding Time)
▮▮▮▮⚝ 尽量缩短锁的持有时间,只在必要时才持有锁,并在完成共享资源访问后尽快释放锁
▮▮▮▮⚝ 避免在锁保护的代码段中执行耗时操作,如 I/O 操作、复杂计算等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <mutex>
2#include <thread>
3#include <iostream>
4#include <vector>
5std::mutex mtx;
6int shared_counter = 0;
7void increment_counter() {
8for (int i = 0; i < 100000; ++i) {
9std::lock_guard<std::mutex> lock(mtx); // 使用 lock_guard 自动管理锁
10shared_counter++; // 临界区,访问共享资源
11}
12}
13int main() {
14std::vector<std::thread> threads;
15for (int i = 0; i < 4; ++i) {
16threads.emplace_back(increment_counter);
17}
18for (auto& thread : threads) {
19thread.join();
20}
21std::cout << "Shared counter value: " << shared_counter << std::endl;
22return 0;
23}

C.4.2 原子操作的适用性 (Applicability of Atomic Operations)

原子操作 (atomic operations) 提供了轻量级的同步机制,适用于简单的同步场景,如计数器、标志位等。原子操作通常比锁的开销更小,因为它们不涉及线程阻塞和上下文切换,而是通过硬件指令保证操作的原子性。

原子类型 std::atomic<T>
▮▮▮▮⚝ 标准库提供了原子类型 std::atomic<T>,用于封装基本数据类型(如 int, bool, 指针等),并提供原子操作方法,如 load(), store(), fetch_add(), exchange(), compare_exchange_weak() 等。

适用场景
▮▮▮▮⚝ 计数器:多线程并发计数,例如统计访问次数、任务完成数量等。
▮▮▮▮⚝ 标志位:线程间同步标志,例如控制线程的启动、停止、状态切换等。
▮▮▮▮⚝ 简单的状态更新:对共享变量进行简单的原子更新,例如累加、交换等。
▮▮▮▮⚝ 无锁数据结构:作为构建更复杂无锁数据结构的基础。

内存序 (Memory Order)
▮▮▮▮⚝ 原子操作涉及内存序 (memory order) 的概念,用于控制原子操作的可见性顺序性
▮▮▮▮⚝ 常见的内存序包括 std::memory_order_relaxed, std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel, std::memory_order_seq_cst
▮▮▮▮⚝ 选择合适的内存序对于保证程序的正确性和性能至关重要。std::memory_order_seq_cst (顺序一致性) 是最强的内存序,但开销也最大。在不需要强顺序性的场景,可以使用更弱的内存序来提高性能。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <atomic>
2#include <thread>
3#include <iostream>
4#include <vector>
5std::atomic<int> atomic_counter = 0;
6void increment_atomic_counter() {
7for (int i = 0; i < 100000; ++i) {
8atomic_counter++; // 原子自增操作,默认 memory_order_seq_cst
9}
10}
11int main() {
12std::vector<std::thread> threads;
13for (int i = 0; i < 4; ++i) {
14threads.emplace_back(increment_atomic_counter);
15}
16for (auto& thread : threads) {
17thread.join();
18}
19std::cout << "Atomic counter value: " << atomic_counter << std::endl;
20return 0;
21}

C.4.3 线程局部存储的合理使用 (Proper Use of Thread-Local Storage)

线程局部存储 (Thread-Local Storage, TLS) 允许每个线程拥有独立的变量副本,避免了多线程环境下的数据竞争和同步开销。合理使用线程局部存储可以提高并发程序的性能,尤其是在某些需要为每个线程维护独立状态的场景。

thread_local 关键字
▮▮▮▮⚝ C++11 引入了 thread_local 存储说明符,用于声明线程局部变量。
▮▮▮▮⚝ thread_local 变量具有线程生命周期,每个线程访问的是其自身的独立副本,互不影响。

适用场景
▮▮▮▮⚝ 每个线程独立的计数器、ID 生成器:避免使用全局计数器或 ID 生成器带来的锁竞争。
▮▮▮▮⚝ 线程安全但非线程共享的资源:例如每个线程独立的日志对象、随机数生成器等。
▮▮▮▮⚝ 递归函数的局部状态:在某些递归算法中,可以使用线程局部变量来存储每个递归调用的局部状态。

注意事项
▮▮▮▮⚝ 线程局部存储不是线程间通信 (thread communication) 的方式,它只是为每个线程提供了独立的存储空间。
▮▮▮▮⚝ 过度使用线程局部存储可能会增加内存消耗,需要根据实际情况权衡。
▮▮▮▮⚝ 线程局部变量的初始化时机需要注意,静态线程局部变量在线程启动时初始化,动态线程局部变量在使用时初始化。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <thread>
2#include <iostream>
3#include <vector>
4thread_local int thread_local_counter = 0; // 线程局部变量
5void increment_thread_local_counter() {
6for (int i = 0; i < 5; ++i) {
7thread_local_counter++;
8std::cout << "Thread ID: " << std::this_thread::get_id() << ", counter: " << thread_local_counter << std::endl;
9}
10}
11int main() {
12std::vector<std::thread> threads;
13for (int i = 0; i < 2; ++i) {
14threads.emplace_back(increment_thread_local_counter);
15}
16for (auto& thread : threads) {
17thread.join();
18}
19return 0;
20}

C.4.4 异步操作与任务分解 (Asynchronous Operations and Task Decomposition)

异步操作 (asynchronous operations) 和任务分解 (task decomposition) 是提高并发程序性能的有效方法。通过将程序分解为多个独立异步任务 (asynchronous tasks)并行执行,可以充分利用多核处理器的计算能力,提高程序的吞吐量和响应速度。

std::asyncstd::future
▮▮▮▮⚝ std::async 函数用于异步启动一个任务,返回一个 std::future 对象,用于获取任务的执行结果。
▮▮▮▮⚝ std::future 对象可以阻塞等待任务完成,并获取任务的返回值或异常。
▮▮▮▮⚝ std::async启动策略 (launch policy) 可以是 std::launch::async (在新线程中异步执行) 或 std::launch::deferred (延迟到 future::get() 时执行)。

任务分解策略
▮▮▮▮⚝ 数据并行 (data parallelism):将数据划分为多个部分,每个部分分配给一个线程处理,例如并行计算数组元素的平方和。
▮▮▮▮⚝ 任务并行 (task parallelism):将程序分解为多个独立的任务,每个任务分配给一个线程执行,例如生产者-消费者模型、多阶段处理流程等。

任务调度与负载均衡 (Task Scheduling and Load Balancing)
▮▮▮▮⚝ 合理的任务调度可以避免线程饥饿 (thread starvation)提高资源利用率
▮▮▮▮⚝ 负载均衡 (load balancing) 可以将任务均匀分配到各个线程,避免某些线程过载,而另一些线程空闲。
▮▮▮▮⚝ 可以使用线程池 (thread pool) 来管理和复用线程,减少线程创建和销毁的开销。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <future>
2#include <iostream>
3#include <vector>
4#include <numeric>
5int calculate_sum(const std::vector<int>& data, int start, int end) {
6int sum = 0;
7for (int i = start; i < end; ++i) {
8sum += data[i];
9}
10return sum;
11}
12int main() {
13std::vector<int> data(1000000);
14std::iota(data.begin(), data.end(), 1); // 初始化数据
15int num_threads = 4;
16std::vector<std::future<int>> futures;
17int chunk_size = data.size() / num_threads;
18for (int i = 0; i < num_threads; ++i) {
19int start = i * chunk_size;
20int end = (i == num_threads - 1) ? data.size() : (i + 1) * chunk_size;
21futures.push_back(std::async(std::launch::async, calculate_sum, std::ref(data), start, end)); // 异步启动任务
22}
23int total_sum = 0;
24for (auto& future : futures) {
25total_sum += future.get(); // 获取任务结果,可能阻塞等待
26}
27std::cout << "Total sum: " << total_sum << std::endl;
28return 0;
29}

C.5 其他优化建议 (Other Optimization Tips)

除了容器、算法和并发相关的性能优化,还有一些通用的优化技巧可以帮助提升 C++ 标准库代码的性能。

C.5.1 编译优化选项 (Compiler Optimization Options)

编译器优化选项是提升程序性能最简单有效的方法之一。现代编译器提供了丰富的优化选项,可以自动进行代码优化,例如循环展开、内联、指令重排、寄存器分配等。常见的优化级别包括 -O1, -O2, -O3, -Ofast 等,优化级别越高,编译器进行的优化越多,但编译时间也会相应增加。

常用优化级别
▮▮▮▮⚝ -O0无优化,默认级别,主要用于调试。
▮▮▮▮⚝ -O1基本优化,进行一些基本的优化,如删除冗余代码、简单循环优化等,编译速度和代码大小适中。
▮▮▮▮⚝ -O2中等优化,进行更全面的优化,如指令调度、寄存器分配、代码内联等,编译速度稍慢,但生成代码性能较好。
▮▮▮▮⚝ -O3高级优化,进行更激进的优化,如循环展开、向量化、函数内联等,编译速度较慢,生成代码性能最佳,但可能会增加代码大小。
▮▮▮▮⚝ -Ofast最快优化,除了 -O3 的所有优化外,还启用了一些可能违反严格标准但能显著提高性能的优化,如忽略浮点数运算的精度、假设程序没有别名访问等。

选择合适的优化级别
▮▮▮▮⚝ 调试阶段:使用 -O0-Og (优化调试) 级别,方便调试和错误定位。
▮▮▮▮⚝ 发布版本:使用 -O2-O3 级别,以获得最佳性能。
▮▮▮▮⚝ 性能关键代码:可以使用 -Ofast 级别,但需要仔细测试,确保程序的正确性不受影响。
▮▮▮▮⚝ 权衡编译时间和运行时间:优化级别越高,编译时间越长,但运行时间越短。需要根据项目需求和开发周期进行权衡。

平台相关优化
▮▮▮▮⚝ 编译器还提供了许多平台相关的优化选项,例如针对特定 CPU 架构的指令集优化 (-march=native, -mavx2 等)。
▮▮▮▮⚝ 可以根据目标平台选择合适的优化选项,进一步提高性能。

C.5.2 使用 const 引用和右值引用 (Using const References and Rvalue References)

合理使用 const 引用和右值引用可以减少不必要的拷贝,提高程序性能。const 引用用于传递只读参数,避免函数内部修改实参,同时避免拷贝开销。右值引用用于移动语义,将资源所有权从临时对象转移到新对象,避免深拷贝。

const 引用的应用
▮▮▮▮⚝ 函数参数:对于只读大型对象,使用 const 引用传递,避免拷贝构造函数的调用。
▮▮▮▮⚝ 循环遍历:在 for-range 循环中,如果不需要修改元素,使用 const auto& 迭代,避免拷贝。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <string>
3#include <vector>
4void print_string(const std::string& str) { // 使用 const 引用,避免拷贝
5std::cout << str << std::endl;
6}
7int main() {
8std::string long_string = "This is a very long string...";
9print_string(long_string);
10std::vector<std::string> strings = {"hello", "world"};
11for (const auto& s : strings) { // 使用 const auto& 迭代,避免拷贝
12print_string(s);
13}
14return 0;
15}

右值引用的应用
▮▮▮▮⚝ 函数返回值:返回大型对象时,如果对象是临时对象或即将销毁的对象,使用右值引用返回,触发移动语义,避免拷贝。
▮▮▮▮⚝ 移动构造和移动赋值:自定义类型应实现移动构造函数移动赋值运算符,以便在移动语义上下文中高效转移资源。
▮▮▮▮⚝ std::move:显式将左值转换为右值,用于触发移动语义。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <string>
3#include <vector>
4std::string generate_string() { // 返回临时对象
5std::string result = "Generated string";
6return result; // 返回右值,触发移动语义
7}
8int main() {
9std::string str1 = generate_string(); // 移动构造
10std::string str2;
11str2 = generate_string(); // 移动赋值
12std::string str3 = "Original string";
13std::string str4 = std::move(str3); // 显式移动,str3 变为有效但未指定状态
14return 0;
15}

C.5.3 减少不必要的函数调用 (Reducing Unnecessary Function Calls)

函数调用本身有一定的开销,尤其是在频繁调用小函数中,函数调用开销可能占据相当大的比例。优化策略包括:

内联函数 (Inline Functions)
▮▮▮▮⚝ 将短小频繁调用的函数声明为 inline,建议编译器将函数调用替换为函数体本身,减少函数调用开销。
▮▮▮▮⚝ 编译器是否真正内联函数取决于编译器的判断,inline 只是一个建议。
▮▮▮▮⚝ 适用于函数体非常简单,且调用次数很多的函数,例如简单的 getter/setter 函数、数学运算函数等。

宏 (Macros)
▮▮▮▮⚝ 宏也可以实现代码内联,但宏缺乏类型检查作用域限制,容易引入错误,且不利于调试
▮▮▮▮⚝ 宏主要用于条件编译代码生成等场景,不推荐用于函数内联。

循环展开 (Loop Unrolling)
▮▮▮▮⚝ 对于循环次数固定较少的循环,可以手动或由编译器进行循环展开,减少循环控制的开销。
▮▮▮▮⚝ 循环展开可以减少循环判断和跳转指令的执行次数,提高指令流水线的效率。
▮▮▮▮⚝ 编译器优化通常会自动进行循环展开,无需手动展开。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2inline int add(int a, int b) { // 内联函数
3return a + b;
4}
5int main() {
6int sum = 0;
7for (int i = 0; i < 10; ++i) {
8sum += add(i, 1); // 内联函数调用
9}
10std::cout << "Sum: " << sum << std::endl;
11return 0;
12}

C.5.4 性能分析工具的使用 (Using Performance Profiling Tools)

性能分析工具 (performance profiling tools) 是进行性能优化的重要辅助工具。性能分析工具可以测量程序运行时的性能指标,如 CPU 时间、内存使用、函数调用次数、热点代码等,帮助开发者定位性能瓶颈,并针对性地进行优化。

常用性能分析工具
▮▮▮▮⚝ gprof (GNU Profiler):
▮▮▮▮▮▮▮▮⚝ 描述:GCC 自带的性能分析工具,基于采样 (sampling) 技术,收集函数调用次数和执行时间。
▮▮▮▮▮▮▮▮⚝ 优点:简单易用,跨平台。
▮▮▮▮▮▮▮▮⚝ 缺点:采样精度有限,只能分析函数级别的性能,无法分析指令级别的性能。

▮▮▮▮⚝ perf (Performance Counters for Linux):
▮▮▮▮▮▮▮▮⚝ 描述:Linux 系统自带的性能分析工具,基于硬件性能计数器 (hardware performance counters),可以收集 CPU 指令周期、缓存命中率、分支预测失败率等硬件指标。
▮▮▮▮▮▮▮▮⚝ 优点:精度高,可以分析指令级别的性能,支持多种性能事件。
▮▮▮▮▮▮▮▮⚝ 缺点:仅限 Linux 系统,使用相对复杂。

▮▮▮▮⚝ Valgrind (Callgrind):
▮▮▮▮▮▮▮▮⚝ 描述:强大的内存调试和性能分析工具套件,Callgrind 是 Valgrind 的一个组件,用于函数级别的性能分析,基于指令插桩 (instrumentation) 技术。
▮▮▮▮▮▮▮▮⚝ 优点:精度高,可以分析函数调用关系、指令级别的性能,还可以检测内存错误。
▮▮▮▮▮▮▮▮⚝ 缺点:性能开销较大,运行速度较慢。

▮▮▮▮⚝ Intel VTune AmplifierAMD μProf:
▮▮▮▮▮▮▮▮⚝ 描述:商业性能分析工具,功能强大,界面友好,支持多种性能分析方法,包括采样、追踪、硬件性能计数器等。
▮▮▮▮▮▮▮▮⚝ 优点:功能全面,易于使用,性能分析结果可视化。
▮▮▮▮▮▮▮▮⚝ 缺点:商业软件,需要付费。

性能分析流程
▮▮▮▮⚝ 编译程序时启用调试信息 (-g),方便性能分析工具定位代码行数和函数名。
▮▮▮▮⚝ 运行性能分析工具,收集程序运行时的性能数据。
▮▮▮▮⚝ 分析性能报告,识别热点函数 (hot functions)性能瓶颈
▮▮▮▮⚝ 针对性能瓶颈进行优化,例如选择更高效的容器和算法、减少内存分配、避免锁竞争等。
▮▮▮▮⚝ 重复性能分析和优化,直到达到预期的性能目标。

性能分析示例 (使用 gprof)

  1. 编译程序时添加 -pg 选项,启用 gprof 性能分析:
1.双击鼠标左键复制此行;2.单击复制所有代码。
1g++ -pg -O2 my_program.cpp -o my_program
  1. 运行程序
1.双击鼠标左键复制此行;2.单击复制所有代码。
1./my_program

程序运行结束后,会在当前目录下生成 gmon.out 性能数据文件。
3. 使用 gprof 分析性能数据

1.双击鼠标左键复制此行;2.单击复制所有代码。
1gprof ./my_program gmon.out > profile.txt
  1. 查看 profile.txt 性能报告,分析函数调用次数、执行时间、调用关系等信息。

通过系统地进行性能分析和优化,可以显著提高 C++ 标准库代码的性能,编写出更高效、更可靠的程序。

<END_OF_CHAPTER/>

Appendix D: C++ 标准库版本演进 (Evolution of C++ Standard Library Versions)

附录 D 详细列出 C++ 标准库在不同版本 (C++98, C++11, C++14, C++17, C++20) 中的新增特性和改进,方便读者了解标准库的发展历程。

Appendix D.1: C++98 标准库 (C++98 Standard Library)

C++98 标准是 C++ 语言的第一个国际标准,它奠定了现代 C++ 标准库的基础。本节概述了 C++98 标准库的主要组成部分和核心特性。

Appendix D.1.1: 核心组件 (Core Components)

C++98 标准库的核心是标准模板库 (STL),以及对输入/输出 (I/O)、字符串处理、数值计算等基础功能的支持。

标准模板库 (Standard Template Library, STL): STL 是 C++ 标准库的基石,提供了通用的容器 (containers)、算法 (algorithms) 和迭代器 (iterators) 组件,极大地提升了 C++ 的泛型编程能力和代码复用率。
▮▮▮▮ⓑ 容器 (Containers): STL 提供了多种容器,用于存储和组织数据,包括:
▮▮▮▮▮▮▮▮❸ vector (向量): 动态数组,支持快速随机访问。
▮▮▮▮▮▮▮▮❹ deque (双端队列): 双端动态数组,支持快速头部和尾部插入/删除。
▮▮▮▮▮▮▮▮❺ list (列表): 双向链表,支持高效的任意位置插入/删除。
▮▮▮▮▮▮▮▮❻ set (集合): 有序唯一元素集合,基于红黑树实现,支持快速查找。
▮▮▮▮▮▮▮▮❼ multiset (多重集合): 有序可重复元素集合。
▮▮▮▮▮▮▮▮❽ map (映射): 有序键值对集合,基于红黑树实现,支持键的快速查找。
▮▮▮▮▮▮▮▮❾ multimap (多重映射): 有序可重复键值对集合。
▮▮▮▮ⓙ 算法 (Algorithms): STL 提供了大量的通用算法,用于操作容器中的元素,例如:
▮▮▮▮▮▮▮▮❶ 查找算法 (find, binary_search 等)。
▮▮▮▮▮▮▮▮❷ 排序算法 (sort, stable_sort 等)。
▮▮▮▮▮▮▮▮❸ 复制算法 (copy, transform 等)。
▮▮▮▮▮▮▮▮❹ 数值算法 (accumulate, inner_product 等)。
▮▮▮▮ⓞ 迭代器 (Iterators): 迭代器是连接容器和算法的桥梁,提供了统一的访问容器元素的方式,使得算法可以独立于容器类型。STL 定义了五种迭代器类别:输入迭代器 (input iterator)、输出迭代器 (output iterator)、前向迭代器 (forward iterator)、双向迭代器 (bidirectional iterator) 和随机访问迭代器 (random access iterator)。

输入/输出库 (Input/Output Library, iostream): iostream 库提供了用于输入和输出操作的类和对象,支持标准输入输出、文件 I/O 和字符串流等。
▮▮▮▮ⓑ 标准流对象: cin (标准输入), cout (标准输出), cerr (标准错误), clog (标准日志).
▮▮▮▮ⓒ 流类: istream (输入流), ostream (输出流), iostream (输入输出流), ifstream (文件输入流), ofstream (文件输出流), fstream (文件输入输出流), stringstream (字符串流) 等。
▮▮▮▮ⓓ 格式化 I/O: 通过操纵符 (manipulators) 和格式标志 (format flags) 实现格式化输入输出。

字符串库 (String Library): std::string 类提供了方便的字符串操作功能,相比 C 风格字符串,std::string 更加安全易用,并支持动态内存管理。

数值库 (Numerics Library): 提供了数学函数、复数 (complex numbers)、数值数组 (std::valarray) 等数值计算相关的支持。

其他工具: 例如 std::pair (对组), std::auto_ptr (自动指针) (在 C++11 中被废弃) 等实用工具。

Appendix D.1.2: 局限性 (Limitations)

C++98 标准库虽然功能强大,但也存在一些局限性,例如缺乏对多线程、智能指针、哈希容器等现代编程特性的支持,以及在泛型编程方面仍有改进空间。

缺乏多线程支持: C++98 标准库没有提供官方的多线程编程支持,开发者需要依赖平台相关的线程库。

auto_ptr 的缺陷: std::auto_ptr 在所有权转移语义上存在缺陷,容易导致程序错误,因此在后续标准中被废弃,并被更安全的智能指针取代。

性能问题: 某些容器和算法在特定场景下可能存在性能瓶颈,例如 std::map 的键查找效率在数据量较大时可能不如哈希表。

泛型编程的限制: C++98 的模板机制在某些复杂场景下,错误信息不够友好,编译期检查不够严格。

Appendix D.2: C++11 标准库 (C++11 Standard Library)

C++11 标准是对 C++ 语言的一次重大革新,极大地扩展和增强了标准库的功能,引入了诸多现代编程特性,显著提升了 C++ 的开发效率和程序性能。

Appendix D.2.1: 核心增强 (Core Enhancements)

C++11 标准库在容器、算法、并发编程、智能指针、通用工具等方面进行了全面增强。

并发编程支持 (Concurrency Support): C++11 标准库正式引入了多线程编程的支持,提供了以下关键组件:
▮▮▮▮ⓑ 线程库 (Threads Library) <thread>: std::thread 类用于创建和管理线程,支持线程的启动、join (汇合)、detach (分离) 等操作。
▮▮▮▮ⓒ 互斥量 (Mutexes) <mutex>: std::mutex (互斥量), std::recursive_mutex (递归互斥量), std::timed_mutex (定时互斥量) 等互斥量类型,以及 std::lock_guard (锁守护器), std::unique_lock (唯一锁) 等锁管理器,用于保护共享数据,防止数据竞争 (data race)。
▮▮▮▮ⓓ 条件变量 (Condition Variables) <condition_variable>: std::condition_variable 用于线程间的同步和通信,允许线程在特定条件满足时才被唤醒。
▮▮▮▮ⓔ 期物 (Futures) <future>: std::future (期物), std::promise (承诺), std::packaged_task (打包任务) 用于异步操作的结果获取和状态传递。
▮▮▮▮ⓕ 原子操作 (Atomic Operations) <atomic>: std::atomic<T> (原子类型) 提供了原子操作,用于在多线程环境下安全地访问和修改共享变量,避免使用锁的开销。

智能指针 (Smart Pointers) <memory>: C++11 引入了更完善的智能指针,取代了 std::auto_ptr,用于自动内存管理,防止内存泄漏 (memory leak)。
▮▮▮▮ⓑ std::unique_ptr (独占指针): 独占所有权的智能指针,确保同一时间只有一个 unique_ptr 指向特定对象,支持移动语义 (move semantics)。
▮▮▮▮ⓒ std::shared_ptr (共享指针): 共享所有权的智能指针,允许多个 shared_ptr 指向同一对象,通过引用计数 (reference counting) 管理对象生命周期。
▮▮▮▮ⓓ std::weak_ptr (弱指针): 弱引用智能指针,不增加引用计数,用于解决 shared_ptr 可能产生的循环引用 (circular references) 问题。
▮▮▮▮ⓔ std::make_shared (make_shared)std::make_unique (make_unique): 用于安全高效地创建 shared_ptrunique_ptr

容器的增强 (Container Enhancements): C++11 标准库新增了一些有用的容器,并对现有容器进行了改进。
▮▮▮▮ⓑ std::array (数组) <array>: 固定大小数组,是对 C 风格数组的封装,提供了边界检查和容器接口。
▮▮▮▮ⓒ std::forward_list (单向链表) <forward_list>: 单向链表,相比 std::list 节省了内存开销,适用于只需要单向遍历的场景。
▮▮▮▮ⓓ 无序容器 (Unordered Containers) <unordered_map>, <unordered_set>: 基于哈希表 (hash table) 实现的无序容器,包括 std::unordered_map (无序映射), std::unordered_multimap (无序多重映射), std::unordered_set (无序集合), std::unordered_multiset (无序多重集合),提供平均常数时间复杂度的查找、插入和删除操作。
▮▮▮▮ⓔ 容器的移动语义: 所有标准库容器都支持移动语义,通过移动构造函数 (move constructor) 和移动赋值运算符 (move assignment operator),在容器元素转移时避免不必要的拷贝,提高性能。

算法的增强 (Algorithm Enhancements): C++11 标准库在算法方面主要受益于 Lambda 表达式和移动语义,使得算法的使用更加灵活高效。

通用工具 (General Utilities): C++11 引入了一些通用的实用工具,提升了代码的表达能力。
▮▮▮▮ⓑ std::tuple (元组) <tuple>: 固定大小的不同类型元素的集合,可以看作是 std::pair 的推广。
▮▮▮▮ⓒ std::function (函数对象包装器) <functional>: 用于封装各种可调用对象 (callable objects),例如函数指针、函数对象、Lambda 表达式、成员函数指针等。
▮▮▮▮ⓓ std::bind (绑定器) <functional>: 用于函数绑定,可以绑定函数的部分参数,生成新的可调用对象。
▮▮▮▮ⓔ std::chrono (时间库) <chrono>: 提供了时间相关的duration (持续时间), time_point (时间点), clock (时钟) 等组件,用于高精度的时间测量和计算。
▮▮▮▮ⓕ 随机数生成 (Random Number Generation) <random>: 提供了更强大和灵活的随机数生成器和分布方式。
▮▮▮▮ⓖ 类型 traits (Type Traits) <type_traits>: 提供了编译期类型检查和判断的工具,例如 std::is_integral, std::is_class 等。
▮▮▮▮ⓗ 移动语义 (Move Semantics) 与右值引用 (Rvalue References): C++11 引入了移动语义和右值引用,极大地提升了程序性能,特别是在处理临时对象和容器元素转移时。

Lambda 表达式 (Lambda Expressions): C++11 引入 Lambda 表达式,使得在代码中定义匿名函数变得简洁方便,常用于算法的谓词 (predicate) 和函数对象。

noexcept 规范 (noexcept Specifier): noexcept 规范用于声明函数是否会抛出异常,有助于编译器进行优化,并提升异常安全性 (exception safety)。

对齐 (Alignment) 支持: C++11 提供了 alignof (对齐大小), alignas (对齐方式) 关键字,以及 <aligned_storage><aligned_union> 模板,用于控制数据对象的内存对齐方式。

Appendix D.2.2: 意义 (Significance)

C++11 标准库的增强是划时代的,它使得 C++ 能够更好地应对现代软件开发的需求,尤其是在并发编程、高性能计算、泛型编程等方面。

提升开发效率: 智能指针、容器的增强、Lambda 表达式等特性,简化了代码编写,减少了错误,提高了开发效率。

增强程序性能: 移动语义、无序容器、并发编程支持等特性,使得 C++ 程序能够更好地利用硬件资源,提升程序性能。

现代化 C++: C++11 标准库的引入,使得 C++ 语言更加现代化,更具竞争力,吸引了更多开发者。

Appendix D.3: C++14 标准库 (C++14 Standard Library)

C++14 标准是对 C++11 标准的小幅改进和完善,主要目的是修复 C++11 中的一些缺陷,并添加一些小的但实用的新特性,以提升 C++11 的易用性和完整性。

Appendix D.3.1: 主要改进 (Key Improvements)

C++14 标准库的改进相对 C++11 较小,主要集中在 Lambda 表达式、泛型编程、以及一些小的工具函数上。

泛型 Lambda 表达式 (Generic Lambdas): C++14 允许 Lambda 表达式的参数类型使用 auto 关键字,从而使其可以接受任意类型的参数,增强了 Lambda 表达式的泛型能力。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1auto generic_lambda = [](auto x, auto y) { return x + y; };

Lambda 捕获表达式的增强 (Lambda Capture Enhancements): C++14 允许在 Lambda 捕获列表中使用表达式进行捕获,使得 Lambda 表达式的捕获更加灵活。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1int value = 10;
2auto lambda_capture_expression = [v = value + 5]() { return v; };

std::make_unique (make_unique) <memory>: C++14 引入了 std::make_unique 函数,用于安全地创建 std::unique_ptr,解决了 C++11 中 std::make_shared 而无 std::make_unique 的不一致性,并提供了更好的异常安全性。

二进制字面量 (Binary Literals) 和数字分隔符 (Digit Separators): C++14 允许使用二进制字面量 (例如 0b101010) 和数字分隔符 (例如 1'000'000),提高了数值字面量的可读性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1int binary_literal = 0b1010;
2int large_number = 1'000'000;

std::exchange (exchange) <utility>: std::exchange 函数用于原子地用新值替换旧值,并返回旧值,简化了一些常见的交换操作。

std::integer_sequence (整数序列) <utility>: std::integer_sequence 及其辅助模板 std::make_integer_sequencestd::index_sequence,用于在编译期生成整数序列,是元编程 (metaprogramming) 的有力工具。

std::get<Index>(tuple) (元组按索引访问) 的改进: C++14 允许在 std::get<Index>(tuple) 中使用变量模板作为索引,使得元组的按索引访问更加灵活。

constexpr 的放宽限制 (Relaxed constexpr Restrictions): C++14 放宽了 constexpr 函数的限制,允许在 constexpr 函数中使用更复杂的语句,例如局部变量、循环等,使得 constexpr 函数更加实用。

Appendix D.3.2: 意义 (Significance)

C++14 标准库虽然新增特性不多,但都是对 C++11 的重要补充和完善,进一步提升了 C++ 的易用性和表达能力,为后续的 C++17 和 C++20 标准打下了基础。

提升易用性: 泛型 Lambda 表达式、Lambda 捕获表达式的增强、std::make_unique 等特性,使得 C++ 更加易学易用。

增强泛型编程能力: 泛型 Lambda 表达式、std::integer_sequence 等特性,进一步提升了 C++ 的泛型编程和元编程能力。

Appendix D.4: C++17 标准库 (C++17 Standard Library)

C++17 标准是 C++ 语言的又一次重要演进,它引入了许多重要的语言和库特性,旨在简化 C++ 编程,提高代码的效率和可读性,并进一步增强 C++ 在现代软件开发领域的竞争力。

Appendix D.4.1: 关键特性 (Key Features)

C++17 标准库引入了变体类型、可选值、字符串视图、并行算法、文件系统库等重要组件。

std::optional (可选值) <optional>: std::optional<T> 用于表示一个值可能存在也可能不存在的情况,是类型安全的 nullptr 替代品,可以避免空指针异常 (null pointer exception)。

std::variant (变体类型) <variant>: std::variant<T1, T2, ...> 用于表示一个值可以是多种类型中的一种,是类型安全的联合体 (union) 替代品,可以提高代码的类型安全性。

std::any (任意类型) <any>: std::any 用于表示任意类型的值,可以在运行时存储和检索不同类型的数据,但类型安全性较弱,需要谨慎使用。

std::string_view (字符串视图) <string_view>: std::string_view 是字符串的非拥有视图,可以高效地访问字符串数据,避免不必要的字符串拷贝,提高性能。

并行算法 (Parallel Algorithms) <algorithm>: C++17 标准库引入了并行版本的算法,例如 std::for_each, std::transform, std::sort 等,可以通过执行策略 (execution policies) 指定算法的并行执行方式,利用多核处理器提升算法的执行效率。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::vector<int> data = /* ... */;
2std::for_each(std::execution::par, data.begin(), data.end(), /* ... */); // 并行 for_each

执行策略包括:
▮▮▮▮ⓐ std::execution::seq (顺序执行策略): 顺序执行,不并行。
▮▮▮▮ⓑ std::execution::par (并行执行策略): 允许并行执行。
▮▮▮▮ⓒ std::execution::par_unseq (并行且向量化执行策略): 允许并行和向量化执行。

文件系统库 (Filesystem Library) <filesystem>: C++17 标准库正式引入了文件系统库,提供了跨平台的的文件和目录操作功能,例如路径操作、文件和目录的创建、删除、遍历等。

类模板实参推导 (Class Template Argument Deduction, CTAD): C++17 引入了类模板实参推导,允许在创建类模板对象时,编译器自动推导模板参数类型,简化了类模板的使用。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::vector v{1, 2, 3}; // 自动推导为 std::vector<int>
2std::pair p{1, "hello"}; // 自动推导为 std::pair<int, const char*>

折叠表达式 (Fold Expressions): C++17 引入了折叠表达式,简化了对参数包 (parameter pack) 的操作,可以方便地进行参数包的求和、求积、逻辑运算等。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<typename ...Args>
2auto sum(Args... args) {
3return (args + ...); // 右折叠表达式,计算参数包的和
4}

内联变量 (Inline Variables): C++17 允许声明内联变量 (inline variables),使得可以在头文件中定义变量,而不会违反单一定义规则 (One Definition Rule, ODR)。

结构化绑定 (Structured Bindings): C++17 引入了结构化绑定,允许将元组 (tuple)、pair (对组) 或结构体 (struct) 的元素直接绑定到变量,提高了代码的可读性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::pair<int, std::string> p{1, "hello"};
2auto [id, name] = p; // 结构化绑定,id 绑定到 p.first, name 绑定到 p.second

Appendix D.4.2: 意义 (Significance)

C++17 标准库的增强进一步提升了 C++ 的现代化程度,使其在通用编程、高性能计算、系统编程等领域更具优势。

增强代码表达能力: std::optional, std::variant, std::string_view 等特性,使得 C++ 代码更加简洁、安全、高效。

提升程序性能: 并行算法、std::string_view 等特性,有助于提升 C++ 程序的性能,尤其是在多核处理器和大数据处理场景下。

简化文件系统操作: 文件系统库的引入,使得 C++ 具备了跨平台的文件和目录操作能力,增强了 C++ 在系统编程领域的竞争力。

Appendix D.5: C++20 标准库 (C++20 Standard Library)

C++20 标准是 C++ 语言的又一次重大飞跃,引入了 Concepts (概念)、Ranges (范围)、Coroutines (协程)、Modules (模块) 等革命性的语言和库特性,旨在进一步提升 C++ 的抽象能力、代码组织能力和程序性能,使 C++ 成为更加强大和现代化的编程语言。

Appendix D.5.1: 革命性特性 (Revolutionary Features)

C++20 标准库的增强主要围绕 Concepts、Ranges、Coroutines、Modules 等核心特性展开。

Concepts (概念) <concepts>: Concepts 是 C++20 最重要的语言特性之一,它为模板 (templates) 引入了约束 (constraints),可以对模板参数的类型进行编译期检查,提高模板代码的类型安全性,并改进了模板错误信息的可读性。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1template<typename T>
2concept Integral = std::is_integral_v<T>; // 定义一个名为 Integral 的概念
3template<Integral T> // 使用 Integral 概念约束模板参数 T
4T add(T a, T b) {
5return a + b;
6}

Ranges (范围) <ranges>: Ranges 库是 C++20 标准库的又一核心组件,它提供了一种新的处理数据集合的方式,基于迭代器 (iterators) 和视图 (views) 的概念,可以实现更简洁、更高效的数据处理流水线 (data processing pipelines)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1std::vector<int> data = {1, 2, 3, 4, 5, 6};
2auto even_numbers = data | std::views::filter([](int n){ return n % 2 == 0; })
3| std::views::transform([](int n){ return n * n; });
4// even_numbers 是一个范围,表示 data 中偶数的平方
5for (int n : even_numbers) {
6std::cout << n << " "; // 输出 4 16 36
7}

Ranges 库的核心概念包括:
▮▮▮▮ⓐ 视图 (Views): 视图是范围的轻量级代理,不拥有数据,只提供数据的访问方式,可以进行组合和转换,例如 std::views::filter (过滤视图), std::views::transform (转换视图), std::views::take (截取视图) 等。
▮▮▮▮ⓑ 算法 (Algorithms): Ranges 库提供了新的基于范围的算法,例如 std::ranges::for_each, std::ranges::sort, std::ranges::copy 等,可以更方便地操作范围。
▮▮▮▮ⓒ 适配器 (Adapters): Ranges 库提供了范围适配器,用于将已有的迭代器范围转换为 Ranges 视图,例如 std::ranges::subrange (子范围适配器).

Coroutines (协程) <coroutine>: Coroutines 是 C++20 引入的协程支持,允许编写异步 (asynchronous)、非阻塞 (non-blocking) 的代码,可以提高程序的并发性和响应性,特别是在 I/O 密集型 (I/O-bound) 应用中。C++20 协程是基于栈帧 (stack frame) 的无栈协程 (stackless coroutines)。

1.双击鼠标左键复制此行;2.单击复制所有代码。
1#include <iostream>
2#include <coroutine>
3std::coroutine_handle<> printer() {
4std::cout << "Hello, ";
5co_await std::suspend_always{}; // 挂起协程
6std::cout << "world!" << std::endl;
7}
8int main() {
9auto handle = printer();
10handle.resume(); // 恢复协程执行
11handle.resume(); // 再次恢复协程执行
12handle.destroy(); // 销毁协程句柄
13return 0;
14}