📖 第 2.1 章 ⏱️ 约 60 分钟 🎯 入门

第 2.1 章:你的第一个 C++ 程序

📝 前置条件: 这是第一章——无需任何前置知识!你不需要任何编程经验。按顺序从头读到尾,完成本章后你将写出第一个真正的 C++ 程序。

欢迎!完成本章后,你将:

  • 搭建好可用的 C++ 开发环境(用在线编译器只需 5 分钟)
  • 编写、编译并运行第一个 C++ 程序
  • 理解代码中每一行的含义
  • 学会变量、数据类型和输入输出
  • 完成 13 道练习题并查看完整题解

2.1.0 搭建开发环境

写代码之前,你需要一个可以编写和运行代码的地方。有两种选择:在线编译器(推荐新手使用——无需安装)和本地开发环境(可选,适合离线工作)。

选项 A:在线编译器(推荐——从这里开始!)

只需一个浏览器,打开以下任意网站:

网站地址说明
Codeforces IDEcodeforces.com免费注册账号,在任意题目页面点击「Submit code」即可打开代码编辑器
Replitreplit.com新建「C++ project」,获得完整编辑器 + 终端
Ideoneideone.com粘贴代码,选 C++17,点「Run」——最简单的选项
OnlineGDBonlinegdb.com内置调试器,功能完善

使用 Ideone(新手最简单):

  1. 打开 ideone.com
  2. 在语言下拉框选择「C++17 (gcc 8.3)」
  3. 将代码粘贴到文本区
  4. 点击绿色「Run」按钮
  5. 在底部面板查看输出

就这么简单!无需安装,无需配置。

选项 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 分钟
LinuxUbuntu/Debian:sudo apt install g++ cmake;Fedora:sudo dnf install gcc-c++ cmake

第二步:安装 CLion

  1. 前往 CLion 下载页面 下载对应系统的安装包
  2. 运行安装包并按提示完成安装(保持默认选项即可)
  3. 首次启动时选择**「激活」**→ 用 JetBrains 学生账号登录,或开始 30 天免费试用

第三步:创建第一个项目

  1. 打开 CLion,点击**「New Project」**
  2. 选择**「C++ Executable」,将语言标准设为C++17**
  3. 点击**「Create」**——CLion 会自动生成包含 main.cpp 的项目
  4. main.cpp 中编写代码,点击右上角绿色**▶ Run** 按钮即可编译运行
  5. 输出会出现在底部的**「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 并告诉操作系统程序成功结束。(非零返回值表示发生了错误。)

编译流程

图示:编译流程

C++ Compilation Pipeline

上图展示了从源代码到可执行文件的三个阶段:你的 .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::coutstd::vectorstd::sort。有了 using namespace std;,直接写 coutvectorsort——简洁得多。

I/O 加速行

ios_base::sync_with_stdio(false);
cin.tie(NULL);

这两行让 cincout 快得多。没有它们,读取大量输入可能慢 10 倍,即使算法正确也可能导致「超时(TLE)」。每次都要加上它们。

🐛 常见错误: 加了这两行后,不要混用 cin/coutscanf/printf。选一种风格坚持用。


2.1.3 变量与数据类型

变量是内存中一个有名字的存储位置。C++ 中每个变量都有一个类型——类型告诉计算机需要分配多少内存,以及里面会存什么数据。

🧠 思维模型:变量就像带标签的盒子

当你写:   int score = 100;

计算机做了三件事:
  1. 创建一个足以容纳整数的盒子(4 字节)
  2. 在盒子上贴上标签 "score"
  3. 把数字 100 放进盒子

Variable Memory Box

竞赛编程必备类型

📄 查看代码:竞赛编程必备类型
#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++ 数据类型参考

C++ Variable Types

如何选择正确的类型

使用场景选用类型
计数、小数字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保留关键字

⚠️ 区分大小写! scoreScoreSCORE 是三个完全不同的变量。这是常见 bug 来源——命名时保持一致。

常见命名风格

C++ 中有几种广泛使用的命名规范。竞赛编程中不必只选一种,但了解它们有助于读懂别人的代码:

风格示例通常用于
驼峰式(camelCase)numStudentstotalScore局部变量、函数参数
帕斯卡式(PascalCase)MyClassGraphNode类、结构体、类型名
下划线式(snake_case)num_studentstotal_score变量、函数(C/Python 风格)
全大写(ALL_CAPS)MAX_NMODINF常量、宏
单字母nmij循环下标、数学风格竞赛编程

竞赛编程中最常用驼峰式单字母名称。公司的生产代码中,根据风格指南通常用 snake_casecamelCase

命名最佳实践

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 生产代码的命名对比

方面竞赛编程生产/学校项目
变量名长度简短即可:nmdpadj有描述性:numStudentsadjacencyList
循环变量永远用 ijkijk 也没问题
常量MAXNMODINFkMaxSizekModulus(Google 风格)
注释极少——速度优先详尽——可读性优先
目标快速编写、快速解题编写别人能维护的代码

💡 本书中: 我们会混用两种风格——讲解时用描述性名称保持清晰,解题时用简短名称。关键原则:看到变量名就应该立刻知道它存的是什么。

深入了解:charstring 与字符-整数转换

我们已经简要介绍了 charstring。由于许多 USACO 题目涉及字符处理、数字提取和字符串操作,让我们深入看看这两种重要类型。


char 与 ASCII——每个字符都是一个数字

C++ 中的 char1 字节整数(0-255)存储。每个字符按照 ASCII 表(美国信息交换标准代码)映射到一个数字。不需要背整张表,但记住几个关键范围非常有用:

ASCII Table Key Ranges

关键关系:
• '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;
}

charint 转换——最常用的技巧

竞赛编程中,你需要频繁地在字符数字和整数值之间转换。以下是完整指南:

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*

为什么竞赛编程中 stringchar[] 更好:

特性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 用 cincout 进行输入输出

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" 而不是 endlendl 会清空输出缓冲区,比 "\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 / 53,不是 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,打印它们的和、差、积、整数商和余数。

分析思路:

  1. 需要两个变量存储 N 和 M
  2. cin 读取
  3. cout 打印每个结果
  4. N 和 M 可能很大,应该用 long long 吗?安全起见用它。

💡 新手解题流程:

遇到题目时,别急着写代码。先用自然语言想清楚步骤:

  1. 理解题目:输入是什么?输出是什么?约束是什么?
  2. 手动推演样例:用样例输入,手算出输出,确认自己理解了题目
  3. 考虑数据范围:N 和 M 最大多少?会不会溢出?
  4. 写伪代码读取 → 计算 → 输出
  5. 翻译成 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使用 endlcout << x << endl;endl 清空输出缓冲区,大量输出时比 "\n" 慢 10 倍以上,可能导致超时"\n"
3忘记 I/O 加速缺少 sync_with_stdiocin.tie默认情况下 cin/cout 与 C 的 scanf/printf 同步,大量输入时极慢始终加上这两行
4整数除法意外7/2 期望 3.5 却得到 3两个整数相除,C++ 截断小数部分强转 double:(double)7/2
5缺少分号cout << xC++ 每条语句必须以 ; 结尾,否则编译失败cout << x;
6混用 cin >>getlinecin >> n 然后 getline(cin, s)cin >> 在缓冲区留下 \ngetline 读到空行中间加 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 / ba % 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/coutscanf/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 核心用法)介绍 vectorsort 等 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

只打印象限数字:1234

样例输入 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 更清晰,速度一样快