第 1 章 变量与数据类型与基本输入输出
本文是《XCPCer 从零开始的 C/C++ 入门指南(算法竞赛向)》系列第 1 章,系列训练大纲见本文。
1.0 配环境
目前市面上常见的IDE(代码编辑器)有:VS Code , Visual Studio , CLion , CODEBLOCKS
这里我们来教学一下vscode的配置,运行和插件
1.1 导语
程序处理的数据,必须先放进变量才能参与运算。变量不是一种通用容器——每一种变量都有确定的类型,类型决定了它能存什么、能存多大、参与运算时遵循什么规则。
类型选错是算法竞赛中最隐蔽的错误来源之一:算法正确,仅仅因为变量类型装不下答案,评测结果就是 Wrong Answer(WA);更隐蔽的情况下,程序在本地小数据上一切正常,提交后在大数据上出错。
1.2 变量的声明、初始化与命名
1.2.1 声明
声明一个变量的语法为:
1 | 类型 变量名; |
(示例 2-1:基本声明与一次声明多个变量)
1 | int a , b , c ; |
一条语句可以声明同类型的多个变量,以逗号分隔。算法竞赛代码中常见 int a, b, c; 的写法,但初学阶段建议一行一个,可读性更好。
1.2.2 初始化
声明的同时给定初值称为初始化,常见写法有三种:
1 | int a = 0; // 赋值式,我们最常用的是这一种 |
必须养成的习惯:在定义后尽量进行初始化。 函数内部的局部变量如果不初始化,它的值不是 0,而是一块不确定的垃圾值,是未定义行为。这类错误的危险之处在于:程序不会编译错误,但大多数时候甚至能”碰巧”得到正确结果。
(示例 2-2:未初始化局部变量的错误示范——留白)
1 | int a ; |
唯一的例外是全局变量:定义在所有函数之外的变量会被自动清零。这一点在数组上尤其有用,将在 1.8 节展开。
1.2.3 命名
语法层面的规则只有四条:
- 只能由字母、数字、下划线组成;
- 不能以数字开头;
- 不能使用C/C
4.++关键字; - 区分大小写,
sum与Sum是两个变量。
tips : 避开库名。y1、j1、rank、index、time 等名字容易与标准库中的名称冲突,可能引发难以理解的编译错误,建议改写为 yy1、rk、idx。
1.3 基本数据类型
1.3.1 类型总表
| 类型 | 字节 | 表示范围 | printf / scanf | 字面量后缀 | 典型场景 |
|---|---|---|---|---|---|
bool |
1 | true / false |
%d |
— | 标记、flag |
char |
1 | -128 ~ 127 | %c |
— | 单个字符 |
int |
4 | -2,147,483,648 ~ 2,147,483,647(约 ±2.1×10⁹) | %d |
— | 默认整数 |
unsigned int |
4 | 0 ~ 4,294,967,295(约 4.2×10⁹) | %u |
1u |
慎用,见 1.5 节 |
long long |
8 | ±9,223,372,036,854,775,807(约 ±9.2×10¹⁸) | %lld |
1LL |
大整数、多数答案 |
unsigned long long |
8 | 0 ~ 18,446,744,073,709,551,615(约 1.8×10¹⁹) | %llu |
1ULL |
位运算、哈希 |
float |
4 | 有效数字约 7 位 | %f |
1.0f |
少用 |
double |
8 | 有效数字约 15~16 位 | %f(输出)/ %lf(输入) |
— | 默认浮点 |
long double |
16(g++ 环境) | 有效数字约 18~19 位 | %Lf |
1.0L |
高精度浮点、计算几何 |
注:long double g++ 环境下为 16 字节(80 位扩展精度)
1.3.2 整型的选择
两条基本原则:
- **默认用
int**,预估结果可能超过 2.1×10⁹ 时直接用long long,不要抱侥幸心理; - **算法竞赛不写
long**。long在 Windows 上是 4 字节、在 Linux 评测机上通常是 8 字节,行为不一致,容易埋坑;需要大整数时直接使用确定 8 字节的long long。
1.3.3 浮点型:float、double 与 long double
浮点数不讨论”表示范围”,讨论有效数字位数:float 约 7 位,double 约 1516 位,19 位。这意味着一个超过 10⁷ 的整数(如 123456789)用 long double 约 18float 存储就会开始丢精度,而 double 可以精确表示 10¹⁵ 以内的整数。
(示例 3-2:float 存储大整数丢精度——留白)
因此算法竞赛中的浮点数默认使用 double,float 基本可以忽略;涉及计算几何或需要累加大量小数时,可进一步使用 long double。输入输出有一个易错点:printf 输出 float 和 double 都用 %f,但 scanf 读入 double 必须用 %lf,用 %f 会写出错误的内存内容且编译器不报错;long double 的读写统一用 %Lf。
1.3.4 字符型 char
char 的本质是一个 1 字节的整数,里面存的是字符的 ASCII 编码:字符 'A' 在内存中就是数字 65。正因为它本质是整数,字符可以直接参与算术运算,例如 'A' + 1 的结果是 'B'——这是第 2 章字符运算的基础。
书写字符字面量用单引号 'A',双引号 "A" 是字符串,两者不是一回事。
1.3.5 布尔型 bool
bool 只有两个值:true 和 false。与整数互换的规则是:true 转为整数是 1,false 是 0;反过来,任何非零整数赋给 bool 都是 true,只有 0 是 false。
(示例 3-3:bool 与整数的互转规则——留白)
算法竞赛中 bool 最常见的用途是标记(visited、flag),配合数组使用将在第 6 章大量出现。
1.4 溢出:合理选择数据范围的原因
1.4.1 什么是溢出
整数类型的存储空间是固定的,当运算结果超出类型能表示的最大值时,程序不会报错,结果会从最小值重新”绕回”——就像里程表转到极限后归零,或 23 点再加 2 小时变成 1 点。
对 int 来说,最大值 2147483647 加 1,得到的不是 2147483648,而是最小值 -2147483648:
(示例 4-1:INT_MAX 加 1 的回绕现象——留白)
1 | int i = 2147483648 ; |
1.4.2 更隐蔽的:中间结果溢出
一个常见误区是”把结果存进 long long 就安全了”。看下面的写法:
(示例 4-2:int 乘 int 赋给 long long 仍然溢出——留白)
问题在于:表达式的类型由参与运算的操作数决定,与赋值给谁无关。两个 int 相乘,整个乘法在 int 范围内完成,溢出发生在赋值之前,右边的 long long 收到的已经是绕回后的垃圾值。
正确的写法是让运算在 long long 中进行:把其中一个操作数转成 long long,或在表达式前乘 1LL。只要表达式中有一个操作数是 long long,整个运算就会按 long long 的规则进行(隐式转换的完整规则见 1.5 节)。
这边的建议是
全都使用long long 来存数
// 十年OI一场空 , 不开long long 见祖宗
// 三年ACM一场空, 不开long long 见祖宗
1.4.3 数据范围 → 类型选择
比赛中看题的第一步永远是读数据范围,然后对照下表选类型:
| 答案与中间运算的最大值 | 选择的类型 |
|---|---|
| 不超过 ±2.1×10⁹ | int |
| 不超过 ±9.2×10¹⁸ | long long |
| 更大 | __int128(见 1.4.4)或高精度 |
判断依据是”答案与中间运算可能到达的最大值“,而不只是输入的范围。例如输入只有 10⁵,但题目要求两数相乘,中间结果就到了 10¹⁰,int 已经装不下。
所以这边的建议是全都使用long long 来存数
·
1.4.4 拓展:__int128
g++ 内置了 128 位整数类型 __int128,可表示约 ±1.7×10³⁸ 的整数,作为乘法可能溢出 long long 时的中间计算工具非常顺手。需要注意它没有配套的 scanf/printf 格式符,读写需要自行转换。初学阶段只需知道它的存在,具体用法在需要时再查即可。
1.5 类型转换
1.5.1 隐式转换
混合类型参与运算时,编译器会自动完成类型转换,规则有三条:
- 小类型向大类型转换。
int与long long混合运算,int先转为long long;整型与浮点型混合运算,整型先转为浮点型。1.4.2 节”表达式前乘1LL“利用的正是这条规则。 - 整型提升。
char、bool、short参与任何算术运算之前,先提升为int。因此'A' + 1的类型是int,值为66;两个char相加,结果也是int。 - 有符号与无符号混合时,有符号数转为无符号数。这条规则是危险之源:
-1转为unsigned int后会变成一个约 42 亿的巨大正数,导致-1 < 1u的比较结果为false。初学阶段的应对策略很简单:不主动使用无符号类型。
1.5.2 强制转换
需要主动转换类型时,使用强制转换运算符,有两种等价写法:
1 | (int)x // C 风格 |
两者含义完全相同。很明显,我们更习惯于前者。
(示例 5-1:两种强制转换写法——留白)
1.5.3 三个经典场景
场景一:整数除法转浮点。 3 / 2 的结果是 1(整数除法直接舍去小数),要得到 1.5 必须先把其中一个操作数转为浮点:(double)3 / 2。注意 (double)(3 / 2) 得到的仍是 1.0
场景二:浮点转整型的截断规则。 浮点转 int 是向零取整:(int)3.9 为 3,(int)-3.9 为 -3。对负数而言这不是数学上的”向下取整”,需要真正的向下取整请使用 <cmath>库中的 floor。
场景三:四舍五入。 对非负浮点数,(int)(x + 0.5) 是常用的四舍五入技巧;更通用的做法是直接使用 <cmath> 中的 round(x),它对正负数都按四舍五入处理。
1.6 输入与输出
1.6.1 两套 IO 工具
C++ 程序有两套输入输出工具,都需要认识:
- C 风格:
scanf/printf(头文件<cstdio>),用格式符控制读写,格式精确; - C++ 风格:
cin/cout(头文件<iostream>),自动识别类型。// 这才是现代人改用的东西(划掉)
两套工具各有所长,本讲义都会讲。但在同一个程序中不要混用两套(混用是大忌啊)。
1.6.2 读入单个数据
1 | scanf("%d", &a); // C 风格 |
要点:scanf 的变量名前必须加取地址符 &(cin 不需要)。漏写 & 是最高频的WA原因,因为编译器不一定会报错。
1.6.3 读入多个数据
多个数据直接连写,格式符与变量一一对应:
1 | scanf("%d%lld%lf", &a, &b, &c); |
无论输入时用空格、换行还是 Tab 分隔,两者都按”空白字符”切分,写法没有区别。
1.6.4 读入字符
用 %c 读字符时有一个陷阱:**%c 不会跳过空白字符**,如果输入流里还留着一个换行(比如上一行数字敲完回车),%c 会把这个换行读进来,而不是你想要的那个字符。cin >> c 则默认跳过空白字符,行为相反。
处理办法是读字符前先”吃掉”残留的空白(具体技巧在第 3 章展开),初学阶段只需记住这个现象。
1.6.5 输出结果
printf 的格式串中,普通文字原样输出,格式符的位置替换为对应变量的值;cout 用 << 依次拼接:
1 | printf("a = %d, sum = %lld\n", a, sum); |
换行使用 \n(两套通用),cout 也可以用 endl。
输出还可以控制宽度:%5d 表示至少占 5 个字符宽、右对齐,%-5d 为左对齐,%05d 表示不足 5 位时左侧补 0。打印表格类对齐输出时非常有用。
1.6.6 转义字符
反斜杠 \ 开头的字符序列称为转义字符,用来表示无法直接书写的特殊字符。常用转义字符如下:
| 转义字符 | 含义 |
|---|---|
\n |
换行 |
\t |
水平制表(Tab) |
\\ |
反斜杠本身 |
\" |
双引号 |
\' |
单引号 |
\0 |
空字符,字符串的结束标志(第 7 章会用到) |
容易出错的是后三个:要输出一个反斜杠必须写 \\,要在字符串中输出双引号必须写 \",否则编译器会把它们当成转义序列的开始,导致格式串解析混乱。
(示例 6-3:转义字符的输出效果——留白)
1.6.7 控制小数位数
输出浮点数经常需要指定位数,两套的做法不同:
1 | printf("%.2f\n", x); // C 风格:保留两位小数 |
%.2f 按四舍五入保留两位小数,%.nf 即保留 n 位。
(示例 6-4:精度输出对照——留白)
1.6.8 格式符
| 格式符 | 用途 |
|---|---|
%d |
int |
%lld |
long long |
%u / %llu |
unsigned int / unsigned long long |
%f |
输出 float/double;scanf 中仅用于 float |
%lf |
scanf 读入 double |
%Lf |
long double 读写 |
%c |
单个字符 |
%s |
字符串(字符数组,读入时不加 &) |
%nd |
至少占 n 个字符宽,右对齐(左对齐用 %-nd) |
%0nd |
宽度 n,不足时左侧补 0 |
%.nf |
保留 n 位小数输出 |
%% |
输出百分号本身 |
cin/cout 不需要格式符,类型自动匹配。再次提醒两个易错点:scanf 读普通变量必须加 &;读入 double 必须用 %lf 而不是 %f。
1.7 常量:const 与 #define
1.7.1 为什么需要常量
程序中不应出现来路不明的”魔法数字”——一个没有名字的字面量,读者无法知道它代表什么。把有固定含义的值命名为常量,既让代码自解释,也让修改只需改动一处。
C++ 中定义常量的两种方式:const 和 #define,算法竞赛代码中两种都很常见,都需要掌握。
1.7.2 const 定义常量
1 | const int MAXN = 100005; |
const 定义的常量有明确的类型,遵守普通变量的作用域规则,定义之后不允许再赋值,否则编译报错。它在本质上是一个”只读变量”。
1.7.3 #define 宏定义
1 |
#define 是预处理指令,在编译之前把代码中的名字原样替换成后面的文本。它没有类型、没有作用域、也不占内存。两个使用要点:行尾不加分号;替换是纯粹的文本操作,因此宏的内容若涉及运算(如 #define A 1 + 2),参与表达式时可能因优先级产生意外,稳妥做法是给宏的内容整体加括号。
1.7.4 超级大数:INF = 0x3f3f3f3f
算法竞赛中经常需要一个”无穷大”来表示”尚未计算”或”不可达”,惯用写法是:
1 | const int INF = 0x3f3f3f3f; |
选择这个值有两个精确的理由:其一,它约为 1.06×10⁹,对绝大多数题目已足够大,同时两个 INF 相加仍不超出 int 范围,不会在松弛运算中溢出;其二,它的每个字节都是 0x3f,可以用 memset(a, 0x3f, sizeof a) 把整个数组一次性初始化为 INF,这个技巧从第 6 章数组开始会反复使用。
(示例 7-1:const、#define 与 INF 的用法——留白)
1.7.5 拓展:constexpr
C++ 11 引入了 constexpr,表示”编译期就能确定值的常量”,约束比 const 更强,常用于数组长度等必须是编译期常量的场合。初学阶段遇到时能认识即可。
1.8 作用域与全局变量
1.8.1 三层作用域
变量的作用域是它从定义到失效的有效范围,C++ 中有三层:
- 全局变量:定义在所有函数之外,整个程序中都可以访问;
- 局部变量:定义在函数内(如
main函数内),只在该函数中有效; - 块级变量:定义在某一对花括号内(如
for循环体、if分支内),离开这对花括号立即失效。
一个典型例子是循环变量:for (int i = 0; ...) 中定义的 i 只在循环体内有效,循环结束后即失效,因此两个并列的 for 循环可以各自定义自己的 i 而互不干扰。
1.8.2 同名遮蔽
内层作用域可以定义与外层同名的变量,此时内层变量遮蔽外层变量——在内层作用域中访问该名字,看到的是内层的值;离开内层后,外层变量恢复可见。
遮蔽规则保证了局部命名的自由,但也容易写出”以为改的是全局、实际改的是局部”的错觉代码。建议避免重名变量。
1.8.3 全局变量自动清零
回忆 1.2.2 节”局部变量不初始化是未定义行为”。全局变量(以及块外的变量)则不同:不加初始化时,它们会被自动清零——数值型为 0,bool 为 false,char 为 '\0'。
这条规则最实用的场景是数组:把数组定义为全局变量,所有元素自动为 0,免去了手动清零的循环;标记数组、计数数组在算法竞赛中几乎都这样使用。
至于”大数组为什么建议开全局”(函数栈空间的限制),将在第 <待定> 章数组中展开。
1.9 常见错误对照
错误一(CE):中文标点混入
(示例 9-1:使用中文分号或中文引号的代码——留白)
现象:编译失败,错误信息指向的行号和内容看起来莫名其妙,往往和真正的错误位置差了好几行。
原因:从聊天软件、文档或全角输入法状态下复制的代码混入了全角字符(中文分号 ;、中文引号 "" 等)。编译器只认识半角符号,全角字符会把后续的解析全部打乱。
修复:在报错位置附近仔细检查标点是否为全角,逐个重打。
错误二(RE):scanf 漏写 &
(示例 9-2:scanf 漏写 & 的代码——留白)
现象:编译正常通过,程序一运行到读入就崩溃(本地直接闪退,评测结果为 RE)。
原因:scanf 需要的是变量的地址,漏写 & 传入的是变量的值。scanf 把这个数值当成内存地址去写入,属于非法内存访问,操作系统直接终止程序。
修复:scanf 中所有普通变量前加 &;唯一的例外是字符数组,数组名本身就是地址,不需要加。
错误三(WA):int 溢出
(示例 9-3:大数相乘仍用 int 的代码——留白)
现象:程序逻辑完全正确,小样例全部通过,提交后大数据全部 WA,整个过程没有任何报错。
原因:答案或中间运算结果超出了 int 的表示范围,发生了 1.4 节讲的静默溢出(回绕)。
修复:按 1.4.3 的对照表把相关变量升级为 long long;特别注意乘法运算前先把操作数转成 long long(乘 1LL),防止中间结果溢出。
排错口诀
- CE:从编译信息的第一条错误看起,看不懂先查标点;
- RE:先查数组越界,再查
scanf的&; - WA:先查数据范围与变量类型是否匹配,再怀疑算法逻辑。
1.10 本章小结与配套习题
本次训练所写的题将会在 SYUCTOJ 中发布。标题大致为2026SYUCTACM第一次训练。
本章习题的参考题解将于下次训练前以独立博文发布。前5位解出所有题目的同学在国庆回来后找我领取自选奶茶。