☰
C++ STL中的常用遍历算法分享
2026/10/21 22:30:22 网站建设 项目流程

前言


提到「遍历容器」,很多人第一反应是范围 for 循环或者下标循环。这当然没错,但 STL 在<algorithm>和<numeric>里提供了一组表达力更强的算法:std::for_each、std::transform、std::copy_if、std::count_if、std::any_of、std::accumulate等。它们的价值不只是「少写几行循环」,而是把「遍历 + 做什么」拆成两个独立的部分,让意图更清楚,也更容易组合和复用。


常见的误解有四个。第一,认为算法一定比手写循环快——是否更快取决于是否内联、是否发生额外拷贝、数据是否命中缓存,不能一概而论,应该测量而不是猜。第二,认为std::remove_if会删除元素——它只是把要保留的元素前移并返回新的逻辑末尾,容器大小不变,必须配合erase。第三,认为std::accumulate的初始值类型无关紧要——初始值决定了累加过程中使用的类型,用0累加double会在每一步做整数运算。第四,认为for_each里可以随意修改容器结构——遍历期间增删元素会导致迭代器失效,属于未定义行为。


本文按「遍历变换」「查找统计」「归约删除」三类介绍常用算法,然后讨论执行策略与性能注意事项。代码以 C++17 为基准;涉及并行算法的地方会注明标准版本和实现支持差异。本机没有 C++ 工具链,所有示例只做标准语义推演。


一、遍历与变换:for_each、transform、copy


std::for_each(first, last, f)对区间内每个元素调用f,返回传入的可调用对象(C++11 起)。它的典型用途是「对每个元素做一件有副作用的事」,例如打印、累加到外部变量、调用成员函数。f可以按值接收元素,也可以按引用接收;如果希望修改元素,参数应写成T&:


// 适用于 C++17
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> values{1, 2, 3, 4, 5};

std::for_each(values.begin(), values.end(), [](int& x) {
x *= 2; // 通过引用修改元素
});

std::for_each(values.begin(), values.end(), [](int x) {
std::cout << x << ' ';
});
std::cout << '\n';
}

std::transform则是「把每个元素映射成新值」。一元版本接收输入区间、输出起始迭代器和变换函数,返回输出区间末尾的后一个位置;二元版本从两个输入区间各取一个元素做运算。输出可以写入另一个容器,也可以写入std::back_inserter追加:


// 适用于 C++17
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <string>
#include <vector>

int main() {
std::vector<int> values{1, 2, 3, 4, 5};
std::vector<int> squares;
squares.reserve(values.size());

std::transform(values.begin(), values.end(),
std::back_inserter(squares),
[](int x) { return x * x; });

std::vector<std::string> names{"alpha", "beta"};
std::vector<std::size_t> lengths;
lengths.reserve(names.size());
std::transform(names.begin(), names.end(),
std::back_inserter(lengths),
[](const std::string& s) { return s.size(); });
}

std::copy和std::copy_if负责复制。copy复制整个区间,copy_if只复制满足谓词的元素。两者都返回输出区间中最后一个被写入元素之后的位置,便于继续链式写入。使用back_inserter时,目标容器会自动扩容;如果目标容器已经预留了足够空间,也可以直接传dest.begin()。


// 适用于 C++17
#include <algorithm>
#include <iterator>
#include <vector>

int main() {
std::vector<int> source{1, 2, 3, 4, 5, 6};
std::vector<int> evens;

std::copy_if(source.begin(), source.end(),
std::back_inserter(evens),
[](int x) { return x % 2 == 0; });

std::vector<int> copied;
copied.resize(source.size());
auto end = std::copy(source.begin(), source.end(), copied.begin());
copied.erase(end, copied.end()); // 按实际复制数量收缩
}

需要特别注意重叠区间:std::copy要求目标起点不在源区间内,否则行为未定义;目标在源之后(往右复制)时应使用std::copy_backward,或者干脆先复制到临时容器。transform也不要把输出写回与输入重叠的位置,除非你非常清楚该算法的要求;稳妥做法是使用独立的目标容器。


