在 C++ 中,当我们需要对容器区间里的元素进行批量处理时,经常会用到 <algorithm> 头文件中的 std::for_each() 和 std::transform() 函数。
这两个函数的写法看起来有些相似,但职责分工很明确:std::for_each() 侧重于对每个元素执行打印或原地修改等操作,而 std::transform() 侧重于把每个元素按规则算出新值并写入目标位置。
std::for_each() 函数
std::for_each() 会顺着指定的区间,从起始位置到结束边界依次取出每个元素进行处理。
语法:
std::for_each(first, last, fn);说明:
first 和 last 分别表示待处理区间的起止迭代器,实际处理的是 [first, last) 左闭右开区间。第 3 个参数 fn 是普通函数或 Lambda 表达式,用来接收并处理当前遍历到的元素。
示例 1:原地修改容器元素
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {1, 2, 3, 4};
// 将每个元素乘以 2
std::for_each(nums.begin(), nums.end(), [](int& val) {
val *= 2;
});
for (int val : nums) {
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
2 4 6 8分析:
在这个例子中,std::for_each() 依次把 nums 中的每个元素传给 Lambda 表达式。注意参数写成了引用形式 int& val,表示直接操作原容器中的元素本身,因此执行 val *= 2; 后原容器变成了 2 4 6 8。
如果我们把参数写成不带 & 的普通值传递 int val,修改的就只是拷贝副本,原容器里的数据不会发生任何改变。
如果只是把整个容器从头到尾修改一遍,直接使用范围 for 循环不就可以了吗?确实如此,但 std::for_each() 的优势在于可以只对容器中间的子区间执行操作。比如只想把前 2 个元素乘以 10,写成下面这样即可:
// 只将前 2 个元素乘以 10
std::for_each(nums.begin(), nums.begin() + 2, [](int& val) {
val *= 10;
});std::transform() 函数
std::transform() 专门用来完成 “读取原数据 → 计算新值 → 写入目标位置” 的映射转换任务,其中 transform 在英文里就是 “转换、变换” 的意思。
语法:
std::transform(first, last, output, fn);说明:
前两个参数 first 和 last 表示输入数据的起止边界迭代器,后面两个参数的作用如下:
- output(必选):目标容器起始写入位置的迭代器,表示转换后的新值从哪里开始存放。
- fn(必选):用于转换数据的函数或 Lambda 表达式,内部必须通过
return返回转换后的新值。
与 std::for_each() 相比,这里最关键的区别在于返回值。std::for_each() 里的 Lambda 通常直接打印或通过引用修改元素,不需要写 return。
而 std::transform() 需要把 fn 算出的结果填入 output 位置,因此它的 Lambda 内部必须使用 return 返回新值,否则会直接编译报错。根据转换结果的存放位置不同,平时主要有以下 3 种常见用法。
1. 转换结果存入新容器
如果我们希望保留原容器的数据不变,同时把转换后的结果保存到另一个新容器中,需要先为新容器准备好足够的元素空间。比如在 vector 初始化时直接指定长度,而且新容器的元素类型还可以与原容器不同。
示例 2:计算半价并存入新容器
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> prices = {15, 25, 40};
// 提前分配与 prices 相同的元素空间
std::vector<double> halves(prices.size());
std::transform(prices.begin(), prices.end(), halves.begin(),
[](int p) {
return p * 0.5;
});
for (double val : halves) {
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
7.5 12.5 20分析:
在这个例子中,我们将 halves.begin() 作为第 3 个参数传给了 std::transform(),用来指定结果的起始写入位置。函数每从 prices 取出一个整数原价,就计算一次 p * 0.5 并依次填入 halves。
执行完成后,原容器 prices 中的整数保持不变,而 halves 则保存了转换后的 double 小数 7.5 12.5 20。这种跨类型的批量映射转换,是原地修改无法做到的。
2. 转换结果写回原容器
当转换前后的元素类型相同,且我们不需要保留原容器的旧数据时,也可以把第 3 个参数 output 直接写成原容器自己的 begin(),让算出来的新值直接覆盖回原来的位置。下面我们结合 Lambda 捕获来看一个例子。
示例 3:转换结果写回原容器
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {10, 20, 30, 40};
int factor = 3;
// 将转换结果直接覆盖回 nums
std::transform(nums.begin(), nums.end(), nums.begin(),
[factor](int val) {
return val * factor;
});
for (int val : nums) {
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
30 60 90 120分析:
[factor] 表示按值捕获外部局部变量 factor。同时,第 3 个参数直接传入了 nums.begin(),表示每次算出的 val * factor 直接写回 nums 当前元素所在的位置,省去了额外创建新容器的开销。
3. 使用 back_inserter() 追加元素
把转换结果存入新容器时,初学者最容易踩的一个坑是:新容器刚定义出来还是空的,却直接把它的 begin() 传给了 std::transform()。由于 std::transform() 只负责往已有位置填数据,不会自动帮容器扩容,下面这种写法在运行时会直接越界崩溃。
std::vector<int> squares; // 长度为 0 的空容器
std::transform(nums.begin(), nums.end(), squares.begin(),
[](int val) {
return val * val; // 运行崩溃
});要避免这个问题,除了像示例 2 那样提前指定容器长度,我们还可以引入 <iterator> 头文件,使用 std::back_inserter() 将空容器包装成插入迭代器。请看下面例子。
示例 4:向空容器追加转换结果
#include <algorithm>
#include <iostream>
#include <iterator> // 提供 std::back_inserter()
#include <vector>
int main() {
std::vector<int> nums = {1, 2, 3, 4};
std::vector<int> squares; // 定义空容器
// 自动在 squares 末尾追加转换结果
std::transform(nums.begin(), nums.end(),
std::back_inserter(squares), [](int val) {
return val * val;
});
for (int val : squares) {
std::cout << val << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
1 4 9 16分析:
std::back_inserter(squares) 将空容器 squares 包装成尾部插入迭代器。每算出一个平方值,内部就会自动调用 squares.push_back() 追加到末尾,哪怕目标容器一开始为空也完全安全。
注意 把转换结果存入新容器时,小伙伴们千万不要直接传入空容器的 begin()。如果提前知道元素个数,优先像示例 2 那样预先指定长度以减少扩容次数。如果事先不确定有多少个元素,则配合 std::back_inserter() 自动追加。
如何选择批量处理方式?
在实际开发中,面对范围 for 循环、std::for_each() 和 std::transform() 这 3 种批量处理方式,小伙伴们可以参考下面的标准来选择:
- 遍历或修改整个容器:如果只是把整个容器从头到尾打印或简单修改一遍,我们优先使用范围
for循环,语法最简洁直观,不需要手写begin()和end()。 - 处理指定子区间:如果只需要对容器中间的某一段子区间
[first, last)执行操作,或者已经有一个现成的处理函数想直接传入复用,使用std::for_each()更合适。 - 批量映射生成新数据:如果需要根据一组输入数据按规则计算出另一组新数据,尤其是转换前后数据类型不同时,我们优先选择
std::transform(),输入与输出的对应关系最清晰。
