二、计算:变量、运算与输入输出
这一页 5 章 49 道题,是整个讲义里题量最大的一页。
它只解决一件事:把题目给的数据读进来,按题目说的关系算出去。
听起来简单,但这一页会暴露初学者最多的两类问题:一是把题目里的数字直接手算成答案写死在代码里,二是算出来了但格式不对(小数位、整除、单位)。这两类问题在后面每一章都会重复出现,所以在这里必须彻底解决。
学完这一页,你应该能拿到任何一道「给几个数、求一个结果」的题,十分钟内写出能过样例的代码。
3. 变量与整数输入
第 1、2 章的数据都是你写死的。这一章开始,数据由题目在运行时给你。
3.1 变量:给数据起个名字
int a; // 声明一个叫 a 的整数变量
a = 5; // 把 5 放进去
int b = 3; // 声明的同时放进去
int 是 integer 的缩写,表示整数。变量名是给内存起的名字,后面用名字就能取到里面的数。
命名的硬规则只有三条:只能用字母、数字、下划线;不能数字开头;不能和 int、return、for 这些关键字重名。另外 C++ 区分大小写,a 和 A 是两个不同的变量。
软规则是:题目只有两三个数时用 a b c 没问题;变量一多,sum、age、total 这种名字能让你三天后还看得懂自己写了什么。
3.2 scanf:把数据读进来
int a, b;
scanf("%d %d", &a, &b);
scanf 和 printf 是镜像的,对比着记最快:
printf |
scanf |
|
|---|---|---|
| 格式串 | "%d\n" |
"%d" |
| 后面的参数 | 变量本身:x |
变量的地址:&x |
| 换行 | 要自己写 \n |
千万别写 \n |
第一个坑:& 不能丢
scanf("%d", &a); // 对
scanf("%d", a); // 错,程序会崩溃
& 是「取地址」。printf 是把值送出去,所以告诉它值就行;scanf 是把值接收进来,必须告诉它「放到内存的哪个位置」,所以给地址。
漏掉 & 的后果不是编译错误,是运行时崩溃或者读到奇怪的值。这是 scanf 最高频的错误,没有之一。
第二个坑:格式串里不要写 \n
scanf("%d\n", &a); // 错
scanf("%d", &a); // 对
scanf 的格式串描述的是「要读什么」,不是「要输出什么」。写 \n 会让它去匹配一个换行符,匹配不上就一直等——表现是程序卡在那里,光标闪着,就是不往下走。
很多人第一次遇到这个,会以为是自己电脑坏了。
格式符对照表
| 变量类型 | scanf 用 | printf 用 | 例子 |
|---|---|---|---|
int |
%d |
%d |
scanf("%d", &a); |
long long |
%lld |
%lld |
scanf("%lld", &x); |
float |
%f |
%f |
scanf("%f", &f); |
double |
%lf |
%f |
scanf("%lf", &d); |
char |
%c |
%c |
scanf("%c", &c); |
| 字符数组 | %s |
%s |
scanf("%s", s); |
这张表里有三处会咬人,提前说:
double 的读写用的不是同一个格式符
double d;
scanf("%lf", &d); // 读:%lf
printf("%f\n", d); // 写:%f
scanf 读 double 必须 %lf(l 是 long,表示这个是 8 字节的浮点数)。用了 %f 会只写进去 4 个字节,剩下 4 个字节是垃圾值,读出来是个天文数字。
而 printf 输出 double 用 %f 就行——printf 看到浮点参数会自动提升成 double,所以 %f 通吃 float 和 double。
一句话:读 double 用 %lf,写 double 用 %f。 不对称,记住。
%c 会读到空格和换行
这个坑要到第 31 章才算真正解决,但你现在就应该知道:
int n; char c;
scanf("%d", &n);
scanf("%c", &c); // 读到的很可能是上一行末尾的换行符!
输入 5 然后回车时,scanf("%d") 只吃掉了 5,回车还留在缓冲区里。下一个 %c 就把这个回车读走了。
标准解法是在 %c 前面加一个空格:scanf(" %c", &c);。那个空格的意思是「先跳过所有空白字符」。
以后凡是 %c,前面都加空格,养成习惯就不会错。
%s 读字符串时不加 &,因为数组名本身就是地址。这个到第 31 章再说。
多个输入
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
格式串里的空格不表示「必须有一个空格」,它表示「跳过任意数量的空白(空格、换行、制表符)」。所以下面两种输入都能正确读进三个数:
3 5 8
3
5
8
实际写的时候,%d%d%d 和 %d %d %d 效果完全一样。加空格只是为了你自己看得清楚。
为什么这本讲义用 scanf 而不是 cin
C++ 里还有一种写法 cin >> a >> b;,不用写 &、不用管格式符,看起来更省事。但还是建议你用 scanf,三个理由:
- 快。
cin默认和 C 的输入输出同步,读大量数据时慢 3–5 倍。第 12 章开始你会读几万个数,这个差距就是「过」和「超时」。 - 能精确控制格式。
scanf("%d:%d", &h, &m)可以直接读12:30这种数据,用cin得自己处理冒号。 - 和
printf配套。 输入输出的写法统一,不用在两套语法之间来回切换。
如果哪天你非要用 cin,记得在 main 开头写 ios::sync_with_stdio(false); cin.tie(0); 关掉同步。但既然都要写这行了,不如直接用 scanf 干净。
第一次读输入
这道题就是读两个数、输出和。故意把 scanf 和 printf 各写一行,别图省事压成一行——后面题目复杂了你会感谢自己。
如果本地跑出来是 0、随机数,或者干脆崩溃,按这个顺序查:
&写了吗?- 格式串是不是写成了
"%d\n"? printf的坑数和参数个数对得上吗?
3.3 别手算,让表达式算
初学者最常犯的错:看到「3 个苹果每个 5 元」,就在代码里写 printf("15\n");。样例能过,一交就错——题目换组数据你就全错。
正确做法是把题目的关系翻译成表达式:
int n, price;
scanf("%d %d", &n, &price);
printf("%d\n", n * price);
题目说「n 个、每个 price 元」,代码就写 n * price。输入怎么变都对。
翻译时最常出错的是括号。数学里写 (a+b)×c 有括号,代码里必须也写括号:
printf("%d\n", (a + b) * c); // 对
printf("%d\n", a + b * c); // 错:先算了 b*c
把关系翻译成表达式
四道题,难度是递增的。做的时候心里默念一遍「题目说的是什么关系」,再落笔。
第 4 题可能会让你犹豫:(a+b)/c 如果除不尽怎么办?先按整数除法做,看看样例对不对——下一节正好讲这个。
3.4 整除和取余:题目里的两种「除」
生活里的除法有两种问法,C++ 里对应两个符号:
| 题目里的问法 | 用 | 例子 |
|---|---|---|
| 能装几袋 / 每人几个 / 能做几次 | / |
17 / 5 = 3 |
| 还剩几个 / 余多少 | % |
17 % 5 = 2 |
int total = 17, per = 5;
printf("%d\n", total / per); // 3 能装满几袋
printf("%d\n", total % per); // 2 还剩几个
整数除法直接砍掉小数,不四舍五入。 7 / 2 是 3,1 / 2 是 0。这不是 bug,是规则。
还有一条极其隐蔽的坑,几乎每个人都会踩一次:
int f = 100;
printf("%d\n", 5 * (f - 32) / 9); // 37 对
printf("%d\n", 5 / 9 * (f - 32)); // 0 错!
第二行错在:先算 5 / 9,两个整数相除得 0,再乘什么都还是 0。
结论:连乘连除时,先乘后除。 把除法留到最后一步,中间结果才不会被截断。
整除、取余,还有那个 0
第 3 章验收
- 能用
scanf("%d %d", &a, &b);读入两个整数 - 能把「先加后乘」正确地翻译成带括号的表达式
- 分得清
/和%各回答什么问题 - 知道
5 / 9等于 0,所以要先乘后除
4. 整数运算与数字处理
这一章所有题都围绕一件事:把一个整数的每一位拆出来,再拼回去。
为什么重要?因为后面判断回文、数位统计、进制转换、日期处理,全是从这里长出来的。
4.1 拆出每一位:/10 和 %10
一个十进制数 152,拆法是这样的:
int n = 152;
printf("%d\n", n % 10); // 2 个位
printf("%d\n", n / 10); // 15 去掉个位
printf("%d\n", n / 10 % 10); // 5 十位
printf("%d\n", n / 100); // 1 百位
记这两条,比记公式有用:
% 10取出最后一位/ 10丢掉最后一位
反复做这两步,就能从右往左走完整数:
int n = 152;
while (n > 0) {
printf("%d ", n % 10); // 2 5 1
n = n / 10;
}
这个 while 循环第 16 章会正式讲,现在先看懂它的意思:只要还有数位,就取出最后一位、再砍掉它。
拆一个三位数
4.2 拼回去:逆序与重组
拆出来之后,重新拼成一个新数,靠的是乘 10 加:
// 把 1、5、2 拼成 152
int x = 1;
x = x * 10 + 5; // 15
x = x * 10 + 2; // 152
x = x * 10 + d 这个动作,就是把 x 整体左移一位,再在末尾填上 d。
逆序的道理一样,只是取位顺序反了:从右往左取,就往右往左填。
把数倒过来
- 反向输出一个三位数 —— 三位数逆序,位数固定,可以直接用三个变量做
- 反向输出一个三位数(加强版) —— 加强版。位数不固定了,得用循环,见 4.1 末尾那段
while - 数字反转 —— 注意末尾可能出现的 0
三道题做完,你应该能回答:为什么 x = x * 10 + d 能实现「拼接」?
4.3 加密、日期、格式:把数位当零件用
这一类题不再求「和是多少」,而是把每一位当成零件,按题目给的规则重新组装。
比如四位数加密、日期 20260315 拆成 2026-03-15,本质都是同一件事:拆位 → 按规则处理 → 拼回。
int d = 20260315;
int y = d / 10000; // 2026
int m = d / 100 % 100; // 3
int day = d % 100; // 15
注意 m 那一行:d / 100 得到 202603,再 % 100 拿到最后两位 03。这是「先砍掉尾巴、再取末尾」的组合技,比硬算简单。
数位当零件
4.4 整除的坑:不够一份也要算一份
上一章讲过 / 是「能装满几份」。但题目经常问的是「至少需要几份」——比如 17 个苹果每袋装 5 个,需要 4 袋而不是 3 袋。
这两种问法差一个「向上取整」,写法完全不同:
int n = 17, per = 5;
printf("%d\n", n / per); // 3 能装满几袋(向下取整)
printf("%d\n", (n + per - 1) / per); // 4 至少需要几袋(向上取整)
(n + per - 1) / per 是整数向上取整的标准写法。它的道理是:先在总数上补「差一点点就够一份」的量,再整除,就把余数也顶成一份了。
别用 ceil()
你可能会想到 ceil(17.0/5)。能算对,但浮点数在大数时会有精度误差,ceil 偶尔会差 1。整数题就用整数写法,(n + per - 1) / per 永远不会有误差。
还有一种问法:「超过这个数就要多算一份」,比如虫子每 2 小时吃一个苹果,问 n 小时后剩几个。这类题的坑在于边界——正好整除时不能多算。
差一份和多一份
- 苹果和虫子 —— 第一次遇到「正好整除」的边界
- 苹果和虫子(加强版) —— 加强版,条件更绕,建议先在本子上算几个小例子再写代码
两题的共同点:先在纸上手算 3 组数据,确认你的公式对,再写代码。特别是「正好整除」那一组,它是唯一的失分点。
4.5 剩下的应用题
前面学的都用得上,剩下的题是把公式套进实际情境。
公式与应用
- 等差数列的第 k 项 —— 纯公式
a + (k-1)*d - 小玉买文具 —— 学完 4.4 的向上取整再回来看这题,会顺很多
这类题的通用做法:先写出通项公式,再把题目给的量代进去。别一上来就写代码,先在纸上把公式写出来。
第 4 章验收
- 能说出
% 10和/ 10各自的作用 - 会用
x = x * 10 + d拼数 - 知道「能装几份」和「至少几份」是两个不同的算法
- 有「先手算三组小数据」的习惯
5. 浮点运算与输出格式
前面全是整数。这一章引入小数,而小数带来的第一个麻烦不是计算,是输出格式。
5.1 double 与「保留几位小数」
double x = 3.14159;
printf("%f\n", x); // 3.141590 默认 6 位
printf("%.2f\n", x); // 3.14 保留 2 位
printf("%.0f\n", x); // 3 不要小数
%.2f 拆开看是两截:
f—— 这是个浮点数.2—— 小数点后保留 2 位
printf 输出浮点数默认保留 6 位,所以要几位就写几位。保留是四舍五入的:
printf("%.1f\n", 3.45); // 3.5
printf("%.1f\n", 3.44); // 3.4
这只是显示上的处理,变量里存的值没变。 x 还是 3.14159,只是打印时被截了。
顺带记住另一个高频写法 %02d:宽度 2 位,不足补 0。日期时间题必用。
printf("%02d:%02d\n", 9, 5); // 09:05
同理 %5d 是宽度 5、补空格右对齐,%-5d 是左对齐。第 12 章输出图形时会用到。
除法要注意:double 除以 int 得到 double,但两个 int 相除还是 int:
int a = 7, b = 2;
printf("%.2f\n", a / b); // 3.00 ← 错!
printf("%.2f\n", 1.0 * a / b); // 3.50 ← 对
第一行为什么错?因为 a / b 在赋值或输出之前就已经按整数算完了,得到 3,转成 double 也只是 3.0000。
输出格式
5.2 圆、球与 M_PI
几何题离不开圆周率。有两种写法:
const double PI = 3.14159; // 题目给了就用题目给的
double r; scanf("%lf", &r);
printf("%.2f\n", PI * r * r);
题目给的 π 值必须照抄,不能自己写 3.14 或 3.1415926。有些题明确说「取 π = 3.14」,用了更精确的值反而错。
M_PI 是 cmath 里的常量,约 3.14159265358979。只有在题目没说 π 取多少时才用。
圆和球
5.3 三角形面积:海伦公式
已知三边求面积,用海伦公式:
p = (a + b + c) / 2
S = √(p(p-a)(p-b)(p-c))
double a, b, c;
scanf("%lf %lf %lf", &a, &b, &c);
double p = (a + b + c) / 2;
printf("%.2f\n", sqrt(p*(p-a)*(p-b)*(p-c)));
sqrt() 是开平方,在 cmath 里。注意 p 必须是 double——如果 a+b+c 是奇数,整数除法会让 p 丢掉 0.5,后面全错。
顺带一提,判断三条边能不能组成三角形,是第 10 章的内容,这里先用「题目保证能构成」的前提。
三角形面积
5.4 生活计算:打折、利息、体重
这一组题不难,但每一题的坑都在「单位」和「取整方式」上:
- 打折题:是打几折(乘 0.85)还是减多少钱(减 15),看清楚
- 利息题:年利率要不要除以 100,存几年要不要乘年数
- 换算题:英尺英寸、斤公斤,换算系数题目会给,照抄
// 打折示例:原价 x,打 y 折
double x; int y;
scanf("%lf %d", &x, &y);
printf("%.2f\n", x * y / 10);
y 折 表示原价的 y/10。八折就是 8/10 = 0.8。这里 y 是整数,y/10 会是 0,所以要先乘后除:x * y / 10。
单位换算与打折
5.5 综合
把前面学的拼起来
- 找零钱 —— 可能需要整数和小数混用
- 计算多项式的值 —— 多项式求值,注意每一项的系数和幂次
- 逗号分隔符 —— 输出格式题,考的是标点位置
- 计算两个数的和差积商 —— 和差积商,四个结果一起输出
396 和 83 这两题纯粹考输出格式:空格在哪、逗号在哪、最后有没有换行。做完把输出和自己的预想逐字比一遍。
第 5 章验收
- 会用
fixed << setprecision(n)控制小数位 - 知道两个
int相除会丢小数,要写1.0 * a / b或4.0/3 - 题目给的 π 值会照抄,不自己发挥
- 做生活计算题时,先确认单位再动笔
6. 字符与 ASCII
这一章换个视角:字符在计算机里其实就是整数。
6.1 char 就是一个小整数
char c = 'A';
printf("%c\n", c); // A
printf("%d\n", c); // 65
'A' 在内存里存的不是字母 A,是数字 65。printf("%c", c) 时它按字符解释,输出 A;(int)c 强制按整数解释,输出 65。
这张对照表你必须记住三个锚点:
| 字符 | 码值 |
|---|---|
'0' |
48 |
'A' |
65 |
'a' |
97 |
其他都能推:'B' 是 66,'b' 是 98,'1' 是 49。
三个关键规律:
- 数字字符
'0'~'9'是连续的,所以'7' - '0'= 7 - 大写字母连续,
'D' - 'A'= 3 - 小写字母连续,
'd' - 'a'= 3 - 但大写和小写之间差 32,中间隔了几个符号,不连续
注意 'A' 和 "A" 是两回事:单引号是字符,双引号是字符串(第 31 章讲)。
字符的码值
- 输出字母的ASCII值 —— 就是
(int)c - 字母的序号 ——
A是第几?用c - 'A' + 1
第 2 题那个 +1 别漏:'A' - 'A' 是 0,但题目说的序号通常从 1 开始。
6.2 字符和数字的互转
这是初学者最容易混的一组概念,分清楚:
// 字符 '5' → 数字 5
char c = '5';
int n = c - '0'; // 5
// 数字 5 → 字符 '5'
int x = 5;
char d = x + '0'; // '5'
为什么要减 '0'? 因为 '5' 的码值是 53,直接 (int)'5' 得到的是 53 不是 5。减去 '0'(48)才是真正的数字 5。
反过来,5 + '0' = 53,正好是 '5' 的码值。
同理,数字转字母:1 + 'A' - 1 或 x - 1 + 'A',具体看题目是 0 起还是 1 起。
字符与数字互转
6.3 在字母表上移动
凯撒密码、字母邻居、字母距离,本质都是同一件事:把字符转成编号,做加减,再转回字符。
char c = 'x';
char d = c + 3; // '{' ← 错了,跑到字母表外面
char e = (c - 'a' + 3) % 26 + 'a'; // 'a' ← 对,转回来了
% 26 是关键。字母表是环形的,z 后面接 a,用取模就能实现「转一圈回来」。
写这类题的统一套路:
- 字符减
'a'(或'A'),变成 0~25 的编号 - 编号做加减,需要回绕就
% 26 - 加回
'a'(或'A'),变回字符
char shift(char c, int k) {
return (c - 'a' + k % 26 + 26) % 26 + 'a';
}
那个 + 26 是为了处理 k 为负数的情况——C++ 里负数取模结果是负的,(-3) % 26 是 -3 不是 23。加上 26 再取模就正常了。(这段是个函数,第 27 章会讲。)
字母表上的移动
第 6 章验收
- 记得住
'0'=48、'A'=65、'a'=97 - 知道
'5' - '0'是 5,(int)'5'是 53 - 会用
% 26实现字母表回绕 - 知道负数取模要先
+ 26
7. 综合应用
这一章四道题,知识点都在前面出现过,考的是能不能组合起来用。
7.1 取模让数字循环
星期几、时钟、环形队列,都是「到顶了回到开头」:
// 今天是星期 d(1~7),问 n 天后是星期几
int ans = (d - 1 + n) % 7 + 1;
先减 1 变成 0~6,加 n,取模 7 绕回来,再加 1 变回 1~7。
为什么不能直接 (d + n) % 7? 因为那样 7 会变成 0,而星期没有 0。减 1 加 1 就是为了把 1~7 映射到 0~6 再映射回来。
日期里的取模
- 后天是星期几 —— n=2 的简单情况,先做这题找感觉
7.2 位权与进制
152 这个数,其实是 1×10² + 5×10¹ + 2×10⁰。每一位乘的 10 的幂,叫「位权」。
换进制就把 10 换成别的:
// 二进制 1011 转十进制
int b = 1011;
int ans = 0, w = 1;
while (b > 0) {
ans += (b % 10) * w; // 当前位 × 位权
w *= 2; // 位权翻倍
b /= 10;
}
// ans = 11
这是「按位拆分」和「乘方」的组合,两个都在前面学过。
进制与幂
7.3 时间的换算
时、分、秒之间的换算是 60 进制,跟前面 10 进制的思路一样,只是除数是 60:
int total; // 总秒数
scanf("%d", &total);
int h = total / 3600;
int m = total % 3600 / 60;
int s = total % 60;
注意第二行:total % 3600 先去掉整小时,剩下的再 /60 得到分钟。
时间计算
- 时间计算问题 —— 可能是换算也可能是加减,读清楚题目
如果涉及跨天,注意小时不要出现负数,需要 +24 再 %24。
第 7 章验收
五章 49 题全部做完,你应该达到这个状态:
- 拿到任何「读入几个数、算一个结果」的题,10 分钟内能写出能过样例的代码
- 看到除法会先问:要不要小数?向上还是向下取整?
- 看到小数输出会先找题目要求保留几位
- 看到字符会本能地想「它的码值是多少」
如果这四件事还做不到,不要急着进第 8 章。 判断和循环的题会把计算当工具用,工具不熟,后面每一步都会慢半拍。
延伸阅读
服务器上有一整套 OI-Wiki(621 页),是给竞赛选手写的参考手册。下面几页跟这一页相关,但不是每一页都该现在看——标了「现在跳过」的,等以后再说。
看不懂是正常的,不是你的问题
OI-Wiki 默认读者已经会写程序、目标是打 NOI。你现在需要的那部分,讲义里已经写了; wiki 是用来「以后查得更深」的,不是用来补课的。