文章

【信奥业余科普】C++ 的奇妙之旅 | 12:程序的交互与加工——数据的输入与算术运算

在上一篇文章中,我们介绍了变量的概念,理解了程序是如何在内存中开辟“收纳空间”存放不同类型数据的。然而,如果一个程序只能在代码里写死固定的数字(比如永远只算 12 + 5),那它只具备计算器的单一计算功能,算不上灵活的软件或算法。

为了让程序能够根据现实情况动态处理问题,它必须具备从外部获取数据的能力,并在内部完成特定的数学加工。今天,我们就来探讨 C++ 程序获取外界信息和进行基础数据运算的标准逻辑。

写在前面的话:这是一系列专为对信奥(信息学奥赛)感兴趣的中小学生及家长朋友们准备的科普文章。笔者受自身学识所限,文中若存在不严谨之处,还望各位读者指正。

本系列文章往期回顾:

第二部分 【C++的奇妙之旅】

  • [【信奥业余科普】C++ 的奇妙之旅09:信奥赛场的核心语言——C++ 的前世今生](https://www.coderli.com/cs-basics-09-cpp-intro)
  • [【信奥业余科普】C++ 的奇妙之旅10:代码是如何运行的?——编译过程与“Hello, World”](https://www.coderli.com/cs-basics-10-cpp-helloworld)
  • [【信奥业余科普】C++ 的奇妙之旅11:程序的处理核心——变量与常用数据类型](https://www.coderli.com/cs-basics-11-cpp-variables)

一、 如何获取外部输入:cin 的工作逻辑

在 C++ 中,最常用的获取外部输入数据的方法是使用标准输入缓存流工具:std::cin(读作 c-in)。

1. 流动方向:向右的箭头 >>

如果我们要让用户在键盘上敲下一个数字,并存入我们提前准备好的变量 score 中,代码通常如下:

1
2
int score;
std::cin >> score;

与输出数据到屏幕的 std::cout << 相对应,输入数据时搭配的符号是两个大于号 >>。在 C++ 语法结构中,这被称为提取运算符(Extraction Operator)

初学者往往容易把这两个符号写反。我们可以从“数据流动”的视觉逻辑去记忆它:箭头总是指向数据流动的目的地方向。

  • 当输出(显示)时 cout << "Hello":箭头指向代表屏幕的 cout 对象,意味着文本数据流向了屏幕。
  • 当输入(读取)时 cin >> score:箭头指向代表具体内存块的变量 score,意味着数据从外部的 cin 键盘流中被抽出,并准确注入了这块内存里。

2. 运行时的主动暂停(阻塞机制)

当程序按照指令从上到下逐行运行时,一旦 CPU 遇到了一句含有 cin 的代码,程序并不会立刻跳到下一行继续执行,而是会触发系统阻塞(Blocking)机制

处于阻塞状态的程序会暂时交出硬件的控制流,在后台默默挂起。它此时正在等待操作系统通过输入通道向它投递字符信息。在控制台界面中,这意味着程序正停在原地,等待用户用键盘敲打字符并最终按下回车键(Enter)

按下回车键这一动作,相当于推开了一扇数据交互的闸门。操作系统会将暂时记录在缓冲区的一大串字符转化为 C++ 期待的数据类型(例如我们声明的整型数字类型 int ),将其填入变量 score 所在的内存空间中。一旦数据填入成功,程序才会马上解除阻塞,继续执行下文的代码逻辑。在将来的信奥竞赛(如 CSP)在线评测平台上,系统后台也正是利用这种标准的输入通道,将比赛的测试用例文件批量且迅速地“灌入”到你编写的解答程序中的。

二、 数据的加工处理:基础算术运算的设计规则

拿到输入数据后,最基础的加工方式就是数学运算。C++ 标准中提供了与基础数学高度一致的算术运算符:加(+)、减(-)、乘(*)、除(/)以及取模求余(%)。

大多数运算符的使用逻辑与数学课本完全相通,但在底层强类型数据的设计约束下,除法(/取模(% 有两项常常导致新手失分的特殊计算规则。

1. 自动截断:整数除法的设计逻辑

在常规数学中,5 除以 2 的结果是精确的 2.5。但在运行 C++ 程序时执行以下代码,得出的结果却不符合常规预期:

1
2
int result = 5 / 2;
// 此时 result 的值是 2,而不是 2.5

这并非 C++ 的计算结构出现了错误,而是因其底层强类型机制衍生出的一项运算规则:如果参与除法的双方都是整数类型(例如 int),那么编译器会直接调用 CPU 的整数运算硬件指令去执行除法,且最终产出的结果也强制维持为整数类型。 在计算机处理整数指令的环境里,是不存在处理附加小数位功能设定的。因此这个除法会直接以向下取整的方式执行(也被称为“截断舍位”),底层直接抛弃了末尾的 .5,结果运算只保留了整数商 2

如果想要得到完整且精确的 2.5,就必须至少要求参与运算的其中一方为浮点数值类型(表示为 5.0 / 2 )。只有这样,编译器才会切换轨道,调用浮点数的法则进行带小数的复杂计算。这也是我们在做算法竞赛题时,一旦题目涉及平均数计算、折半百分比等极易产生小数的内容,就必须及早声明和使用 double 变量类型的根本逻辑依据。

扩展思考:为什么 Python 等动态语言会自动算出 2.5 很多接触过 Python 的同学可能知道,在 Python 里执行 5 / 2 会自动得到带小数的结果 2.5。这是因为 Python 等高级动态语言在底层默默做了“自动转换与包办”的工作:当解释器发现除不尽时,它会在内存中自动生成一个新的浮点数(Float)对象来存放结果。 这种设计对编写代码的人极其友好,无需关心底层数据是怎么分配的。但正如我们在上一篇文章中所说,C++ 为了追求指令执行的绝对高效内存的精确控制权,坚决摒弃了这种“后台悄悄转换”的机制。在 C++ 设计者的理念中:既然你输入的是两个整数,CPU 就只该走最高效的整数运算通道;任何数据类型轨道的切换,都绝对不能由机器自作主张,必须由程序员在代码中显式指定。

2. 取模运算(%):算法题目的切片工具

除了基础的加减乘除,C++ 提供了一个在数据结构处理中使用非常频繁的运算符:%(通常读作 Modulo,或简称为“取模运算”与“求余数运算”)。 它的作用是专门计算两个整数触发整数除法后,剩下来的那个余数。例如 5 % 2 的结果是数字 1(因为5除以2刚好商2且剩余1)。

这个运算符在日常生活中的算账体系下似乎用得不多,但在计算机底层算法(特别是信奥相关的考题中)的应用率极高。它的典型用处包括但不限于:

  • 判断数的奇偶性:对任意整型数执行 n % 2,如果结果刚好等于 0 则说明它可以被二整除是一个偶数,如果结果等于 1 则是奇数。大部分算法程序便是借此在逻辑判断的分叉路口做出选择决策。
  • 剥离数值的各个部分:当程序遇到一个较大的数字(如 1583),想要单独取出它的末尾个位数 3 存进新变量,只需执行 1583 % 10 即可轻松办到。配合整数除法向下截断位数的规则,程序就能把任意庞大的十进制数按照个十百千位独立拆解开来进行下一步分析。
  • 循环周期限制机制:如果要让一个持续增长的计数器在达到某个上限后重新归零(比如代表一周七天的固定循环),可以直接对变量持续使用 % 7。这样无论经历多少天数的累加,结果最终都会被强行收缩限制在 06 这个固定的有效结果区间内。

三、 一个基础的综合代码示例

结合上面关于输入工具与数学加工的逻辑知识,我们可以很快写出一段能够完整跑通“输入数据-处理加工-输出展示”这一全基本流程的代码样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

int main() {
    int total_apples;
    int kids;

    // 1. 外部数据输入阶段
    std::cout << "请输入苹果总数与小朋友人数:";
    std::cin >> total_apples >> kids;  // 此时程序产生阻塞,停在原地等待键盘输入(支持用空格隔开依次填入)

    // 2. 内部逻辑加工处理阶段
    int apples_per_kid = total_apples / kids; // 调用整数除法机器指令,求出每人实际能分到几个完整的苹果
    int apples_left = total_apples % kids;    // 调用取模求余运算,求出最后还剩下几个不足以平分的苹果

    // 3. 屏幕输出展示结果阶段
    std::cout << "每个小朋友可以分到:" << apples_per_kid << " 个苹果" << std::endl;
    std::cout << "筐子里最后还剩下:" << apples_left << " 个苹果" << std::endl;

    return 0; // 回报程序无错执行完毕
}

结语

从外部获取输入(Input)、并在内部借助系统运算符进行数学加工与处理(Process)、进而面向用户最终展示(Output),这就构成了一切计算系统和程序运作的最底层流水线大框架。

正如你在分配苹果的示例程序中所看到的那样,通过简单组合算术符号,程序具备了基础的数据加工能力。在此过程中,我们自然地向内存录入了数据并执行了运算。但这引出了两个我们在编程时必须面对的客观物理限制:第一,计算机由于受到内存容量的限制,存放数值的上限并不是无限的;第二,受到底层二进制机制的约束,机器在处理带有小数的算术运算时,无法做到绝对的精确。

下期预告: 如果不了解这两个底层的客观规律,我们在编写程序时极易触发“数据溢出(常见为爆 int)”以及“浮点精度丢失”(例如 0.1 + 0.2 ≠ 0.3)等问题。在下一篇文章中,我们将暂缓新语法的学习,转而探讨计算机底层的二进制机制是如何存储数据的,并客观解释产生这些溢出和精度偏差的根本原因。

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

GESP 学习专题站:GESP WIKI

"luogu-"系列题目可在洛谷题库进行在线评测。

"bcqm-"系列题目可在编程启蒙题库进行在线评测。

欢迎加入Java、C++、Python技术交流QQ群(982860385),大佬免费带队,有问必答

欢迎加入C++ GESP/CSP认证学习QQ频道,考试资源总结汇总

欢迎加入C++ GESP/CSP学习交流QQ群(688906745),考试认证学员交流,互帮互助

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
本文由作者按照 CC BY-NC-SA 4.0 进行授权