二、查找、计数与判定


查找类算法的共同点是「找到就停」,因此比手写完整遍历更早结束。std::find按值查找,std::find_if按谓词查找,找不到时返回last。拿到返回值后必须先与end()比较,再解引用:


// 适用于 C++17
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> values{3, 7, 11, 14, 18};

auto it = std::find_if(values.begin(), values.end(),
[](int x) { return x % 2 == 0; });
if (it != values.end()) {
std::cout << "first even: " << *it << '\n';
} else {
std::cout << "not found\n";
}
}

计数与判定算法包括:



  • std::count(first, last, value):统计等于某值的元素个数。

  • std::count_if(first, last, pred):统计满足谓词的元素个数。

  • std::any_of(first, last, pred):是否存在满足谓词的元素。

  • std::all_of(first, last, pred):是否所有元素都满足谓词;空区间返回true。

  • std::none_of(first, last, pred):是否没有元素满足谓词;空区间返回true。


这些算法都只遍历一次,any_of和none_of在找到反例后立即返回,适合做前置条件检查。注意空区间的语义:all_of和none_of都返回true,这是「真空真」的约定,写业务判断时要考虑空集合是否符合预期。


// 适用于 C++17
#include <algorithm>
#include <vector>

int main() {
std::vector<int> values{2, 4, 6, 8};

bool all_even = std::all_of(values.begin(), values.end(),
[](int x) { return x % 2 == 0; });
bool has_negative = std::any_of(values.begin(), values.end(),
[](int x) { return x < 0; });
auto even_count = std::count_if(values.begin(), values.end(),
[](int x) { return x % 2 == 0; });
// all_even 为 true,has_negative 为 false,even_count 为 4
}

如果区间已经排序,查找可以改用std::lower_bound、std::upper_bound或std::binary_search,复杂度从线性降到对数。但前提是排序确实成立;对无序区间使用二分查找,结果没有意义。std::find本身不要求有序,适用范围更广。


三、归约与删除:accumulate、remove_if


std::accumulate定义在<numeric>中,做的是左折叠:从初始值开始,依次把每个元素与当前结果交给二元操作。默认操作是+,也可以传入自定义函数:


// 适用于 C++17
#include <numeric>
#include <string>
#include <vector>

int main() {
std::vector<int> values{1, 2, 3, 4, 5};

int sum = std::accumulate(values.begin(), values.end(), 0);
double avg = std::accumulate(values.begin(), values.end(), 0.0)
/ static_cast<double>(values.size());

std::vector<std::string> words{"a", "b", "c"};
std::string joined = std::accumulate(
words.begin(), words.end(), std::string{},
[](const std::string& acc, const std::string& w) {
return acc + w;
});
}

初始值0的类型是int,因此第一个例子的累加在int中进行;第二个例子用0.0,累加在double中进行。如果把0用于double容器,每一步都会把中间结果转回int,小数部分被截断,结果与预期不同。这是accumulate最常见的坑。字符串拼接则要用std::string{}作为初始值,不能传"",否则模板参数会推导成const char*,+的语义完全不对。


std::remove和std::remove_if的名字容易误导:它们不删除元素,也不改变容器大小,只是把不需要删除的元素依次前移,并返回新的逻辑末尾。要真正缩短容器,必须使用 erase-remove 惯用法:


// 适用于 C++17
#include <algorithm>
#include <vector>

int main() {
std::vector<int> values{1, 2, 3, 4, 5, 6};

values.erase(
std::remove_if(values.begin(), values.end(),
[](int x) { return x % 2 == 0; }),
values.end());
}

remove_if返回的迭代器指向最后一个保留元素之后的位置,erase从该位置删到end()。对std::vector来说,删除尾部元素的代价与删除数量相关;对std::list或std::forward_list,应优先使用容器自己的remove/remove_if成员函数,它们直接调整链表指针,不需要先搬运元素。


适用于所有序列容器的erase_if直到 C++20 才加入标准(std::erase_if),C++17 及以前仍需手写 erase-remove。若使用 C++20,可以直接写std::erase_if(values, pred);,但需要确认标准库实现已支持。


