☰
输入输出加强版:正整数A+B的完整解法与避坑指南
2026/10/15 10:42:54 网站建设 项目流程

“7-1-2 输入输出 plus 正整数A+B”,这串编号刚看时特别不起眼。不少初学者拿到这道题,第一反应是:A+B 嘛,两个整数加起来输出就完事了,有什么好讲的。但等你真的在在线评测系统上提交,才会发现这个“plus”版本根本没这么温柔。第一个坑是输入格式:A 和 B 不一定只由数字组成,可能混着空格和多余字符;第二个坑是输出格式:某一位非法时要用 ? 占位,空格位置错一个就算错;第三个坑是很多人压根没意识到要自己切字符串。这篇博文把这道题从题面读到多语言实现、从边界测试到提交排错完整过一遍。适合刚学完 scanf/printf、第一次做综合性输入输出练习的读者,也适合在入门课上帮学生扫雷的老师。

1. 从题面读出三件事:输入格式、数据范围、输出格式

任何一道输入输出题,做题第一步不是写代码,而是先回答三个问题:输入长什么样?数据范围多大?输出必须是什么格式?这三个问题的答案通常都写在题面里,但新手很容易跳过,直接上手写逻辑,然后踩坑。

1.1 基础版只有一行,plus 版却要“自己切字符串”

最原始的正整数 A+B 题,输入就是一行两个正整数:

123 456

用scanf("%d%d", &a, &b)或者 Python 的input().split()就能干净地取到两个数。这种题考的是最基础的读取与输出,属于“热身”。

但标题里的 plus 不是白加的。常见的加强版会这样描述:输入在第一行给出 A 和 B,A 是第一个空格之前的部分,B 是第一个空格之后的整行内容;A 和 B 去掉首尾空白后,必须是在某个范围内的正整数(常见范围是 1 到 1000,具体以手上题面为准),否则视为非法,输出时对应位置用 ? 代替。

注意 B 的定义——它可能是34这种干净字符串,也可能是34、甚至34 56这种带着多余空格和字符的字符串。这就意味着你不能用scanf("%d")一个数一个数地读,因为%d遇到空白会自动跳过,会把本该属于 B 的多余字符悄悄漏掉。正确做法是先把整行读进来,再按“第一个空格”切开。

1.2 先定“合法正整数”的判定标准,再写判题逻辑

我在给初学者答疑时发现,很多人代码写到一半才开始纠结“什么叫合法”,结果越改越乱。正确顺序是先把判定标准写死在注释里:

  • 不能是空串;
  • 每一个字符都必须是数字;
  • 数值必须在题目给定的范围内(例如 1 到 1000);
  • 如果题目对“标准写法”有额外要求,还要处理前导零,比如00012算不算合法,这种细节题面没说清楚就得按样例来判断。

提示:判定标准不确定时,优先看样例和题面最后的说明。作业题和比赛题对前导零的处理经常不一样,别拿自己的直觉当规则。

这个“先定规则再写代码”的习惯,对后面所有解析类题目都适用。包括范围检查也最好在解析函数里一步做完,而不是在主流程里到处补判断。

2. 四种语言的 plus 版实现,每一行都解释为什么这么写

下面给出 C、C++、Python、Java 四种写法。虽然是同一套逻辑,但每种语言的“读整行”“找空格”“去空白”手段都不一样,正好对比一下。

2.1 C 语言:fgets 读整行,手写 digit 校验

先看完整代码:

