这组文章面向三题制的 C++ 上机考试,目标不是重新学习完整的 C++ 语言,而是在一周内恢复“读题后能够迅速写出、编译并调通程序”的能力。正文统一使用 C++17、标准头文件和显式 std:: 前缀;C++20 语法只放在可选补充中,不作为考场模板的依赖。

系列共六篇:

  1. C++17 基础与输入输出;
  2. 字符串、进制、位运算与字节流;
  3. STL 容器与常用数据结构;
  4. 排序、查找与二分模板;
  5. 双指针、滑动窗口与高频组合模式;
  6. 设计题、业务模拟与三题实战。

本篇先解决最基础、也最容易造成无谓失分的问题:程序骨架、类型选择、函数参数、输入解析、输出格式和边界处理。

一、考场程序的基本结构

完整程序至少包含头文件、辅助函数和 main()。建议把题目的核心逻辑写成独立函数,让 main() 只负责输入、调用和输出。这样既方便本地调试,也能快速迁移到只要求实现函数的 LeetCode 式平台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// verify: cpp17
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

struct Summary {
long long sum;
int minimum;
int maximum;
double average;
};

Summary analyze(const std::vector<int>& values) {
const auto [minIt, maxIt] = std::minmax_element(values.begin(), values.end());
const long long sum = std::accumulate(values.begin(), values.end(), 0LL);

return Summary{
sum,
*minIt,
*maxIt,
static_cast<double>(sum) / static_cast<double>(values.size())
};
}

int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

int n = 0;
if (!(std::cin >> n) || n <= 0) {
std::cout << "empty\n";
return 0;
}

std::vector<int> values(static_cast<std::size_t>(n));
for (int& value : values) {
std::cin >> value;
}

const Summary result = analyze(values);
std::cout << "min=" << result.minimum << '\n';
std::cout << "max=" << result.maximum << '\n';
std::cout << "sum=" << result.sum << '\n';
std::cout << std::fixed << std::setprecision(2);
std::cout << "avg=" << result.average << '\n';
return 0;
}

输入:

1
2
5
10 20 -5 7 8

输出:

1
2
3
4
min=-5
max=20
sum=40
avg=8.00

这段程序体现了几条可直接复用的原则:

  • main() 检查输入是否成功,并在调用 analyze() 前保证数组非空;
  • 只读且可能较大的参数使用 const T&,避免复制;
  • 求和的初始值写成 0LL,使 std::accumulate() 从一开始就按 long long 累加;
  • 平均值计算前显式转换为 double,避免整数除法;
  • 输出使用 \n。只有确实需要立即刷新缓冲区时才使用 std::endl

std::ios::sync_with_stdio(false) 可以关闭 C++ 流与 C 标准 I/O 的同步。使用它以后,不要在同一个程序中混用 std::cinstd::coutscanf()printf(),否则输出顺序可能不符合预期。

二、考场常用类型

算法题首先要根据数据范围选择类型。下面不是完整的语言类型表,而是上机考试最常用的选择。

类型 常见用途 主要注意事项
int 下标、计数、绝对值不超过约 20 亿的整数 两个 int 相乘会先按 int 计算,结果可能在赋给 long long 前已经溢出
long long 总和、乘积、距离、时间戳 通常至少为 64 位,但仍要检查题目上界
double 平均值、比例、允许误差的计算 不要直接用 == 判断复杂浮点计算结果
char 单个字节字符 char 是否有符号由实现决定,传给 <cctype> 函数前要谨慎转换
bool 条件和状态标记 输出默认是 01,可使用 std::boolalpha 输出 falsetrue
std::size_t 容器大小和下标 是无符号类型,不要让它参与可能得到负数的计算

最常见的溢出错误如下:

1
2
3
4
5
int width = 100000;
int height = 100000;

long long wrong = width * height; // 乘法先以 int 计算,可能已经溢出
long long correct = 1LL * width * height;

如果题目给出 n <= 10^5、单个数值 value <= 10^9,总和的上界可达到 10^14,应直接使用 long long