四、执行策略与性能注意


C++17 为许多算法增加了执行策略重载,定义在<execution>中:


// 适用于 C++17(并行算法需标准库支持)
#include <algorithm>
#include <execution>
#include <vector>

int main() {
std::vector<int> values{1, 2, 3, 4, 5};
std::for_each(std::execution::par,
values.begin(), values.end(),
[](int& x) { x += 1; });
}

并行算法是 C++17 引入的,但很多标准库实现到较晚版本才提供,且通常需要额外链接并行运行时(例如 GCC 的 libstdc++ 需要 TBB)。使用前应确认所用标准库版本与链接配置。使用执行策略时要注意:



  • 传给算法的可调用对象可能被多个线程同时调用,不能有无保护的共享可变状态。

  • 标准执行策略下,若元素访问函数抛出异常,标准要求调用std::terminate,异常不会正常传播。

  • 并行版本不保证执行顺序,for_each的并行重载返回void,不能依赖返回值。

  • 对小容器,线程调度开销可能超过收益;是否启用应以测量为准。


不使用执行策略时,算法通常是顺序执行的。std::for_each按从头到尾的顺序应用函数;std::accumulate严格从左到右折叠;std::reduce(C++17)则允许任意顺序合并,因此要求操作满足结合律和交换律,浮点加法在严格意义上不满足结合律,使用时要想清楚。


性能方面,算法本身通常会被内联展开成与手写循环相近的代码,尤其是 lambda 作为模板参数传入时。真正的开销往往来自:输出容器没有预留空间导致反复扩容、按值传递大对象产生拷贝、谓词里有隐藏的分配或锁、以及缓存不友好地跳跃访问。优化应从这些地方入手,并用std::chrono::steady_clock或性能分析工具测量,不要凭「算法一定更快」的直觉下结论。


常见坑点



  1. ❌ 调用std::remove_if后就认为元素已经删除。


✅ 它只返回新的逻辑末尾,必须再erase(new_end, end())才真正缩短容器。



  1. ❌ 用std::accumulate(v.begin(), v.end(), 0)累加double。


✅ 初始值决定累加类型,应写0.0或T{},避免中间结果被截断。



  1. ❌ 字符串拼接时把""作为accumulate的初始值。


✅ 用std::string{},否则模板参数会推导为const char*。



  1. ❌ 在for_each的回调里push_back或erase当前容器。


✅ 遍历期间改变容器结构会使迭代器失效;先收集操作,遍历结束后再修改。



  1. ❌ 对无序区间使用std::binary_search。


✅ 二分查找要求区间已按相同比较规则排序,否则结果无意义。



  1. ❌ 解引用find的返回值前不检查是否等于end()。


✅ 找不到时返回last,解引用end()是未定义行为。



  1. ❌ 在copy中让目标区间与源区间重叠。


✅ 目标起点不能在源区间内;目标在源之后时用copy_backward,或先复制到临时容器。



  1. ❌ 认为并行算法一定更快,或在线程间共享无保护的计数器。


✅ 小数据量下调度开销可能更大;并行回调必须避免数据竞争。


总结




需求常用算法头文件关键点



对每个元素执行操作for_each、for_each_n<algorithm>按引用接收才能修改

映射为新值transform<algorithm>返回输出区间末尾

按条件复制copy_if<algorithm>配合back_inserter追加

查找find、find_if<algorithm>找不到返回last

统计/判定count_if、any_of、all_of<algorithm>空区间语义要留意

归约accumulate、reduce<numeric>初始值类型决定运算类型

条件删除remove_if+erase<algorithm>单独remove_if不改变大小



STL 遍历算法的核心价值是「用名字表达意图」:读者看到count_if就知道是在统计,看到transform就知道是在映射,不必逐行分析循环体。使用时要记住三条底线:删除必须配erase、归约的初始值决定类型、遍历期间不要改变容器结构。把这三条守住,算法就能成为比手写循环更清晰、也更容易维护的工具。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询