C++ 基础语法教程:从 Hello World 到指针
阅读前准备好两件事:
- 装好 IDE:还没装的话先看《IDE 选择与配置教》(CLion / VS Code / Dev-C++ 三选一,装好 MinGW 编译器、能跑通第一个程序再回来)。
- 注册好洛谷:luogu.com.cn 注册账号(做题和提交都要用)。
🚀 第一次提交:打开题目页(比如 P1001)→ 把代码复制进提交框 → 选 C++ 语言 → 点提交。结果含义:AC 是通过,WA 是答案错误,RE 是运行时错误(多半是数组越界或除以 0),TLE 是超时(多半是算法太慢),CE 是编译错误(多半是语法写错)。
目录
- 第 1 章 程序骨架与输出(P1000)
- 第 2 章 变量与输入(P1001、P3954、P1425)
- 第 3 章 数据类型与运算符(P5703、P5704)
- 第 4 章 浮点数与输出格式(P5706、P5708、P5709)
- 第 5 章 分支结构(P5711、P5710、P5714、P5716)
- 第 6 章 循环结构(P5719、P5724、P5723)
- 第 7 章 数组(P1046、P1428、P1427、P5727、P5731、P1059)
- 第 8 章 字符串(P5733、P5705、P5015、P5734、P1957)
- 第 9 章 函数与递归(P5735、P5736、P5739、P1035)
- 第 10 章 指针与引用(改写题)
- 附录:速查表(含运算符优先级)/ ASCII 表 / 总题单
第 1 章 程序骨架与输出
本章地图
- 学什么:程序长什么样、
cout输出、字符串、注释、编译错误长什么样- 前置知识:无(需要先装好 IDE,见《IDE 选择与配置教程》)
- 配套题目:例题 1 P1000 超级玛丽游戏
1.1 程序是什么
程序就是按顺序执行的一串指令。你写的代码(源码)经过编译变成电脑能直接执行的文件,运行它,指令就一条一条被执行。
C++ 程序的源码放在后缀为 .cpp 的文件里。所有 C++ 程序,都长成下面这个样子。
1.2 第一个程序
打开你的 IDE,新建一个源代码文件,先保存成后缀为 .cpp 的文件(比如 hello.cpp),然后输入下面这整个程序:
#include <iostream>
using namespace std;
int main() {
cout << "Hello World!" << endl;
return 0;
}
点 IDE 上的"编译运行"按钮。如果一切正常,屏幕上会显示:
Hello World!
🖐️ 用 VS Code 的同学注意:VS Code 默认没有"编译运行"按钮,需要先装扩展并配置好编译器(《IDE 选择与配置》里有详细步骤)。嫌麻烦可以先装 Dev-C++ 起步,熟悉了再换,或者使用Clion。
🖐️ 先照抄,别慌:程序里有几行你现在看不懂?没关系。第一次写程序就是"先照着抄、跑起来、再理解"。1.5 节会把每一行都解释清楚。卡在这一步过不去的人,九成是抄错字符(少了分号、引号没配对、把
cout打成cour),对照上面逐字检查一定能过。
恭喜,你的第一个程序跑起来了!整个程序里,最关键的是这行输出语句,我们先把它拆开看:
cout:读作 "see-out",是 C++ 里负责输出(把内容显示到屏幕上)的工具。c代表 character(字符),out是输出。<<:把右边的数据送进cout,由它显示到屏幕上。可以连续用多个:cout << "a" << "b" << endl;会输出ab。"Hello World!":双引号括起来的内容叫字符串(string),就是一段文字。双引号里的内容会原样输出,包括空格和标点。endl:end line 的缩写,意思是换行——输出完文字后光标移到下一行。(题解里也常写'\n'(反斜杠加字母 n),效果一样是换行,以后看到别慌。)- 行尾的
;:分号。在 C++ 里,一条指令(一条语句)结束时必须写分号,就像中文句子以句号结束。忘写分号是新手最常见的编译错误。
💡 顺便认识一个高频词:编译。把代码"翻译"成电脑能直接执行的语言的过程叫编译,这个动作由你点的"编译运行"按钮完成;编译过程中发现的问题(比如忘写分号)叫编译错误,IDE 会高亮提示你错在哪一行。
剩下的几行——#include <iostream>、using namespace std;、int main()、return 0;——是每个 C++ 程序都有的固定骨架,先照着抄就行,1.5 节会逐行解释。
🖐️ 动手试试:把双引号里的文字改成你的名字(如果用中文名字显示乱码,是文件编码问题,先用英文名字试即可),再运行一次。再把
<< endl(包括它前面的<<)删掉,再运行一次,观察输出有什么变化(应该会变成不换行)。
1.3 例题 1 P1000 超级玛丽游戏
把刚才学的用起来。洛谷 P1000 超级玛丽游戏,题目非常"简单粗暴":
无输入,用字符画的形式原样输出一个超级玛丽场景。字符画长这样(截取前几行):
********
************
####....#.
#..###.....##....
###.......###### ### ###
(完整图形很长,这里只展示开头,去洛谷原题页面能看到完整版)
卡点分析:这道题不需要任何输入,不需要任何计算,只需要把 22 行字符画一行一行"念"出来。这正是 cout 最擅长的活——每输出一行字符画,就是一条 cout 语句。
1.4 输出字符画
思路:字符画有几行,我们就写几条 cout。比如前两行可以这样写:
cout << " ********" << endl;
cout << " ************" << endl;
关键细节:字符画每一行开头的空格也必须原样保留!cout 输出字符串时,双引号里的每个字符(包括空格)都会一字不差地显示出来。很多新手第一遍提交 WA(答案错误),就是因为复制字符画时把行首空格弄丢了。
完整参考代码(建议先自己敲,实在卡住再看):
#include <iostream>
using namespace std;
int main() {
cout << " ********" << endl;
cout << " ************" << endl;
cout << " ####....#." << endl;
cout << " #..###.....##...." << endl;
cout << " ###.......###### ### ###" << endl;
cout << " ........... #...# #...#" << endl;
cout << " ##*####### #.#.# #.#.#" << endl;
cout << " ####*******###### #.#.# #.#.#" << endl;
cout << " ...#***.****.*###.... #...# #...#" << endl;
cout << " ....**********##..... ### ###" << endl;
cout << " ....**** *****...." << endl;
cout << " #### ####" << endl;
cout << " ###### ######" << endl;
cout << "##############################################################" << endl;
cout << "#...#......#.##...#......#.##...#......#.##------------------#" << endl;
cout << "###########################################------------------#" << endl;
cout << "#..#....#....##..#....#....##..#....#....#####################" << endl;
cout << "########################################## #----------#" << endl;
cout << "#.....#......##.....#......##.....#......# #----------#" << endl;
cout << "########################################## #----------#" << endl;
cout << "#.#..#....#..##.#..#....#..##.#..#....#..# #----------#" << endl;
cout << "########################################## ############" << endl;
return 0;
}
🖐️ 强烈建议:这 22 行不要复制,自己对着洛谷的字符画一行一行敲。虽然很枯燥,但这是你第一次"精确抄写",能帮你建立对空格、引号、分号的敏感度——这些细节以后会天天折磨你,现在练好不亏。
1.5 程序骨架
你 1.2 节照抄的程序里,除了 cout 那一行,还有几行"看起来不像人话"的东西。它们是每个 C++ 程序都有的固定结构:
#include <iostream> // 第 1 行:引入输入输出库
using namespace std; // 第 2 行:声明使用标准命名空间
int main() { // 第 3 行:程序的主入口
// ... 你的指令写在这里 ...
return 0; // 程序正常结束,返回 0
}
逐个解释:
#include <iostream>:iostream是 C++ 自带的输入输出库(库可以理解为别人写好的、打包好的功能集合),cout就在这个库里。#include的意思是"把这个库引入我的程序"。以后我们还会引入别的库(比如数学库cmath)。用到哪个库才 include 哪个库,用不到的不用管。using namespace std;:C++ 的现成功能大多住在std这个命名空间(可以理解为"名字的抽屉")里,这句话的意思是"我下面写的名字,默认都去 std 里找"。现在不用理解原理,照着写就行。(题解里也常见std::cout的写法,意思一样,只是把名字写全。)int main() { ... }:函数是"一组打包好的指令"。main是特殊的函数——程序从 main 的第一行开始执行,执行到最后一行的}结束。一对花括号{ }把属于 main 的指令圈起来。函数的具体知识第 9 章才讲,现在你只需要知道:你的指令都写在 main 的花括号里。return 0;:告诉系统"程序正常跑完了"。写不写一般不影响判题,但建议养成习惯写上。
分号 ; 和换行:每条完整语句(一个动作)末尾都要写分号 ;,它是“这句话说完了”的句号。换行只是排版——C++ 不在乎一行写几条语句(a = 1; b = 2; 写同一行合法但难读),也不在乎一条语句占几行。惯例:一条语句一行,读起来清楚,报错也好找行号。
注释:代码里 // 开头的内容是注释,给人看的,电脑会直接忽略。写注释是很好的习惯,以后看自己旧代码时会感谢当时的自己。
⚠️ 新手三大编译错误(看到报错别慌,多半是这三种):
- 语句末尾忘写分号
;- 双引号只写了一边(
"不配对)- 花括号
{ }不配对(少了一个})
1.6 本章小结
| 知识点 | 一句话记忆 |
|---|---|
cout << 内容 << endl; |
输出内容并换行 |
字符串 "..." |
双引号包住的文字,原样输出 |
endl / '\n' |
换行 |
; |
语句结束符,忘写必报错 |
#include <iostream> |
引入输入输出库 |
int main() { ... } |
程序入口,指令写在花括号里 |
// |
注释,给人看的 |
练手:不急着刷题,先在本地把 P1000 独立敲一遍并 AC。这是唯一一道"不用动脑、纯练手速和细心"的题,过不了多半是行首空格或引号丢了,对照题面再敲一遍即可。
第 2 章 变量与输入
本章地图
2.1 例题 2 P1001 A+B Problem
洛谷的"元老级"题目,几乎所有 OJ 的入门第一题:
输入两个整数 a、b,输出它们的和。
卡点分析:程序要学会"读数据"——数据不是写死在代码里的,而是程序运行时从键盘输入。读进来的数要存起来才能用,这就用到变量。
我们分步写。第一步,先搭好程序骨架,声明两个变量:
#include <iostream>
using namespace std;
int main() {
int a, b; // 声明两个 int(整数)类型的变量
return 0;
}
第二步,把键盘输入的两个数读进变量。cin 是 cout 的兄弟,负责输入,>> 方向朝右,表示"把输入的数据送进变量":
#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b; // 读入两个整数,分别存进 a 和 b
return 0;
}
第三步,计算并输出。由数据、变量和运算符组成的、能算出一个结果的式子叫表达式——a + b 算出来就是两数之和,cout 可以直接输出表达式的值:
#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
cout << a + b << endl; // 输出 a+b 的结果并换行
return 0;
}
拿去提交,样例 1 2 会输出 3,AC。
💡 从这段代码开始,你以后写的大部分程序都是这个套路:读入 → 计算 → 输出。
2.2 变量:贴了标签的盒子
变量是"存数据的盒子"。声明一个变量,就是拿一个盒子贴上个名字,规定它能装什么类型的数据:
int a; // 一个装整数的盒子,名字叫 a
int b = 10; // 声明的同时放进去一个 10(这叫“初始化”)
(一次声明多个变量可以写一行,用逗号隔开,共用最后一个分号:int a, b; 就是声明 a、b 两个盒子。)
- 类型决定盒子里能装什么:
int装整数(如 3、-5、2026),double装小数,char装单个字符,后面第 3 章细讲。 - 名字(变量名)由你起,规则:只能由字母、数字、下划线组成,不能以数字开头,不能和 C++ 的关键字(如
int、if、return)重名。用有意义的英文名是好习惯:存成绩就叫score,存苹果数就叫apple。 - 变量可以先声明后赋值,也可以在声明时初始化。用之前一定要有值,不然程序会拿"盒子里原来的灰尘"(未定义的值)去算,结果莫名其妙。
cin >> a >> b; 会按类型读入:a 是 int,就按整数读;如果后面 b 是 double,就按小数读。输入的数据之间用空格或换行隔开都行,cin 会自动跳过它们。
常量:const int MAXN = 100; 声明一个“不能改的变量”(叫常量,名字习惯全大写)。它和普通变量一样用,只是赋值后不能再改。好处:把“写死”的数字(数组容量、范围上限)起个名字,代码更清楚,以后想改只改一处(第 7 章开数组会用到)。
2.3 提交与验证
把代码提交到洛谷后,判题系统会用多组数据测试你的程序:不仅测题面给的样例,还会测边界数据(比如很大的数、最小的数)。所以写题时多想想"极端情况",是竞赛的基本功。第 3 章讲 int 能装多大,你就会理解为什么要多想一步。
2.4 例题 3 P3954 成绩
牛牛的成绩:平时成绩占 20%、期中成绩占 30%、期末成绩占 50%。输入三个整数 a、b、c,输出总评成绩(整数)。
卡点分析:如果直接算 a*0.2 + b*0.3 + c*0.5,小数运算会有精度误差(浮点数第 4 章细讲),而且输出要整数。这里提前用到了 *(乘)和 /(除)运算符,先按字面理解:乘除和加减一样都是算术,第 3 章会系统讲。换个思路:先放大再缩小——把百分比当成整数算:总评 = (a×2 + b×3 + c×5) ÷ 10,这样全程整数运算,没有精度问题。
分步写。第一步读入三个成绩:
#include <iostream>
using namespace std;
int main() {
int a, b, c;
cin >> a >> b >> c;
return 0;
}
第二步,按公式计算并输出。注意运算顺序:先算括号里的乘法和加法,最后除 10:
#include <iostream>
using namespace std;
int main() {
int a, b, c;
cin >> a >> b >> c;
cout << (a * 2 + b * 3 + c * 5) / 10 << endl;
return 0;
}
样例 100 100 100 输出 100;90 90 90 输出 90。
💡 整数除法
/是向 0 截断(正数时就是向下取整):7 / 2等于 3,不是 3.5。这道题里题目保证 a、b、c 都是 10 的倍数,所以 a×2+b×3+c×5 一定能被 10 整除,没有误差。
2.5 例题 4 P1425 小鱼的游泳时间
小鱼从 a 点 b 分游到当天的 c 点 d 分(c:d 一定晚于 a:b),问游了多久。输入 a b c d,输出"小时 分钟"。
卡点分析:时间相减会碰到"借位":比如 12:50 到 13:10,直接 13-12=1 小时、10-50=-40 分钟,不对。统一办法:把时间换算成分钟再算,最后再拆回小时和分钟。
分步写。第一步读入四个数,并换算成"从 0 点起的总分钟数":
#include <iostream>
using namespace std;
int main() {
int a, b, c, d;
cin >> a >> b >> c >> d;
int start = a * 60 + b; // 开始时刻的总分钟数
int end = c * 60 + d; // 结束时刻的总分钟数
return 0;
}
第二步,做差,再拆成小时和分钟。/ 取整、% 取余:
#include <iostream>
using namespace std;
int main() {
int a, b, c, d;
cin >> a >> b >> c >> d;
int start = a * 60 + b;
int end = c * 60 + d;
int diff = end - start; // 总分钟差
cout << diff / 60 << " " << diff % 60 << endl;
return 0;
}
样例 12 50 19 10:start=770,end=1150,diff=380,输出 6 20。
💡
%是取余运算符:380 % 60等于 20(380 除以 60 余 20)。“除出小时、余出分钟”是时间题的标准套路。*/%第 3 章会系统讲,这里先用着。
2.6 int 溢出
int 能装的整数范围是 -2147483648 ~ 2147483647(约 -21 亿到 21 亿)。算出来的结果超过这个范围,就叫溢出——变量会"装不下",结果变成一团乱码数字,而且编译器不报错,最难排查。
比如 100000 * 100000 应该等于 100 亿,但超出 int 范围,程序会输出一个完全对不上的数(实测是 1410065408)。怎么防:预感数字可能很大时,把变量声明成 long long(范围约 ±9×10^18),第 3 章细讲。
⚠️ 动手试试:写个程序输出
100000 * 100000,看看会发生什么,感受一下"溢出"。
2.7 本章小结
| 知识点 | 一句话记忆 |
|---|---|
int a, b; |
声明整数变量 |
cin >> a >> b; |
读入数据,方向朝右 |
cout << a + b << endl; |
输出表达式的结果 |
| 变量名规则 | 字母数字下划线,不能数字开头 |
a * 2 + b * 3 |
表达式,先乘除后加减,括号优先 |
/ % |
整除向 0 截断 / 取余数 |
| 溢出 | int 最大约 21 亿,不够用换 long long |
练手:重写 P1001(不看教程),独立完成 P3954 和 P1425,全部 AC 后再进第 3 章。
第 3 章 数据类型与运算符
本章地图
3.1 五种基本类型
C++ 最常用的五种基本类型:
| 类型 | 装什么 | 例子 | 范围(大概) |
|---|---|---|---|
int |
整数 | 3、-5、2026 | 约 -21 亿 ~ 21 亿 |
long long |
大整数 | 100000000000 | 约 ±9×10^18 |
double |
小数 | 3.14、-0.5 | 约 15~16 位有效数字 |
char |
单个字符 | 'a'、'5'、'\n' | 一个字符 |
bool |
真/假 | true、false | 二选一 |
另外还有一个"常用组合":字符串 string,装一段文字,第 8 章专门讲。
选类型的原则:预估最大结果,再留余量。题目数据范围写着"不超过 10^9",两数相乘就可能到 10^18,int 装不下,直接用 long long。宁可大一点,不要溢出。
3.2 算术运算符
| 运算符 | 意思 | 例子 |
|---|---|---|
+ - * |
加 减 乘 | 3 + 2 = 5 |
/ |
整除(整数除法向 0 截断) | 7 / 2 = 3 |
% |
取余 | 7 % 2 = 1 |
+= -= *= /= |
复合赋值 | a += 3 等价 a = a + 3 |
++ -- |
自增 自减 | a++ 等价 a = a + 1 |
运算顺序(从高到低,只列常用的):括号 ( ) → 乘除取余 * / % → 加减 + - → 赋值 =。同级从左往右。拿不准就加括号,不丢人。
a++ 和 ++a 有区别:a++ 是“先用 a 现在的值,再自增”;++a 是“先自增,再用新值”。单独写一行(a++;)时没区别,但嵌在表达式/条件里就有区别:a[cnt++] = x 是先存到 a[cnt]、再把 cnt 加 1;while (q--) 是先看 q 是不是 0、再减 1。后面例题会用到,先记住这个规则。
⚠️ 两个高频坑:
- 整数除以整数得整数:
5 / 2是 2,不是 2.5。想要小数,得让至少一边是小数:5.0 / 2或5 / 2.0。 - 取余两边是整数:
5.5 % 2编译报错,%只用于整数。
3.3 位运算(认识即可)
电脑里所有整数最终都是二进制存的(一串 0 和 1)。直接对这串 0/1 进行运算就叫位运算。常见六个:
| 运算符 | 名字 | 规则 | 例子 |
|---|---|---|---|
& |
按位与 | 两位都是 1 结果才是 1 | 5 & 3 = 1 |
\| |
按位或 | 有一位是 1 结果就是 1 | 5 \| 3 = 7 |
^ |
按位异或 | 两位不同结果才是 1 | 5 ^ 3 = 6 |
~ |
按位取反 | 0 变 1,1 变 0 | ~5 = -6 |
<< |
左移 | 所有位往左移,右边补 0,等价乘 2 | 5 << 1 = 10 |
>> |
右移 | 所有位往右移,等价除以 2 | 5 >> 1 = 2 |
怎么算的:5 的二进制是 101,3 是 011。逐位看:101 & 011 = 001 = 1;101 | 011 = 111 = 7;101 ^ 011 = 110 = 6。
现在用得上的一招:判断奇偶——x % 2 == 1 可以写成 x & 1(对非负数两者等价;负数时 x & 1 更可靠),位运算比取余快一点。其余位运算先认识、会算即可,后面学状态压缩等算法时再深入。
3.4 例题 5 P5703 苹果采购
输入两个整数 a、b(不超过 10^9),输出 a × b。
卡点分析:数据范围写着“输入不超过 10^9”——两个 10^9 级别的数相乘最坏到 10^18,int 装不下。虽然本题保证答案在 int 范围内,但既然范围给到 10^9 就该警惕,直接用 long long 最稳。养成先看数据范围、拿不准就上 long long 的习惯。
分步写。第一步,用 long long 声明并读入:
#include <iostream>
using namespace std;
int main() {
long long a, b;
cin >> a >> b;
return 0;
}
第二步,输出乘积:
#include <iostream>
using namespace std;
int main() {
long long a, b;
cin >> a >> b;
cout << a * b << endl;
return 0;
}
样例 5 3 输出 15。如果这里用 int,数据一大就溢出,WA 得莫名其妙。
3.5 例题 6 P5704 字母转换
输入一个小写字母,输出它对应的大写字母。
卡点分析:字符怎么"变大写"?字符在电脑里其实是整数(ASCII 码):'a' 是 97,'A' 是 65,'b' 是 98,'B' 是 66……规律是同一个字母大小写相差 32。所以 'a' 转 'A':减去 32。
分步写。第一步,读入一个字符:
#include <iostream>
using namespace std;
int main() {
char c;
cin >> c; // 读入单个字符
return 0;
}
第二步,输出转换结果。字符可以参与算术运算(当整数用),减完 32 再用 char 转回字符输出:
#include <iostream>
using namespace std;
int main() {
char c;
cin >> c;
cout << char(c - 32) << endl;
return 0;
}
样例 a 输出 A。
💡 ASCII 表:记住三个关键值就够:
'0'=48,'A'=65,'a'=97。其他都可以推。附录有完整表。
3.6 类型转换
- 隐式转换:运算时类型会自动"就高不就低"。
5 / 2.0中 int 自动变 double,结果是 2.5。int a = 3.99;会把小数截断成 3(不四舍五入)。 - 显式转换(强转):用
类型(值)或(类型)值明确转换。上面例题里的char(c - 32)就是显式转换——c - 32算出来是 int(97-32=65),再用 char() 包一层告诉编译器"我要把它当字符输出"。
3.7 本章小结
| 知识点 | 一句话记忆 |
|---|---|
long long |
int 装不下的大整数用这个 |
double |
小数类型 |
char |
字符,本质是 ASCII 整数 |
5 / 2 |
整数除法 = 2,要小数就写 5.0/2 |
% |
取余,只用于整数 |
a += 3 |
复合赋值,等价 a = a + 3 |
& \| ^ ~ << >> |
位运算,认识即可,x & 1 判奇偶 |
char(c - 32) |
显式类型转换 |
练手:重写 P5703 和 P5704,再自己验证几个 ASCII 转换(本地跑着看结果就行,不用提交):输入大写输出小写(加 32),输入数字字符输出它的数值(减 48,'5' - '0' = 5)。
第 4 章 浮点数与输出格式
本章地图
4.1 例题 7 P5706 再分肥宅水
一瓶 t 毫升的肥宅水,平均分给 n 个人,每人能喝多少毫升(保留 3 位小数)?需要多少个杯子(每人一个,还要再买 n 个备用)?
卡点分析:t / n 是小数,要输出保留 3 位小数。cout 默认只显示 6 位有效数字、还会用科学计数法(比如 1.66667e+02),不符合要求。需要手动控制格式。
分步写。第一步,读入并计算:
#include <iostream>
using namespace std;
int main() {
double t;
int n;
cin >> t >> n;
return 0;
}
第二步,输出。fixed 表示"固定小数位",setprecision(3) 表示"保留 3 位小数",合起来就是保留 3 位小数。setprecision 在 <iomanip> 库(iomanip = io manipulation,输入输出控制)里,要 include 它:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double t;
int n;
cin >> t >> n;
cout << fixed << setprecision(3) << t / n << endl;
cout << 2 * n << endl; // 杯子数:每人一个 + 备用 n 个
return 0;
}
样例 500 3:第一行输出 166.667,第二行输出 6。
⚠️
fixed << setprecision(3)一旦设置,后面的所有输出都生效,直到你改回去。
4.2 浮点数的精度坑
double 是“近似”存小数的(二进制表示不了一些十进制小数,比如 0.1——就像十进制写 1/3 只能写成 0.333…,0.1 在二进制里也是无限循环小数,只能存个近似值),所以浮点数比较相等是不靠谱的。0.1 + 0.2 == 0.3 在 C++ 里是 false!
判断两个浮点数“相等”的标准写法是看差的绝对值足够小(if 是“如果”,先照抄,第 5 章正式讲):
if (abs(a - b) < 1e-9) // 差小于 10 的 -9 次方,就认为相等
1e-9 是科学计数法写法,等于 0.000000001。abs(取绝对值)在 <cmath> 库(math = 数学)里,记得 include。
做题时优先避免浮点:能用整数算就全程整数(比如第 2 章成绩那题)。实在避不开(比如开根号),就用 double,输出时按题目要求保留位数,不要直接比较相等。
4.3 例题 8 P5708 三角形面积
输入三角形的三条边 a、b、c,用海伦公式求面积,保留 1 位小数。
海伦公式:$p = \frac{a+b+c}{2}$,面积 $s = \sqrt{p(p-a)(p-b)(p-c)}$
卡点分析:要开根号,必须用 double 和 sqrt。这题顺便第一次用 scanf/printf——先照抄,4.4 节正式讲。
分步写。第一步,读入三条边。这里用 scanf 读三个 double:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
double a, b, c;
scanf("%lf%lf%lf", &a, &b, &c);
return 0;
}
第二步,套海伦公式。sqrt 在 <cmath> 里,取平方根。注意算 p 时先算括号里的加法再除 2:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
int main() {
double a, b, c;
scanf("%lf%lf%lf", &a, &b, &c);
double p = (a + b + c) / 2;
double s = sqrt(p * (p - a) * (p - b) * (p - c));
printf("%.1lf\n", s);
return 0;
}
样例 3 4 5:p=6,s=sqrt(6×3×2×1)=sqrt(36)=6,输出 6.0。
4.4 scanf / printf:为什么题解都在用
cin/cout 写起来方便,但速度慢。数据量一大(比如 10 万个数),cin/cout 可能超时。竞赛题解里常见的是 C 语言的 scanf/printf,速度快得多。
| cin / cout | scanf / printf | |
|---|---|---|
| 读入 | cin >> a; |
scanf("%d", &a); |
| 输出 | cout << a; |
printf("%d", a); |
| 速度 | 慢(大数据可能 TLE) | 快 |
常用格式符(写在双引号里):
| 格式 | 对应类型 | 例子 |
|---|---|---|
%d |
int | printf("%d", a); |
%lld |
long long | printf("%lld", x); |
%lf |
double | scanf("%lf", &d); |
%c |
char | scanf("%c", &c); |
%s |
char 数组(第 8 章) | scanf("%s", s); |
%.1lf / %.3lf |
double 保留 1/3 位小数 | printf("%.1lf", s); |
%02d |
int 输出两位,不足补 0 | printf("%02d", 7) 输出 07 |
三个注意点:
- scanf 的
&不能丢:scanf("%d", &a)里的&是"取地址"(第 10 章细讲),表示"把读到的数放到 a 的地址上"。忘了&程序会崩溃或乱读。 - printf 不需要
&:直接传值。 - cin/cout 和 scanf/printf 别混用:两套系统默认不同步,混用会出诡异错误。选一套用到底。
如果坚持用 cin/cout(省事),在 main 开头加两行让它变快,就跟 scanf/printf 一样快了:
ios::sync_with_stdio(false); // 关掉和 C 的同步,提速
cin.tie(0); // 解除绑定,提速
🖐️ 这两行是竞赛必背,以后你写每个程序都会带上。想用哪套就用哪套,但别混。
4.5 例题 9 P5709 苹果和虫子
八尾勇有 m 个苹果,吃一个需要 t 分钟。过了 s 分钟,问还剩几个完整的苹果(正在吃的半个不算)?
卡点分析:吃掉的完整苹果数 = 向上取整(s / t)。比如 205 分钟、10 分钟一个,吃完了 20 个、正在吃第 21 个(半个),完整苹果少 21 个。还有三个坑:
- t 可能等于 0(吃一个不用时间)——除以 0 会 RE,要特判;
- s 可能很大,苹果可能全被吃完甚至"欠"——答案不能是负数,要和 0 取最大;
- 向上取整的整数写法:
(s + t - 1) / t(不用浮点函数,纯整数一步到位)。
分步写。第一步,读入并特判 t=0(这里第一次出现 if,先照抄字面意思,第 5 章正式讲分支):
#include <iostream>
using namespace std;
int main() {
int m, t, s;
cin >> m >> t >> s;
if (t == 0) { // 吃一个不用时间:全吃完
cout << 0 << endl;
return 0;
}
return 0;
}
第二步,算完整苹果数并处理负数:
#include <iostream>
using namespace std;
int main() {
int m, t, s;
cin >> m >> t >> s;
if (t == 0) {
cout << 0 << endl;
return 0;
}
int eaten = (s + t - 1) / t; // 向上取整:吃掉(含正在吃的)的个数
int ans = m - eaten;
if (ans < 0) ans = 0; // 不够吃,剩 0 个
cout << ans << endl;
return 0;
}
样例 50 10 200:eaten = (200+9)/10 = 20,ans = 30,输出 30。
💡 向上取整技巧
(a + b - 1) / b:两个整数 a、b(b>0),算"a 除以 b 向上取整"。原理:a/b 向下取整,加上 b-1 后再除,正好把有余数的情况"顶上去"。这个技巧后面做题会反复用。
4.6 本章小结
| 知识点 | 一句话记忆 |
|---|---|
cout << fixed << setprecision(3) |
保留 3 位小数输出 |
scanf("%d", &a) / printf("%d", a) |
C 风格输入输出,快,别忘 & |
| 两套 IO 别混用 | cin/cout 或 scanf/printf 选一套 |
ios::sync_with_stdio(false); cin.tie(0); |
cin/cout 提速两行咒语 |
(a + b - 1) / b |
整数向上取整 |
| 浮点不比相等 | 用 abs(a-b) < 1e-9 判 |
sqrt / abs |
在 <cmath> 里 |
练手:完成 P5707 上学迟到(⭐ 必做,提示:总时间 = 向上取整(s/v) + 10 分钟;8:00 是第 480 分钟,往前倒推 t = 480 - 总时间,可能倒到前一天——若 t 为负就加 1440(直接取模 1440 时负数会得负值,C++ 的 % 对负数是负的),再拆成 HH:MM,用 printf("%02d:%02d", h, m) 补零)。再做 P5706 和 P5708 的 scanf/printf 版,练熟两套 IO。
第 5 章 分支结构
本章地图
5.1 比较运算符与 if / else
程序默认是从上往下一条一条执行的。但现实里经常要"看情况办事":成绩及格显示"过"、不及格显示"挂";年份是闰年输出 1、不是输出 0。分支就是让程序"根据条件走不同的路"。
先认识比较运算符——比较两个值,结果是"真"(成立)或"假"(不成立):
| 运算符 | 意思 | 例子 |
|---|---|---|
== |
等于 | a == 5:a 正好等于 5 |
!= |
不等于 | a != 5:a 不是 5 |
< > |
小于 / 大于 | a < 5 |
<= >= |
小于等于 / 大于等于 | a <= 5 |
⚠️
==和=别搞混:==是"判断是否相等",=是"赋值"。if (a == 1)判断 a 是不是 1;if (a = 1)是把 1 赋给 a(结果恒为真,经典 bug,编译器通常只给个警告)。
if 的写法:
if (条件) {
// 条件成立(真)时,执行这里的语句
}
条件是一个判断式子(比较运算,或 5.3 的逻辑运算),成立叫"真",不成立叫"假"。- 条件成立 → 执行花括号里的语句,然后继续往下走;不成立 → 整块跳过,直接往下走。
if / else:加上 else,"条件不成立时做另一件事":
if (条件) {
// 条件成立时执行
} else {
// 条件不成立时执行
}
两条路必走一条:为真走 if,为假走 else,然后汇合继续往下。
走一遍真实的小程序:
int x;
cin >> x;
if (x > 0) {
cout << "正数";
} else {
cout << "非正数";
}
cout << " 结束" << endl;
输入 5:x > 0 成立 → 执行 if 的花括号,输出“正数” → 跳过 else 的花括号 → 输出“ 结束”。输入 -2:x > 0 不成立 → 跳过 if 的花括号 → 执行 else 的花括号,输出“非正数” → 输出“ 结束”。一句话:成立就进去执行,不成立就整块跳过。
条件不一定非写比较式:存了真/假的 bool 变量可以直接当条件(if (leap) 就是“如果 leap 为真”);整数也可以(0 是假,非 0 都是真)。
花括号的作用:把“属于这个分支的多条语句”圈在一起。单条语句可以省略花括号(if (a > 0) cout << a; 合法),但新手强烈建议永远写花括号——看这个经典坑:
if (a > 0)
cout << a;
cout << b; // ❌ 这行不在 if 里!缩进只是给人看的,程序只认花括号
以为 cout << b 也在 if 里?不是——它无条件执行。加上花括号就不会搞错。
5.2 例题 10 P5711 闰年判断
输入一个年份 y,如果是闰年输出 1,否则输出 0。闰年规则:能被 4 整除但不能被 100 整除,或者能被 400 整除。
卡点分析:规则是"或者"连接的两个条件,每个条件内部又是"并且"——正好用上刚学的比较运算符,加上 5.3 节的逻辑运算(&& 是"并且"、|| 是"或者")。先把"是不是闰年"算成一个 bool 存起来,再判断输出,逻辑清晰。
分步写。第一步,搭骨架并读入年份:
#include <iostream>
using namespace std;
int main() {
int y;
cin >> y;
return 0;
}
第二步,写判断。&& 和 || 先按字面理解:(y % 4 == 0 && y % 100 != 0) 是"能被 4 整除且不能被 100 整除",|| (y % 400 == 0) 是"或能被 400 整除",和题面规则一一对应:
#include <iostream>
using namespace std;
int main() {
int y;
cin >> y;
bool leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
if (leap) {
cout << 1 << endl;
} else {
cout << 0 << endl;
}
return 0;
}
样例 2000 输出 1,1900 输出 0。
5.3 逻辑运算
判断经常不止一个条件:"x 在 0 到 10 之间"是"x > 0 并且 x < 10"。用逻辑运算符把多个条件组合起来:
| 运算符 | 名字 | 规则 | 例子 |
|---|---|---|---|
&& |
与 | 两边都真才真 | a > 0 && a < 10 |
|| |
或 | 一边真就真 | x < 0 || x > 100 |
! |
非 | 真变假,假变真 | !(a == 0) |
真值表(把所有组合列出来,一眼看懂):
| a | b | a && b | a | b | !a | |
|---|---|---|---|---|---|---|
| 真 | 真 | 真 | 真 | 假 | ||
| 真 | 假 | 假 | 真 | 假 | ||
| 假 | 真 | 假 | 真 | 真 | ||
| 假 | 假 | 假 | 假 | 真 |
bool 类型的变量只装 true(真)和 false(假),正好用来存"某个条件是否成立":bool leap = (y % 4 == 0); 就是把判断结果存进变量。用 cout 输出 bool 时显示 1(真)/ 0(假)。
优先级提醒(从高到低):算术(* / % + -)> 比较(== != < >)> && > || > 赋值(=)。拿不准就加括号,(a && b) || c 比 a && b || c 清楚。⚠️ 数学里的 a < x < b 在 C++ 里不是这个意思(会先算 a < x 得 0/1,再拿它和 b 比),要写成 a < x && x < b。
5.4 例题 11 P5710 数的性质
一些数字可能拥有以下性质:
- 性质 1:是偶数;
- 性质 2:大于 4 且不大于 12。
输入 x,依次输出四个 1/0:同时满足两个性质、至少满足一个、恰好满足一个、两个都不满足。
卡点分析:先把两个性质分别算成两个 bool,再组合。四个输出分别是:a && b(同时)、a || b(至少一个)、恰好一个、!a && !b(都不)。"恰好一个"是难点——可以写成 (a && !b) || (!a && b);还记得第 3 章位运算的 ^(异或)吗?bool 本质是 0/1,a ^ b 正好就是"恰好一个满足"。
分步写。第一步,读入 x,算出两个性质:
#include <iostream>
using namespace std;
int main() {
int x;
cin >> x;
bool a = (x % 2 == 0); // 性质1:偶数
bool b = (x > 4 && x <= 12); // 性质2
return 0;
}
第二步,输出四个组合:
#include <iostream>
using namespace std;
int main() {
int x;
cin >> x;
bool a = (x % 2 == 0);
bool b = (x > 4 && x <= 12);
cout << (a && b) << " "; // 同时满足
cout << (a || b) << " "; // 至少一个
cout << (a ^ b) << " "; // 恰好一个(异或)
cout << (!a && !b) << endl; // 都不满足
return 0;
}
样例 12:a=1,b=1 → 输出 1 1 0 0。样例 9:a=0,b=1 → 输出 0 1 1 0。
5.5 else if 与例题 12 P5714 肥胖问题
判断的情况不止两个时,可以用 else if 一路排下去:
if (条件1) {
// 条件1成立
} else if (条件2) {
// 条件1不成立、条件2成立
} else if (条件3) {
// 条件1、2都不成立、条件3成立
} else {
// 上面都不成立
}
从上往下逐个检查,第一个成立的生效,后面的不再看。这天然适合"区间分段":分数 <60 不及格、<80 中等、<90 良好、否则优秀——每段一个条件,前面的都不成立才轮到后面的。
输入体重 m(kg)和身高 h(米),BMI = m / h²。按 BMI 分三档输出:BMI < 18.5 → 输出
Underweight;18.5 ≤ BMI < 24 → 输出Normal;BMI ≥ 24 → 先输出 BMI 的值,换行再输出Overweight。
卡点分析:三档区间,用 if / else if / else 一路排下来。注意区间是左闭右开:18.5 算 Normal,24 算 Overweight——else if 天然满足"第一个匹配的生效",所以 18.5 落在第二个分支、24 落在第三个分支,不用写 >= <= 的复杂组合。还要注意输入的两个数都是浮点数(体重可能是 62.5),必须全用 double 读,用 int 会把 62.5 拆成 62 和 0.5 两个数,直接算错。
分步写。第一步,读入并算 BMI:
#include <iostream>
using namespace std;
int main() {
double m, h;
cin >> m >> h; // 先体重(kg),再身高(米)
double bmi = m / (h * h); // h*h 是身高的平方
return 0;
}
第二步,分段判断输出。BMI ≥ 24 时要输出两行:
#include <iostream>
using namespace std;
int main() {
double m, h;
cin >> m >> h;
double bmi = m / (h * h);
if (bmi < 18.5) {
cout << "Underweight" << endl;
} else if (bmi < 24) {
cout << "Normal" << endl;
} else {
cout << bmi << endl; // BMI 值,cout 默认精度
cout << "Overweight" << endl;
}
return 0;
}
样例 70 1.72 输出 Normal;100 1.68 输出 35.4308 换行 Overweight。
5.6 switch(按值跳转)
switch 适合"一个整数变量取不同值,做不同的事"——比如月份 1~12 各输出不同天数、菜单选项 1~5 各执行不同操作。它像"查表跳转":
switch (变量) {
case 值1:
// 变量 == 值1 时执行这里
break;
case 值2:
// 变量 == 值2 时执行这里
break;
default:
// 所有 case 都没匹配上时执行这里
break;
}
逐块解释:
switch (变量):括号里放整数(int 或 char 都可以),程序拿它的值去和下面的 case 逐个比对。case 值::一个"分支入口"。变量等于这个值,就从这里开始执行。值必须是写死的常量(如1、'A'),不能是变量。- case 里可以写多条语句:从
case进去后一条一条往下执行,想写几行写几行,和 if 花括号里的语句一样。 break;:“执行到这里,跳出整个 switch”。没有它,程序会穿透(fall-through)继续执行下一个 case 的内容——这通常是 bug:
switch (x) {
case 1:
cout << "A"; // 忘了写 break!
case 2:
cout << "B";
break;
}
// x = 1 时输出 "AB":从 case 1 进去后一路往下,把 case 2 的代码也执行了
default::所有 case 都没匹配上时执行,相当于 if 的 else,可写可不写。
⚠️ case 里声明变量:如果要在 case 里定义变量(比如
bool leap),最好用花括号把那段包起来(case 2: { bool leap = ...; ... })——不然编译器可能报"跳过了初始化"的错。
为什么 case 1: case 3: case 5: 能连写? 这不是魔法:case 1: 后面没有语句也没有 break,程序"穿透"到 case 3: 继续找,直到 case 5: 后面才有真正的语句——所以 1、3、5 三个值共用同一段代码,等价于"1 或 3 或 5 都走这里"。
switch vs if:判断“等于某个值”用 switch 更清晰;判断范围(a > 0 && a < 10)或复杂条件只能用 if。竞赛里 switch 用得不多(if 更灵活),但读题解时常遇到,认识它不亏。
5.7 例题 13 P5716 月份天数
输入年份 y 和月份 m,输出该月有多少天(2 月要按闰年算)。
卡点分析:一个月有几种天数:31 天的大月、30 天的小月、28/29 天的 2 月。判断对象是"月份 m",正好用 switch。1、3、5、7、8、10、12 月都是 31 天,可以用 case 连写共用一段代码(5.6 讲的"穿透"用法)。
分步写。第一步,读入:
#include <iostream>
using namespace std;
int main() {
int y, m;
cin >> y >> m;
return 0;
}
第二步,switch 分月:
#include <iostream>
using namespace std;
int main() {
int y, m;
cin >> y >> m;
switch (m) {
case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
cout << 31 << endl;
break;
case 4: case 6: case 9: case 11:
cout << 30 << endl;
break;
case 2: {
bool leap = (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
cout << (leap ? 29 : 28) << endl;
break;
}
}
return 0;
}
⚠️ 为什么
case 2:里要加花括号?因为bool leap是带初始化的变量声明——如果后面还有其他 case,程序跳到那些 case 时会“绕过”这个初始化,编译器会报错(“跳过了初始化”)。用花括号把 leap 圈在 case 2 的领地里,既合法又安全。case 里不声明变量时,花括号可写可不写(靠 break 划界)。
这里 leap ? 29 : 28 是三目运算符:条件成立取冒号左边的值,否则取右边的,等价于一行 if-else,返回一个值。样例 2000 2 输出 29,1900 2 输出 28,2026 8 输出 31。
🔑 分号速记(什么时候要分号):普通语句(赋值、cout、声明)、
return后面要分号;if/for/while的条件括号后面不写分号(写了就变成空语句,循环体没了);for括号里用两个分号隔开三段;do-while的while(条件);结尾有分号(它是一条完整语句)。
5.8 本章小结
| 知识点 | 一句话记忆 |
|---|---|
if (条件) { } else if { } else { } |
分支结构 |
== vs = |
判断相等 vs 赋值,别写混 |
&& \|\| ! |
与 / 或 / 非 |
| bool | 存"成立/不成立" |
a ^ b |
异或 = "恰好一个成立" |
switch ... case ... break |
按整数取值分支,break 别省 |
条件 ? A : B |
三目运算符,返回 A 或 B |
练手:
- P5715 三位数排序 ⭐:输入三个整数,从小到大输出(提示:两两比较交换,或三个 if 找出最小/最大/中间)
- P5717 三角形分类 ⭐⭐:先判断能否构成三角形(两边之和大于第三边),再分锐角/直角/钝角,最后还要判等腰(至少两边相等)和等边(三边相等)——满足几个条件就输出几行(提示:先找出最长边,用勾股定理 $a^2+b^2=c^2$ 判断)
第 6 章 循环结构
本章地图
6.1 for 循环
“重复做某件事”用循环。for 是最常用的,它把“重复”拆成三个部分写在一起:
for (初始化; 条件; 更新) {
// 循环体:条件成立就执行一次
}
三个部分用分号隔开,各管一件事:
- 初始化:循环开始前做一次的准备,通常是“计数器从几开始”。比如
int i = 1就是“拿一个计数器 i,从 1 开始数”。 - 条件:每次进入循环体前先检查的判断——成立(真)就执行一次循环体,不成立(假)就整个循环结束,跳到
}后面继续。比如i <= n是“只要 i 还没超过 n,就继续”。 - 更新:每执行完一次循环体后做的收尾,通常是计数器往前走一步。比如
i++是“i 加 1”。
执行顺序(记住这个节奏):
初始化 → 判断条件 →(真)执行循环体 → 更新 → 判断条件 →(真)执行循环体 → 更新 → …… → 判断条件 →(假)结束
拿 for (int i = 1; i <= 5; i++) 完整走一遍:
| 阶段 | i 的值 | 判断 i <= 5 | 动作 |
|---|---|---|---|
| 初始化 | 1 | — | 开始 |
| 第 1 次 | 1 | 真 | 执行循环体,然后 i 变 2 |
| 第 2 次 | 2 | 真 | 执行循环体,然后 i 变 3 |
| 第 3 次 | 3 | 真 | 执行循环体,然后 i 变 4 |
| 第 4 次 | 4 | 真 | 执行循环体,然后 i 变 5 |
| 第 5 次 | 5 | 真 | 执行循环体,然后 i 变 6 |
| 第 6 次 | 6 | 假 | 循环结束,往下走 |
一共执行 5 次循环体。注意:条件在“执行前”判断,所以如果条件一开始就不成立(比如 i <= 0),循环体一次都不执行。
循环三要素:从哪开始(初始化)、到哪结束(条件)、怎么走(更新)。写循环前先想清楚这三件事——很多“死循环”(程序卡住不结束)都是更新忘写或条件写反。
⚠️ for 那一行的结尾没有分号:for (int i = 1; i <= 5; i++); 多写一个分号,循环体就变成空的了——后面的花括号只会执行一次,i 还白白加到 6。
6.2 例题 14 P5719 分类平均
输入 n 和 k,输出 1~n 中所有能被 k 整除的数的平均值,以及所有不能被 k 整除的数的平均值,都保留 1 位小数。(题目保证两组都不为空)
卡点分析:要遍历(从第一个到最后一个,挨个把每个数访问一遍)1 到 n 的每个数,分别累加“能整除的”和“不能整除的”,最后各自除以个数。遍历用 for,累加用变量。
分步写。第一步,搭骨架,准备四个累加变量:
#include <iostream>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
long long sum1 = 0, sum2 = 0; // 两组的和
int cnt1 = 0, cnt2 = 0; // 两组的个数
return 0;
}
第二步,for 循环遍历 1~n,用 if 分流累加:
#include <iostream>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
long long sum1 = 0, sum2 = 0;
int cnt1 = 0, cnt2 = 0;
for (int i = 1; i <= n; i++) {
if (i % k == 0) {
sum1 += i;
cnt1++;
} else {
sum2 += i;
cnt2++;
}
}
return 0;
}
第三步,输出平均值。注意:整数相除是整除,要先转成 double 再除;fixed << setprecision(1) 保留 1 位小数:
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
long long sum1 = 0, sum2 = 0;
int cnt1 = 0, cnt2 = 0;
for (int i = 1; i <= n; i++) {
if (i % k == 0) {
sum1 += i;
cnt1++;
} else {
sum2 += i;
cnt2++;
}
}
cout << fixed << setprecision(1);
cout << (double)sum1 / cnt1 << " " << (double)sum2 / cnt2 << endl;
return 0;
}
样例 100 16:能整除的有 16,32,48,64,80,96(和 336,平均 56.0),其余平均 50.1,输出 56.0 50.1。
6.3 while 循环
for 适合“次数明确”的循环;不知道要循环多少次、只知道“满足条件就继续”时,用 while:
while (条件) {
// 条件成立就执行;每执行完一次,重新判断条件
}
和 for 对照:while 只有“条件”和“循环体”两样——没有初始化、没有更新(需要的话自己在循环体里写)。所以 while 更适合“循环次数由运行时数据决定”的场景。
经典用法是读入直到某个值:
int x;
cin >> x; // 先读第一个
while (x != 0) { // 不是 0 就处理
// ...处理 x...
cin >> x; // 再读下一个(“更新”就藏在这里)
}
读一个、处理一个、再读一个……直到读进 0,条件变假,循环结束。for 能写的 while 都能写,反之亦然,看哪个顺手。
⚠️ while 最容易犯的错:忘写“让条件最终变假”的那一步(上面例子里的
cin >> x)——条件永远成立,死循环。
6.4 例题 15 P5724 求极差
输入 n 和 n 个数,输出这 n 个数中最大值减最小值。
卡点分析:"擂台法":先假设第一个数是擂主(最大和最小都是它),然后让后面的数一个一个来打擂,赢了就换擂主。这样一遍扫描就同时找到最大和最小。
分步写。第一步,读入 n 和第一个数,初始化擂台:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int x;
cin >> x;
int mx = x, mn = x; // 最大值、最小值都先设为第一个数
return 0;
}
第二步,循环读入剩下 n-1 个数,逐个打擂:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int x;
cin >> x;
int mx = x, mn = x;
for (int i = 2; i <= n; i++) {
cin >> x;
if (x > mx) mx = x;
if (x < mn) mn = x;
}
cout << mx - mn << endl;
return 0;
}
样例 4 1 2 3 4 输出 3(4-1)。
💡 擂台法(边读边比)是省内存的经典套路:不存全部数据,读一个处理一个。第 7 章学了数组后会看到"先存起来再统一处理"的另一种思路。
6.5 break 与 continue
break:立刻跳出当前这层循环(循环结束,去执行循环后面的代码)。⚠️ 循环套循环时,break 只跳出离它最近的那一层(6.6 例题 16 会用到);它和第 5 章 switch 里的 break 是一回事——都是“立刻停在这层,跳到这层的后面”。continue:跳过本次循环的剩余部分,直接进入下一次。
for (int i = 1; i <= 10; i++) {
if (i == 3) continue; // 跳过 3,继续 4、5...
if (i == 7) break; // 到 7 直接结束整个循环
cout << i << " ";
}
// 输出:1 2 4 5 6
6.6 例题 16 P5723 质数口袋
从 2 开始,依次把质数装进口袋,直到口袋里的质数之和将要超过 L(L≤100000)。输出装进去的每个质数(每行一个),最后一行输出个数。
卡点分析:两层循环:外层从 2 开始试每个数是不是质数,内层判断质数。判断质数用"试除法":看 2 到 sqrt(x) 有没有能整除 x 的。注意两个细节:判断到 j * j <= i 就够了(超过 sqrt 就不用试了);口袋里和超过 L 就停。
分步写。第一步,读入 L,搭好外层循环框架:
#include <iostream>
using namespace std;
int main() {
int L;
cin >> L;
int sum = 0; // 口袋里的质数和
int cnt = 0; // 个数
for (int i = 2; ; i++) { // 从 2 开始逐个试
// ...判断 i 是不是质数,是就装袋...
}
return 0;
}
⚠️ 这一步是中间状态:for 的条件空着(
; ;),现在直接编译运行会死循环。先别运行,第二步把它补完整。
第二步,内层试除判断质数:
#include <iostream>
using namespace std;
int main() {
int L;
cin >> L;
int sum = 0, cnt = 0;
for (int i = 2; ; i++) {
bool prime = true;
for (int j = 2; j * j <= i; j++) {
if (i % j == 0) {
prime = false;
break; // 找到因子,不是质数,跳出内层
}
}
if (!prime) continue; // 不是质数,试下一个数
if (sum + i > L) break; // 装不下了,结束
sum += i;
cnt++;
cout << i << endl;
}
cout << cnt << endl;
return 0;
}
样例 100:装入 2 3 5 7 11 13 17 19 23(和恰好 100,下一个 29 装不下),最后输出 9。
⚠️ 注意
sum + i > L用的是"将要超过",所以和恰好等于 L 时还能继续装(样例里 2+3+...+23=100 刚好装满)。判断顺序:先判断质数,再判断装不装得下,最后才装袋——三个动作别乱。
6.7 do-while(认识即可)
do-while 和 while 几乎一样,唯一区别:先执行一次循环体,再判断条件——所以至少会执行一次:
do {
// 至少执行一次
} while (条件); // ⚠️ 结尾这个分号不能丢!
⚠️ 结尾分号不能丢:for/while 那一行结尾不写分号,do-while 的 while(条件); 结尾必须写分号——它是这条 do-while 语句的一部分,漏了编译报错。
适用场景:操作本身要先做一次,做完看情况决定要不要继续。比如"先输出再问要不要继续"。后面学 STL 的 next_permutation(生成全排列)时,标准用法就是:
do {
// 处理当前排列
} while (next_permutation(...));
现在知道它的存在和“至少执行一次”这个特性就行,第 7 章例题 20(冰雹猜想)会看到实际用法。
6.8 本章小结
| 知识点 | 一句话记忆 |
|---|---|
for (int i=1; i<=n; i++) |
次数确定的循环 |
while (条件) |
条件成立就一直循环 |
break |
跳出整个循环 |
continue |
跳到下一次循环 |
do { } while (条件) |
至少执行一次 |
| 擂台法 | 边读边比,找最大/最小 |
| 试除法判质数 | j*j <= i 为止,找到因子 break |
练手:
- P5722 数列求和 ⭐:循环累加 1~n(不许用公式)
- P5721 数字直角三角形 ⭐:输出数字三角形,第 i 行输出 n-i+1 个数(行数递减),每个数 2 位补前导 0(提示:用
printf("%02d", cnt),正好练第 4 章的格式) - P5718 找最小值 ⭐:擂台法找最小
第 7 章 数组
本章地图
7.1 为什么需要数组
第 6 章的擂台法"边读边比"很省内存,但有个致命局限:数据读进来就丢了。如果题目要"先全部读进来,再倒序输出"或"再统一处理",就得把数据存起来。存一串同类型数据,用数组。
7.2 一维数组
int a[100]; // 声明一个能装 100 个 int 的数组,名字叫 a
- 数组像一排编了号的盒子,编号从 0 开始:
a[0]、a[1]、……、a[99]。没有a[100]!(为什么从 0 起而不是 1?第 10 章学了指针——a[i]就是“首地址往后数 i 个”——自然就懂了,先记规则) - 下标就是盒子的编号:
a[i]读作“a 的第 i 号盒子”,i叫下标(也叫索引)。a[0]是第一个盒子,a[1]是第二个……所以“第 i 个元素”对应a[i-1]。用下标能存取任意一个盒子:a[2] = 99;是“把 99 放进 2 号盒子”,cout << a[2];是“把 2 号盒子里的数拿出来输出”。 - 声明时
[100]是容量(最多装几个),必须是写死的常量,不能是运行时读入的变量(cin >> n; int a[n];不行);但可以用第 2 章学的 const 常量:const int N = 105; int a[N];——编译器知道 N 是固定值,可以当容量。给大不给小:题目说 n≤100,开a[105],留余量防越界。 - 用循环配合数组是标配:读入
for (int i = 0; i < n; i++) cin >> a[i];,输出同理。循环变量正好可以当下标:i=0 读进 a[0]、i=1 读进 a[1]……i=n-1 读进 a[n-1],正好 n 个数。 - 初始化:
int a[100] = {};把每个格子都清零;int b[5] = {1, 2, 3};前三个填 1、2、3,剩下的自动补 0;int dx[] = {0, 1, 0, -1};不写容量,按大括号里元素个数自动定大小。⚠️ 局部数组不初始化的话,里面是垃圾值(随机旧数据)——用数组做计数、标记之前一定要初始化。 - ⚠️ 数组越界:访问
a[-1]或a[100](超过容量)不报编译错,但运行时可能 RE 或读出垃圾值。这是 RE 的头号原因。
7.3 例题 17 P1046 陶陶摘苹果
10 个苹果,给出每个苹果离地面的高度(100~200 之间的整数)。陶陶身高 h,站在 30 厘米的板凳上,能摘到几个苹果?(够得到的标准:苹果高度 ≤ h + 30)
卡点分析:先读入 10 个高度存进数组,再读陶陶身高,最后遍历数组数一数有多少个 ≤ h+30。数组在这里是必须的——苹果高度要等身高读进来之后才用得上。
分步写。第一步,读入 10 个高度到数组:
#include <iostream>
using namespace std;
int main() {
int a[10];
for (int i = 0; i < 10; i++) {
cin >> a[i];
}
int h;
cin >> h;
return 0;
}
第二步,遍历计数:
#include <iostream>
using namespace std;
int main() {
int a[10];
for (int i = 0; i < 10; i++) {
cin >> a[i];
}
int h;
cin >> h;
int reach = h + 30;
int cnt = 0;
for (int i = 0; i < 10; i++) {
if (a[i] <= reach) cnt++;
}
cout << cnt << endl;
return 0;
}
7.4 例题 18 P1428 小鱼比可爱
n 条鱼排成一排,输入每条鱼的可爱程度。对每条鱼,输出它左边比它不可爱(可爱程度更低)的鱼的数量。
卡点分析:对每一条鱼 i,都要回头数一遍 0~i-1 里有多少比它小的——双重循环:外层定"当前鱼",内层数"左边的鱼"。
分步写。第一步,读入 n 条鱼:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[105];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
return 0;
}
第二步,双重循环统计。外层 i 遍历每条鱼,内层 j 数左边比 a[i] 小的:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[105];
for (int i = 0; i < n; i++) {
cin >> a[i];
}
for (int i = 0; i < n; i++) {
int cnt = 0;
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) cnt++;
}
cout << cnt << " ";
}
cout << endl;
return 0;
}
样例 6 4 3 0 5 1 2 输出 0 0 0 3 1 2。
7.5 例题 19 P1427 小鱼的数字游戏
输入一串正整数,以 0 结束。把这串数倒序输出(0 本身不输出)。
卡点分析:两个新东西:① 输入不知道有几个数,读到 0 才停——这是"多组输入"模式;② 要倒序输出,必须先全部存下来。
分步写。第一步,用 while 边读边存。cin >> x 成功读入且 x 不等于 0 就存进数组,同时用 n 记个数。为什么下标正好是 n? 第 1 个数存 a[0]、第 2 个存 a[1]……个数和下标都从 0 数起,所以存到第 n 个数时下标恰好是 n:
#include <iostream>
using namespace std;
int main() {
int a[105];
int n = 0;
int x;
while (cin >> x && x != 0) {
a[n] = x;
n++;
}
return 0;
}
第二步,倒序输出。下标从 n-1 往 0 走:
#include <iostream>
using namespace std;
int main() {
int a[105];
int n = 0;
int x;
while (cin >> x && x != 0) {
a[n] = x;
n++;
}
for (int i = n - 1; i >= 0; i--) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
样例 1 2 3 4 5 0 输出 5 4 3 2 1。
💡
while (cin >> x && x != 0)的含义:先读一个数,读到了且不是 0,就进入循环。这种"读到特定值结束"的模式,以及更常见的"一直读到文件结束"(while (cin >> x)),都是竞赛输入的标准姿势,多组数据的题全靠它。判题时输入文件读完,cin >> x会返回"失败",循环自动结束。
7.6 例题 20 P5727 冰雹猜想
对一个正整数 n:如果是偶数就除以 2,如果是奇数就乘 3 加 1,重复直到变成 1。从最后的 1 开始,倒序输出整个过程中出现的所有数(含 n 和 1)。
卡点分析:这个过程叫"角谷猜想",任何正整数最终都会到 1。注意题面要求倒序——从 1 往前输出。正着生成序列很容易,但没法边生成边倒着输出(下一个数依赖上一个数),所以必须先把整个序列存进数组,再倒着遍历输出。这正是数组存在的意义:有些东西算完之前,你不知道要按什么顺序输出。
序列至少包含 n 本身(n=1 时序列只有 1)——"至少存一次"正好用上第 6 章的 do-while。
分步写。第一步,搭骨架,读入 n,准备数组:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005]; // 存变化过程的每个数(n ≤ 100,开大点)
int cnt = 0; // 已存了几个数
return 0;
}
第二步,用 do-while 边变换边存进数组。do-while 保证 n 本身一定先被存进去;while (true) 配合 if (n == 1) break; 在存完 1 之后结束:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005];
int cnt = 0;
do {
a[cnt++] = n; // 存进数组,cnt 加 1
if (n == 1) break;
if (n % 2 == 0) n /= 2;
else n = n * 3 + 1;
} while (true);
return 0;
}
第三步,倒序输出——下标从最后一个(cnt-1)往 0 走:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[1005];
int cnt = 0;
do {
a[cnt++] = n;
if (n == 1) break;
if (n % 2 == 0) n /= 2;
else n = n * 3 + 1;
} while (true);
for (int i = cnt - 1; i >= 0; i--) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
样例 20:变化过程是 20 10 5 16 8 4 2 1,倒序输出 1 2 4 8 16 5 10 20;输入 1 时序列只有 1,输出 1。
7.7 二维数组
一维数组是"一排盒子",二维数组是"一张表格":
int a[3][4]; // 3 行 4 列的表格,共 12 个格子
- 访问:
a[行][列],行和列都从 0 开始。a[0][0]是左上角第一个。 - 用双重循环遍历:外层管行,内层管列。
for (int i = 0; i < 3; i++) { // 行
for (int j = 0; j < 4; j++) { // 列
cin >> a[i][j];
}
}
矩阵、棋盘、地图,都是二维数组的天下。
7.8 例题 21 P5731 蛇形方阵
输入 n,输出 n×n 的方阵:从 1 开始,按顺时针螺旋填到 n²。每个数占 3 个字符(右对齐,
printf("%3d"))。
卡点分析:填数顺序是“右 → 下 → 左 → 上 → 右……”循环转向。两个技巧:
- 方向数组:把四个方向写成两个数组
dx[]、dy[],用下标 0~3 代表右/下/左/上,转向就是dir = (dir + 1) % 4; - 撞墙(越界或格子已填)就转向。判断“格子已填”:二维数组初始全 0,填过的格子非 0。
💡 这里故意从 1 开始编号(第 0 行第 0 列空着不用),这样判断“撞墙”(
nx < 1 || nx > n)更直观——下标从 0 还是从 1 开始是你的选择,写清楚就行。
分步写。第一步,读入 n,准备方向数组:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[10][10] = {}; // 全部初始化为 0
int dx[] = {0, 1, 0, -1}; // 右、下、左、上 的行变化
int dy[] = {1, 0, -1, 0}; // 右、下、左、上的列变化
int x = 1, y = 1, dir = 0; // 从 (1,1) 出发,先向右
return 0;
}
第二步,循环填数。每次先填当前格,再试探下一步:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
int a[10][10] = {};
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
int x = 1, y = 1, dir = 0;
for (int i = 1; i <= n * n; i++) {
a[x][y] = i; // 当前格填入 i
int nx = x + dx[dir]; // 试探下一步位置
int ny = y + dy[dir];
if (nx < 1 || nx > n || ny < 1 || ny > n || a[nx][ny] != 0) {
dir = (dir + 1) % 4; // 越界或已填:转向
nx = x + dx[dir];
ny = y + dy[dir];
}
x = nx;
y = ny;
}
return 0;
}
第三步,输出。%3d 表示每个数占 3 个字符右对齐(还记得第 4 章的 printf 吗,这里正好用上):
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
cin >> n;
int a[10][10] = {};
int dx[] = {0, 1, 0, -1};
int dy[] = {1, 0, -1, 0};
int x = 1, y = 1, dir = 0;
for (int i = 1; i <= n * n; i++) {
a[x][y] = i;
int nx = x + dx[dir];
int ny = y + dy[dir];
if (nx < 1 || nx > n || ny < 1 || ny > n || a[nx][ny] != 0) {
dir = (dir + 1) % 4;
nx = x + dx[dir];
ny = y + dy[dir];
}
x = nx;
y = ny;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
printf("%3d", a[i][j]);
}
printf("\n");
}
return 0;
}
样例 n=3:
1 2 3
8 9 4
7 6 5
💡 方向数组
dx/dy是竞赛高频技巧,走迷宫、搜索、BFS 全都要用,现在记住这个模式。
7.9 vector 与 sort(顺带认识)
遇到"不知道要存多少个数据"的情况,C++ 还提供动态数组 vector——可以随时往末尾加元素,不用提前定容量:
#include <vector>
vector<int> v; // 空的动态数组(能装 int)
v.push_back(5); // 末尾加一个 5
v.push_back(3); // 现在里面是 [5, 3]
v.size(); // 元素个数,现在是 2
v[0]; // 按下标访问,是 5(下标也从 0 开始)
配套的还有排序函数 sort,一行把数组/vector 从小到大排好:
#include <algorithm>
sort(v.begin(), v.end()); // 对 vector 排序
sort(a, a + n); // 对普通数组 a[0..n-1] 排序(a+n 是数组结尾的“尾后指针”,第 10 章讲)
v.begin() 和 v.end() 叫迭代器,可以理解为“指向开头/结尾的指针”,现在先照抄这个用法,第 10 章学了指针、后面学 STL 时会深入。v.size() 返回的是无符号整数,和 int 混用比较有坑,所以后面都写 (int)v.size() 强转(第 8 章细讲)。vector 和 sort 都在 <vector> / <algorithm> 库里。
7.10 例题 22 P1059 明明的随机数
生成 N 个 1~1000 的随机整数(可能有重复),要求去重后从小到大输出。第一行输出去重后的个数,第二行输出这些数。
卡点分析:排序 + 去重。有两条路:① 用 sort 排序,然后遍历时跳过重复的;② 因为数值范围只有 1~1000,用"桶标记"(bool 数组记录每个数出没出现过)天然有序且去重。两条都值得会。
路线 A:vector + sort。第一步,读入全部数到 vector:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> v;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
v.push_back(x);
}
return 0;
}
第二步,排序 + 输出去重结果。排完序后重复的数一定相邻,遍历时"和上一个不一样才输出":
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> v;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
v.push_back(x);
}
sort(v.begin(), v.end());
int cnt = 0;
for (int i = 0; i < (int)v.size(); i++) {
if (i == 0 || v[i] != v[i - 1]) { // 第一个数,或和上一个不同
cnt++;
cout << v[i] << " ";
}
}
cout << endl;
return 0;
}
等等,第一行还要先输出个数。调整:先数 cnt,再换行输出序列。最终完整代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> v;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
v.push_back(x);
}
sort(v.begin(), v.end());
int cnt = 0;
for (int i = 0; i < (int)v.size(); i++) {
if (i == 0 || v[i] != v[i - 1]) cnt++;
}
cout << cnt << endl;
for (int i = 0; i < (int)v.size(); i++) {
if (i == 0 || v[i] != v[i - 1]) {
cout << v[i] << " ";
}
}
cout << endl;
return 0;
}
路线 B:桶标记(更简单,值得掌握)。数值范围 1~1000,开一个 bool 数组当"桶",出现过的数在桶里打勾,最后从小到大把打过勾的数全倒出来:
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
bool appear[1005] = {};
for (int i = 0; i < n; i++) {
int x;
cin >> x;
appear[x] = true;
}
int cnt = 0;
for (int i = 1; i <= 1000; i++) {
if (appear[i]) cnt++;
}
cout << cnt << endl;
for (int i = 1; i <= 1000; i++) {
if (appear[i]) cout << i << " ";
}
cout << endl;
return 0;
}
桶标记(计数数组)是超级常用的技巧:用"值"当下标,用"出现过没有/出现几次"当内容。以后统计字符频率、哈希思想都是它。两条路线对比:桶标记快且简单,但要求数值范围小(这里 ≤1000 完美契合);vector+sort 通用,什么数据都能排。
7.11 本章小结
| 知识点 | 一句话记忆 |
|---|---|
int a[105]; |
一维数组,下标从 0 开始,容量用常量 |
| 数组越界 | RE 头号原因,容量开大点 |
while (cin >> x && x != 0) |
读到特定值/读到结束的多组输入 |
int a[3][4]; |
二维数组=表格,双重循环遍历 |
方向数组 dx[] dy[] |
右/下/左/上 = {0,1,0,-1} {1,0,-1,0} |
vector<int> v; v.push_back(x) |
动态数组,不用定容量 |
sort(v.begin(), v.end()) |
排序,一行搞定 |
| 桶标记 | 用值当下标,天然有序去重 |
练手:
- P5725 求三角形 ⭐:先输出 n×n 正方形(2 位补 0),空一行,再输出右对齐数字三角形(提示:
printf("%02d", x)补 0;右对齐就是在行首补2*(n-i)个空格) - P5732 杨辉三角 ⭐:输出前 n 行杨辉三角(提示:每行首尾是 1,中间
a[i][j] = a[i-1][j-1] + a[i-1][j]) - P5726 打分 ⭐⭐:n 个评委打分,去掉一个最高分一个最低分(同分也只去一个),求平均(提示:先找最大最小;平均 =
(double)(总和 - 最大 - 最小) / (n - 2),先转 double 再除,直接整数除会截断小数;保留 2 位小数) - P1047 校门外的树 ⭐⭐:马路 0~L 每个整数点一棵树,移走 M 个区间(含端点)的树,求剩余(提示:bool 数组标记被移走的树;树总数是 L+1 棵;区间含两端点)
- P2141 珠心算测验 ⭐⭐⭐:集合中有多少个数,恰好等于集合中另外两个不同数之和(提示:双重循环枚举两个加数,把“和”标记出来,最后数集合中哪些元素被标记了——注意只数集合里的数,别把不在集合里的“和”也数进去;每个数只计一次)
第 8 章 字符串
本章地图
8.1 string 基础
为什么需要 string:char 只能装一个字符,装一段文字要一连串字符。C 风格的做法是 char 数组(char s[100]),但长度要自己管、容易越界、不能直接拼接和比较,很麻烦——C++ 提供了 string 类型,自动管理长度、随便拼随便比。
string(字符串)在 <string> 库,iostream 通常已带:
string s = "hello";
string t;
cin >> t; // 读入一个单词(遇空格/换行停)
cout << s << t << endl; // 直接输出
s += " world"; // 拼接:s 变成 "hello world"
s.length(); // 长度:11
s.size(); // 也是长度
s[0]; // 按下标取字符:'h'(下标从 0 开始)
字符串可以当"字符数组"用:s[i] 取第 i 个字符,配合 for 遍历:
for (int i = 0; i < (int)s.length(); i++) {
// 处理 s[i]
}
⚠️ s.length() 返回的类型是 size_t(无符号整数),和 int 比较/做减法容易出诡异 bug,习惯性在前面加 (int) 强转,或直接用 int i < s.size() 配合强转。
8.2 例题 23 P5733 自动修正
输入一个字符串(可能含大写字母),把所有大写字母转成小写输出。
卡点分析:遍历每个字符,判断是不是大写(ASCII 在 'A'~'Z' 之间),是就加 32 变小写(第 3 章学过大小写相差 32)。也可以用 <cctype> 库的 tolower。
分步写。第一步,读入字符串并遍历:
#include <iostream>
using namespace std;
int main() {
string s;
cin >> s;
for (int i = 0; i < (int)s.length(); i++) {
// 处理 s[i]
}
return 0;
}
第二步,大写转小写。判断 s[i] >= 'A' && s[i] <= 'Z',然后 s[i] += 32(char 参与运算按 ASCII 整数算,算完还是存回字符):
#include <iostream>
using namespace std;
int main() {
string s;
cin >> s;
for (int i = 0; i < (int)s.length(); i++) {
if (s[i] >= 'A' && s[i] <= 'Z') {
s[i] += 32;
}
}
cout << s << endl;
return 0;
}
样例 Luogu 输出 luogu。等价写法:s[i] = tolower(s[i]);(tolower 在 <cctype> 库)。
8.3 例题 24 P5705 数字反转
输入一个不小于 100、小于 1000 且小数点后只有一位的小数(如 123.4),把它的数字顺序倒过来输出(4.321)。
卡点分析:当成字符串处理最省事——不用管小数点位置,整体倒序输出每个字符即可。倒序输出的套路第 7 章例题 19 见过:下标从 length()-1 走到 0。
分步写:
#include <iostream>
using namespace std;
int main() {
string s;
cin >> s;
for (int i = (int)s.length() - 1; i >= 0; i--) {
cout << s[i];
}
cout << endl;
return 0;
}
样例 123.4 输出 4.321。
8.4 getline:读一整行
cin >> s 遇到空格会停。想读"包含空格的一整行",用 getline:
string line;
getline(cin, line); // 读一整行(含空格),存进 line
⚠️ 经典大坑:getline 和 cin 混用。cin >> n 读完数字后,行尾的换行符还留在缓冲区,紧接着的 getline 会先把这个换行读走,得到一个空字符串:
int n;
cin >> n;
string line;
getline(cin, line); // ❌ line 是空的!
解决:在 getline 之前先把残留的换行吃掉:
cin >> n;
getchar(); // 吃掉换行(getchar 在 <cstdio> 里)
// 或 cin.ignore();
string line;
getline(cin, line); // ✅ 正常读到整行
8.5 例题 25 P5015 标题统计
输入一行标题(可能含空格),统计非空格字符的个数。
卡点分析:要读整行(含空格),必须 getline;然后遍历数非空格字符。
分步写。第一步,吃掉可能残留的换行,再 getline:
#include <iostream>
using namespace std;
int main() {
string s;
getline(cin, s); // 这题输入就一行,直接读
return 0;
}
第二步,遍历统计:
#include <iostream>
using namespace std;
int main() {
string s;
getline(cin, s);
int cnt = 0;
for (int i = 0; i < (int)s.length(); i++) {
if (s[i] != ' ') cnt++;
}
cout << cnt << endl;
return 0;
}
样例 Ca 45 输出 4(C、a、4、5 四个字符,空格不算)。
8.6 string 常用方法
| 方法 | 作用 | 例子 |
|---|---|---|
s.length() / s.size() |
长度 | string s="abc"; s.length() = 3 |
s.substr(a, b) |
从第 a 个字符开始取 b 个(a 从 0 起) | string s="hello"; s.substr(1, 3) = "ell" |
s.find(t) |
找子串 t 第一次出现的位置,找不到返回 string::npos |
string s="hello"; s.find("ll") = 2 |
s.insert(a, t) |
在第 a 个字符前插入 t | string s="hello"; s.insert(2, "xx") = "hexxllo" |
s.erase(a, b) |
删除从第 a 个起的 b 个字符 | string s="hello"; s.erase(1, 2) = "hlo" |
s += t |
尾部追加 | string s="a"; s += "b"; 后 s = "ab" |
to_string(x) |
数字转字符串 | to_string(123) = "123" |
stoi(s) |
字符串转 int | stoi("123") = 123 |
s1 < s2 |
直接比较(字典序) | string a="abc", b="abd"; a < b |
⚠️ 注意:这些方法都要用变量调用(先
string s = ...再s.xxx()),不能直接写在字符串字面量上("abc".length()会编译失败);+拼串也不能直接加两个字面量("a" + "b"不合法),先存进变量再操作。⚠️
find找不到返回string::npos,判断时写if (s.find(t) == string::npos),不要用-1或者省略判断,npos 是很大的数不是 -1(虽然数值上常等于 -1 的转换,但写 npos 最规范)。
8.7 例题 26 P5734 文字处理软件
初始有一段文档,接下来 q 次操作:
1 str:在文档末尾追加 str2 a b:截取文档第 a 个字符开始的 b 个字符作为新文档3 a str:在文档第 a 个字符前插入 str4 str:输出 str 在文档中第一次出现的位置,找不到输出 -1操作 1/2/3 执行后要输出当前文档,操作 4 输出位置。
卡点分析:这就是 string 方法全家桶——+=(追加)、substr(截取)、insert(插入)、find(查找)各就各位。注意 q 和初始文档分两行输入,cin >> q >> s 自动跳过换行,连续读两个即可;操作 2 截取后文档就变成截取出来的部分了。
分步写。第一步,读入 q 和初始文档(cin 连续读两个即可,自动跳过换行):
#include <iostream>
using namespace std;
int main() {
int q;
string s;
cin >> q >> s;
return 0;
}
第二步,循环处理 q 个操作。操作 1:追加并输出:
#include <iostream>
using namespace std;
int main() {
int q;
string s;
cin >> q >> s;
while (q--) { // q--:先用 q 当前值判断(非 0 就进循环),再减 1——正好循环 q 次
int op;
cin >> op;
if (op == 1) {
string t;
cin >> t;
s += t;
cout << s << endl;
}
// 其余操作继续写...
}
return 0;
}
第三步,补全操作 2/3/4:
#include <iostream>
using namespace std;
int main() {
int q;
string s;
cin >> q >> s;
while (q--) {
int op;
cin >> op;
if (op == 1) {
string t;
cin >> t;
s += t;
cout << s << endl;
} else if (op == 2) {
int a, b;
cin >> a >> b;
s = s.substr(a, b); // 文档变成截取的部分
cout << s << endl;
} else if (op == 3) {
int a;
string t;
cin >> a >> t;
s.insert(a, t);
cout << s << endl;
} else {
string t;
cin >> t;
int pos = s.find(t); // find 返回位置;用 int 存,找不到时是 -1(string::npos)
if (pos == string::npos) {
cout << -1 << endl;
} else {
cout << pos << endl;
}
}
}
return 0;
}
8.8 sscanf:从字符串里提取数据
scanf 是从键盘/输入流读,sscanf 是从字符串里按格式读——把字符串当输入源(格式符和 scanf 完全一样:%d/%lf/%c…,第 4 章那张表):
#include <cstdio>
char buf[100]; // 字符数组(C 风格字符串)
sscanf(buf, "%d %d", &a, &b); // 从 buf 中按格式提取两个整数
// string 转成字符数组再提取:
string s;
sscanf(s.c_str(), "%d %d", &a, &b); // c_str() 把 string 变成 C 风格字符串
什么时候用:数据"藏"在字符串里(比如一行里混着运算符和数字、格式不规则),直接 cin 拆不开,就上 sscanf 按格式抠。顺带认识字符数组:char buf[100] 是 C 语言风格的字符串(一串 char),scanf("%s", buf) 能读入单词,但不如 string 安全方便,现在知道它长什么样即可。
8.9 例题 27 P1957 口算练习题
输入 n 道口算题。每题一行,可能是"运算符 + 两个数"(运算符 a=加、b=减、c=乘),也可能只有两个数(运算符沿用上一题)。输出完整算式(如
64+46=110)和它的长度。
卡点分析:难点在每行的数据个数不定(2 个或 3 个)。思路:整行读进来,看第一个字符是不是运算符(a/b/c):
- 是 → 用 sscanf 按"字符 整数 整数"提取;
- 不是 → 用 sscanf 按"整数 整数"提取,运算符沿用上一个。
另外注意 getline 前要先吃掉 cin >> n 残留的换行(8.4 的坑,这里实际踩一遍)。
分步写。第一步,吃掉换行,整行循环读入:
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
cin >> n;
getchar(); // 吃掉 cin >> n 留下的换行
char op = 'a'; // 运算符('a'加/'b'减/'c'乘),题目保证第一行是完整算式,这里先随便给
while (n--) {
string line;
getline(cin, line);
// 解析 line...
}
return 0;
}
第二步,判断并解析。line[0] 是行首字符,是 a/b/c 就是运算符行,否则是纯数字行:
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
cin >> n;
getchar();
char op = 'a';
while (n--) {
string line;
getline(cin, line);
int x, y;
if (line[0] == 'a' || line[0] == 'b' || line[0] == 'c') {
sscanf(line.c_str(), "%c %d %d", &op, &x, &y);
} else {
sscanf(line.c_str(), "%d %d", &x, &y);
}
// 计算并输出...
}
return 0;
}
第三步,计算并输出算式和长度。用 to_string 拼算式(顺便求长度),op 是 'a'/'b'/'c',算的时候转成 + - *:
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
int n;
cin >> n;
getchar();
char op = 'a';
while (n--) {
string line;
getline(cin, line);
int x, y;
if (line[0] == 'a' || line[0] == 'b' || line[0] == 'c') {
sscanf(line.c_str(), "%c %d %d", &op, &x, &y);
} else {
sscanf(line.c_str(), "%d %d", &x, &y);
}
int z;
char sign; // 算式里的运算符符号
if (op == 'a') { z = x + y; sign = '+'; }
else if (op == 'b') { z = x - y; sign = '-'; }
else { z = x * y; sign = '*'; }
string expr = to_string(x) + sign + to_string(y) + "=" + to_string(z);
cout << expr << endl;
cout << expr.length() << endl;
}
return 0;
}
样例第一题 a 64 46:z=110,expr=64+46=110,长度 9(6、4、+、4、6、=、1、1、0 共 9 个字符),输出 64+46=110 和 9。
8.10 本章小结
| 知识点 | 一句话记忆 |
|---|---|
string s; s[i] |
字符串,可当下标访问的字符序列 |
s.length() |
长度,配合遍历时记得 (int) 强转 |
getline(cin, s) |
读一整行(含空格) |
| getline 前吃换行 | cin >> n 后 getchar() 或 cin.ignore() |
s.substr(a, b) s.insert(a, t) s.find(t) |
截取 / 插入 / 查找,找不到是 string::npos |
to_string(x) / stoi(s) |
数字↔字符串互转 |
sscanf(s.c_str(), ...) |
从字符串按格式提取数据 |
练手:
- P1055 ISBN 号码 ⭐⭐:ISBN 前 9 位逐位加权(1~9)求和 mod 11,校验位对得上输出 Right,否则输出修正后的完整 ISBN(提示:跳过 '-' 字符;字符转数字用
c - '0';结果 10 用 'X' 表示) - P1597 语句解析 ⭐⭐⭐:解析一串
a:=3;b:=4;c:=a;格式的赋值语句,输出 a b c 最终值(提示:用getline读整行,从下标 0 手写解析:读一个变量名 → 跳过:=→ 读值(是数字就转成数,是变量名就读它的当前值)→ 跳过;;用数组val[256]按字符存变量当前值,未赋值过的默认 0) - P1308 统计单词数 ⭐⭐⭐:给定单词和文章,统计该单词作为独立单词(整词匹配,部分匹配不算)出现的次数和第一次出现的位置(从 0 起),没出现输出 -1(提示:文章含空格,必须 getline;大小写不敏感,全部转小写再比;位置是首字母下标。整词匹配的配方:用
find找到位置后,检查前一个字符必须是空格或开头、后一个字符必须是空格或结尾,否则继续往后找;或把文章按空格拆成单词逐个比)
第 9 章 函数与递归
本章地图
9.1 为什么需要函数
先看一个场景:第 6 章例题 16(质数口袋)里判断质数的代码写了一大坨。如果一道题里要判断 10 次质数,难道复制 10 遍?代码复制粘贴有三个坏处:占地方(主程序又长又乱)、改起来要改 10 处(漏一处就出 bug)、读起来费劲(满屏重复,看不出重点)。
函数就是把一段指令打包,起个名字,随用随调:
- 代码只写一遍,到处调用;
- 主程序变短,逻辑清晰(看函数名就知道在干嘛);
- 出 bug 只改一处。
打个比方:函数就像"食堂的固定菜谱"——做红烧肉的步骤只写一遍贴在厨房,谁点这道菜,后厨按菜谱做一份端出来,不用每次重新发明做法。
9.2 函数基础
返回类型 函数名(参数列表) {
// 函数体:要执行的指令
return 返回值;
}
拆开看每一块:
- 返回类型:函数算完,吐出一个什么类型的值(
int、double、bool…)。不需要吐值的用void(这时可以不写 return 或写return;)。 - 函数名:你给这段指令起的名字,调用时就用它。
- 参数列表:函数需要的"原料"。可以没有(空括号),也可以多个(逗号隔开)。调用时传进去的值叫实参,函数定义里接收的变量叫形参——实参按顺序对应形参。
- 函数体:打包的指令,写在大括号里。
return 返回值;:把结果交出去,同时函数立刻结束(return 后面的语句不再执行)。
一个完整的例子:
#include <iostream>
using namespace std;
int add(int x, int y) { // 返回 int,两个 int 形参
return x + y; // 把 x+y 的结果交出去
}
int main() {
int s = add(3, 5); // 调用:实参 3、5 按顺序给 x、y,s = 8
cout << s << endl;
return 0;
}
调用 add(3, 5) 时发生了什么:程序跳到 add 函数,把 3 交给 x、5 交给 y,执行 return x + y 算出 8 交回来,再跳回 main 继续——调用时程序暂停当前的事,跳去执行函数体,return 带着结果跳回调用处。
上面这种传法是值传递(传的是副本,函数里改不到外面):
#include <iostream>
using namespace std;
void change(int x) {
x = 100; // 改的是副本
}
int main() {
int a = 1;
change(a);
cout << a << endl; // 输出 1:a 没变
return 0;
}
想改到外面的变量怎么办?第 10 章的指针/引用传参就是干这个的。
⚠️ 函数要写在 main 前面,main 才能调用它——C++ 按从上到下的顺序找名字。也可以把函数写在 main 后面,但必须在 main 前面加一行函数声明(也叫原型,以分号结尾、没有函数体):long long fac(int n);——告诉编译器“这个函数存在,定义在后面”。
9.3 例题 28 P5735 距离函数
输入平面三个点的坐标(实数),输出它们围成的三角形周长,保留 2 位小数。
两点距离公式:$d = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$
卡点分析:三条边都要算距离,而"算两点距离"是同一段计算用了三遍(只是坐标不一样)——这正是函数最典型的用法:把重复计算打包,调用三次。这题也展示 double 类型函数怎么写。
分步写。第一步,把"算两点距离"打包成函数:
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy); // 距离公式
}
注意:函数里的 x1、y1 这些是形参,和 main 里要读的变量名字可以一样也可以不一样,各是各的。
第二步,main 里读三个点、调三次 dist、求和输出:
#include <iostream>
#include <cmath>
#include <iomanip>
using namespace std;
double dist(double x1, double y1, double x2, double y2) {
double dx = x1 - x2;
double dy = y1 - y2;
return sqrt(dx * dx + dy * dy);
}
int main() {
double x1, y1, x2, y2, x3, y3;
cin >> x1 >> y1 >> x2 >> y2 >> x3 >> y3;
double c = dist(x1, y1, x2, y2) + dist(x2, y2, x3, y3) + dist(x3, y3, x1, y1);
cout << fixed << setprecision(2) << c << endl;
return 0;
}
如果不用函数,这三条边得把距离公式抄三遍,又长又容易抄错。函数一次写好,三处调用,清清楚楚。
9.4 例题 29 P5736 质数筛
输入 n 个正整数,去掉非质数,输出剩下的质数。注意 1 不是质数。
卡点分析:判断质数在第 6 章写过一次(试除法),现在把它封装成 bool 函数——返回"是不是质数"的判断结果。以后任何地方要判质数,一行调用。这是函数第二个典型用法:把一段逻辑打包成"是/否"的判断器。
分步写。第一步,封装判断质数的函数:
bool isPrime(int x) {
if (x < 2) return false; // 1 和负数不是质数
for (int j = 2; j * j <= x; j++) {
if (x % j == 0) return false; // 找到因子,不是质数,立刻结束函数
}
return true; // 试完都没有因子,是质数
}
return false 在循环里出现也没问题——return 会让函数立刻结束,后面的代码不再执行。
第二步,main 里逐个判断输出:
#include <iostream>
using namespace std;
bool isPrime(int x) {
if (x < 2) return false;
for (int j = 2; j * j <= x; j++) {
if (x % j == 0) return false;
}
return true;
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
if (isPrime(x)) { // 一行调用,main 里完全看不到试除法的细节
cout << x << " ";
}
}
cout << endl;
return 0;
}
样例 5 3 4 5 6 7 输出 3 5 7。
💡 对比 9.3:dist 是"算一个值"(double 函数),isPrime 是"判断是/否"(bool 函数)——函数可以吐出任意的类型,这就是"返回类型"的意义。
9.5 递归:函数调用自己
递归:函数在函数体里调用自己。听起来像"自己生自己",但它不是死循环——关键在于每次调用自己时,问题的规模都变小了一点,直到小到不用再调自己。
阶乘是最经典的例子:$n! = n \times (n-1)!$,而且 $0! = 1! = 1$。翻译成函数:
long long fac(int n) {
if (n <= 1) return 1; // ① 递归出口:不再调用自己
return n * fac(n - 1); // ② 递归调用:调用自己,规模从 n 变 n-1
}
递归三要素(缺一不可):
- 递归出口:某个情况下不再调用自己,直接返回(否则会无限调用,栈溢出 RE);
- 递归调用:函数体里调用自己;
- 规模越来越小:每次调用都往出口靠近(n → n-1 → n-2 → … → 1)。
执行过程:先"递"后"归"。算 fac(3) 时:
fac(3) = 3 × fac(2) ← 先往下递:算 fac(2)
= 3 × (2 × fac(1)) ← 再递:算 fac(1)
= 3 × (2 × 1) ← 触底:fac(1) 走出口,直接返回 1
= 3 × 2 ← 然后一层层往回归:把结果带上来
= 6
像一列火车:车头先一路往前开(递),到终点后每节车厢依次往后传消息(归)。初学觉得绕很正常,把上面的展开式手写几遍就懂了。
两个容易懵的点:① 每层调用里的 n 是各自的副本——fac(4) 里的 n=4 和 fac(3) 里的 n=3 互不影响;② 每次调用都会占一小块内存记下“回到哪、还差什么没算”,层数太深(比如出口写错)内存耗尽就会 RE——这就是“爆栈”。
递归 vs 循环:能递归的都能循环,能循环的也都能递归,但擅长的事不同:
| 循环 | 递归 | |
|---|---|---|
| 适合 | 次数明确、线性推进 | 问题天然分层(树的遍历、分治) |
| 代码 | 长一点但直接 | 短、符合数学定义 |
| 风险 | 边界写错死循环 | 出口忘写爆栈(栈溢出 RE) |
初学阶段:能用循环就用循环(好调试);递归先把"怎么写出口、怎么缩小规模"练熟,后面学搜索、分治时它就是主力。
9.6 例题 30 P5739 计算阶乘
输入 n(≤20),输出 n!(1×2×…×n)。
卡点分析:n≤20 时 13! 就超过 int 范围了,返回值用 long long。阶乘有两种写法:循环累乘(第 6 章学过的思路)或递归——上一节刚讲过,这题就是把递归用起来。
分步写。第一步,写递归函数(就是 9.5 那个 fac):
long long fac(int n) {
if (n <= 1) return 1; // 递归出口:n=0 或 1 时直接返回 1
return n * fac(n - 1); // 递归:规模缩小
}
第二步,main 里读入并调用:
#include <iostream>
using namespace std;
long long fac(int n) {
if (n <= 1) return 1;
return n * fac(n - 1);
}
int main() {
int n;
cin >> n;
cout << fac(n) << endl;
return 0;
}
样例 5:fac(5) = 5×4×3×2×1 = 120。跟着 9.5 的展开式走一遍:fac(5) → fac(4) → … → fac(1) 触底,再一层层乘回来。
9.7 例题 31 P1035 级数求和
已知 $S_n = 1 + \frac{1}{2} + \frac{1}{3} + \dots + \frac{1}{n}$。输入 k,求最小的 n 使得 $S_n > k$。
卡点分析:要从小到大试 n,每次在上一个 Sn 的基础上加一项(增量累加),而不是从头重算——否则 k 稍大就 O(n²) 超时(k=12 时实测能跑到 5 秒以上)。这题体现的是"性能意识",竞赛里 TLE 的头号原因就是这种无谓的重复计算。
分步写。第一步,搭骨架:
#include <iostream>
using namespace std;
int main() {
int k;
cin >> k;
double s = 0; // 当前的 Sn
int n = 0;
return 0;
}
第二步,循环累加,直到 Sn > k。注意 1.0 / n 要写成浮点除法(1 / n 是整数除法恒为 0):
#include <iostream>
using namespace std;
int main() {
int k;
cin >> k;
double s = 0;
int n = 0;
while (s <= k) { // 还没超过就继续加
n++;
s += 1.0 / n; // 增量累加:只加新的一项
}
cout << n << endl;
return 0;
}
样例 1:S1=1 不大于 1,继续;S2=1.5 > 1,n=2,输出 2。
⚠️ 性能坑:如果写成"每试一个 n 就重新算一遍完整的 Sn",n 到几千就明显卡顿。凡是"求第 n 项/前 n 项和",优先想增量:上一轮的结果留着,只算新变化的部分。
9.8 全局变量与作用域
前面写的变量都声明在函数里面,它们有个共同特点:只在所属函数的花括号里有效。看代码:
#include <iostream>
using namespace std;
int total = 0; // 全局变量:写在所有函数外面
void addScore(int s) { // void:不需要返回值
total += s; // 直接改全局变量 total
}
int main() {
addScore(80);
addScore(90);
cout << total << endl; // 输出 170:两次调用改的是同一个 total
return 0;
}
- 全局变量(写在所有函数外面):所有函数都能访问,整个程序运行期间都活着。上面的
total被 addScore 改了,main 里能读到改后的值。 - 局部变量(写在函数里面):只能在它所在的花括号里用。比如 addScore 里如果声明
int x = 1;,main 里用 x 会编译报错"未声明"——出了花括号它就不存在了。 - 作用域 = "名字有效的地盘"。写在哪对的花括号里,作用域就到哪。
对比一下:如果 total 是 main 里的局部变量,addScore 根本看不到它(编译报错);正因为 total 是全局的,两个函数才共享它。这就是全局变量的用途:跨函数共享数据。
⚠️ 两个注意点:
- 全局变量能不用就不用(局部变量优先)——全局意味着"谁都能改",代码一多就分不清是谁改的,难调试;
- 局部变量和全局变量重名时,函数里用的是局部变量("就近原则")。这种代码很难读,别这么写。
💡 全局变量有一个实用场景:数组开在 main 外面会自动全部清零(局部数组不会)。如果一道题需要很大的数组、又希望它初始全是 0,可以声明成全局数组。但要注意局部数组不初始化是垃圾值(第 7 章讲过),能写 = {} 就用 = {}。
9.9 本章小结
| 知识点 | 一句话记忆 |
|---|---|
返回类型 函数名(参数) { return 值; } |
函数:打包指令,随用随调 |
void |
不需要返回值的函数 |
| 函数典型用法 | ① 重复计算打包(dist 调三次)② 判断逻辑打包(isPrime) |
| 递归 | 函数调用自己,三要素:出口/调用/规模缩小,先递后归 |
| 递归 vs 循环 | 能用循环先用循环,递归强在分层问题 |
| 增量累加 | 只算新变化的部分,别重复计算(防 TLE) |
| 全局变量 / 作用域 | 写在函数外、全体共享;能不用就不用 |
练手:
- P5737 闰年展示 ⭐⭐:输出区间 [x, y] 内所有闰年(提示:把闰年判断写成一个 bool 函数,main 里循环调用,先输出个数再输出列表)
- P5738 歌唱比赛 ⭐⭐:n 名选手 m 个评委打分,每人去掉一个最高分一个最低分求平均,输出最高平均分(提示:写一个"算平均"的函数,里面先找最大最小,平均 =
(double)(总和 - 最大 - 最小) / (m - 2)——先转 double 再除;保留 2 位小数) - P1028 数的计算 ⭐⭐⭐:对 n,可以在数列末尾不断添加一个不超过末项一半的正整数,求能生成多少种不同数列(提示:设 f[i] 为"以 i 开头的数列个数",f[i] = 1 + f[1] + f[2] + ... + f[i/2],从小到大算,不要用纯递归——n=1000 时纯递归会超时,用数组递推,数组用
long long更稳)
第 10 章 指针与引用
本章地图
- 学什么:地址、指针、指针算术、指针传参 vs 引用传参
- 前置知识:第 7 章(数组)、第 9 章(函数)
- 配套题目:例题 32(改写 P1046)、练习(改写 P1427 等)
- 🎬 学习建议:指针用纯文字讲比较抽象,建议先找 B 站的"C++ 指针入门"视频看一遍(15 分钟那种就够),再回来读本章做系统梳理和练习。本教程的目标是"会用":会加加减减、会查元素值、会用指针和引用传参、能区分两者区别。更深的指针玩法留到以后。
注:洛谷没有专门的"指针题",本章例题和练习都是用指针重写前面章节的题,检验你是否真的会用了。
10.1 地址:变量的门牌号
内存就像一栋公寓楼,每个变量占据一个房间。地址就是房间的门牌号。用 & 可以问“这个变量住在几号房”(还记得第 4 章 scanf 里那个不能丢的 & 吗?就是它):
int a = 5;
cout << &a << endl; // 输出类似 0x7ffd3a2b1c44 的一串数字 = a 的地址
每次运行地址都可能不同(房间是程序运行时分配的),这很正常。
10.2 指针:装地址的变量
指针(pointer)就是"用来装地址的变量"。声明时写 类型*:
int a = 5;
int* p = &a; // p 是一个指针,里面装的是 a 的地址
有了地址,就能顺着门牌号找到房间、取出里面的值——用 * 解引用:
cout << *p << endl; // 到 p 存的地址去取值 = 5
*p = 10; // 顺着地址改值:a 变成了 10
cout << a << endl; // 10
* 的三个身份(看上下文区分):
- 声明指针:
int* p; - 解引用取值:
*p - 乘法:
a * b
10.3 指针与数组
数组名就是数组第一个元素的地址:a 等价于 &a[0]。所以指针可以直接指向数组,配合指针算术(加加减减)遍历:
int a[5] = {10, 20, 30, 40, 50};
int* p = a; // p 指向 a[0]
cout << *p << endl; // 10
cout << *(p + 1) << endl; // 20:p+1 指向下一个元素
cout << *(a + 2) << endl; // 30,等价于 a[2]
p++; // 指针加一:指向 a[1]
注意:p + 1 不是"地址加 1 字节",而是"指向下一个元素"(int 指针 +1 实际跳 4 字节)。这就是指针"加加减减"的含义——在数组里前后移动。
用指针遍历数组(和下标遍历等价):
for (int* q = a; q < a + 5; q++) {
cout << *q << " "; // 从 a[0] 走到 a[4]
}
10.4 例题 32 改写 P1046(指针遍历)
第 7 章例题 17 的陶陶摘苹果,原来用下标 a[i] 遍历,现在改用指针。第一种:指针加偏移:
#include <iostream>
using namespace std;
int main() {
int a[10];
for (int i = 0; i < 10; i++) {
cin >> a[i];
}
int h;
cin >> h;
int reach = h + 30;
int cnt = 0;
for (int* p = a; p < a + 10; p++) { // 指针从 a[0] 走到 a[9]
if (*p <= reach) cnt++; // 解引用取值判断
}
cout << cnt << endl;
return 0;
}
第二种:指针加下标(p[i] 和 *(p+i) 完全等价):
#include <iostream>
using namespace std;
int main() {
int a[10];
for (int i = 0; i < 10; i++) {
cin >> a[i];
}
int h;
cin >> h;
int reach = h + 30;
int cnt = 0;
int* p = a; // p 指向数组开头
for (int i = 0; i < 10; i++) {
if (p[i] <= reach) cnt++; // p[i] 等价 *(p+i) 等价 a[i]
}
cout << cnt << endl;
return 0;
}
两种写法 AC 效果一样。看懂 a[i] 和 *(a+i) 的等价关系,你就算真正理解数组和指针了。
10.5 传参:值传递 / 指针 / 引用
函数传参有三种方式,区别在能不能改到外面的原变量。以"交换两个数"为例:
// 方式一:值传递 —— 传的是副本,改不到外面 ❌
void swap1(int x, int y) {
int t = x;
x = y;
y = t;
}
// 方式二:指针传参 —— 传地址,函数里解引用操作原数据 ✅
void swap2(int* x, int* y) {
int t = *x;
*x = *y;
*y = t;
}
// 方式三:引用传参 —— 参数是实参的别名,直接操作原数据 ✅(int& 的引用语法马上 10.6 讲)
void swap3(int& x, int& y) {
int t = x;
x = y;
y = t;
}
int main() {
int a = 3, b = 5;
swap1(a, b); // ❌ a b 还是 3 5
swap2(&a, &b); // ✅ 调用时要传地址
swap3(a, b); // ✅ 调用时和普通函数一样
return 0;
}
10.6 引用
引用(reference)是变量的"别名":声明 int& r = a; 之后,r 和 a 是同一个房间的两个名字,改 r 就是改 a:
int a = 5;
int& r = a; // r 是 a 的别名
r = 99; // a 也变成 99
引用和指针都能"间接操作原数据",区别:
| 指针 | 引用 | |
|---|---|---|
| 声明 | int* p = &a; |
int& r = a; |
| 使用 | 要解引用 *p |
直接用 r(就是 a 的别名) |
| 传参调用 | 要传地址 swap2(&a, &b) |
和普通调用一样 swap3(a, b) |
| 安全性 | 可能指向空/野地址 | 必须初始化,绑定后不能换 |
| 空值 | 可以(危险) | 不可以 |
结论:现代 C++ 写函数,能用引用就用引用——写法像值传递(舒服),效果是直接操作原数据(高效),还不会像指针那样踩空指针的坑。指针要掌握(看题解、理解底层都需要),但自己写代码优先引用。
⚠️ 顺带澄清:
&在 C++ 里有三个身份,看上下文区分——① 位运算的“按位与”(第 3 章a & b);② “取地址”(第 4 章 scanf、本章&a);③ “引用声明”(本章int& r)。💡 后面学 STL、结构体时会看到
const int&这种写法:const表示“只读不修改”,&表示“不复制直接引用”。传大东西(比如长字符串、结构体)时避免复制开销,这是竞赛里的常见优化。
10.7 本章小结
| 知识点 | 一句话记忆 |
|---|---|
&a |
取变量 a 的地址(门牌号) |
int* p = &a; |
指针 = 装地址的变量 |
*p |
解引用:顺着地址取值/改值 |
p + 1 |
指向下一个元素(不是 +1 字节) |
a[i] 等价 *(a+i) |
数组名是首元素地址 |
| 值传递 | 传副本,改不到外面 |
| 指针传参 | 传地址,函数里 *p 操作原数据 |
| 引用传参 | 参数是实参别名,直接操作原数据,首选 |
完整的知识点↔题目对照见附录 C 总题单。
练手(全部用指针/引用重写):
- 改写 P1427 ⭐⭐:倒序输出——指针从
a + n - 1一路减到a,每步解引用输出 - 改写 P1046 ⭐⭐:三种遍历(下标 / 指针偏移 / p[i])各写一遍
- swap 三版对比 ⭐⭐:把 10.5 的 swap1/2/3 完整跑一遍,验证只有值传递那版失败,理解"为什么"
- 改写 P1428 ⭐⭐⭐(选做):用指针完成双重循环统计
🏁 教程完成标准:全部例题能不看教程独立 AC;练习至少完成每个 ⭐ 和 ⭐⭐;挑战题(⭐⭐⭐)至少完成 3 道。达成后,你的 C++ 语法关就正式打通——可以开始学结构体、STL 和算法了。
附录
A. 知识点速查表
程序骨架
#include <iostream> // 输入输出库
#include <cstdio> // printf/scanf(用 C 风格 IO 时)
#include <cmath> // sqrt/abs 等数学函数
#include <iomanip> // setprecision 等格式控制
#include <vector> // vector 动态数组
#include <algorithm> // sort 等算法
using namespace std;
int main() {
ios::sync_with_stdio(false); // 用 cin/cout 时提速
cin.tie(0);
// 你的代码
return 0;
}
变量与类型
| 类型 | 范围 | 备注 |
|---|---|---|
int |
约 ±21 亿 | 默认整数 |
long long |
约 ±9×10^18 | 大数用这个 |
double |
约 15~16 位有效数字 | 小数;浮点别直接比相等 |
char |
一个字符 | 本质是 ASCII 整数 |
bool |
true / false | 存条件成立与否 |
string |
一段文字 | 需 <string>(iostream 通常已带) |
输入输出
| 写法 | 说明 |
|---|---|
cin >> a; |
读入,遇空格/换行停 |
cout << a << endl; |
输出并换行('\n' 等价) |
scanf("%d", &a); |
C 风格读入,别忘了 & |
printf("%d", a); |
C 风格输出 |
%d %lld %lf %c %.2lf |
int / long long / double / char / 两位小数 |
fixed << setprecision(3) |
保留 3 位小数(iomanip) |
getline(cin, s); |
读一整行(含空格),前面有 cin 先 getchar() |
while (cin >> x) |
读到文件结束(多组数据) |
sscanf(s.c_str(), "%d", &x) |
从字符串里提取数据 |
运算符(从高到低,常用部分)
| 优先级 | 运算符 |
|---|---|
| 高 | ( ) |
* / % |
|
+ - |
|
<< >>(位运算左移右移) |
|
< > <= >= |
|
== != |
|
&(位与) |
|
^(位异或) |
|
\|(位或) |
|
&&(逻辑与) |
|
\|\|(逻辑或) |
|
| 低 | = += 等赋值 |
拿不准就加括号。
分支与循环
if (条件) { } else if (条件) { } else { }
switch (整数变量) { case 值: ... break; default: ... }
for (int i = 1; i <= n; i++) { }
while (条件) { }
do { } while (条件); // 至少执行一次
break; // 跳出循环
continue; // 跳到下一次
数组
int a[105]; // 一维,容量写常量,开大一点
int b[10][10] = {}; // 二维(表格),{} 全部初始化为 0
a[i] / b[i][j] // 下标从 0 开始
字符串常用方法
s.length() / s.size() // 长度
s.substr(a, b) // 从 a 开始取 b 个
s.find(t) // 找子串位置,找不到 string::npos
s.insert(a, t) // 在 a 前插入
s += t // 尾部追加
to_string(x) / stoi(s) // 数字↔字符串
函数与递归
返回类型 函数名(参数) { return 值; } // 定义
函数名(实参); // 调用
// 递归:函数调用自己,必须有出口
指针与引用
&a // 取 a 的地址
int* p = &a; // 指针:装地址
*p // 解引用:取/改地址里的值
p + 1 // 指向下一个元素
a[i] 等价 *(a + i)
int& r = a; // 引用:a 的别名
void f(int& x) { } // 引用传参:直接改原数据
B. ASCII 表
常用字符(码值 32~126,另附 127 DEL;其余是控制字符):
| 码值 | 字符 | 码值 | 字符 | 码值 | 字符 | 码值 | 字符 |
|---|---|---|---|---|---|---|---|
| 32 | (空格) | 56 | 8 | 80 | P | 104 | h |
| 33 | ! | 57 | 9 | 81 | Q | 105 | i |
| 34 | " | 58 | : | 82 | R | 106 | j |
| 35 | # | 59 | ; | 83 | S | 107 | k |
| 36 | $ | 60 | < | 84 | T | 108 | l |
| 37 | % | 61 | = | 85 | U | 109 | m |
| 38 | & | 62 | > | 86 | V | 110 | n |
| 39 | ' | 63 | ? | 87 | W | 111 | o |
| 40 | ( | 64 | @ | 88 | X | 112 | p |
| 41 | ) | 65 | A | 89 | Y | 113 | q |
| 42 | * | 66 | B | 90 | Z | 114 | r |
| 43 | + | 67 | C | 91 | [ | 115 | s |
| 44 | , | 68 | D | 92 | \ | 116 | t |
| 45 | - | 69 | E | 93 | ] | 117 | u |
| 46 | . | 70 | F | 94 | ^ | 118 | v |
| 47 | / | 71 | G | 95 | _ | 119 | w |
| 48 | 0 | 72 | H | 96 | ` | 120 | x |
| 49 | 1 | 73 | I | 97 | a | 121 | y |
| 50 | 2 | 74 | J | 98 | b | 122 | z |
| 51 | 3 | 75 | K | 99 | c | 123 | { |
| 52 | 4 | 76 | L | 100 | d | 124 | | |
| 53 | 5 | 77 | M | 101 | e | 125 | } |
| 54 | 6 | 78 | N | 102 | f | 126 | ~ |
| 55 | 7 | 79 | O | 103 | g | 127 | DEL |
常用控制字符:'\0'(字符串结束符)=0、'\t'(制表符)=9、'\n'(换行)=10、'\r'(回车)=13。
规律:'0'~'9' 连续(48~57),'A'~'Z' 连续(65~90),'a'~'z' 连续(97~122),大小写相差 32。数字字符转数值:c - '0';大写变小写:c + 32。
C. 总题单
全部为洛谷题目。例题(正文讲解过)建议独立重写一遍;练习(只给提示)是自己检验掌握程度的试金石。⭐ 必做 / ⭐⭐ 巩固 / ⭐⭐⭐ 挑战。
例题清单
| 章节 | 题号 | 题名 | 核心知识点 |
|---|---|---|---|
| 1 | P1000 | 超级玛丽游戏 | cout、字符画、空格敏感 |
| 2 | P1001 | A+B Problem | cin、变量、加法 |
| 2 | P3954 | 成绩 | 表达式、运算顺序 |
| 2 | P1425 | 小鱼的游泳时间 | 换算分钟、/ % |
| 3 | P5703 | 苹果采购 | long long、溢出 |
| 3 | P5704 | 字母转换 | char、ASCII |
| 4 | P5706 | 再分肥宅水 | 浮点、fixed+setprecision |
| 4 | P5708 | 三角形面积 | sqrt、scanf/printf |
| 4 | P5709 | 苹果和虫子 | 向上取整、除零特判 |
| 5 | P5711 | 闰年判断 | if、逻辑运算 |
| 5 | P5710 | 数的性质 | bool、异或 |
| 5 | P5714 | 肥胖问题 | 多分支、浮点输入 |
| 5 | P5716 | 月份天数 | switch |
| 6 | P5719 | 分类平均 | for、累加 |
| 6 | P5724 | 求极差 | 擂台法 |
| 6 | P5723 | 质数口袋 | 嵌套循环、break、试除法 |
| 7 | P1046 | 陶陶摘苹果 | 数组读入、遍历计数 |
| 7 | P1428 | 小鱼比可爱 | 双重循环 |
| 7 | P1427 | 小鱼的数字游戏 | while 读到 0、倒序 |
| 7 | P5727 | 冰雹猜想 | 数组存储、倒序输出 |
| 7 | P5731 | 蛇形方阵 | 二维数组、方向数组 |
| 7 | P1059 | 明明的随机数 | vector、sort、去重、桶标记 |
| 8 | P5733 | 自动修正 | 字符串遍历、ASCII |
| 8 | P5705 | 数字反转 | 字符串倒序 |
| 8 | P5015 | 标题统计 | getline |
| 8 | P5734 | 文字处理软件 | substr/insert/find |
| 8 | P1957 | 口算练习题 | sscanf、to_string |
| 9 | P5735 | 距离函数 | double 函数(重复计算打包) |
| 9 | P5736 | 质数筛 | bool 函数 |
| 9 | P5739 | 计算阶乘 | 递归 |
| 9 | P1035 | 级数求和 | 增量累加(防 TLE) |
| 10 | P1046(改写) | 陶陶摘苹果 | 指针遍历 |
练习清单
| 难度 | 章节 | 题号 | 题名 | 提示 |
|---|---|---|---|---|
| ⭐ | 4 | P5707 | 上学迟到 | 向上取整 s/v + 10;倒推时间,为负则 +1440 |
| ⭐ | 5 | P5715 | 三位数排序 | 两两比较交换 |
| ⭐⭐ | 5 | P5717 | 三角形分类 | 先判能否构成,勾股分类,再判等腰/等边 |
| ⭐ | 6 | P5722 | 数列求和 | 循环累加 |
| ⭐ | 6 | P5721 | 数字直角三角形 | 嵌套循环,%02d 补 0 |
| ⭐ | 6 | P5718 | 找最小值 | 擂台法 |
| ⭐ | 7 | P5725 | 求三角形 | 正方形+空行+右对齐三角 |
| ⭐ | 7 | P5732 | 杨辉三角 | 二维数组递推 |
| ⭐⭐ | 7 | P5726 | 打分 | 去一个最高一个最低求平均 |
| ⭐⭐ | 7 | P1047 | 校门外的树 | 区间标记,L+1 棵树 |
| ⭐⭐⭐ | 7 | P2141 | 珠心算测验 | 枚举两数之和,数集合中被标记的元素 |
| ⭐⭐ | 8 | P1055 | ISBN 号码 | 跳过 '-'、字符转数字、mod 11 |
| ⭐⭐⭐ | 8 | P1597 | 语句解析 | getline 手写解析,val[256] 存变量 |
| ⭐⭐⭐ | 8 | P1308 | 统计单词数 | getline、整词匹配(前后界判定)、全转小写 |
| ⭐⭐ | 9 | P5737 | 闰年展示 | 闰年判断写成函数 |
| ⭐⭐ | 9 | P5738 | 歌唱比赛 | 求平均写成函数 |
| ⭐⭐⭐ | 9 | P1028 | 数的计算 | 递推 f[i]=1+Σf[1..i/2],别纯递归 |
| ⭐⭐ | 10 | P1427(改写) | 小鱼的数字游戏 | 指针从尾走到头 |
| ⭐⭐ | 10 | P1046(改写) | 陶陶摘苹果 | 三种遍历各写一遍 |
| ⭐⭐ | 10 | — | swap 三版 | 值/指针/引用传参对比 |
| ⭐⭐⭐ | 10 | P1428(改写) | 小鱼比可爱 | 指针双重循环 |
完成标准
- 所有例题能不看教程独立写出来并 AC;
- 完成全部 ⭐ 和 ⭐⭐ 练习;
- 挑战题(⭐⭐⭐)至少完成 3 道。
达成以上三条,你的 C++ 语法关就正式打通。接下来就可以学结构体、STL(set/map/sort/lower_bound/迭代器)这些进阶内容了——你的基础语法已经全部就位,专注学新东西就行。加油 💪