三、初始化、auto、引用与 const

1. 声明时完成初始化

局部基本类型不要只声明、不赋值:

1
2
3
4
int count = 0;
long long total = 0;
bool found = false;
std::string answer;

容器可以使用列表初始化:

1
2
std::vector<int> values{3, 1, 4};
std::pair<int, int> range{0, 0};

2. auto 适合消除重复类型,不适合隐藏关键类型

1
2
auto iterator = values.begin();
auto position = std::find(values.begin(), values.end(), 4);

当整数宽度直接关系到正确性时,仍应明确写出 long longstd::uint64_t 等类型。

范围 for 循环要特别注意是否复制元素:

1
2
3
4
5
6
7
8
9
10
11
for (int value : values) {          // 读取副本
std::cout << value << '\n';
}

for (int& value : values) { // 修改原容器
value *= 2;
}

for (const int& value : values) { // 只读且不复制,适合较大的元素类型
std::cout << value << '\n';
}

int 这类小类型,按值读取通常更直接;对 std::string、结构体等较大对象,优先使用 const T&

3. 函数参数的快速选择

目的 推荐形式
读取小对象 int value
读取大对象且不修改 const std::vector<int>& values
修改调用者对象 std::vector<int>& values
对象所有权需要转移 本系列不作为应试重点,先按值传递并在必要时移动

返回局部 std::vectorstd::string 或结构体时,通常直接按值返回即可,不要为了“避免复制”返回局部变量的引用。

四、四种高频输入形式

1. 固定数量的空白分隔数据

operator>> 会自动跳过空格、换行和制表符,因此不必关心数据是否全部写在同一行。

1
2
3
4
5
6
7
int n = 0;
std::cin >> n;

std::vector<int> values(static_cast<std::size_t>(n));
for (int& value : values) {
std::cin >> value;
}

2. 多组测试数据

题目第一项给出测试组数 t 时,外层循环只负责重复调用单组逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void solveOneCase() {
int n = 0;
int limit = 0;
std::cin >> n >> limit;

int answer = 0;
for (int i = 0; i < n; ++i) {
int value = 0;
std::cin >> value;
if (value >= limit) {
++answer;
}
}
std::cout << answer << '\n';
}

int t = 0;
std::cin >> t;
while (t-- > 0) {
solveOneCase();
}

把每组状态定义在 solveOneCase() 内,可以避免上一组数据污染下一组。

3. 读取到文件结束

题目没有给出记录数量时,可以把读取操作本身作为循环条件:

1
2
3
4
5
long long left = 0;
long long right = 0;
while (std::cin >> left >> right) {
std::cout << left + right << '\n';
}

不要写成 while (!std::cin.eof())。文件结束标志通常要在一次读取失败后才会被设置,这种写法容易让最后一组数据被重复处理。

4. 读取完整一行

std::getline() 会保留行内空格,但不会保留行末换行符:

1
2
3
4
5
6
7
8
9
#include <limits>
#include <string>

int n = 0;
std::cin >> n;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

std::string line;
std::getline(std::cin, line);

std::cin >> n 会把换行符留在输入缓冲区。如果随后直接调用 std::getline(),第一次得到的往往是空字符串。ignore() 的作用是丢弃这一行剩余的字符。

五、下标循环与边界

容器的半开区间是 [begin, end):起点包含在范围内,终点指向最后一个元素之后。下标区间同样优先写成 [0, n)

1
2
3
for (std::size_t i = 0; i < values.size(); ++i) {
std::cout << values[i] << '\n';
}

反向遍历无符号下标时,不要写 i >= 0

1
2
3
for (std::size_t i = values.size(); i-- > 0;) {
std::cout << values[i] << '\n';
}

更简单的场景可以使用反向迭代器:

1
2
3
for (auto it = values.rbegin(); it != values.rend(); ++it) {
std::cout << *it << '\n';
}

当必须计算 right - left 且结果可能为负数时,使用有符号整数保存逻辑下标,并在访问容器前确认范围合法。

