做设备调试这些年,我有个很明显的感觉:S7-200 里能把程序写干净的套路就那么几个,指针(间接寻址)算是最值得吃透的一个。最近帮一位做烘箱设备的朋友处理程序,他的需求很典型——一批温度数据已经连续存放在 V 区,要一次算出平均值、最大值、最小值,还要每个指令都带注释,方便后来接手的人看懂。听起来简单,但真用直接寻址去写,要么程序冗长得离谱,要么数据一多就失控;换成指针加循环,几十行就能搞定。这篇文章就把这个需求的完整要求、程序写法、逐行注释和踩坑经验一次说清楚,适合正在学 S7-200 指针的初学者,也适合需要移植类似功能的老工程师对照检查。
1. 这个需求在真实项目里的样子:为什么非用指针不可
1.1 三类最典型的应用场景
我接触到的项目里,这类“连续一批数据求平均、求最大、求最小”的需求几乎常年出现,归纳起来就三类:
- 多点温度统计。烘箱、固化炉、恒温槽里装 8~16 个测温点,温度信号经模拟量模块转换后,被程序或上位机写进内存中一段连续地址。需要统计平均温度判断温区均匀性,记录最高温度和最低温度用于超温报警。
- 连续称重检重。包装线上电子秤连续采 10 包或 20 包重量,算平均值和极差,判断是否在公差范围内。数据通常是每包一个重量值,依次存到同一段缓冲区。
- 模拟量多次采样滤波。压力、液位这类波动较大的模拟量,直接拿单次采样值做控制会被噪声带偏,工程上常见的做法是连续采 N 次,取平均值作为过程值,同时把最大值、最小值也记下来,方便诊断传感器是否出现异常尖峰。
这些场景的共同特征是:数据量不是固定的 1 个或 2 个,而是 N 个;数据在内存里连续存放;对每个数据的处理逻辑完全一样,无非是累加、比较。当你想用一套代码处理“任意 N 个连续数据”时,指针就是唯一优雅的解法。
1.2 直接寻址为什么不行
假设 10 个数据存放在 VW100~VW118,用直接寻址写,就是 10 段重复累加、10 段最大值比较、10 段最小值比较,程序从几行膨胀到几十行。如果现场把测温点从 10 个改成 12 个,程序又得从头改一遍。更麻烦的是,很多场景下 N 本身是运行时由上位机下发的变量,今天 8 个点,明天 12 个点,直接寻址根本没有办法应对“数量可变”。
用指针之后,循环体只写一遍,循环次数由 N 决定,数据区首地址由一行MOVD指定。这就是“程序和数据分离”——程序不变,变的是指针指向的位置和循环次数。维护时改 N 一个变量,所有统计逻辑自动适配。
1.3 需求落到程序上的硬性要求
朋友最初需求说得模糊,我把它整理成了一张可验收的要求表,这也是我自己写这类程序前必定先做的一步:
| 序号 | 需求项 | 具体内容 |
|---|---|---|
| R1 | 数据区 | 从 VB100 开始连续存放 N 个 16 位有符号整数,每个数据占一个字(2 字节),即 VW100、VW102、VW104…… |
| R2 | 数据个数 | N 存放在 VW6,由外部写入,允许范围 1~50;N≤0 时不运算并给出错误提示 |
| R3 | 触发方式 | M20.0 上升沿启动一次运算,避免每个扫描周期都重复计算 |
| R4 | 输出结果 | VW14=平均值(整除取整),VW16=最大值,VW18=最小值 |
| R5 | 完成标志 | 运算结束后 M20.2 置位,供触摸屏或上位机读取 |
| R6 | 运算时间 | N=50 时 FOR 循环几十条指令,微秒级完成,必须在单个扫描周期内跑完 |
| R7 | 可复用性 | 修改指针首地址和 N 即可迁移到其他数据区,不修改结构 |
R6 特别提醒一点:S7-200 的 FOR 循环和 S7-300/1200 一样,是在“一个扫描周期内连续执行完所有循环次数”的,不是每个扫描周期执行一轮。这意味着中间结果你在线监控是看不到的,只能看到最终算完的值。后面第 5 章我会讲怎么在这种前提下做调试。
2. S7-200 指针前置课:&、*、VD 和自增步长
2.1 三个关键符号
不少初学者卡在指针,不是因为指令复杂,而是因为三个符号的含义没完全建立起来。我习惯用一句话概括:&是“取地址”,*是“按地址取内容”,指针本身必须放在双字(VD)里。
&取址符。MOVD &VB100, VD0的意思是把 VB100 的地址值(一个 32 位的数字,比如 100)存进 VD0。注意,这个值不是一个普通数据,而是“内存门牌号”。S7-200 里地址是以字节为基本粒度的,所以大家统一写成&VB100这种字节地址形式,含义最清晰,不容易产生歧义。*间接访问符。MOVW *VD0, VW20的意思是:把 VD0 当成一个指针,去它指向的那个内存地址里读一个“字”存入 VW20。*号后面跟着的访问宽度由指令决定:MOVB *VD0, VBxx取字节,MOVW *VD0, VWxx取字,MOVD *VD0, VDxx取双字。去掉*号,MOVW VD0, VW20的含义就变成把地址值本身当数据拷走,程序完全变味。- 指针必须藏在 VD 里。指针是 32 位值,所以只能存双字变量。如果图省事放进 VW,就会丢失一半地址信息,程序立刻乱套。这也是 S7-200 指针最常见的新手错误。
2.2 自增步长为什么是 2
这是整个程序最容易写错的一个数。16 位整数占 2 个字节,而指针是以“字节地址”为单位的,所以下一轮想访问“下一个字”,必须让指针值加 2:
+D +2, VD0如果数据是字节(8 位),步长是 1;如果数据是双字(32 位)或 REAL 浮点数,步长是 4。你可以把指针想象成一个书签:它记录的不是“第几页”,而是“第几行的第几个汉字”。S7-200 的指针粒度到“字节”,移动距离必须按字节数算。
2.3 指针的适用范围和边界
指针不光能指 V 区,I、Q、M 区都可以,只要把取址对象换成&IB0、&QB0、&MB0。本文数据放在 V 区,因为 V 区空间大、掉电保持行为清晰,也方便触摸屏直接读取。
这里有个隐藏风险必须讲透:指针自增时不能越过所在区域的末尾。假如数据首地址是 VB100,区末尾假设是 VB10239(不同 CPU 的 V 区大小不同),那么最后一个数据字占用的是 VW(100+2N-2),必须满足100 + 2N - 2 ≤ 区域末尾。S7-200 对越界访问不一定报错,它只是老老实实按地址去读,读出来的可能是其他程序留下的垃圾值。所以程序里最好加一道 N 上限检查,别把这个问题留给现场。
3. 程序要求书:数据区、触发、输出、边界条件
3.1 地址分配表
写任何带指针的程序,第一步都是把地址规划好。我这次用的地址分配如下,读者可以直接照抄:
| 地址 | 用途 | 说明 |
|---|---|---|
| VD0 | 数据指针 | 初始化指向 &VB100,循环体里每轮 +2 |
| VW4 | FOR 循环索引 | 由 FOR 指令自动管理,程序里不需要手动改 |
| VW6 | 数据个数 N | 外部写入,范围 1~50 |
| VD10 | 累加总和 | 32 位双字,防止累加溢出 |
| VW14 | 平均值结果 | 除法后取整数部分 |
| VW16 | 最大值结果 | 运算完成后保持 |
| VW18 | 最小值结果 | 运算完成后保持 |
| VW20 | 当前值暂存 | 每轮循环从指针指向的地址读出 |
| VD24 | 数据类型转换暂存 | 当前值从 16 位扩展成 32 位 |
| VD30 | 平均值计算暂存 | 总和复制到这里再参与除法 |
| VD34 | N 的双字转换暂存 | 把 VW6 扩展成 32 位供/D使用 |
| M20.0 | 触发信号 | 上升沿启动一次运算 |
| M20.1 | 计算进行标志 | 循环期间为 ON |
| M20.2 | 计算完成标志 | 运算结束后置位 |
表格里标粗的几个地址彼此不能重叠,这个我在第 5 章的坑 4 会专门说。
3.2 每条需求背后的理由
很多人拿到需求就急着写代码,我建议先问自己三个“为什么”。
为什么用上升沿触发而不是常 ON 触发?如果 M20.0 一直为 ON,程序每个扫描周期都会重新初始化、重新循环、重新计算。结果虽然一致,但完成标志每周期都被刷新,上位机根本无法判断“这次运算是什么时候完成的”。用 EU 上升沿指令,只在信号从 0 变 1 的那一个扫描周期执行一遍,触发一次算一次,干净利落。
为什么累加和必须用 32 位?假设 N=10,每个数据是 5000,总和就是 50000,已经超过 16 位有符号整数的上限 32767,16 位加法必然溢出。所以循环里每一轮都要先用 ITD 把当前值从 16 位扩展成 32 位,再做双字加法。ITD 的作用可以类比成“把矮个子数据先垫高,再装进大箱子”,垫高本身不影响数值。
为什么最大值初值要给 -32768,最小值初值给 +32767?这两个数是 16 位有符号整数的理论下界和上界。这样设计是为了保证:数组里任意一个真实值,哪怕全是负数,第一次参与比较时都能击败初值,完成正确替换。如果有人偷懒把最大值初值写 0,一旦数据全部为负,最大值就永远是 0,这是程序逻辑里最隐蔽的一个雷。
3.3 边界条件清单
我列了一张边界检查表,写程序时对照着处理,基本可以覆盖现场会遇到的坑:
| 边界情况 | 程序表现 | 处理建议 |
|---|---|---|
| N=0 | /D除数为 0,SM1.1 置位,平均值错误 | 启动前增加 N>0 判断,或除完后检查 SM1.1 |
| N=1 | 平均值=最大值=最小值=那个数据本身 | 逻辑自然成立,无需特判 |
| N 超过数据区实际长度 | 指针读出 V 区其他数据,结果错乱 | 程序里做 N 上限比较,超限则禁止启动 |
| 数据全部为负数 | 最大值初值若为 0 则结果错误 | 初值必须用 -32768 |
| 运算进行中又来新触发 | 初始化被第二次打断,循环条件被破坏 | 触发逻辑里判断 M20.1,忙时忽略新触发 |
4. 完整 STL 程序与逐行注释
4.1 程序全貌
我用 STL(语句表)呈现,是因为它天然适合逐行注释。如果你平时用梯形图,Micro/WIN 里可以直接把 STL 切回 LAD 视图,网络结构是一一对应的。
// 功能:对 VB100 开始的 N 个 16 位整数求平均值、最大值、最小值 // 触发:M20.0 上升沿启动一次;完成:M20.2 置位 // 结果:VW14=平均值,VW16=最大值,VW18=最小值 // ========== 网络1:初始化 ========== LD M20.0 // 读触发信号 EU // 检测上升沿,仅执行一个扫描周期 MOVD &VB100, VD0 // 指针VD0 = 数据区首字节地址(100) MOVD +0, VD10 // 累加和VD10清零 MOVW -32768, VW16 // 最大值初值 = 理论最小整数,保证任意数据都能替换 MOVW +32767, VW18 // 最小值初值 = 理论最大整数 R M20.2, 1 // 清除上一次运算的完成标志 S M20.1, 1 // 置位“计算进行”标志,用于开启下方FOR循环 // ========== 网络2:FOR 循环头 ========== LD M20.1 // 计算进行标志为真才进入循环 FOR VW4, +1, VW6 // 索引VW4从1计数到VW6(N),共循环N次 // ========== 网络3:读取当前数据 ========== LD SM0.0 // SM0.0恒为ON,保证循环体每一轮都执行 MOVW *VD0, VW20 // 把VD0指针指向的字数据读到VW20 // ========== 网络4:累加到总和 ========== LD SM0.0 ITD VW20, VD24 // 当前值16位扩展为32位,防止累加溢出 +D VD24, VD10 // 总和VD10 = VD10 + 当前值 // ========== 网络5:与最大值比较 ========== LD SM0.0 LDW >= VW20, VW16 // 当前值 >= 已记录最大值? MOVW VW20, VW16 // 条件成立则更新最大值 // ========== 网络6:与最小值比较 ========== LD SM0.0 LDW <= VW20, VW18 // 当前值 <= 已记录最小值? MOVW VW20, VW18 // 条件成立则更新最小值 // ========== 网络7:指针移动到下一个数据 ========== LD SM0.0 +D +2, VD0 // 指针加2,指向下一个16位数据;勿写成+1 // ========== 网络8:FOR 循环尾 ========== NEXT // ========== 网络9:计算平均值并复位标志 ========== LD M20.1 // 循环结束回到这里,仍在本扫描周期内 MOVD VD10, VD30 // 复制累加和到VD30,参与除法 ITD VW6, VD34 // 数据个数N从16位扩展为32位 /D VD34, VD30 // 双字除法:VD30 = VD30 / VD34,商为整数 MOVW VD30, VW14 // 把商的低16位存入平均值VW14 R M20.1, 1 // 复位“计算进行”标志 S M20.2, 1 // 置位“完成”标志4.2 十个网络逐段拆解
网络1 是整个程序的地基。所有变量在这里获得确定的起点:指针指向数据区首地址、总和清零、最大值和最小值分别设置成两个极端值、完成标志复位、计算标志置位。注意顺序,“置位计算标志”放在最后,因为紧接着网络2 就要判断这个标志。如果初始化指令顺序写反了,把S M20.1写在最前面,那么同一扫描周期后面几条初始化的状态就会“传导”到循环体里,造成首轮数据处理的错乱。这个细节不容易被注意到,但它正是程序稳定性的关键。
网络2 的FOR VW4, +1, VW6有三个操作数:索引寄存器 VW4、起始值 +1、结束值 VW6。执行语义是:VW4 从 1 开始,每执行一轮循环体加 1,直到 VW4 大于 VW6 时停止,所以循环次数恰好等于 N。结束值放 VW6 这个写法意味着 N 是运行时变量,程序本身不用改。如果 N 固定,也可以直接写FOR VW4, +1, +10。索引寄存器 VW4 在本程序里没有直接使用,它纯粹是 FOR 指令的计数器,但你必须给它分配一个专用的 VW 地址。
网络3 是“指针”二字的核心体现。VD0 在初始化时被放入数字 100,MOVW *VD0, VW20的执行过程是:先读 VD0 得到门牌号 100,再去地址 100 处读一个字。梯形图里这对应一条“取指针指向的数据”指令,没有任何可见的操作数变化,初学者容易把它当成普通数据传送。一定要想清楚:如果去掉星号,MOVW VD0, VW20拷贝的是指针值本身,而不是数据。
网络4 处理累加。ITD VW20, VD24是把 16 位有符号整数扩展成 32 位,然后+D VD24, VD10做双字加法。为什么不用+I直接加?回到 3.2 节的溢出问题:16 位加法最多算到 32767,N 个稍大的正数相加立刻穿帮。双字加法允许总和到 21 亿,对 50 个以内的现场数据完全够用。
网络5 和网络6 是一对镜像结构。LDW >=和LDW <=是字比较指令,梯形图里对应一个比较触点。比较结果为真时,后续的MOVW才执行,也就是用当前值覆盖旧的极值。这里不需要考虑“相等时是否覆盖”——相等时更新与否对最大值结果没有影响,所以用>=和<=都行,逻辑上安全。
网络7 的+D +2, VD0是地址自增,不是数据相加。每一轮循环读完一个数据后,把指针向后移动 2 个字节,下一轮才能读到下一个字。这句是全部程序里最容易写错的地方,第 5 章我会用一个具体数字例子演示写错+1会变成什么样。
网络8 的NEXT是 FOR 结构的下半截,它和网络2 成对出现。如果忘了写 NEXT,Micro/WIN 会直接报错;如果 FOR 和 NEXT 之间夹了对其他逻辑不该有的程序段,那部分也会被当成循环体反复执行,这是结构性错误,比单条指令错误更难发现。
网络9 在循环结束后执行。此时累加和 VD10 已经等于 N 个数据的总和,数据个数 N 存在 VW6。平均值 = 总和 / N,用双字除法完成。MOVW VD30, VW14只取商的低 16 位,也就是整数部分。最后复位计算标志、置位完成标志,让整套逻辑恢复待命状态。
4.3 注释背后的三个设计取舍
第一个取舍:指针自增放在循环体末尾,而不是开头。如果把+D +2, VD0放在读取数据之前,指针初值就得指向第一个数据的前一个字节,也就是&VB98,可读性差而且容易算错边界。我的原则是“先读后移”,指针初值和数据区首地址一模一样,逻辑最直观。这样做的代价是最后一轮读完,指针已经越界指向数据区之后的地址,但循环到此结束,不会再执行读取,所以没有实际危害。理解这个“恰好够用的边界”,比背下整段程序更重要。
第二个取舍:结果放在 V 区而不是 M 区。V 区可以被触摸屏、上位机通过 Modbus 直接读,调试时在状态表里输入 VW14 就能看结果;M 区虽然也能做保持设置,但地址空间小、不如 V 区直观。放 V 区还有个附带好处:程序导出或交接时,地址表一眼就能对应到数据块。
第三个取舍:用 FOR/NEXT 而不是自己写“计数器加跳转”。S7-200 的 FOR 指令自动管理索引、自动判断结束条件,扫描时间可控。自己用计数器、比较指令和跳转指令搭循环,会引入首轮跳过、末轮越界、标志复位时机等一系列边界问题,能不用就不用。
5. 程序跑起来之后的实测心得与典型坑
5.1 坑1:指针自增写成 +1,结果错得毫无规律
这是我见过最多的问题。假设数组只有 4 个数:100、200、300、400,正确结果应为平均值 250、最大值 400、最小值 100。如果错误地把+D +2, VD0写成+D +1, VD0,实际读取过程会变成这样:
| 轮次 | 指针值 | 实际读到的字 | 读到的值 |
|---|---|---|---|
| 1 | 100 | VW100 | 100 |
| 2 | 101 | VW101(VB101+VB102) | 25600 |
| 3 | 102 | VW102(VB102+VB103) | 51200 |
| 4 | 103 | VW103(VB103+VB104) | 11264 |
原因在于 S7-200 数据按字节编址,字数据高字节在前。VW100 占 VB100、VB101,VW102 占 VB102、VB103。指针只加 1,第二轮读的是“VW100 的低字节 + VW102 的高字节”,被硬生生缝成一个新值。你看到的结果是平均值 22041、最大值 51200,和手工算的完全对不上,而且数组越长错得越离谱。
排查这种问题,最有效的手段是“让循环慢下来”。FOR 循环在单个扫描周期内跑完,在线监控时你根本看不到每一轮中间状态。我会临时把程序改成一个“每扫描周期处理一个数据”的调试版:用一个 M 位做步进,每次触发只执行一次读取、一次累加、一次指针自增,然后用状态表盯着 VD0 和 VW20 的变化。几个扫描周期下来,VD0 的增长幅度是 1 还是 2 一目了然。确认无误后再恢复 FOR 循环,这是我自己验证指针步长的标准流程。
5.2 坑2:最大值/最小值初值取错,负数数据全军覆没
有一次现场反馈“最低温度显示 0 度,但实际只有 -5 度”。查下来程序里最小值初值写的是 0,数组里全是负温度,任何数都不小于 0,最小值就永远是 0。反过来,如果有人的最大值初值写 0,而数据全是负值,最大值也会变成 0。
这类问题的根源是把“业务上的合理初值”和“算法上的安全初值”混为一谈。做统计的算法要求:最大值初值取理论最小数(-32768),最小值初值取理论最大数(+32767),这样任何一个真实数据都能在第一轮比较中“接管”极值。如果你的数据范围明确是 0~32000,把最大值初值设 0 看起来没问题,但一旦数据范围扩展或换成其他传感器,又得回来改程序。不如一开始就用极端值,一劳永逸。
5.3 坑3:循环体内误用触发信号做条件,某些轮次被跳过
有位读者把循环体里的每一行都写成LD M20.0开头,想着“触发一次就带条件执行一次”。问题是 M20.0 是外部信号,如果它在 FOR 循环执行期间被其他逻辑复位,后面几轮循环体的指令就全部不执行了,结果就是少算了几轮,平均值和极值全是错的。
正确做法是:触发逻辑只保留在网络1 和网络9,循环体内的每一行固定用LD SM0.0打头。SM0.0 是 S7-200 的“常 ON”特殊继电器,永远为真,它在这里的作用是给循环体每一轮提供一个稳定的执行条件。这也是 S7-200 编程的通用习惯:SM0.0 最常见的使用场景就是 FOR 循环体内的常开条件。
5.4 坑4:V 区变量重叠,编译器不会替你报警
S7-200 没有像高级语言那样的变量重名检查,V 区地址是你自己规划出来的。比如数据区从 VW100 开始,如果你把累加和定义在 VD102,那么 VD102 占 VB102~VB105,恰好踩掉了数据区的第二个字 VW102 和第三个字 VW104。循环开始后,指针读出来的根本不是原始数据,而是被累加过程自己写脏的内存。
所以我会严格按“8 字节一组”的方式分配关键双字变量,并在地址分配表里逐个核对:VD0 占 VB0~VB3,VW4 占 VB4~VB5,VW6 占 VB6~VB7,VD10 占 VB10~VB13,全部独立不交叉。写完程序后再用“查找引用”功能把所有用到的 VW、VD、VB 列一遍,人工确认没有重叠。这个过程花不了两分钟,但能省掉现场一晚上的排查时间。
5.5 排查方法论:按“输入→处理→输出”倒查
程序结果不对时,我习惯按三段倒查,而不是瞎猜:
- 先确认输入数据本身正确。用状态表按地址逐个看 VW100、VW102、VW104……确认数组里的值和上位机下发的一致。这一关经常能发现“数据根本没写进来”或者“数据被别的程序改过”的问题。
- 再确认处理逻辑正确。重点盯指针 VD0 的自增步长,以及 VW20 在每轮读到的值。由于 FOR 循环中间过程看不到,就用 5.1 节的单步调试法,把一个周期的循环拆成多个扫描周期观察。
- 最后确认输出没有被其他程序改写。在工程里搜索所有对 VW14、VW16、VW18 的写入指令,有时别的程序段会在你算完之后又覆盖一遍结果。
按这三步走,绝大多数指针问题都能定位到“地址错位”这一类上。
6. 变体玩法:浮点数、四舍五入、无符号数据
6.1 REAL 浮点版
如果数据不是 16 位整数,而是浮点数(例如某些温度模块直接输出 REAL 类型),程序骨架完全不用动,只需改四类指令:
- 指针自增步长从 2 改成 4:
+D +4, VD0 - 取数用浮点传送:
MOVR *VD0, VD20 - 累加用浮点加:
+R VD20, VD10 - 比较用浮点比较:
LDR >= VD20, VD16、LDR <= VD20, VD18 - 除法用浮点除:
/R VD34, VD30
其余初始化、循环结构、边界处理全部一样。这也说明“指针+循环”这套骨架的复用价值:它和数据类型解耦,换数据类型只换指令助记符和步长。
6.2 平均值四舍五入
第 4 章的程序用/D做整数除法,结果是截断取整。如果现场要求四舍五入,可以在除法前给总和加半个 N:
ITD VW6, VD34 // N 转双字 MOVD VD34, VD38 // 复制一份 N /D +2, VD38 // VD38 = N / 2 +D VD38, VD30 // 总和 + N/2 /D VD34, VD30 // (总和 + N/2) / N,结果四舍五入原理很简单:整数除法本质是向零截断。先加除数的一半,截断后就和四舍五入等价。比如 sum=7、N=3,(7+1)/3=2,7/3≈2.33 四舍五入也是 2;sum=8、N=3,(8+1)/3=3,8/3≈2.67 四舍五入是 3。这个技巧对正数完全成立,负数场景四舍五入的语义在不同行业有差异,现场做之前先和工艺确认清楚。
6.3 模拟量无符号数据的处理
模拟量模块输出的原始值通常是 0~32000 的正整数,没有超过 16 位有符号整数的上限 32767,所以第 4 章的程序不用改,直接可用。
如果遇到 0~65535 的无符号范围数据,就不建议硬塞进 16 位有符号比较了。两个简单备选方案:一是把每个数据先减去 32768 平移成有符号数处理,算完结果再加回 32768;二是全部转成 REAL 浮点再做统计。第一种省内存,第二种更直观,项目里按数据量和精度要求取舍。
最后说个我自己的习惯。写完这类带指针的程序,我一定会做一次“改数验证”:把 N 改成 3,数组里手工放 3 个简单数,比如 10、20、30,然后看 VW14 是不是 20、VW16 是不是 30、VW18 是不是 10。这一步 30 秒都不到,但能挡住 80% 的地址错位和逻辑错误。实际项目里我还会把数据区首地址、N 的上下限范围集中写在程序首段注释里,方便下一任维护工程师一眼看懂。S7-200 的指针并不难,难的是对“地址”这个概念有没有敬畏心——每个&、每个*、每次+2,都是和内存的一问一答。把这套程序吃透,再遇到任何“批量数据处理”需求,基本都能一招鲜。