C++标准库算法实战指南:从迭代器到并行编程的现代C++高效编程
1. 项目概述:为什么你需要这份实战指南?
如果你正在用C++写代码,却还在手写for循环来查找、排序、拷贝数据,那你可能正在浪费大量时间,并且亲手埋下潜在的Bug。C++标准库算法,这个从C++98时代就存在的“宝藏”,是每一位C++开发者从“会用语言”到“善用语言”的关键分水岭。它不是一堆枯燥的函数签名,而是一套经过千锤百炼、高度优化、且能极大提升代码表达力和执行效率的“瑞士军刀”。
我见过太多项目,代码里充斥着重复、冗长且脆弱的循环逻辑。一个简单的数据过滤,可能就需要十几行代码来处理边界条件和迭代器移动,既容易出错,又难以一眼看懂意图。而标准库算法,如std::find_if、std::copy_if,用一行声明式的代码就能清晰表达“我要做什么”,将“怎么做”的细节完全交给标准库实现。这不仅让代码更简洁、更安全(避免了手写循环的差一错误),更重要的是,它为编译器优化和未来的并行化改造(如C++17的并行算法)铺平了道路。
这份指南不是一份简单的API罗列文档。市面上那样的资料已经很多了。我将从一个有十多年一线开发经验的工程师视角出发,带你穿透函数签名的表象,深入理解每个算法背后的设计哲学、适用场景、性能考量和那些官方手册里不会写的“坑”。我们会从最基础的“只读”和“修改”操作开始,逐步深入到排序、数值计算,并结合最新的C++11/14/17/20特性,探讨如何与现代C++特性(如Lambda表达式、范围for、执行策略)结合,写出既高效又优雅的现代C++代码。无论你是正在准备面试、希望优化现有项目,还是想系统性地提升自己的C++内力,这份实战指南都将为你提供直接的、可落地的解决方案。
2. 核心设计哲学:理解算法与迭代器的共生关系
在深入具体算法之前,我们必须先建立一个核心认知:C++标准库算法是建立在迭代器抽象之上的,它不关心容器的具体类型。这是其设计最精妙也最强大之处。std::vector、std::list、std::deque,甚至原生数组,只要提供了符合要求的迭代器,算法就能工作。这种“泛型”设计,使得算法的复用性达到了极致。
2.1 迭代器类别:算法能力的基石
算法对迭代器有类别要求,这直接决定了算法的能力和效率。理解这一点,是正确选用算法的前提。
- 输入迭代器(InputIterator):只能单向读取,且只能读取一次。典型代表是
std::istream_iterator。像std::find、std::count这类只读算法,通常只要求输入迭代器。 - 输出迭代器(OutputIterator):只能单向写入。典型代表是
std::ostream_iterator。std::copy的目标迭代器就要求是输出迭代器。 - 前向迭代器(ForwardIterator):可以多次读写,并且能单向移动。
std::forward_list的迭代器就是前向迭代器。std::search、std::unique需要前向迭代器,因为它们可能需要多次比较序列中的元素。 - 双向迭代器(BidirectionalIterator):可以双向移动(
++和--)。std::list、std::set的迭代器属于此类。std::reverse算法必须要求双向迭代器,因为它需要从后向前遍历。 - 随机访问迭代器(RandomAccessIterator):可以像指针一样进行算术运算(
+n,-n,[]),能在常数时间内跳转到任意位置。std::vector、std::deque、原生数组的迭代器是随机访问迭代器。std::sort、std::nth_element这类需要随机访问的算法,效率最高。
实操心得:当你对一个算法的时间复杂度有疑问时,先看它对迭代器的要求。要求随机访问迭代器的算法(如
std::sort)通常时间复杂度优于O(n²),而只要求前向迭代器的算法(如std::list::sort成员函数)可能效率较低。对于std::list,直接使用其成员函数sort()通常比用std::sort更合适,因为后者无法工作(std::list的迭代器不是随机访问的)。
2.2 算法命名约定:_if,_copy,_n的奥秘
标准库算法的命名有一套清晰的约定,理解它能让你快速推断出新算法的功能。
_if:表示该算法接受一个谓词(Predicate)函数(或函数对象、Lambda),而不是一个具体的值。例如,std::find找值,std::find_if找满足条件的元素。std::vector<int> vec = {1, 4, 2, 8, 5}; auto it = std::find(vec.begin(), vec.end(), 4); // 找值为4的元素 auto it2 = std::find_if(vec.begin(), vec.end(), [](int x){ return x > 5; }); // 找第一个大于5的元素_copy:表示该算法不会修改原始序列,而是将结果输出到另一个由迭代器指定的目标位置。你需要确保目标区间有足够空间。例如,std::remove会“移除”元素(实际是移动),而std::remove_copy则保持原序列不变,将“移除”后的结果拷贝到新位置。std::vector<int> src = {1, 2, 3, 2, 4}; std::vector<int> dst; dst.resize(5); // 必须预先分配空间! auto new_end = std::remove_copy(src.begin(), src.end(), dst.begin(), 2); // src 仍然是 {1, 2, 3, 2, 4} // dst 的前三个元素是 {1, 3, 4},new_end指向dst[3]_n:表示该算法操作的是“数量”而非“区间”。例如,std::fill_n(begin, 5, 0)将从begin开始填充5个0。
掌握这些约定,你就能举一反三,面对数十个算法也不会感到混乱。
3. 只读算法实战:查询与检查的艺术
只读算法是使用最频繁的一类,它们不修改容器内容,主要用于数据查询和检查。用好它们,能让你告别手写循环。
3.1 查找算法:find家族深度解析
std::find是最基础的查找,但其兄弟std::find_if和std::find_if_not(C++11)才是实战中的主力。
核心要点:
- 返回值:返回指向第一个匹配元素的迭代器,如果未找到,则返回第二个参数(通常是
end())。永远不要直接使用返回的迭代器而不检查! - 复杂度:线性时间O(n)。对于已排序的区间,应使用
std::lower_bound(对数时间O(log n))。
实战场景与避坑: 场景:在一个员工列表中查找第一个工资超过10000的员工。
struct Employee { std::string name; int salary; }; std::vector<Employee> employees = {{"Alice", 8000}, {"Bob", 12000}, {"Charlie", 9500}}; // 传统循环(易出错,不推荐) auto it = employees.begin(); for(; it != employees.end(); ++it) { if (it->salary > 10000) break; } if (it != employees.end()) { /* 找到 */ } // 使用 std::find_if + Lambda (清晰、安全) auto it = std::find_if(employees.begin(), employees.end(), [](const Employee& emp){ return emp.salary > 10000; }); if (it != employees.end()) { std::cout << "Found: " << it->name << std::endl; }注意事项:Lambda捕获。如果谓词需要外部的比较值,注意按值(
[=])或按引用([&])捕获。对于简单类型,按值捕获更安全,避免悬垂引用。对于要在多个地方复用的谓词,可以考虑定义为函数对象(仿函数)或使用std::bind(但Lambda通常更简洁)。
3.2 计数与极值:count,minmax_element
std::count和std::count_if用于计数。std::min_element和std::max_element返回最小/最大元素的迭代器。C++11引入了std::minmax_element,一次调用同时获取最小和最大值,效率高于分别调用两次。
性能对比:
std::vector<int> data = {5, 2, 8, 1, 9, 2}; // 低效做法(遍历两次) auto minIt = std::min_element(data.begin(), data.end()); auto maxIt = std::max_element(data.begin(), data.end()); // 高效做法(遍历一次) auto [minIt, maxIt] = std::minmax_element(data.begin(), data.end()); // C++17 结构化绑定对于大型数据集,一次遍历和两次遍历的性能差异是显著的。
3.3 区间比较与搜索:equal,search,mismatch
std::equal:判断两个区间是否相等。重要陷阱:它默认不检查第二个区间是否足够长!如果第二个区间比第一个短,且前面部分相等,它会访问越界内存,导致未定义行为。安全的做法是使用C++14引入的四参数版本,或先比较大小。std::vector<int> v1 = {1, 2, 3}; std::vector<int> v2 = {1, 2}; // 危险!可能崩溃 bool unsafe = std::equal(v1.begin(), v1.end(), v2.begin()); // 安全做法 (C++14) bool safe = std::equal(v1.begin(), v1.end(), v2.begin(), v2.end()); // 或先比较大小 bool safe_old = (v1.size() == v2.size()) && std::equal(v1.begin(), v1.end(), v2.begin());std::search:在序列A中查找序列B第一次出现的位置(子串匹配)。它的实现通常是朴素的字符串匹配算法,对于性能敏感的场景(如长文本搜索),可能需要更专业的算法(如Boyer-Moore),但std::search的通用性无可替代。std::mismatch:返回两个序列中第一对不相等元素的位置。常用于比较两个版本的数据差异。
4. 可变序列算法:安全高效地修改数据
这类算法会修改容器内容。理解它们的行为,特别是关于迭代器失效和空间分配的规则,至关重要。
4.1 拷贝与变换:copy,transform,move(C++11)
std::copy:基础拷贝。确保目标区间有足够空间是调用者的责任。对于C风格数组,std::copy比循环更安全。int src[] = {1, 2, 3, 4, 5}; int dst[5]; std::copy(std::begin(src), std::end(src), std::begin(dst)); // 安全,避免手动计算大小std::copy_if(C++11):带条件的拷贝。常与std::back_inserter配合,动态扩展容器。std::vector<int> src = {1, -2, 3, -4, 5}; std::vector<int> dst; std::copy_if(src.begin(), src.end(), std::back_inserter(dst), [](int x){ return x > 0; }); // dst 现在为 {1, 3, 5},无需预先分配大小std::transform:对每个元素应用一个函数,并将结果写入目标区间。它是“映射”(Map)操作的实现。可以是一元或二元操作。std::vector<int> vec = {1, 2, 3}; std::vector<int> squared; squared.resize(vec.size()); std::transform(vec.begin(), vec.end(), squared.begin(), [](int x){ return x * x; }); // 一元变换 // squared: {1, 4, 9} std::vector<int> a = {1, 2, 3}; std::vector<int> b = {4, 5, 6}; std::vector<int> sum; sum.resize(a.size()); std::transform(a.begin(), a.end(), b.begin(), sum.begin(), std::plus<int>()); // 二元变换,使用标准函数对象 // sum: {5, 7, 9}std::move(C++11):将元素从一个区间“移动”到另一个区间。对于像std::string或std::vector这样持有资源的对象,移动比拷贝高效得多,因为它转移资源所有权而非复制内容。std::vector<std::string> oldVec = {"hello", "world"}; std::vector<std::string> newVec; newVec.reserve(oldVec.size()); std::move(oldVec.begin(), oldVec.end(), std::back_inserter(newVec)); // 此后,oldVec中的字符串处于有效但未指定的状态(通常为空)
4.2 填充与生成:fill,generate,iota(C++11)
std::fill/std::fill_n:用指定值填充区间。std::generate/std::generate_n:用生成器函数的结果填充区间。常用于初始化随机数或序列号。std::vector<int> randomNumbers(10); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1, 100); std::generate(randomNumbers.begin(), randomNumbers.end(), [&](){ return dis(gen); });std::iota(C++11):用连续递增的值填充区间。非常方便。std::vector<int> seq(10); std::iota(seq.begin(), seq.end(), 0); // seq: 0, 1, 2, ..., 9
4.3 “移除”与“去重”:理解remove和unique的陷阱
这是可变序列算法中最容易误解的两个算法。
std::remove和std::remove_if: 它们并不真正从容器中删除元素!其工作流程是:遍历区间,将所有不满足移除条件的元素,向前移动覆盖掉那些满足条件的元素。算法返回一个指向新的“逻辑终点”的迭代器。从该迭代器到原end()之间的元素,其值处于未指定状态(但依然存在)。要真正删除元素,必须结合容器的erase方法。这就是著名的“Erase-Remove”惯用法。
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5}; // 移除所有值为2的元素 auto new_end = std::remove(vec.begin(), vec.end(), 2); // 此时 vec 的内容可能是 {1, 3, 4, 5, ?, ?, ?}, new_end指向第二个'?'的位置 // 真正删除 vec.erase(new_end, vec.end()); // vec 现在为 {1, 3, 4, 5}对于std::list,它有成员函数remove和remove_if,它们会真正删除元素,效率更高。
std::unique: 移除相邻的重复元素。同样,它不改变容器大小,只是将不重复的元素移到前面,并返回新的逻辑终点。通常与std::sort联用,以移除所有重复项。
std::vector<int> vec = {1, 2, 2, 3, 3, 3, 4, 2}; // 注意最后的2 std::sort(vec.begin(), vec.end()); // 先排序: {1, 2, 2, 2, 3, 3, 3, 4} auto last = std::unique(vec.begin(), vec.end()); // 移除相邻重复: {1, 2, 3, 4, ?, ?, ?, ?} vec.erase(last, vec.end()); // 真正删除: {1, 2, 3, 4}核心避坑指南:
- 永远记住:
std::remove和std::unique不改变容器物理大小,必须配合erase。std::unique只处理相邻重复,全局去重必须先sort。- 对于关联容器(
std::set,std::map),它们本身元素唯一,无需使用这些算法。直接使用erase成员函数删除特定值或条件。
5. 排序与分区算法:掌控数据的秩序
排序是算法中的核心操作。C++标准库提供了多种排序和相关算法,以满足不同需求。
5.1 基础排序:sort,stable_sort,partial_sort
std::sort:默认使用<运算符进行升序排序。它不保证相等元素的原始顺序(即非稳定排序)。实现通常是内省排序(IntroSort),是快速排序、堆排序和插入排序的混合,平均和 worst-case 时间复杂度都是O(N log N)。std::sort(vec.begin(), vec.end()); // 升序 std::sort(vec.begin(), vec.end(), std::greater<int>()); // 降序std::stable_sort:稳定排序,保证相等元素的相对顺序不变。当元素顺序有额外意义时使用(例如,先按分数排序,再按姓名排序,希望同分者保持姓名顺序)。通常基于归并排序,需要额外内存。std::partial_sort:部分排序。将区间中最小的前N个元素放到前面并排序,其余元素顺序不定。当你只需要前K个最小(或最大)元素时,它比完全排序快得多。std::vector<int> vec = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3}; // 找出最小的3个元素,并放在vec的前三位 std::partial_sort(vec.begin(), vec.begin() + 3, vec.end()); // vec 前三位是 {0, 1, 2},顺序已排好,后面元素顺序未定义std::nth_element:更极致的部分排序。它重新排列区间,使得第n个位置的元素(假设区间已排序)就位,并且它左边的元素都不大于它,右边的元素都不小于它。但它不保证左右两边的内部顺序。常用于找中位数、第K大/小的元素。std::vector<int> vec = {5, 7, 4, 2, 8, 6, 1, 9, 0, 3}; auto mid = vec.begin() + vec.size()/2; std::nth_element(vec.begin(), mid, vec.end()); std::cout << "中位数是: " << *mid << std::endl; // 输出可能是4或5
5.2 分区与划分:partition,stable_partition
分区算法根据谓词将区间重新排列,使所有使谓词为真的元素排在使谓词为假的元素之前。std::partition不保证两部分内部的原始顺序,而std::stable_partition保证。
典型应用:快速排序的partition步骤,或者将满足某个条件的元素集中到一起处理。
std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9}; auto bound = std::partition(vec.begin(), vec.end(), [](int x){ return x % 2 == 0; }); // vec 可能变为 {2, 4, 6, 8, 1, 3, 5, 7, 9},bound指向1 // 现在 [begin, bound) 是所有偶数,[bound, end) 是所有奇数C++11引入了std::partition_copy,它不修改原序列,而是将结果分别拷贝到两个输出区间,非常实用。
5.3 堆操作:make_heap,push_heap,pop_heap,sort_heap
标准库将堆(默认最大堆)也实现为一系列算法,作用于随机访问迭代器上。它们不管理内存,只调整元素顺序。
std::make_heap:将区间组织成堆结构。std::push_heap:假设区间[begin, end-1)已是堆,将*(end-1)元素加入堆中。std::pop_heap:将堆顶元素(最大值)移动到区间末尾,并重新调整剩余区间为堆。std::sort_heap:将一个堆区间排序成升序序列。
手动实现优先队列:
std::vector<int> heap; // 插入 heap.push_back(new_value); std::push_heap(heap.begin(), heap.end()); // 获取最大 int max = heap.front(); // 删除最大 std::pop_heap(heap.begin(), heap.end()); heap.pop_back();6. 数值算法与C++17并行算法
6.1 数值算法:accumulate,inner_product,adjacent_difference
这些算法定义在<numeric>头文件中。
std::accumulate:累加(或更广义的“折叠”)。经典用法是求和,但通过提供自定义二元操作,可以实现乘积、字符串连接等。std::vector<int> vec = {1, 2, 3, 4, 5}; int sum = std::accumulate(vec.begin(), vec.end(), 0); // 初始值0,求和 int product = std::accumulate(vec.begin(), vec.end(), 1, std::multiplies<int>()); // 初始值1,求积 std::vector<std::string> strs = {"Hello", " ", "World"}; std::string concat = std::accumulate(strs.begin(), strs.end(), std::string("")); // 字符串连接std::inner_product:计算两个序列的内积(点积),也可用于实现其他归约操作。std::adjacent_difference:计算相邻元素的差,可用于近似微分。第一个元素是原序列的第一个元素。std::partial_sum:计算前缀和。
6.2 C++17 并行算法:性能的飞跃
C++17最大的亮点之一就是为大多数标准库算法增加了并行版本。通过指定执行策略(Execution Policy),可以轻松利用多核CPU。
三种执行策略:
std::execution::seq:顺序执行(默认,同C++17前)。std::execution::par:并行执行(可能在不同线程)。std::execution::par_unseq:并行且向量化执行(可能使用SIMD指令)。
使用示例:
#include <execution> // 需要包含此头文件 #include <vector> #include <algorithm> int main() { std::vector<int> data(1000000); std::iota(data.begin(), data.end(), 0); // 顺序排序 std::sort(std::execution::seq, data.begin(), data.end()); // 并行排序 std::sort(std::execution::par, data.begin(), data.end()); // 并行且可能向量化的变换 std::transform(std::execution::par_unseq, data.begin(), data.end(), data.begin(), [](int x){ return x * x; }); return 0; }重要注意事项:
- 线程安全:并行算法要求你提供的函数对象(如Lambda)是线程安全的。避免在内部修改共享状态(除非使用互斥锁)。
- 异常安全:如果使用并行策略,且操作抛出异常,行为是调用
std::terminate。顺序策略则传播异常。- 性能并非绝对:对于小数据集,线程创建和调度的开销可能抵消并行收益。通常数据量越大,并行收益越明显。
- 算法限制:并非所有算法都支持并行。例如,
std::accumulate的并行版本是std::reduce,因为累加的顺序在并行下无法保证。
7. 现代C++风格融合:Lambda、范围库与概念
7.1 与Lambda表达式共舞
Lambda是算法的最佳搭档,它让自定义操作变得极其简洁。掌握Lambda的捕获列表和返回类型推导是关键。
std::vector<Person> people; // 按年龄排序 std::sort(people.begin(), people.end(), [](const Person& a, const Person& b) { return a.age < b.age; }); // 查找第一个名字以'A'开头的人 auto it = std::find_if(people.begin(), people.end(), [](const Person& p) { return !p.name.empty() && p.name[0] == 'A'; }); // 复杂的捕获:计算平均年龄,然后找出年龄大于平均年龄的人 double totalAge = std::accumulate(people.begin(), people.end(), 0.0, [](double sum, const Person& p){ return sum + p.age; }); double avgAge = totalAge / people.size(); std::vector<Person> aboveAverage; std::copy_if(people.begin(), people.end(), std::back_inserter(aboveAverage), [avgAge](const Person& p){ return p.age > avgAge; }); // 按值捕获avgAge7.2 C++20 范围库:告别迭代器对
C++20引入的范围库(Ranges Library)是对算法库的一次革命性升级。它允许我们直接对容器或视图进行操作,代码更简洁、更易读。
// 传统方式 std::vector<int> result; std::copy_if(vec.begin(), vec.end(), std::back_inserter(result), [](int x){ return x % 2 == 0; }); std::sort(result.begin(), result.end()); // C++20 范围库方式 #include <ranges> namespace views = std::views; auto result = vec | views::filter([](int x){ return x % 2 == 0; }) | views::transform([](int x){ return x * x; }) | ranges::to<std::vector>(); // C++23 的 ranges::to 更便捷 // 或者直接排序 std::vector<int> sorted_evens = vec | views::filter([](int x){ return x % 2 == 0; }) | ranges::to<std::vector>(); ranges::sort(sorted_evens);管道操作符|让数据流清晰可见。范围库还提供了懒求值视图(如filter_view,transform_view),只有在需要时才计算,提升了性能。
7.3 自定义函数对象与std::bind
虽然Lambda是主流,但了解函数对象和std::bind仍有必要,尤其是在需要存储状态或与旧代码交互时。
- 函数对象(仿函数):重载了
operator()的类。可以拥有状态,比函数指针更高效(通常可内联)。struct GreaterThan { int threshold; GreaterThan(int t) : threshold(t) {} bool operator()(int x) const { return x > threshold; } }; std::vector<int> vec = {1, 5, 3, 8, 2}; GreaterThan gt(4); int count = std::count_if(vec.begin(), vec.end(), gt); // 统计大于4的元素 std::bind:用于部分应用和参数重排序。但在C++11后,Lambda几乎总是更好的选择,因为它更清晰、类型安全且可能更高效。using namespace std::placeholders; bool is_divisible(int a, int b) { return a % b == 0; } auto is_divisible_by_3 = std::bind(is_divisible, _1, 3); // 等价于 Lambda: [](int a){ return a % 3 == 0; }
8. 常见问题、性能陷阱与调试技巧
8.1 迭代器失效问题
这是使用算法(尤其是可变序列算法)时最常见的坑。当容器发生内存重分配(如vector的push_back导致扩容)或元素被插入/删除(对于vector,deque)时,指向该容器的迭代器、指针和引用可能会失效。
黄金法则:在调用可能修改容器结构的操作(如insert,erase,resize)后,不要使用之前保存的迭代器,除非该操作明确返回了新的有效迭代器(如erase返回被删除元素之后元素的迭代器)。
典型错误:
std::vector<int> vec = {1, 2, 3, 4, 5}; auto it = std::find(vec.begin(), vec.end(), 3); if (it != vec.end()) { vec.erase(it); // it 在erase后失效 // 错误!使用失效的迭代器 std::cout << *it << std::endl; // 未定义行为! } // 正确做法:使用erase的返回值更新迭代器,或者避免在删除后使用它。对于std::remove+erase惯用法,remove返回的迭代器是安全的,可以用于erase。
8.2 谓词的副作用与状态
传递给算法的谓词(函数、Lambda、函数对象)应该是“纯函数”,即输出只依赖于输入,没有副作用。特别是对于并行算法,有副作用的谓词会导致数据竞争和未定义行为。
错误示例:
int call_count = 0; std::vector<int> vec = {1, 2, 3, 4, 5}; // 错误的谓词:有副作用(修改外部变量) auto bad_predicate = [&call_count](int x) { ++call_count; return x > 2; }; std::partition(vec.begin(), vec.end(), bad_predicate); // call_count 的值在并行执行下是不确定的。8.3 性能优化小贴士
- 预分配内存:对于
std::copy,std::transform等输出到容器的算法,如果知道结果大小,先用reserve预分配内存,避免多次重分配。 - 选择正确的算法:需要稳定排序用
stable_sort;只需要前N个元素用partial_sort或nth_element;已排序区间查找用binary_search,lower_bound。 - 使用移动语义:对于存有资源的对象(如
std::string,std::vector),在算法中考虑使用std::move_iterator或接受移动操作的算法变体(如std::move算法)。 - 考虑缓存友好性:对
std::vector等连续内存容器的算法通常比std::list等节点式容器快得多,因为缓存命中率高。在性能关键路径上,谨慎选择容器类型。 - 测量,而不是猜测:使用性能分析工具(如perf, VTune)来定位热点。有时一个手写的、经过良好优化的循环可能比通用算法更快,但这属于极端情况,需要数据支撑。
8.4 调试与可视化
对于复杂的算法链,中间状态难以观察。可以编写一个简单的“调试迭代器”或使用std::for_each打印中间结果。
std::vector<int> vec = {5, 1, 4, 2, 3}; // 打印排序前 std::cout << "Before sort: "; std::for_each(vec.begin(), vec.end(), [](int x){ std::cout << x << ' '; }); std::cout << std::endl; std::sort(vec.begin(), vec.end()); // 打印排序后 std::cout << "After sort: "; std::for_each(vec.begin(), vec.end(), [](int x){ std::cout << x << ' '; }); std::cout << std::endl;对于更复杂的自定义类型,确保为其定义了正确的比较运算符(operator<,operator==等),否则排序和查找算法将无法工作。
掌握C++标准库算法,本质上是掌握一种更高级、更声明式的编程思维。它强迫你将问题抽象为对数据区间的操作,从而写出更清晰、更健壮、更高效的代码。从今天开始,尝试在下一个for循环前停下来思考一下:“标准库里有现成的算法可以替代它吗?” 你会发现,你的C++代码将因此焕然一新。