六、输出格式

1
2
3
4
5
6
7
#include <iomanip>

double ratio = 1.0 / 3.0;
std::cout << std::fixed << std::setprecision(3) << ratio << '\n'; // 0.333
std::cout << std::boolalpha << true << '\n'; // true
std::cout << std::hex << 255 << '\n'; // ff
std::cout << std::dec << 255 << '\n'; // 255

流格式会持续生效。输出十六进制后,如果后面仍要输出十进制,应显式切回 std::dec;设置 std::fixed 和精度后也要确认后续输出是否仍需要该格式。

七、代表题与思考过程

代表题一:批量读数统计

篇首完整程序就是本题答案。解题过程如下:

  1. 输入规模决定使用 std::vector<int> 保存读数;
  2. 单个值使用 int,总和使用 long long
  3. 非空数组才能解引用 std::minmax_element() 的结果;
  4. std::accumulate() 的初始值决定累加类型;
  5. 平均值需要浮点除法和固定精度输出。

时间复杂度为 O(n),保存输入需要 O(n) 空间。如果题目只要求最小值、最大值、总和与平均值,也可以在读取时同步统计,把额外空间降为 O(1)

代表题二:阈值计数

给定一组整数和阈值,统计大于等于阈值的元素数量。核心函数可以直接使用标准算法:

1
2
3
4
5
6
7
8
9
10
#include <algorithm>
#include <vector>

int countAtLeast(const std::vector<int>& values, int limit) {
return static_cast<int>(std::count_if(
values.begin(),
values.end(),
[limit](int value) { return value >= limit; }
));
}

Lambda [limit] 按值捕获阈值。函数只遍历一次数组,时间复杂度为 O(n),额外空间复杂度为 O(1)

代表题三:逐行处理日志

如果每条日志占一整行且字段之间可能包含空格,应使用 std::getline()。处理流程通常是:

1
2
3
4
5
6
7
std::string line;
while (std::getline(std::cin, line)) {
if (line.empty()) {
continue;
}
// 在这里解析并处理 line。
}

不要在没有确认题意时自动跳过空行:有些题目把空字符串视为合法数据。上面的判断只适用于题目明确说明空行无效的场景。

八、常见错误与调试顺序

1. 编译错误

依次检查:

  1. 是否包含了声明该类型或函数的标准头文件;
  2. 是否漏写 std::
  3. 圆括号、花括号、方括号和分号是否成对;
  4. 函数声明、定义与调用的参数类型是否一致;
  5. 使用的特性是否属于 C++17。

2. 结果错误

使用最小样例逐项打印中间状态,重点检查:

  • 空输入、单元素、全相等、全为负数;
  • 0、最大值和最小值;
  • 循环是否少处理或多处理一个元素;
  • 除法发生在转换为浮点数之前还是之后;
  • 乘法和累加是否已经溢出。

3. 超时

先判断算法复杂度是否合适,再考虑 I/O。sync_with_stdio(false) 无法挽救本应为 O(n log n) 却写成 O(n^2) 的算法。

九、考前速查清单

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);

// 1. 读取输入
// 2. 调用独立的核心函数
// 3. 严格按题目格式输出
return 0;
}
  • 总和或乘积可能超过 int:使用 long long,乘法写 1LL * a * b
  • 整数平均值需要小数:除法前转换为 double
  • std::accumulate() 需要 64 位结果:初始值写 0LL
  • operator>> 后接 std::getline():先处理残留换行;
  • 读到文件结束:while (std::cin >> value)
  • 容器范围:优先使用 [begin, end)[0, n)
  • 核心逻辑写入函数,main() 只负责输入输出;
  • 提交前至少测试空边界、单元素和最大数据范围。

十、C++20 可选补充

C++20 提供 std::ssize(container),可以直接获得有符号的容器大小。在本系列的 C++17 基线中,仍使用 container.size(),并避免在无符号类型上做可能为负的运算。

十一、延伸练习

参考资料