
之前的文章中我们探讨了STL的各类容器(如vector、list、set)以及迭代器的核心概念。今天,我们将聚焦于STL三大核心组件的最后一环——算法,特别是其中最基础且频繁使用的遍历算法。
在STL框架中,算法被设计为一组模板化的通用函数,它们封装了如遍历、排序、查找、修改等常见操作逻辑。其最大优势在于“通用性”:算法并不依赖于具体容器的内部实现,只要容器能够提供符合要求的迭代器,算法便能无缝工作。这完美体现了泛型编程的思想。例如,排序算法sort既可用于vector,也可用于deque,甚至是通过指针模拟迭代器的普通数组。
STL算法与容器、迭代器的协同关系
在深入遍历算法之前,理解容器、迭代器与算法三者如何协同工作是关键。它们共同构成了STL的骨架,其关系如下图所示:

- 容器:负责数据的存储与管理,是存放数据的“仓库”。
- 迭代器:作为连接容器与算法的“桥梁”,它抽象并统一了访问容器元素的方式,无论底层是连续内存(如
vector)还是链式结构(如list)。
- 算法:通过迭代器接口操作容器中的数据,实现各种数据处理逻辑,与具体的容器类型解耦。
简单比喻:容器是“盒子”,迭代器是“盒子的把手”,而算法则是通过“把手”来操作“盒子”里物品的“手”。
STL算法的功能分类
STL算法根据功能主要可分为以下几类,其中遍历算法是构建其他复杂操作的基础:
- 遍历算法:访问容器中的每一个元素,如
for_each、find、count。
- 修改算法:改变容器内元素的值或顺序,如
replace、fill、reverse。
- 排序算法:调整元素顺序,如
sort、stable_sort、partial_sort。
- 集合算法:对两个有序序列进行集合运算,如
set_union、set_intersection。
- 数值算法:进行数值计算,如
accumulate、inner_product。
掌握这些算法与数据结构的高效结合,是进行高性能C++开发的核心技能之一。
常用遍历算法详解
遍历算法是所有算法的基础,下面我们详细解析四个最常用的成员。
1. std::for_each:通用遍历神器
std::for_each的核心是遍历一个迭代器范围 [first, last),并对其中每个元素执行指定的操作(函数、函数对象或Lambda表达式)。
基本语法:
std::for_each(迭代器first, 迭代器last, 操作函数);
示例:使用Lambda表达式遍历并累加
#include <iostream>
#include <vector>
#include <algorithm> // STL算法头文件
int main(){
std::vector<int> v = {1, 2, 3, 4, 5, 3, 3};
int sum = 0;
std::for_each(v.begin(), v.end(), [&sum](int num){
std::cout << num << " ";
sum += num;
});
std::cout << std::endl << "累加和:" << sum << std::endl;
return 0;
}
// 输出:1 2 3 4 5 3 3 累加和:21
与手动编写迭代器循环相比,for_each将遍历逻辑与操作逻辑清晰分离,代码意图更明确。
获取操作状态:
for_each的返回值是其第三个参数(操作对象)。若操作对象是有状态的(例如存储了中间结果),可通过返回值获取。
#include <iostream>
#include <vector>
#include <algorithm>
// 有状态的函数对象
class SumAccumulator{
public:
SumAccumulator() : sum(0) {}
void operator()(int num) { sum += num; }
int getSum() const { return sum; }
private:
int sum;
};
int main(){
std::vector<int> v = {1, 2, 3, 4, 5};
// 调用并接收返回的函数对象
SumAccumulator accum = std::for_each(v.begin(), v.end(), SumAccumulator());
std::cout << "累加和:" << accum.getSum() << std::endl; // 输出:15
return 0;
}
2. std::count / std::count_if:精准统计专家
std::count用于统计范围内与指定值相等的元素个数。std::count_if则根据自定义条件进行统计。
语法:
size_t std::count(迭代器first, 迭代器last, const T& value);
size_t std::count_if(迭代器first, 迭代器last, 条件判断函数);
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main(){
std::vector<int> v = {1, 2, 3, 4, 5, 3, 3};
// 统计值为3的元素个数
size_t count_3 = std::count(v.begin(), v.end(), 3);
std::cout << "值为3的元素个数:" << count_3 << std::endl;
// 统计大于3的元素个数
size_t count_gt3 = std::count_if(v.begin(), v.end(), [](int num){
return num > 3;
});
std::cout << "大于3的元素个数:" << count_gt3 << std::endl;
return 0;
}
3. std::find / std::find_if:快速定位元素
std::find在范围内查找第一个与指定值相等的元素,并返回指向它的迭代器;若未找到则返回last迭代器。std::find_if支持自定义查找条件。
语法:
迭代器 std::find(迭代器first, 迭代器last, const T& value);
迭代器 std::find_if(迭代器first, 迭代器last, 条件判断函数);
示例:查找与修改
#include <iostream>
#include <vector>
#include <algorithm>
int main(){
std::vector<int> v = {1, 2, 3, 4, 5, 3, 3};
// 查找值为4的元素
auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end()) {
std::cout << "找到元素4,位置:" << it - v.begin() << std::endl;
*it = 40; // 通过迭代器修改元素
std::cout << "修改后的值:" << *it << std::endl;
}
// 查找第一个大于3的元素
it = std::find_if(v.begin(), v.end(), [](int num){ return num > 3; });
if (it != v.end()) {
std::cout << "第一个大于3的元素:" << *it << std::endl;
}
return 0;
}
4. std::ranges::for_each (C++20):更简洁的范围遍历
C++20引入的范围库(Ranges Library)提供了std::ranges::for_each,其最大优点是语法更简洁,无需显式写出begin()和end()。
语法:
std::ranges::for_each(容器或范围, 操作函数);
示例:
#include <iostream>
#include <vector>
#include <algorithm> // C++20 ranges仍在<algorithm>中
int main(){
std::vector<int> v = {1, 2, 3, 4, 5};
// 直接传入容器,代码更清晰
std::ranges::for_each(v, [](int num){
std::cout << num << " ";
});
// 输出:1 2 3 4 5
return 0;
}
遍历算法常见陷阱与规避方法
1. 迭代器失效问题
错误示例(在遍历中直接删除):
std::vector<int> v = {1,2,3,4,5};
std::for_each(v.begin(), v.end(), [&v](int num){
if (num == 3) {
// 错误!删除元素会导致迭代器失效,引发未定义行为
v.erase(&num);
}
});
正确做法:使用“erase-remove”惯用法
// 第一步:remove将待删除元素移到末尾,返回新逻辑末尾的迭代器
auto it = std::remove(v.begin(), v.end(), 3);
// 第二步:erase删除末尾的无效元素
v.erase(it, v.end());
2. 迭代器范围理解错误
STL算法均使用左闭右开区间 [first, last)。first指向的元素包含在内,last指向的元素不包含在内。
std::vector<int> v = {1,2,3,4,5};
// 正确:遍历前3个元素(索引0,1,2)
std::for_each(v.begin(), v.begin() + 3, [](int num){ std::cout << num << " "; });
// 输出:1 2 3
// 错误理解:v.begin()+3-1 只会遍历前2个元素
3. 参数传递方式误用
在Lambda或函数对象中,需根据意图选择正确的参数传递方式。
- 修改元素:使用非常量引用
int &num。
- 只读访问:使用值传递
int num 或常量引用 const int &num(后者可避免大对象拷贝)。
std::vector<int> v = {1,2,3,4,5};
// 错误:值传递,修改的是副本,原容器不变
std::for_each(v.begin(), v.end(), [](int num){ num *= 2; });
// 正确:引用传递,真正修改容器元素
std::for_each(v.begin(), v.end(), [](int &num){ num *= 2; });
总结与选型建议
遍历算法是操作STL容器的基石。理解其原理并正确使用,能显著提升代码的简洁性、可读性和运行效率,是现代C++编程的必备技能。
选型指南:
std::for_each:适用于需要对每个元素执行自定义复杂操作的场景,灵活性最高。
std::count/count_if:专为统计元素数量设计,语义明确,代码简洁。
std::find/find_if:专为查找特定元素并获取其位置(迭代器)设计,便于后续操作。
std::ranges::for_each (C++20):在支持C++20的项目中,作为for_each的现代化替代,语法更简洁,是连接容器与操作更优雅的“桥梁”。
掌握这些算法,并理解其背后的迭代器与容器协作机制,你将能更加游刃有余地处理各种数据操作任务。