文章目录
- [1.4 常量](#1.4 常量)
- [1.5 注释](#1.5 注释)
- [1.6 顺序结构应用](#1.6 顺序结构应用)
- [1.7 文件操作](#1.7 文件操作)
1.4 常量
在前面的例题中,我们使用了 13、0.5、1.0 等数据直接参与算术运算,像这种用真实数据直接表示的量称为常量。13 是整型常量,0.5 和 1.0 是浮点型常量。由于整型和浮点型有多种不同范围的类型,因此 C++ 规定了常量的默认类型,整型常量默认是 int 类型,浮点型常量默认是 double 类型 。其它类型的常量可以通过添加后缀实现,例如 3LL 和 3ll 表示 long long 型常量,3.14F 和 3.14f 表示 float 型常量。
【小贴士】浮点型常量的表示方式是很灵活的,除了上面的表示方式,还可以省略整数部分或者小数部分,例如 0.5 可以写作 .5,1.0 可以写作 1.。还有一种表示方式称为科学计数法,例如 0.0314E2 和 314.15e-2,分别表示 0.0314 × 10 2 0.0314×10^2 0.0314×102 和 314.15 × 10 − 2 314.15×10^{-2} 314.15×10−2。
字符型常量比较特殊,需要用单引号包裹起来,例如 'A'、'h'、'7'、'='、'*'、' '(空格)。另外有一些特殊字符无法直接表示,C 语言中规定了一些转义字符进行表达。所谓转义字符,是以反斜杠(\)开头的字符,表示将其之后字符的原本含义进行转换,例如 '\n'(换行)、'\0'(空字符)、'\\'(反斜杠)。
由于计算机只能识别二进制信息,因此字符数据必须转换为二进制信息,转换过程称为编码。char 类型采用的编码规则是 ASCII 码(美国信息交换标准代码),规定了常用的 128 个字符的编码,包括大小写字母、数字、标点符号以及常用的控制字符。例如字符 'A' 的 ASCII 码为 65,计算机会将 65 转换为二进制并存储起来,其余大写字母的 ASCII 码按照字母表顺序依次增加,'a' 为 97,'0' 为 48,剩余小写字母与数字也是按顺序递增。
上面所说的常量称为字面值常量 ,简称字面量,在实际使用时并不方便,尤其是大量使用同一字面量时,不论是代码的可读性还是可维护性都会大打折扣。为了解决这个问题,我们引入符号常量。
【D1039 [OpenJudge] 与圆相关的计算】给出圆的半径,求圆的直径、周长和面积。输出时数与数之间以一个空格分开,每个数保留小数点后 4 位,约定 π = 3.14159 π=3.14159 π=3.14159。
这里会多次使用 π π π 这个常量,如果将其声明为符号常量,后续修改起来会方便许多。虽然使用变量来存储也能达到相同的效果,但是如果不小心在程序中修改了 π π π 的值,后续计算的结果都将出错。符号常量的意义在于,当你尝试在程序中修改其值时,编译器会拒绝编译并给出编译错误的提示信息,即符号常量的值不允许在程序中修改。这也意味着符号常量在声明的同时必须给定初始值。下面两种方式都是符合 C++ 语法规则的。
cpp
const double PI(3.14159);
double const PI = 3.14159;
其中 const 表明这是一个符号常量的声明,可以出现在类型标识符之前或之后。第一种设定初始值的方式专门用于变量或符号常量的初始化中;第二种方式中 = 称为赋值运算符,表示将其右边表达式的值赋给左边的变量或符号常量,可以用在任何允许赋值的地方。为了便于在程序中区分符号常量和变量,通常会用大写字母表示符号常量。
C 语言可以通过宏定义的方式来实现类似的效果,代码格式如下,会在编译之前的预处理过程中用 3.14159 替换掉所有的标识符 PI。
cpp
#define PI 3.14159
不论哪种风格的常量,一般都会将其定义在 main 函数上面,详见代码。
cpp
#include <iostream>
#include <cstdio>
using namespace std;
const double PI = 3.14159;
int main()
{
double r;
cin >> r;
printf("%.4f %.4f %.4f\n", 2 * r, 2 * PI * r, PI * r * r);
return 0;
}
由于需要输出多个结果,因此 printf 使用了多个格式控制符。前面说到格式控制符的作用之一是占位,除了格式控制符会被替换掉,其余信息会原样输出,于是可以在相邻两个控制符之间添加 1 个空格来满足输出格式要求。最后的 '\n' 表示换行,printf 只能用 '\n' 来换行。除此之外,格式控制符和变量必须在数量、顺序上保持一致。
1.5 注释
为了进一步增加代码的可读性和可维护性,可以通过注释对代码做更详细的说明,比如在源代码开头添加作者、程序功能等信息,亦或是对程序中较为复杂的部分进行注解说明。而编译器会对这些注释"视而不见",将它们统一当作空格来处理。
cpp
/* Author: Mr. Gao
* Date: 2025-12-17
* Function: 根据圆的半径计算圆的直径、周长、面积
*/
#include <iostream>
#include <cstdio>
using namespace std;
// 圆周率
const double PI = 3.14159;
int main()
{
double r; // 半径为浮点型数据
cin >> r;
printf("%.4f %.4f %.4f\n", 2 * r, 2 * PI * r, PI * r * r);
return 0;
}
上面的程序开头部分使用 /* 和 */ 包裹起来的部分称为多行注释,这两个符号之间的所有内容全都会被编译器认为是注释,中间 2 行开头的 * 是为了美观。注意多行注释不能嵌套书写。
使用 // 开头的部分称为单行注释,它会将其后直到行末的内容都设置成注释。
1.6 顺序结构应用
至此,我们已经学会了算法竞赛中可能用到的绝大部分顺序结构知识和技巧,接下来通过解决几个稍微复杂的问题,让我们的编程能力和解决问题能力更进一步。
【D1091 [21 年 9 月一级] 交换输出】输入两个整数 a a a、 b b b,将它们交换输出, 0 < a < 10 8 0<a<10^8 0<a<108, 0 < b < 10 16 0<b<10^{16} 0<b<1016。
解决这个问题是比较容易的,不过我们要学习的是如何在程序中交换两个变量的值,而不是通过调整输出顺序来完成,因为这样的问题可能只是之后更加复杂问题中的一个步骤。
我们可以通过赋值运算来修改一个变量的值,同时该变量中原来的值会丢失。因此,如果我们写出如下代码,是不能交换变量 a 和 b 的值的,只会让它们的值相同。
cpp
int a = 3, b = 11;
a = b; // 执行结束后,a 的值为 11,b 的值为 11
b = a; // 执行结束后,a 的值为 11,b 的值为 11
不难想到用一个临时变量先将 a 的值存储起来,然后将 b 的值赋给 a,最后将临时变量中的值赋给 b。这样一来就避免了在把变量 b 的值赋给 a 之后,a 的值被覆盖的情况。于是解决这个问题的关键代码如下。
cpp
long long t = a;
a = b;
b = t;
我们还可以利用加减法运算,结合顺序结构的特性来解决这个问题。
cpp
// 假设输入的 a 和 b 分别是 3 和 11
a = a + b; // 执行结束后,a 的值为 14,b 的值为 11
b = a - b; // 执行结束后,a 的值为 14,b 的值为 3
a = a - b; // 执行结束后,a 的值为 11,b 的值为 3
值得一提的是,上述代码中的表达式 a = a + b 也可以写作 a += b。事实上,C++ 允许任意符合 a = a + () 的表达式缩写为 a += (),其它算术运算也有类似的缩写规则,我们将类似于 +=、-= 这样的运算符称为复合赋值运算符。
【P2799 [ABC222A] Four Digits】给出一个介于 0 和 9(包括 0 和 9)之间的整数 n n n。请将其打印为四位数字字符串,如果需要的话,在其前面添加必要数量的前导零。
该问题可以通过格式控制来解决。首先需要将输出宽度设置为 4,由于输入数据最多 4 位,于是将场宽设置为 4 即可。所谓场宽,是指输出数据的最小宽度 。如果数据宽度大于场宽,则会按照实际宽度输出,否则会将数据右对齐输出。C++ 中设置场宽的工具是 setw。
然后需要设置右对齐之后的填充字符,默认在左侧填充空格,可以通过工具 setfill 来修改填充字符。关键代码如下,注意包含头文件 iomanip。
cpp
cout << setw(4) << setfill('0') << n;
C 语言中的 printf 工具也可以应对用 '0' 填充的情况,也只能应对用 '0' 填充的情况。格式控制符 %d 用于输出 int 型整数,%4d 表明场宽为 4,%04d 表明用 '0' 填充。
cpp
printf("%04d", n);
如果需要输出 long long 型数据,格式控制符为 %lld。如果要设置浮点数的场宽,可以使用类似 %10.2f 的格式控制符,其中 10 表示场宽为 10,.2 表示保留 2 位小数。
【G1181 [GESP2503 一级] 图书馆里的老鼠】图书馆里有 n n n 本书,不幸的是,还混入了一只老鼠,老鼠每 x x x 小时能啃光一本书,假设老鼠在啃光一本书之前,不会啃另一本。请问 y y y 小时后图书馆里还剩下多少本完整的书。保证 y y y 小时后至少会剩下一本完整的书。
y y y 小时后老鼠应该正在啃第 ⌈ y / x ⌉ ⌈y/x⌉ ⌈y/x⌉ 本书,符号 ⌈ t ⌉ ⌈t⌉ ⌈t⌉ 的含义是向上取整,表示不小于 t t t 的最小整数。当 y y y 和 x x x 均为正数时,C++ 表达式 y / x y / x y/x 的值是 ⌊ y / x ⌋ ⌊ y/x ⌋ ⌊y/x⌋,即向下取整(不大于 t t t 的最大整数)。想要求出 ⌈ y / x ⌉ ⌈y/x⌉ ⌈y/x⌉ 的值,可以使用表达式 ( y + x − 1 ) / x (y+x-1)/x (y+x−1)/x,于是剩下的完整书籍数量就是 n − ( y + x − 1 ) / x n-(y+x-1)/x n−(y+x−1)/x。
【P2800 [ABC235A] Rotate】让 x y z xyz xyz 表示一个 3 位整数,其中从左到右的数字分别是 x x x、 y y y、 z z z。给定一个 3 位整数 a b c abc abc,其中没有任何一位数字是 0,求 a b c + b c a + c a b abc+bca+cab abc+bca+cab。
我们可以利用 C++ 中的取余运算和整除特性将整数 n n n 中的任一数位分离出来,遵循的规则是用 n n n 除以对应的位权,再对 10 取余。
cpp
a = n / 100 % 10; // 百位
b = n / 10 % 10; // 十位
c = n / 1 % 10; // 个位,除以 1 可以省略
观察题目中的表达式,每一个数码在个位、十位、百位各出现了一次,于是该表达式等价于 a a a + b b b + c c c aaa+bbb+ccc aaa+bbb+ccc,提取公因式可得 111 × ( a + b + c ) 111×(a+b+c) 111×(a+b+c)。
【G1061 [GESP2312 一级] 小杨的考试】今天是星期 x x x,小杨还有 n n n 天就要考试了,你能推算出小杨考试那天是星期几吗?(本题中使用 7 表示星期日)
此题可以使用取余运算来解决。表达式 ( x + n ) % 7 (x + n) \% 7 (x+n)%7 的取值范围是 0 ∼ 6 0\sim 6 0∼6,当值为 0 时应当输出 7。我们可以将表达式修正为 ( x + n ) % 7 + 1 (x + n) \% 7 + 1 (x+n)%7+1,以确保计算结果在 1 ∼ 7 1\sim 7 1∼7 之间。然而这样的修正会使得计算结果多 1,可以在取余之前先减 1 来进一步修正,于是最终表达式为 ( x + n − 1 ) % 7 + 1 (x + n - 1) \% 7 + 1 (x+n−1)%7+1。
【P2829 [ABC305A] Water Station】有一条全长为 100 km 的超级马拉松赛道。沿着赛道每 5 km 设置一个水站,包括起点和终点,总共 21 个水站。高桥位于赛道的 n n n km 处。找到离他最近的水站的位置。在问题的限制条件下,可以证明最近的水站是唯一确定的。
可以利用表达式 n / 5.0 计算出距离高桥最近的水站,具体来说,如果 n / 5.0 的值小于 x.5,那么距离高桥最近的水站编号就是 x,否则就是 x+1。于是可以进一步利用四舍五入与强制类型转换求出答案,最终 C++ 表达式为 int(n / 5.0 + 0.5) * 5。
【P6035 时间的差】给定两个标准格式的时间
h:m:s,即 "时:分:秒",你的任务是计算这两个时间之间相差多少秒。本题保证第一个时间一定大于第二个时间。
这道题的输入数据并不是用空格隔开的整数,而是用冒号隔开。直接使用 cin >> h >> m >> s; 会导致程序一直卡在输入阶段,这是因为 cin 输入整数时,会在非数字字符或者超出存储范围时停止,将目前已经读取到的数据存入变量。也就是说,用整型变量 h 接受第一个整数时会在第一个冒号处停止,下一个整型变量 m 会因为前面的冒号而无法读取,因此必须要处理掉冒号。由于冒号是一个字符,我们可以声明一个 char 型变量,将冒号接收掉,这样后面的 m 就可以正常读取了。
cpp
int h, m, s;
char c;
cin >> h >> c >> m >> c >> s;
在 C 语言中,我们可以使用 scanf 这个工具更灵活的处理这样的输入。与 printf 类似,在 scanf 中出现的非格式控制符需要原样输入,即 scanf("%d:%d:%d", &h, &m, &s);。注意每个变量前面都有一个符号 &,在这里叫做取地址符,含义是获取变量的地址。
【小贴士】什么是地址?前面我们学到每个变量都是内存中的一段空间,不同的变量会占用不同的字节空间。为了便于操作系统识别,内存中的每个字节都有一个唯一固定不变的编号,这就是地址。声明变量可以当作是将内存编号与变量名进行绑定,便于我们在程序中使用这段空间。注意 int 型有连续 4 个字节,就有 4 个地址,取地址符取出的是第一个字节的地址,称为首地址。
上面 scanf 的写法不够灵活,如果分隔的字符不是冒号,那么输入同样会出问题。更灵活的写法还是将冒号输入到一个 char 型变量中,scanf 和 printf 处理 char 型字符的格式控制符是 %c,于是可以使用 scanf("%d%c%d%c%d", &h, &c, &m, &c, &s);。事实上,变量 c 的唯一作用就是消除输入中的无用字符,C 语言考虑到了这种情况,于是提供了一种不需要变量也能消除无用字符的格式控制规则 %*c,我们是不需要定义变量来接收 %*c 读取到的信息的,即更灵活的写法是 scanf("%d%*c%d%*c%d", &h, &m , &s);。
【小贴士】scanf 的大部分格式控制符与 printf 是一致的,在一些细节用法上不同,例如 printf 需要考虑场宽和小数位数,而 scanf 需要考虑无效字符的消除。scanf 在浮点数的输入上是严格区分的,%f 用于读取 float 型数据,%lf 用于读取 double 型数据,而 printf 没有区分。
此外,%c 是可以读取空白字符的,因此在明确不需要读取空白字符的情况下,可以在 %c 前面添加一个空格,即 scanf(" %c", &c);,作用是过滤掉第一个可见字符前面的所有空白符,这是一个好习惯。
本节习题
- D1260 [23 年 9 月一级] 日期输出
- D1167 [22 年 6 月一级] 平方差计算
- D1142 [22 年 3 月一级] 足球联赛积分
- P2793 [ABC254A] Last Two Digits
- P2801 [ABC258A] When?
- P2204 [ABC105A] AtCoder Crackers
- P6036 简单 a * b
1.7 文件操作
前面我们所有程序的输入输出都是在控制台进行的,下面我们学习一下如何从文件中进行输入输出。尽管这对我们的算法思维并没有提升,但是许多竞赛要求从文件中进行输入输出,比如 CSP-J/S 第二轮、NOIP、USACO,因此文件操作成了算法竞赛的必备技能。要知道一个文件的名字是包含后缀名的,所谓后缀名,是用于区分文件类型的标志信息。在大多数算法竞赛中,输入文件的后缀名统一为 .in,输出文件为 .out。
在 C++ 中是通过文件流对象来操作文件的,比如我们想从文件 test.in 中读取两个整数,并将它们的和输出到 test.out 这个文件中,就需要用到下面的代码。
cpp
ifstream fin("test.in"); // 声明输入流对象 fin,并绑定文件 test.in
ofstream fout("test.out"); // 声明输出流对象 fout,并绑定文件 test.out
int a, b;
fin >> a >> b; // 使用输入流对象 fin 从文件 test.in 输入
fout << a + b << endl; // 使用输出流对象 fout 将结果输出到文件 tets.out
fin.close(); // 关闭输入流对象
fout.close(); // 关闭输出流对象
ifstream 和 ofstream 分别是输入和输出文件流,这两个工具声明在头文件 fstream 中。fin 和 fout 相当于是变量名,后面的双引号中应该写的是文件路径,这里我们使用的是相对路径,默认这两个文件和源代码文件在同一个目录之中。使用文件流的好处是 cin 和 cout 仍然会在控制台进行,坏处是在算法竞赛中实用性不高,因为调试很麻烦。
下面介绍 C 语言中的文件重定向操作,在算法竞赛中相当实用,这个工具与 printf 和 scanf 一样,声明在头文件 cstdio 中。
cpp
freopen("test.in", "r", stdin); // 将输入重定向到test.in,方式为只读
freopen("test.out", "w", stdout); // 将输出重定向到test.out,方式为覆写
int a, b;
cin >> a >> b; // 输入方式仍然使用标准输入
cout << a + b << endl;
fclose(stdin); // 关闭重定向
fclose(stdout);
上述文件重定向的操作在调试时只需要将上面两行和下面两行注释掉即可,非常方便。