第 2.1 章:你的第一个 C++ 程序
📝 前置条件: 这是第一章——无需任何前置知识!你不需要任何编程经验。按顺序从头读到尾,完成本章后你将写出第一个真正的 C++ 程序。
欢迎!完成本章后,你将:
- 搭建好可用的 C++ 开发环境(用在线编译器只需 5 分钟)
- 编写、编译并运行第一个 C++ 程序
- 理解代码中每一行的含义
- 学会变量、数据类型和输入输出
- 完成 13 道练习题并查看完整题解
2.1.0 搭建开发环境
写代码之前,你需要一个可以编写和运行代码的地方。有两种选择:在线编译器(推荐新手使用——无需安装)和本地开发环境(可选,适合离线工作)。
选项 A:在线编译器(推荐——从这里开始!)
只需一个浏览器,打开以下任意网站:
| 网站 | 地址 | 说明 |
|---|---|---|
| Codeforces IDE | codeforces.com | 免费注册账号,在任意题目页面点击「Submit code」即可打开代码编辑器 |
| Replit | replit.com | 新建「C++ project」,获得完整编辑器 + 终端 |
| Ideone | ideone.com | 粘贴代码,选 C++17,点「Run」——最简单的选项 |
| OnlineGDB | onlinegdb.com | 内置调试器,功能完善 |
使用 Ideone(新手最简单):
- 打开 ideone.com
- 在语言下拉框选择「C++17 (gcc 8.3)」
- 将代码粘贴到文本区
- 点击绿色「Run」按钮
- 在底部面板查看输出
就这么简单!无需安装,无需配置。
选项 B:使用 CLion(推荐本地 IDE)
如果你想在自己的电脑上离线编写和运行 C++ 代码,强烈推荐 CLion ——JetBrains 出品的专业 C/C++ IDE。它拥有智能代码补全、一键编译运行和内置调试器,能大幅提升你的效率。
💡 学生免费! CLion 是付费软件,但 JetBrains 为学生提供免费教育许可。用你的
.edu邮箱在 JetBrains 学生许可页面 申请即可。
安装步骤:
第一步:安装 C++ 编译器(CLion 需要外部编译器)
| 操作系统 | 安装方式 |
|---|---|
| Windows | 安装 MSYS2。安装完成后在 MSYS2 终端运行:pacman -S mingw-w64-x86_64-gcc,然后将 C:\msys64\mingw64\bin 添加到系统 PATH |
| Mac | 打开终端运行:xcode-select --install,在弹出的对话框中点击「安装」,等待约 5 分钟 |
| Linux | Ubuntu/Debian:sudo apt install g++ cmake;Fedora:sudo dnf install gcc-c++ cmake |
第二步:安装 CLion
- 前往 CLion 下载页面 下载对应系统的安装包
- 运行安装包并按提示完成安装(保持默认选项即可)
- 首次启动时选择**「激活」**→ 用 JetBrains 学生账号登录,或开始 30 天免费试用
第三步:创建第一个项目
- 打开 CLion,点击**「New Project」**
- 选择**「C++ Executable」,将语言标准设为C++17**
- 点击**「Create」**——CLion 会自动生成包含
main.cpp的项目 - 在
main.cpp中编写代码,点击右上角绿色**▶ Run** 按钮即可编译运行 - 输出会出现在底部的**「Run」**面板中
🔧 CLion 自动检测编译器: 首次启动时,CLion 会自动扫描已安装的编译器(GCC / Clang / MSVC)。如果检测成功,你会在「Settings → Build → Toolchains」中看到绿色对勾 ✅。若未检测到,请确认第一步的编译器已正确安装并添加到 PATH。
CLion 竞赛编程实用功能:
- 内置终端:底部的 Terminal 标签可以直接输入测试数据
- 调试器:设置断点、逐行执行代码、查看变量值——追踪 bug 的必备工具
- 代码格式化:Ctrl + Alt + L(Mac:Cmd + Option + L)自动整理代码缩进
如何编译和运行(本地)
安装好 g++ 后,编译和运行方法如下:
g++ -o hello hello.cpp -std=c++17
逐字分解这条命令:
| 部分 | 含义 |
|---|---|
g++ | C++ 编译器程序名 |
-o hello | -o 表示「输出文件名」;hello 是我们给程序起的名字 |
hello.cpp | 要编译的源文件(我们的 C++ 代码) |
-std=c++17 | 使用 C++17 版本(功能最丰富) |
运行方法:
./hello # Linux/Mac:./ 表示「在当前目录」
hello.exe # Windows(.exe 会自动添加)
🤔 为什么是
./hello而不是直接hello? 在 Linux/Mac 上,系统默认不会从当前目录运行程序(出于安全考虑)。./明确告诉系统「在当前目录找这个程序」。
2.1.1 Hello, World!
每段编程之旅都从同一个地方开始。下面是最简单的完整 C++ 程序:
#include <iostream> // 告诉编译器我们要使用输入/输出功能
int main() { // 每个 C++ 程序都从 main() 开始执行
std::cout << "Hello, World!" << std::endl; // 打印到屏幕
return 0; // 0 = 成功,程序正常结束
}
运行后你应该看到:
Hello, World!
每一行的含义:
第 1 行:#include <iostream>
这是一条预处理指令——在正式编译之前执行的指令。它的意思是「把 iostream 库的内容复制粘贴到我的程序中」。iostream 库提供了 cin(读取输入)和 cout(打印输出)。没有这行,你的程序就无法输出任何内容。
可以这样理解:做饭之前,你需要先把食材拿进厨房。
第 3 行:int main()
这声明了 main 函数——每个 C++ 程序的起点。运行一个 C++ 程序时,计算机总是从 main() 内部的第一行开始执行。int 表示这个函数返回一个整数(退出码)。每个 C++ 程序必须有且仅有一个 main。
第 4 行:std::cout << "Hello, World!" << std::endl;
这行打印文字。拆解来看:
std::cout—— 「控制台输出」流(可以理解为屏幕)<<—— 「放入」运算符;将数据送入流"Hello, World!"—— 要打印的文字(引号本身不会被打印)<< std::endl—— 添加换行(相当于按回车);—— C++ 中每条语句都以分号结束
第 5 行:return 0;
退出 main 并告诉操作系统程序成功结束。(非零返回值表示发生了错误。)
编译流程
图示:编译流程
上图展示了从源代码到可执行文件的三个阶段:你的 .cpp 文件输入给 g++ 编译器,最终生成可运行的二进制文件。理解这个流程有助于在编译错误发生前就预防它们。
2.1.2 竞赛选手的标准模板
解 USACO 题目时,你会用到一套标准模板。下面是完整带注释的版本:
📄 解 USACO 题目时,你会用到一套标准模板。下面是完整带注释的版本:
#include <bits/stdc++.h> // 「全包含」——一次性包含所有标准库
using namespace std; // 让我们可以写 cout 而不是 std::cout
int main() {
ios_base::sync_with_stdio(false); // 禁用 C 和 C++ I/O 的同步(更快)
cin.tie(NULL); // 解除 cin 和 cout 的绑定(输入更快)
// 你的解题代码写在这里
return 0;
}
为什么用 #include <bits/stdc++.h>?
这是 GCC 专有的头文件,一次性包含所有标准库。不用再写:
#include <iostream>
#include <vector>
#include <algorithm>
#include <map>
// ... 还有 20 多行
一行搞定。在竞赛编程中,这是普遍做法,能节省时间。
注意:
bits/stdc++.h只在 GCC 编译器(USACO 评测机使用的编译器)下有效。竞赛编程中完全没问题,但不要在生产软件中使用。
为什么用 using namespace std;?
标准库把所有内容放在名为 std 的命名空间中。没有这行,你需要到处写 std::cout、std::vector、std::sort。有了 using namespace std;,直接写 cout、vector、sort——简洁得多。
I/O 加速行
ios_base::sync_with_stdio(false);
cin.tie(NULL);
这两行让 cin 和 cout 快得多。没有它们,读取大量输入可能慢 10 倍,即使算法正确也可能导致「超时(TLE)」。每次都要加上它们。
🐛 常见错误: 加了这两行后,不要混用
cin/cout和scanf/printf。选一种风格坚持用。
2.1.3 变量与数据类型
变量是内存中一个有名字的存储位置。C++ 中每个变量都有一个类型——类型告诉计算机需要分配多少内存,以及里面会存什么数据。
🧠 思维模型:变量就像带标签的盒子
当你写: int score = 100;
计算机做了三件事:
1. 创建一个足以容纳整数的盒子(4 字节)
2. 在盒子上贴上标签 "score"
3. 把数字 100 放进盒子
竞赛编程必备类型
📄 查看代码:竞赛编程必备类型
#include <bits/stdc++.h>
using namespace std;
int main() {
// int:整数,范围:-2,147,483,648 到 +2,147,483,647(约 ±20 亿)
int apples = 42;
int temperature = -5;
// long long:大整数,范围:约 ±9.2 × 10^18
long long population = 7800000000LL; // LL 后缀表示「这是 long long 字面量」
long long trillion = 1000000000000LL;
// double:小数/分数
double pi = 3.14159265358979;
double percentage = 99.5;
// bool:只有 true 或 false
bool isRaining = true;
bool finished = false;
// char:单个字符(以 0-255 的数字存储)
char grade = 'A'; // 单引号表示字符
char newline = '\n'; // 特殊字符:换行符
// string:字符序列
string name = "Alice"; // 双引号表示字符串
string greeting = "Hello!";
// 打印所有变量:
cout << "苹果数量: " << apples << "\n";
cout << "人口: " << population << "\n";
cout << "圆周率: " << pi << "\n";
cout << "在下雨: " << isRaining << "\n"; // true 打印 1,false 打印 0
cout << "成绩: " << grade << "\n";
cout << "姓名: " << name << "\n";
return 0;
}
图示:C++ 数据类型参考
如何选择正确的类型
| 使用场景 | 选用类型 |
|---|---|
| 计数、小数字 | int |
| 可能超过 20 亿的数字 | long long |
| 小数/分数答案 | double |
| 是/否标志 | bool |
| 单个字母或字符 | char |
| 单词或句子 | string |
变量命名规则
C++ 对变量名有严格规定。掌握这些规则很关键——命名不当会导致 bug,非法名字则无法编译。
形式规则(编译器强制执行)
✅ 合法名称必须:
- 以字母(a-z、A-Z)或下划线
_开头 - 只包含字母、数字(0-9)和下划线
- 不能是 C++ 保留关键字
❌ 以下名称无法编译:
| 非法名称 | 错误原因 |
|---|---|
3apples | 以数字开头 |
my score | 包含空格 |
my-score | 包含连字符(会被解释为减号) |
int | 保留关键字 |
class | 保留关键字 |
return | 保留关键字 |
⚠️ 区分大小写!
score、Score和SCORE是三个完全不同的变量。这是常见 bug 来源——命名时保持一致。
常见命名风格
C++ 中有几种广泛使用的命名规范。竞赛编程中不必只选一种,但了解它们有助于读懂别人的代码:
| 风格 | 示例 | 通常用于 |
|---|---|---|
| 驼峰式(camelCase) | numStudents、totalScore | 局部变量、函数参数 |
| 帕斯卡式(PascalCase) | MyClass、GraphNode | 类、结构体、类型名 |
| 下划线式(snake_case) | num_students、total_score | 变量、函数(C/Python 风格) |
| 全大写(ALL_CAPS) | MAX_N、MOD、INF | 常量、宏 |
| 单字母 | n、m、i、j | 循环下标、数学风格竞赛编程 |
竞赛编程中最常用驼峰式和单字母名称。公司的生产代码中,根据风格指南通常用 snake_case 或 camelCase。
命名最佳实践
1. 有描述性——从名字就能看出用途:
// ✅ 好——一眼就知道每个变量存什么
int numCows = 5;
long long totalMilk = 0;
string cowName = "Bessie";
int maxScore = 100;
// ❌ 差——语法正确但令人困惑
int x = 5; // x 是什么?计数?下标?值?
long long t = 0; // t 是什么?时间?总计?临时变量?
string n = "Bessie"; // n 通常表示「数字」——用来存名字很误导!
2. 只在含义显而易见时使用单字母名称:
// ✅ 可接受——这些是公认的惯例
for (int i = 0; i < n; i++) { ... } // i、j、k 用于循环下标
int n, m; // n = 计数,m = 第二维
cin >> n >> m; // 竞赛编程中人人都这样写
// ❌ 令人困惑——单字母但没有明确惯例
int q = 5; // q 是计数?查询数?系数?
char z = 'A'; // 为什么用 z?
3. 常量用全大写,方便识别:
const int MAX_N = 200005; // 数组最大长度
const int MOD = 1000000007; // 取模常量
const long long INF = 1e18; // 用于比较的「无穷大」
const double PI = 3.14159265359; // 数学常数
4. 避免外形相似的名称:
// ❌ 容易混淆
int total1 = 10;
int totall = 20; // 这是「total-L」还是手误的「total-1」?
int O = 0; // 字母 O 看起来像数字 0
int l = 1; // 小写 L 看起来像数字 1
// ✅ 更好的替代
int totalA = 10;
int totalB = 20;
5. 不要用下划线加大写字母开头:
// ❌ 能编译,但被 C++ 标准保留
int _Score = 100; // _X 形式的名称被编译器/库保留
int __value = 42; // 双下划线开头始终保留
// ✅ 安全替代
int score = 100;
int myValue = 42;
竞赛编程 vs 生产代码的命名对比
| 方面 | 竞赛编程 | 生产/学校项目 |
|---|---|---|
| 变量名长度 | 简短即可:n、m、dp、adj | 有描述性:numStudents、adjacencyList |
| 循环变量 | 永远用 i、j、k | i、j、k 也没问题 |
| 常量 | MAXN、MOD、INF | kMaxSize、kModulus(Google 风格) |
| 注释 | 极少——速度优先 | 详尽——可读性优先 |
| 目标 | 快速编写、快速解题 | 编写别人能维护的代码 |
💡 本书中: 我们会混用两种风格——讲解时用描述性名称保持清晰,解题时用简短名称。关键原则:看到变量名就应该立刻知道它存的是什么。
深入了解:char、string 与字符-整数转换
我们已经简要介绍了 char 和 string。由于许多 USACO 题目涉及字符处理、数字提取和字符串操作,让我们深入看看这两种重要类型。
char 与 ASCII——每个字符都是一个数字
C++ 中的 char 以 1 字节整数(0-255)存储。每个字符按照 ASCII 表(美国信息交换标准代码)映射到一个数字。不需要背整张表,但记住几个关键范围非常有用:
关键关系:
• 'a' - 'A' = 32 (大小写字母差值)
• '0' 的 ASCII 值是 48(不是 0!)
• 数字、大写字母、小写字母各自在连续范围内
📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
int main() {
char ch = 'A';
// char 本质上是整数——可以打印其数值
cout << ch << "\n"; // 打印:A(字符形式)
cout << (int)ch << "\n"; // 打印:65(ASCII 值)
// 可以对 char 做算术!
char next = ch + 1; // 'A' + 1 = 66 = 'B'
cout << next << "\n"; // 打印:B
// 比较 char(比较 ASCII 值)
cout << ('a' < 'z') << "\n"; // 1(真,因为 97 < 122)
cout << ('A' < 'a') << "\n"; // 1(真,因为 65 < 97)
return 0;
}
char ↔ int 转换——最常用的技巧
竞赛编程中,你需要频繁地在字符数字和整数值之间转换。以下是完整指南:
1. 数字字符 → 整数值(例如 '7' → 7)
char ch = '7';
int digit = ch - '0'; // '7' - '0' = 55 - 48 = 7
cout << digit << "\n"; // 打印:7
// 这行得通是因为数字字符 '0'~'9' 的 ASCII 值是连续的:
// '0'=48, '1'=49, ..., '9'=57
// 所以 ch - '0' 正好是实际数值(0~9)
2. 整数值 → 数字字符(例如 7 → '7')
int digit = 7;
char ch = '0' + digit; // 48 + 7 = 55 = '7'
cout << ch << "\n"; // 打印:7(字符 '7')
// 仅对数字 0~9 有效
3. 大小写互转
📄 C++ 完整代码
char upper = 'C';
char lower = upper + 32; // 'C'(67) + 32 = 'c'(99)
cout << lower << "\n"; // 打印:c
// 更易读的写法:
char lower2 = upper - 'A' + 'a'; // 'C'-'A' = 2,'a'+2 = 'c'
cout << lower2 << "\n"; // 打印:c
// 反向:小写 → 大写
char ch = 'f';
char upper2 = ch - 'a' + 'A'; // 'f'-'a' = 5,'A'+5 = 'F'
cout << upper2 << "\n"; // 打印:F
// 使用内置函数(更推荐,可读性更好):
cout << (char)toupper('g') << "\n"; // 打印:G
cout << (char)tolower('G') << "\n"; // 打印:g
4. 判断字符类型(USACO 中非常实用)
📄 C++ 完整代码
char ch = '5';
// 判断是否为数字
if (ch >= '0' && ch <= '9') {
cout << "是数字!\n";
}
// 判断是否为大写字母
if (ch >= 'A' && ch <= 'Z') {
cout << "大写字母!\n";
}
// 判断是否为小写字母
if (ch >= 'a' && ch <= 'z') {
cout << "小写字母!\n";
}
// 或使用内置函数:
// isdigit(ch), isupper(ch), islower(ch), isalpha(ch), isalnum(ch)
if (isdigit(ch)) cout << "数字!\n";
if (isalpha(ch)) cout << "字母!\n";
5. 经典模式:从字符串中提取数字
string s = "abc123def";
int sum = 0;
for (char ch : s) {
if (ch >= '0' && ch <= '9') {
sum += ch - '0'; // 将数字字符转为整数并累加
}
}
cout << "各位数字之和:" << sum << "\n"; // 1+2+3 = 6
string 详细指南
string 是 C++ 的内置文本类型。与单个 char 不同,string 存储一串字符,并提供许多实用操作。
基本操作:
📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
int main() {
// 创建字符串
string s1 = "Hello";
string s2 = "World";
string empty = ""; // 空字符串
string repeated(5, 'x'); // "xxxxx"——5 个 'x'
// 长度
cout << s1.size() << "\n"; // 5(等同于 s1.length())
// 拼接
string s3 = s1 + " " + s2; // "Hello World"
s1 += "!"; // s1 变为 "Hello!"
// 访问单个字符(从 0 开始,与数组相同)
cout << s3[0] << "\n"; // 'H'
cout << s3[6] << "\n"; // 'W'
// 修改单个字符
s3[0] = 'h'; // "hello World"
// 比较(字典序)
cout << ("apple" < "banana") << "\n"; // 1(真)
cout << ("abc" == "abc") << "\n"; // 1(真)
cout << ("abc" < "abd") << "\n"; // 1(真,逐字符比较)
return 0;
}
遍历字符串:
📄 C++ 完整代码
string s = "USACO";
// 方法一:下标循环
for (int i = 0; i < (int)s.size(); i++) {
cout << s[i] << " "; // U S A C O
}
cout << "\n";
// 方法二:范围 for 循环(更简洁)
for (char ch : s) {
cout << ch << " "; // U S A C O
}
cout << "\n";
// 方法三:引用范围 for(可就地修改)
for (char& ch : s) {
ch = tolower(ch); // 将每个字符转为小写
}
cout << s << "\n"; // "usaco"
常用 string 函数:
📄 C++ 完整代码
string s = "Hello, World!";
// 子串:s.substr(起始位置, 长度)
string sub = s.substr(7, 5); // "World"(从第 7 个字符开始取 5 个)
string sub2 = s.substr(7); // "World!"(从第 7 个到结尾)
// 查找:s.find("文本")——返回下标,找不到返回 string::npos
size_t pos = s.find("World"); // 7(注意类型是 size_t,不是 int!)
if (s.find("xyz") == string::npos) {
cout << "未找到!\n";
}
// 追加
s.append(" Hi"); // "Hello, World! Hi"
// 等价于:s += " Hi";
// 插入
s.insert(5, "!!"); // "Hello!!, World! Hi"
// 删除:s.erase(起始位置, 个数)
s.erase(5, 2); // 从第 5 位删除 2 个字符
// 替换:s.replace(起始位置, 个数, "新文本")
string msg = "I love cats";
msg.replace(7, 4, "dogs"); // "I love dogs"
从输入读取字符串:
📄 C++ 完整代码
// cin >> 读取一个单词(遇到空白字符停止)
string word;
cin >> word; // 输入 "Hello World" → word = "Hello"
// getline 读取整行(包含空格)
string line;
getline(cin, line); // 输入 "Hello World" → line = "Hello World"
// ⚠️ 注意:cin >> 之后,调用 getline 前先 cin.ignore()!
int n;
cin >> n;
cin.ignore(); // 消耗残留的 '\n'
string fullLine;
getline(cin, fullLine); // 现在可以正确读取
string 与数字互转:
📄 C++ 完整代码
// 字符串 → 整数
string numStr = "42";
int num = stoi(numStr); // stoi = "string to int" → 42
long long big = stoll("123456789012345"); // stoll = "string to long long"
// 字符串 → 浮点数
double d = stod("3.14"); // stod = "string to double" → 3.14
// 整数 → 字符串
int x = 255;
string s = to_string(x); // "255"
string s2 = to_string(3.14); // "3.140000"
char 数组(C 风格字符串)——了解即可
在 C(以及旧式 C++ 代码)中,字符串以以 '\0' 结尾的 char 数组存储。竞赛编程中你很少需要用到它(改用 string),但应该能认出它:
// C 风格字符串(char 数组)
char greeting[] = "Hello"; // 实际存储:H e l l o \0(共 6 个字符!)
// '\0'(空字符)标记字符串结束
// 警告:必须确保数组足够大以容纳字符串 + '\0'
char name[20]; // 最多可存 19 个字符 + '\0'
// char 数组与 string 互转
string s = greeting; // char 数组 → string(自动转换)
// string → char 数组:使用 s.c_str() 获得 const char*
为什么竞赛编程中 string 比 char[] 更好:
| 特性 | char[](C 风格) | string(C++) |
|---|---|---|
| 大小 | 需要预定义最大长度 | 自动增长 |
| 拼接 | strcat()——手动、易出错 | s1 + s2——简单 |
| 比较 | strcmp()——返回整数 | s1 == s2——直观 |
| 长度 | strlen()——每次 O(N) | s.size()——O(1) |
| 安全性 | 缓冲区溢出风险 | 安全,由 C++ 管理 |
⚡ USACO 专业技巧: 除非题目明确要求
char数组,否则始终用string。字符串操作更简洁、更安全、更易调试。char数组在竞赛编程中唯一常见的使用场景是用scanf/printf读取超大输入以提速——但加上sync_with_stdio(false)后,string+cin/cout对 99% 的 USACO 题目已经足够快。
快速参考:字符/字符串速查表
| 操作 | 代码 | 示例 |
|---|---|---|
| 数字字符 → 整数 | ch - '0' | '7' - '0' → 7 |
| 整数 → 数字字符 | '0' + digit | '0' + 3 → '3' |
| 大写 → 小写 | ch - 'A' + 'a' 或 tolower(ch) | 'C' → 'c' |
| 小写 → 大写 | ch - 'a' + 'A' 或 toupper(ch) | 'f' → 'F' |
| 是数字? | ch >= '0' && ch <= '9' 或 isdigit(ch) | '5' → true |
| 是字母? | isalpha(ch) | 'A' → true |
| 字符串长度 | s.size() 或 s.length() | "abc" → 3 |
| 子串 | s.substr(起始, 长度) | "Hello".substr(1,3) → "ell" |
| 查找 | s.find("文本") | 返回下标或 npos |
| 字符串 → 整数 | stoi(s) | stoi("42") → 42 |
| 整数 → 字符串 | to_string(n) | to_string(42) → "42" |
| 遍历字符串 | for (char ch : s) | 逐字符遍历 |
⚠️ 整数溢出——竞赛编程中的头号 Bug
当一个数超过它所属类型的范围时会发生什么?
// 把 int 想象成一个从 -2,147,483,648 到 2,147,483,647 的表盘
// 当你超过最大值,它会回绕到最小值!
int x = 2147483647; // int 的最大值
cout << x << "\n"; // 打印:2147483647
x++; // 加 1……会发生什么?
cout << x << "\n"; // 打印:-2147483648(溢出!回绕了!)
这就像老式汽车里程表到了 999999 又回到 000000。
如何避免溢出:
int a = 1000000000; // 10 亿——int 能放下
int b = 1000000000; // 10 亿——int 能放下
// int wrong = a * b; // 溢出!a*b = 10^18,int 放不下
long long correct = (long long)a * b; // 乘之前把其中一个转成 long long
cout << correct << "\n"; // 1000000000000000000 ✓
// 经验法则:如果 N 最大为 10^9,你又需要将两个这样的值相乘,就用 long long
⚡ 专业技巧: 拿不准时,用
long long。它比int稍慢,但能防止难以发现的溢出 bug。
2.1.4 用 cin 和 cout 进行输入输出
用 cout 打印输出
int score = 95;
string name = "Alice";
cout << "分数:" << score << "\n"; // 分数:95
cout << name << " 得了 " << score << "\n"; // Alice 得了 95
// "\n" vs endl
cout << "第一行" << "\n"; // 快——只是一个换行符
cout << "第二行" << endl; // 慢——清空缓冲区并换行
⚡ 专业技巧: 始终用
"\n"而不是endl。endl会清空输出缓冲区,比"\n"慢得多。对于输出量大的题目,使用endl可能导致超时!
用 cin 读取输入
int n;
cin >> n; // 从输入读取一个整数
string s;
cin >> s; // 读取一个单词(遇到空白字符停止——空格、制表符、换行)
double x;
cin >> x; // 读取一个小数
cin >> 会自动跳过空白字符。这意味着空格、制表符和换行都被同等对待。因此以下两种输入格式的处理方式完全相同:
输入格式一(同一行): 42 hello 3.14
输入格式二(分多行):
42
hello
3.14
两种都用以下代码处理:
int a; string b; double c;
cin >> a >> b >> c; // 无论格式如何都能读取这三个值
读取多个值——最常见的 USACO 模式
USACO 题目几乎都以「读取 N,然后读取 N 个值」开头。方法如下:
典型 USACO 输入:
5 ← 第一行:N(元素个数)
10 20 30 40 50 ← 后续行:N 个值
int n;
cin >> n; // 读取 N
for (int i = 0; i < n; i++) {
int x;
cin >> x; // 读取每个元素
cout << x * 2 << "\n"; // 处理它
}
复杂度分析:
- 时间:O(N)——读取 N 个数,每个 O(1) 处理
- 空间:O(1)——只有一个变量
x,不存储所有数据
对于输入 5\n10 20 30 40 50,会打印:
20
40
60
80
100
读取完整的一行(包含空格)
有时输入的一行包含多个单词。cin >> 每次只读一个单词,所以要用 getline:
string fullName;
getline(cin, fullName); // 读取整行,包括空格
cout << "姓名:" << fullName << "\n";
🐛 常见 Bug: 混用
cin >>和getline会出问题。cin >> n之后,缓冲区里还残留一个\n。这时调用getline会读到那个空行而不是下一行。修复方法:在cin >>之后、getline之前调用cin.ignore()。
控制小数输出精度
double y = 3.14159;
cout << y << "\n"; // 3.14159(默认)
cout << fixed << setprecision(2) << y << "\n"; // 3.14(恰好 2 位小数)
cout << fixed << setprecision(6) << y << "\n"; // 3.141590(6 位小数)
2.1.5 基本算术
📄 查看代码:2.1.5 基本算术
#include <bits/stdc++.h>
using namespace std;
int main() {
int a = 17, b = 5;
cout << a + b << "\n"; // 22 (加法)
cout << a - b << "\n"; // 12 (减法)
cout << a * b << "\n"; // 85 (乘法)
cout << a / b << "\n"; // 3 (整数除法——截断向零!)
cout << a % b << "\n"; // 2 (取模——整除后的余数)
// 整数除法示例:
// 17 ÷ 5 = 3 余 2
// 因此:17 / 5 = 3,17 % 5 = 2
double x = 17.0, y = 5.0;
cout << x / y << "\n"; // 3.4(操作数是 double 时进行实数除法)
// 复合赋值运算符:
int n = 10;
n += 5; // 等同于:n = n + 5 → n 现在是 15
n -= 3; // 等同于:n = n - 3 → n 现在是 12
n *= 2; // 等同于:n = n * 2 → n 现在是 24
n /= 4; // 等同于:n = n / 4 → n 现在是 6
n++; // 等同于:n = n + 1 → n 现在是 7
n--; // 等同于:n = n - 1 → n 现在是 6
cout << n << "\n"; // 6
return 0;
}
🤔 为什么整数除法会截断?
当两个操作数都是整数时,C++ 执行整数除法——直接丢弃小数部分。17 / 5 得 3,不是 3.4。这是有意为之,而且非常有用(例如:找出某个东西属于哪个「组」)。
// 200 分钟等于几小时?
int minutes = 200;
int hours = minutes / 60; // 200 / 60 = 3(不是 3.33…)
int remaining = minutes % 60; // 200 % 60 = 20
cout << hours << " 小时 " << remaining << " 分钟\n"; // 3 小时 20 分钟
// 要得到小数结果,至少一个操作数必须是 double:
int a = 7, b = 2;
cout << a / b << "\n"; // 3 (整数除法)
cout << (double)a / b << "\n"; // 3.5 (先把 a 转成 double)
cout << a / (double)b << "\n"; // 3.5 (先把 b 转成 double)
cout << 7.0 / 2 << "\n"; // 3.5 (字面量 7.0 是 double)
2.1.6 你的第一个 USACO 风格程序
让我们把所有内容综合起来,写一个读取输入、产生输出的完整程序——就像真正的 USACO 题目。
题目: 读取两个整数 N 和 M,打印它们的和、差、积、整数商和余数。
分析思路:
- 需要两个变量存储 N 和 M
- 用
cin读取 - 用
cout打印每个结果 - N 和 M 可能很大,应该用
long long吗?安全起见用它。
💡 新手解题流程:
遇到题目时,别急着写代码。先用自然语言想清楚步骤:
- 理解题目:输入是什么?输出是什么?约束是什么?
- 手动推演样例:用样例输入,手算出输出,确认自己理解了题目
- 考虑数据范围:N 和 M 最大多少?会不会溢出?
- 写伪代码:
读取 → 计算 → 输出- 翻译成 C++:将伪代码逐行转化为真实代码
本题:读取两个数 → 执行五种运算 → 输出五个结果。非常直接!
📄 C++ 完整代码
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
long long n, m;
cin >> n >> m; // 在同一行读取两个数
cout << n + m << "\n"; // 和
cout << n - m << "\n"; // 差
cout << n * m << "\n"; // 积
cout << n / m << "\n"; // 整数商
cout << n % m << "\n"; // 余数
return 0;
}
复杂度分析:
- 时间:O(1)——只有固定次数的算术运算
- 空间:O(1)——只有两个变量
样例输入:
17 5
样例输出:
22
12
85
3
2
⚠️ 第 2.1 章常见错误
| # | 错误 | 示例 | 错在哪里 | 修复方法 |
|---|---|---|---|---|
| 1 | 整数溢出 | int a = 1e9; int b = a*a; | a*b = 10^18 超过 int 最大值约 2.1×10^9,结果「回绕」成错误值 | 用 long long |
| 2 | 使用 endl | cout << x << endl; | endl 清空输出缓冲区,大量输出时比 "\n" 慢 10 倍以上,可能导致超时 | 用 "\n" |
| 3 | 忘记 I/O 加速 | 缺少 sync_with_stdio 和 cin.tie | 默认情况下 cin/cout 与 C 的 scanf/printf 同步,大量输入时极慢 | 始终加上这两行 |
| 4 | 整数除法意外 | 7/2 期望 3.5 却得到 3 | 两个整数相除,C++ 截断小数部分 | 强转 double:(double)7/2 |
| 5 | 缺少分号 | cout << x | C++ 每条语句必须以 ; 结尾,否则编译失败 | cout << x; |
| 6 | 混用 cin >> 和 getline | cin >> n 然后 getline(cin, s) | cin >> 在缓冲区留下 \n,getline 读到空行 | 中间加 cin.ignore() |
本章总结
📌 核心要点
| 概念 | 要点 | 为什么重要 |
|---|---|---|
#include <bits/stdc++.h> | 一次性包含所有标准库 | 竞赛中节省时间,不用记每个头文件 |
using namespace std; | 省略 std:: 前缀 | 代码更简洁,竞赛编程的通用做法 |
int main() | 程序唯一入口点 | 每个 C++ 程序必须有且仅有一个 main |
cin >> x / cout << x | 读取输入/写出输出 | USACO 的核心 I/O 方法 |
int vs long long | 约 ±2×10^9 vs 约 ±9.2×10^18 | 类型用错 = 溢出 = 答案错误(竞赛中最常见的 bug) |
"\n" vs endl | "\n" 快 10 倍 | 决定 AC 还是超时 |
a / b 和 a % b | 整数除法和取模 | 时间转换、分组等核心工具 |
| I/O 加速行 | sync_with_stdio(false) + cin.tie(NULL) | 竞赛模板必备,忘加可能超时 |
❓ 常见问题
Q1:bits/stdc++.h 会拖慢编译速度吗?
A:是的,编译时间可能增加 1-2 秒。但竞赛中编译时间不计入时限,不影响结果。生产项目中不要用它。
Q2:默认用 int 还是 long long?
A:经验法则——拿不准就用
long long。它比int稍慢(在现代 CPU 上几乎感受不到),但能防止溢出。特别注意:两个int相乘,结果可能需要long long。
Q3:USACO 里不能用 scanf/printf 吗?
A:可以用!但加了
sync_with_stdio(false)后,不能混用cin/cout和scanf/printf。建议新手坚持用cin/cout,更安全。
Q4:可以省略 return 0; 吗?
A:C++11 及以后,
main()执行到末尾时编译器自动返回 0,技术上可以省略。但写上更清晰。
Q5:代码本地运行正确,USACO 评测却 Wrong Answer,怎么回事?
A:最常见的三个原因:① 整数溢出(本该用
long long却用了int);② 没处理所有边界情况;③ 输出格式错误(多了或少了空格/换行)。
🔗 与后续章节的联系
- 第 2.2 章(控制流)在本章基础上新增
if/else条件和for/while循环,让你能处理「重复 N 次」的任务 - 第 2.3 章(函数与数组)介绍函数(将代码组织成可复用的块)和数组(存储一组数据)——USACO 解题的核心工具
- 第 3.1 章(STL 核心用法)介绍
vector和sort等 STL 工具,大大简化本章手写的逻辑 - 本章学到的整数溢出预防技巧会贯穿全书,特别在第 3.2 章(前缀和)和第 6.1-6.3 章(DP)中反复用到
练习题
按顺序完成所有题目——难度逐渐递增。每题都有完整题解,尝试自己解决后再查看。
🌡️ 热身题
每道题只需新增 1-3 行代码,帮助你练习输入 C++ 代码和运行程序。
热身 2.1.1 — 个人问候 编写一个程序,精确打印以下内容(换成你自己的名字):
Hello, Alice!
My favorite number is 7.
I am learning C++.
(所有值可以硬编码——不需要读取输入。)
💡 题解(点击展开)
思路: 用 cout 打印三行。无需读取输入。
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << "Hello, Alice!\n";
cout << "My favorite number is 7.\n";
cout << "I am learning C++.\n";
return 0;
}
关键点:
- 每条
cout语句以;\n"结尾——\n产生换行 - 也可以将多个
<<连在一条cout上 - 没有输入时不需要
cin
热身 2.1.2 — 五行数字
打印数字 1 到 5,每行一个。用恰好 5 条独立的 cout 语句(还没学循环——第 2.2 章会讲)。
💡 题解(点击展开)
思路: 五条独立的 cout 语句,每条打印一个数字。
#include <bits/stdc++.h>
using namespace std;
int main() {
cout << 1 << "\n";
cout << 2 << "\n";
cout << 3 << "\n";
cout << 4 << "\n";
cout << 5 << "\n";
return 0;
}
关键点:
cout << 1 << "\n"打印数字 1 后跟换行- 第 2.2 章会学用循环来做这件事——但手动写对小数量完全没问题
热身 2.1.3 — 翻倍 从输入读取一个整数,打印它的 2 倍。
样例输入: 7
样例输出: 14
💡 题解(点击展开)
思路: 读入变量,乘以 2,打印。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
cout << n * 2 << "\n";
return 0;
}
关键点:
cin >> n读取一个整数存入n- 可以直接在
cout里做算术:n * 2先计算,再打印 - 若 n 可能很大(最大 10^9),用
long long n,因为n * 2可能溢出int
热身 2.1.4 — 两数之和 读取同一行上的两个整数,打印它们的和。
样例输入: 15 27
样例输出: 42
💡 题解(点击展开)
思路: 读取两个整数,相加,打印。
#include <bits/stdc++.h>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
cout << a + b << "\n";
return 0;
}
关键点:
cin >> a >> b在一条语句中读取两个值——无论它们在同一行还是不同行都有效- 在同一行声明两个变量:
int a, b;等价于int a; int b;
热身 2.1.5 — 打招呼
读取一个单词(名字,不含空格),打印 Hi, [name]!
样例输入: Bob
样例输出: Hi, Bob!
💡 题解(点击展开)
思路: 读取字符串,嵌入问候语中打印。
#include <bits/stdc++.h>
using namespace std;
int main() {
string name;
cin >> name;
cout << "Hi, " << name << "!\n";
return 0;
}
关键点:
string name;声明一个存储文本的变量cin >> name读取一个单词(遇到第一个空格停止)- 注意
cout的链式写法:字符串字面量 + 变量 + 字符串字面量
🏋️ 核心练习题
这些题需要综合运用输入、算术和输出。编码前先想清楚数学关系。
题目 2.1.6 — 年龄换算 读取一个人的整岁年龄,打印其大约的天数(按每年 365 天计算,不考虑闰年)。
样例输入: 15
样例输出: 5475
💡 题解(点击展开)
思路: 年龄乘以 365。由于年龄 × 365 不会超过 int 范围(最大约 150 岁 → 150×365 = 54750,远小于 int 上限),用 int 即可。
#include <bits/stdc++.h>
using namespace std;
int main() {
int years;
cin >> years;
cout << years * 365 << "\n";
return 0;
}
关键点:
years * 365按整数计算——这里不存在溢出风险- 如果还要换算成小时、分钟、秒,为安全起见改用
long long
题目 2.1.7 — 秒数转换 读取秒数 S(1 ≤ S ≤ 10^9),转换为小时、分钟和剩余秒数。
样例输入: 3661
样例输出:
1 hours
1 minutes
1 seconds
💡 题解(点击展开)
思路: 使用整数除法和取模。先除以 3600 得小时数,余数(mod 3600)再除以 60 得分钟数,最后剩余的是秒数。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long s;
cin >> s;
long long hours = s / 3600; // 每小时 3600 秒
long long remaining = s % 3600; // 去掉完整小时后剩余的秒数
long long minutes = remaining / 60; // 每分钟 60 秒
long long seconds = remaining % 60; // 去掉完整分钟后剩余的秒数
cout << hours << " hours\n";
cout << minutes << " minutes\n";
cout << seconds << " seconds\n";
return 0;
}
关键点:
- 用
long long是因为 S 最大 10^9(int 也够,但 long long 是好习惯) - 核心思路:
s % 3600得到去掉完整小时后的秒数,再除以 60 得分钟 - 验证:3661 → 3661/3600=1 小时,3661%3600=61,61/60=1 分钟,61%60=1 秒 ✓
题目 2.1.8 — 矩形 读取矩形的长 L 和宽 W,打印它的面积和周长。
样例输入: 6 4
样例输出:
Area: 24
Perimeter: 20
💡 题解(点击展开)
思路: 面积 = L × W,周长 = 2 × (L + W)。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long L, W;
cin >> L >> W;
cout << "Area: " << L * W << "\n";
cout << "Perimeter: " << 2 * (L + W) << "\n";
return 0;
}
关键点:
- 运算顺序:
2 * (L + W)——括号确保先加 L+W,再乘以 2 - 使用
long long以防 L、W 较大(若 L、W 最大 10^9,L*W 最大 10^18)
题目 2.1.9 — 温度转换
读取摄氏温度,打印对应的华氏温度。公式:F = C × 9/5 + 32
样例输入: 100
样例输出: 212.00
💡 题解(点击展开)
思路: 套用公式。需要小数输出,用 double。陷阱:9/5 整数运算结果是 1,不是 1.8!
#include <bits/stdc++.h>
using namespace std;
int main() {
double celsius;
cin >> celsius;
double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
cout << fixed << setprecision(2) << fahrenheit << "\n";
return 0;
}
关键点:
- 用
9.0 / 5.0(或9.0/5)而不是9/5——后者是整数除法,结果是1而不是1.8! fixed << setprecision(2)强制输出恰好 2 位小数- 验证:100°C → 100 × 9.0/5.0 + 32 = 180 + 32 = 212 ✓
题目 2.1.10 — 硬币计数 读取四个整数:25 分硬币(quarters)、10 分硬币(dimes)、5 分硬币(nickels)和 1 分硬币(pennies)的数量,打印总面值(单位:分)。
样例输入:
3 2 1 4
(3 枚 25 分,2 枚 10 分,1 枚 5 分,4 枚 1 分)
样例输出: 104
💡 题解(点击展开)
思路: 每种硬币数量乘以面值,全部相加。
#include <bits/stdc++.h>
using namespace std;
int main() {
int quarters, dimes, nickels, pennies;
cin >> quarters >> dimes >> nickels >> pennies;
int total = quarters * 25 + dimes * 10 + nickels * 5 + pennies * 1;
cout << total << "\n";
return 0;
}
关键点:
- 每种硬币乘以面值:quarters=25,dimes=10,nickels=5,pennies=1
- 验证:3×25 + 2×10 + 1×5 + 4×1 = 75 + 20 + 5 + 4 = 104 ✓
- 若硬币数量很大,改用
long long
🏆 挑战题
这些题需要更多思考——特别是数据类型和解题方法。
挑战 2.1.11 — 溢出探测器
读取两个整数 A 和 B(各最大 10^9),用两种方式计算它们的乘积:int 类型和 long long 类型,各打印一次结果。观察溢出时的差异。
样例输入: 1000000000 3
样例输出:
int product: -1294967296
long long product: 3000000000
(int 结果因溢出而错误;long long 结果正确。)
💡 题解(点击展开)
思路: 以 long long 读入两个数,然后两种方式各算一次乘积——一次强转为 int,一次用 long long。直观展示溢出效果。
#include <bits/stdc++.h>
using namespace std;
int main() {
long long a, b;
cin >> a >> b;
// 先转成 int 再乘,强制触发整数溢出
int int_product = (int)a * (int)b;
// long long 乘法——值最大 10^9 时不会溢出
long long ll_product = a * b;
cout << "int product: " << int_product << "\n";
cout << "long long product: " << ll_product << "\n";
return 0;
}
关键点:
(int)a * (int)b——两个操作数都转成int再乘,乘法在int范围内溢出a * b(a、b 是long long)——乘法在long long空间内进行,不溢出- 10^9 × 3 的实际结果是 3×10^9,但 int 最大约 2.147×10^9 < 3×10^9,溢出后得到
-1294967296 - 教训: 当任意一个值可能达到约 10^5 或更大时,乘法时始终用
long long
挑战 2.1.12 — USACO 风格大数相乘
给定两个整数 N 和 M(1 ≤ N, M ≤ 10^9),打印它们的乘积。(看似简单,但必须用 long long。)
样例输入: 1000000000 1000000000
样例输出: 1000000000000000000
💡 题解(点击展开)
思路: N 和 M 各自能放进 int,但 N × M = 10^18——超出 int 上限(约 2.1×10^9),勉强放进 long long(上限约 9.2×10^18)。必须用 long long。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
long long n, m;
cin >> n >> m;
cout << n * m << "\n";
return 0;
}
关键点:
- 用
long long变量读取是关键——cin >> n可以处理最大 9.2×10^18 的值 - 若用
int变量:int n, m; cin >> n >> m; cout << n * m;——这会静默溢出,输出错误答案 - 做 USACO 题时,始终检查约束:若 N 最大 10^9,且可能要算 N × N,就需要
long long
挑战 2.1.13 — 象限问题 (USACO 2016 February Bronze) 读取两个非零整数 x 和 y,判断点 (x, y) 在坐标平面的哪个象限:
- 第一象限:x > 0 且 y > 0
- 第二象限:x < 0 且 y > 0
- 第三象限:x < 0 且 y < 0
- 第四象限:x > 0 且 y < 0
只打印象限数字:1、2、3 或 4。
样例输入 1: 3 5 → 输出: 1
样例输入 2: -1 2 → 输出: 2
样例输入 3: -4 -7 → 输出: 3
样例输入 4: 8 -3 → 输出: 4
💡 题解(点击展开)
思路: 判断 x 和 y 的正负。x 和 y 正负的每种组合恰好对应一个象限。使用 if/else-if 链(第 2.2 章会完整讲,但这里很直接)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int x, y;
cin >> x >> y;
if (x > 0 && y > 0) {
cout << 1 << "\n";
} else if (x < 0 && y > 0) {
cout << 2 << "\n";
} else if (x < 0 && y < 0) {
cout << 3 << "\n";
} else { // x > 0 && y < 0
cout << 4 << "\n";
}
return 0;
}
关键点:
&&运算符表示「且」——两个条件都必须为真- 题目保证 x ≠ 0 且 y ≠ 0,无需处理这些边界情况
- 四种情况互斥(对任意输入恰好一种为真),else-if 链完美适用
- 可以用公式简化,但显式 if/else 更清晰,速度一样快