13. 常用算术生成算法:accumulate、fill

算术生成算法,说白了就是帮我们做「累加」和「批量赋值」的。这两个函数在 STL 里不算花哨,但实战中几乎天天见。我个人习惯把它们归为「基础工具类」—— 你未必每次都用,但用的时候往往能省下好几行手写循环。

13.1 accumulate:累加的艺术

accumulate 定义在 头文件里。它的核心作用就一个:对一个区间内的元素做累加。但别小看它,我见过不少新手自己手写累加循环,结果漏了初始值或者类型转换出问题。

基本用法

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 从 0 开始累加
    int sum = std::accumulate(vec.begin(), vec.end(), 0);
    std::cout << sum << std::endl;  // 输出 15
    
    return 0;
}

第三个参数是初始值。这个初始值很关键——它不光决定累加的起点,还决定了返回类型。你想想看,如果传 0,返回 int;如果传 0.0,返回 double。我在项目中遇到过有人传 0 去累加浮点数,结果精度丢了一截,排查了半天。

小技巧: 累加字符串也很常见。比如把 vector<string> 里的所有字符串拼接起来,初始值传空字符串 "" 就行。

自定义累加逻辑

accumulate 还有第四个参数——一个二元操作函数。你可以用它实现「累乘」、「累减」甚至更复杂的操作。

#include <numeric>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 累乘:从 1 开始乘
    int product = std::accumulate(vec.begin(), vec.end(), 1, 
                                  [](int a, int b) { return a * b; });
    std::cout << product << std::endl;  // 输出 120
    
    return 0;
}

这里我用了 lambda 表达式。其实你也可以传函数指针或者函数对象。我个人习惯用 lambda,因为代码更紧凑,逻辑一目了然。

注意: 累乘的初始值一定要传 1,不是 0。传 0 的话结果永远是 0 —— 我曾经犯过这个低级错误,调试了半小时才发现。

13.2 fill:批量赋值的利器

fill 定义在 头文件里。它的作用很简单:把区间内的所有元素赋成同一个值。听起来像 memset?嗯,但 fill 是类型安全的,不会出现 memset 给非 POD 类型赋值导致的内存问题。

基本用法

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec(5);  // 5 个默认初始化的元素
    
    std::fill(vec.begin(), vec.end(), 42);
    
    for (int v : vec) {
        std::cout << v << " ";  // 输出 42 42 42 42 42
    }
    
    return 0;
}

你可能会问:直接用 vector<int> vec(5, 42) 不就行了?没错,构造时就能指定初始值。但 fill 的威力在于运行时重新赋值。比如你有一个已经存在的容器,想把它全部重置为某个值,这时候 fill 就派上用场了。

fill_n:指定个数的填充

fill_nfill 的变体,它接受一个起始迭代器和个数,而不是区间。我个人觉得 fill_n 在某些场景下更直观。

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec(10);
    
    // 从 vec.begin() 开始,填充 5 个元素为 99
    std::fill_n(vec.begin(), 5, 99);
    
    for (int v : vec) {
        std::cout << v << " ";  // 输出 99 99 99 99 99 0 0 0 0 0
    }
    
    return 0;
}
核心区别: fill 需要区间(两个迭代器),fill_n 需要起始迭代器和个数。用哪个看你的数据形式——如果你知道「从哪里到哪里」,用 fill;如果你知道「从哪里开始,填多少个」,用 fill_n。

13.3 实战中的坑与技巧

这两个算法看起来简单,但实际项目中容易踩坑。我挑几个常见的说说。

accumulate 的类型陷阱

刚才提到初始值决定返回类型。看这个例子:

std::vector<double> prices = {19.99, 5.50, 3.75};
double total = std::accumulate(prices.begin(), prices.end(), 0);
// total 的值是 28.0,而不是 29.24!

为什么会这样?因为初始值 0int,累加过程中所有 double 都被截断成 int 再相加。正确写法是传 0.0

double total = std::accumulate(prices.begin(), prices.end(), 0.0);
// 正确结果 29.24
避坑指南: 我曾经在财务计算模块里犯过这个错,导致报表金额差了 0.01 元。虽然金额不大,但财务系统里差一分钱都是大事故。从那以后,我每次用 accumulate 都会刻意检查初始值的类型。

fill 与 resize 的配合

有时候你想把一个 vector 全部填成某个值,但不确定它当前的大小。我的做法是:

std::vector<int> vec;
// ... 一些操作后,vec 可能为空,也可能有元素

// 先确保大小
vec.resize(100);
std::fill(vec.begin(), vec.end(), -1);

或者更简洁:直接用 assign 方法:

vec.assign(100, -1);  // 一步到位

嗯,assign 其实内部就是 fill 的封装。但如果你只需要填充已有容器的一部分,fill 更灵活。

13.4 知识体系总览

下面这张图帮你理清这两个算法的关系和使用场景:

算术生成算法知识体系 accumulate(累加) fill(填充) 基本累加 自定义操作 fill(区间) fill_n(个数) 初始值决定返回类型 lambda / 函数对象 类型安全,替代 memset 注意迭代器有效性 核心:减少手写循环,避免低级错误

13.5 总结

accumulatefill 是 STL 里最基础的算术生成算法。它们不复杂,但用好了能显著提升代码的简洁性和可读性。我个人建议:

  • 能用 STL 算法就别手写循环 —— 代码更短,意图更清晰
  • 注意 accumulate 的初始值类型 —— 这是最容易踩的坑
  • fill 和 fill_n 按需选择 —— 区间已知用 fill,个数已知用 fill_n
  • 不要滥用 —— 如果只是构造时初始化,直接用构造函数或 assign 更合适

嗯,这两个算法就聊到这儿。记住一点:STL 算法不是为了炫技,而是为了让你少写 bug。下次遇到累加或批量赋值,先想想能不能用它们。


公众号:蓝海资料掘金营,微信 deep3321