#include <stdio.h> #include <string.h> #include <ctype.h> int parse(const char *s) { int len = strlen(s); if (len == 0) return -1; // 空串非法 for (int i = 0; i < len; i++) { if (!isdigit((unsigned char)s[i])) return -1; } int v = 0; for (int i = 0; i < len; i++) { v = v * 10 + (s[i] - '0'); if (v > 1000) return -1; // 超出范围直接返回 } if (v == 0) return -1; // 0 不是正整数 return v; } char *trim(char *s) { while (*s && isspace((unsigned char)*s)) s++; int n = strlen(s); while (n > 0 && isspace((unsigned char)s[n - 1])) s[--n] = '\0'; return s; } int main() { char line[200]; while (fgets(line, sizeof(line), stdin)) { line[strcspn(line, "\n")] = '\0'; // 去掉结尾换行 char *space = strchr(line, ' '); char left[200], right[200]; int a, b; if (space != NULL) { *space = '\0'; // 在第一个空格处切断 strcpy(left, line); strcpy(right, space + 1); } else { strcpy(left, line); // 整行都是 A right[0] = '\0'; // B 为空串 } a = parse(trim(left)); b = parse(trim(right)); if (a == -1) printf("? + "); else printf("%d + ", a); if (b == -1) printf("? = ?\n"); else printf("%d = %d\n", b, a + b); } return 0; }

几个地方说明一下。

用fgets而不是gets:gets在 C11 标准里已经被移除,而且不检查缓冲区长度,输入稍长就会踩坏内存;fgets有长度参数,至少不会越界。处理完一行后记得用strcspn找到换行符的位置并置成'\0',否则parse判定时会把'\n'当成非法字符。

parse里用isdigit((unsigned char)s[i]):这里强制转unsigned char是必要的。标准库里isdigit这类函数接收的是int,却只对unsigned char和EOF有定义,直接用char在有些编译器上会出警告甚至误判。

为什么不用strtol?strtol允许正负号、允许数字后面跟尾巴,对“必须全是数字”这种严格校验反而要额外写一堆判断。自己写十几行循环,逻辑一目了然,也方便改成任意范围。

2.2 C++:getline 加 lambda 裁剪,逻辑和 C 一一对应

#include <iostream> #include <string> #include <cctype> int parse(const std::string &s) { if (s.empty()) return -1; for (char c : s) if (!isdigit((unsigned char)c)) return -1; int v = 0; for (char c : s) { v = v * 10 + (c - '0'); if (v > 1000) return -1; } return v == 0 ? -1 : v; } void trim(std::string &s) { size_t b = s.find_first_not_of(" \t"); if (b == std::string::npos) { s.clear(); return; } size_t e = s.find_last_not_of(" \t"); s = s.substr(b, e - b + 1); } int main() { std::string line; while (getline(std::cin, line)) { size_t sp = line.find(' '); std::string left = (sp == std::string::npos) ? line : line.substr(0, sp); std::string right = (sp == std::string::npos) ? "" : line.substr(sp + 1); trim(left); trim(right); int a = parse(left), b = parse(right); if (a == -1) std::cout << "? + "; else std::cout << a << " + "; if (b == -1) std::cout << "? = ?\n"; else std::cout << b << " = " << a + b << "\n"; } return 0; }

这里唯一要提醒的是trim的写法。很多入门教程教“遍历删空格”,但字符串首尾都为空时,手工删特别容易越界。用find_first_not_of和find_last_not_of先定位边界再substr,是最稳的写法。left/right的取值和 C 版本完全对照:find(' ')返回npos就说明整行没有空格,B 为空串。

2.3 Python:split(' ', 1) 的语义正好匹配“第一个空格”

import sys def parse(s: str): s = s.strip() if not s or not s.isdigit(): return None v = int(s) if not 1 <= v <= 1000: return None return v def main(): for line in sys.stdin: line = line.rstrip('\n') if ' ' in line: left, right = line.split(' ', 1) else: left, right = line, '' a = parse(left) b = parse(right) if a is None and b is None: print('? + ? = ?') elif a is None: print(f'? + {b} = ?') elif b is None: print(f'{a} + ? = ?') else: print(f'{a} + {b} = {a + b}') if __name__ == '__main__': main()

一个容易被忽略的点:字符串.split(' ')这种写法,如果连续出现两个空格,会产生空字符串元素,需要自己过滤;而split(' ', 1)只切第一个空格,返回两个元素,第二个元素从第一个空格之后开始——这正好符合题面里“B 是剩下的整行内容”的定义。

所以在 plus 版里别用split()默认的无参拆分,它会按任意空白切干净所有 token,反而把 B 的原始形态弄丢了。

另外s.isdigit()对阿拉伯数字是可靠的,但如果你担心输入里混进全角数字之类的情况,更稳的写法是手动循环if not ('0' <= c <= '9'): return None。OJ 输入一般不会搞这种幺蛾子,但养成手写校验的习惯没坏处。

2.4 Java:Scanner.nextLine 与逐字符检查

import java.util.Scanner; public class Main { static Integer parse(String s) { s = s.trim(); if (s.isEmpty()) return null; for (char c : s.toCharArray()) { if (!Character.isDigit(c)) return null; } int v = Integer.parseInt(s); if (v < 1 || v > 1000) return null; return v; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNextLine()) { String line = sc.nextLine(); int sp = line.indexOf(' '); String left = sp >= 0 ? line.substring(0, sp) : line; String right = sp >= 0 ? line.substring(sp + 1) : ""; Integer a = parse(left); Integer b = parse(right); if (a == null && b == null) { System.out.println("? + ? = ?"); } else if (a == null) { System.out.printf("? + %d = ?%n", b); } else if (b == null) { System.out.printf("%d + ? = ?%n", a); } else { System.out.printf("%d + %d = %d%n", a, b, a + b); } } } }

Java 版里把parse返回值设成Integer而不是int,就是为了用null表示“非法”,避免再另找一个魔法数。初学者写 C 的时候爱用 -1 表非法,这在结果可能为负数的题目里容易打架;在 Java / Python 里用null/None更干净。至于主类为什么必须叫Main,不同评测系统要求不同,提交前看清楚平台说明就好。

3. 判题系统最在意的三件事:换行、空格、多余输出

代码逻辑写对只是第一步。在线评测系统拿你的程序输出和标准答案做的是“逐字符比对”,本地跑得再欢,只要输出多一个空格、少一个换行,都是错。

3.1 本地能跑与提交 AC 之间的那道坎

最常见的“本地通过、提交失败”原因,第一是system("pause")。很多教材为了在 Windows 下让黑窗口暂停,会在return 0前加这一句。这个东西提交到判题环境里,要么因为依赖了本机才有的编译配置导致 CE,要么在运行期打出一条无关的 shell 错误信息,纯粹是负收益。解决办法就一条:提交前把本地调试、暂停、打印日志的代码全部删掉,保留最干净的逻辑。

第二是多余的提示输出。有人会在读入前加一句printf("Please input two numbers: ");,这在课堂演示里是贴心,在判题系统里就是死刑。标准答案里根本没有这句话,你的输出就永远比对不上。

3.2 输出格式里的空格和问号,一个都不能多一个都不能少

plus 版的输出格式通常是:

A + B = sum

非法的一方用 ? 代替。注意两个加号两侧各有一个空格,等号两侧也各有一个空格,? 和数字之间同样有空格。我曾经见过一个同学,逻辑全对,就是printf里少写了等号左边的空格,结果整题 WA。输出格式这种事没有捷径,只能先把格式串抄到注释里,再对着样例逐字符核。

注意:题目说“输出 ?”时,只输出一个 ?,不要自作聪明写成带引号的 “?” 或感叹号,也不要输出两遍。

3.3 末尾换行要不要,题面没说就默认要

绝大多数评测系统对“最后一个字符之后是否换行”是宽容的,也就是说你输出46或46\n基本都能过。但不代表你可以随便在中间夹空行。多组测试是判题机连续喂的,你每处理一行输出一行,程序不退出就行。while (fgets(...) != NULL)这种写法就是为了吃掉所有行。

4. 用一张测试表把边界情况一次打穿

解析类题目的难点不在常规输入,在边界。我建议写完代码后先不急着提交,把下面这些用例逐个跑一遍,跟期望输出对照。

4.1 六组必测用例与期望输出

输入期望输出说明
12 3412 + 34 = 46最常规用例,验证主流程
12 3412 + 34 = 46连续两个空格,验证 B 的前导空白被 trim 掉
12 34 5612 + ? = ?B 是34 56,中间带空格,非法
1212 + ? = ?整行没有空格,B 为空串,非法
12 34? + ? = ?A 是空串非法;B 是12 34也非法(若平台先 trim 整行再切,结果会不同,以题面为准)
0 5? + 5 = ?0 不是正整数
1001 5? + 5 = ?超出范围

这七组里,第 2、3、5 组是最容易漏的。第 2 组验证你的 trim 有没有写对;第 3 组验证你是不是把 B 错误地当成了两个数;第 5 组验证你对“第一个空格”的定义是不是真的从行首开始找。

4.2 从“本地手测”到“批量对拍”的小脚本

手动一条条敲太慢,也容易手滑。我的习惯是把输入和期望输出分别写在in.txt和out.txt里,然后跑一个小脚本对拍。以 Python 为例:

import subprocess with open('in.txt', 'r', encoding='utf-8') as f: data = f.read() with open('out.txt', 'r', encoding='utf-8') as f: expected = f.read() # 假设你的程序编译成 main.out result = subprocess.run(['./main.out'], input=data, capture_output=True, text=True) if result.stdout == expected: print('AC') else: print('WA') print('期望输出:') print(expected) print('实际输出:') print(result.stdout)

C/C++ 的同学把gcc/g++编译命令同样写进脚本,以后做任何题都能复用。对拍不是竞赛选手的专利,入门阶段用它抓边界错误,比对着评测系统一次次提交要高效得多。

5. 提交报错后的排查链路:WA、CE、PE 分别怎么处理

如果你已经对拍了还是 WA,那就得学会看评测系统给的反馈。下面这张表是我这些年常用的排查顺序。

5.1 常见提交状态速查

状态含义优先排查方向
CE 编译错代码没过编译头文件缺没缺;是不是把 Windows 特有的头文件(如 conio.h)带上来了
RE 运行错进程异常退出数组越界、空串访问、指针没判空;重点查 split 之后 left/right 是否可能为空
WA 答案错逻辑或输出与期望不符先查格式串空格,再查边界用例,最后查数据范围
PE 格式错输出内容差不多但格式不对多半是空格、空行、换行数量问题
TLE 超时跑太久这题不太会遇到;如果遇到,查是不是 while 循环没退出

5.2 一次 WA 的典型定位过程

有次 A 同学拿这道题来找我,本地样例全过,提交 WA。我让他把样例输入稍微改一改,把12 34改成12 34(中间两个空格),他本地输出变成了12 + ? = ?。问题立刻暴露:他用的是scanf("%d%d")读两个数,第二个空格后面的内容被 scanf 留在缓冲区里,B 拿到的是34,又因为没 trim 直接判了非法。

这就是典型“读入方式不符合题面定义”的坑。scanf是按空白自动切词的,它天生就不适合“B 是剩余整行”这种需求。换成整行读取、手动切第一个空格、再去 trim,问题就没了。

这个案例想说明的是,WA 之后不要急着改输出,先回去检查“输入到底是怎么被拆开的”。很多解析题的 bug,根源都出在第一步的拆分,而不是后面的加法或打印。

5.3 把这道题的经验沉淀成三个习惯

这道题做完,我希望读者带走的不只是 AC 记录,而是三个可以复用的习惯:第一,拿到任何输入输出题,先在纸上写清输入格式、数据范围、输出格式三行字;第二,解析类题目优先考虑整行读取加手动拆分,别依赖任何自动跳过空白的读法;第三,写一个可复用的对拍脚本,把它变成做题的标准动作。

这三个习惯对后面所有字符串处理题、模拟题都有效。我自己后来做更复杂的题,也一直沿用这套流程。题可以很简单,但流程一旦建立起来,复杂题就是重复套同样的步骤而已。

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

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

立即咨询