$ g++ uninit.cpp -o u # 无警告,太危险了
$ ./u <<< "3
1 2 3"
32772 ← 垃圾值,每次可能不同
$ g++ uninit.cpp -Wall -o u # 加 -Wall
uninit.cpp: In function 'int main()':
uninit.cpp:5:9: warning: 'ans' may be used uninitialized in this function [-Wmaybe-uninitialized]
5 | int ans;
| ^~~
$ g++ uninit.cpp -O2 -Wall -o u
$ ./u <<< "3
1 2 3"
6 ← -O2 下"碰巧"对了,但这不是正确,只是运气
C++修复:声明即初始化
#include<bits/stdc++.h>using namespace std;
int main() {
ios::sync_with_stdio(0);
int n; cin >> n;
long long ans = 0; // ✅ 初始化为 0,同时用 long longfor (int i = 1; i <= n; i++) {
long long x; cin >> x;
if (x > 0) ans += x;
}
cout << ans << endl;
return0;
}
🔍 代码逐段解析 · uninit.cpp(修复版)
第 4~5 行 —— 关同步加速读入;读入 n。
第 6 行 —— long long ans = 0;:两处修复叠加 —— ① 初始化为 0 消灭 UB;② 升级 64 位,105 个 109 级别的正数累加也不溢出。
第 7~10 行 —— 循环体逻辑未动,但 x 也改成 long long,保证中间量全程 64 位。
第 11 行 —— 输出稳定正确:输入 3 / 1 2 3 恒得 6。
易错点 —— 只改 ans 不改 x:小数据侥幸正确,数据变大后 ans += x 的中间累加仍可能出错;修 bug 要"顺手把同类隐患一起清"。
#include<bits/stdc++.h>using namespace std;
const int N = 5;
int a[N]; // 只有 5 个元素int main() {
int n = 8; cin >> n; // 若输入 8,就会越界for (int i = 0; i < n; i++) a[i] = i; // ❌ i 到 7,越界long long s = 0;for (int i = 0; i < n; i++) s += a[i];
cout << s << endl;
return0;
}
🔍 代码逐段解析 · overflow.cpp(有 bug 版)
第 3~4 行 —— const int N = 5,全局数组 a[5] 合法下标只有 0~4,共 20 字节。
第 6 行 —— int n = 8; cin >> n;:赋的 8 会被输入覆盖,写法冗余但能说明问题 —— 输入 8 就触发越界。
第 7 行(病灶) —— 循环上界是 n 而不是 N:i 走到 5、6、7 时 a[i] = i 写到了数组外面的内存上。
为什么不能一直用它交卷?因为它会给每次内存访问加检查,程序通常慢 2~20 倍、内存占用也变大,评测时可能 TLE / MLE。它的定位是本地查错工具,查完就换回普通编译。
运行结果对比
$ g++ overflow.cpp -O2 -o o && ./o <<< 8
28 ← 没崩溃,甚至"看起来对了",纯属运气
$ g++ overflow.cpp -g -fsanitize=address,undefined -o o_san
$ ./o_san <<< 8
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd...
WRITE of size 4 at 0x7ffd... thread T0
#0 0x4012ab in main overflow.cpp:7 ← 直接指出第 7 行(a[i] = i 的写入处)
Address 0x7ffd... is located in stack of thread T0 at offset 20 in frame
#0 0x4011f6 in main overflow.cpp:5
This frame has 1 object(s):
[32, 52) 'a' (line 4) ← 数组 a 只有 20 字节
SUMMARY: AddressSanitizer: stack-buffer-overflow overflow.cpp:7 in main
C++修复:数组开够 + 边界判断
#include<bits/stdc++.h>using namespace std;
const int MAXN = 100005; // ✅ 按数据范围上界开,再多开 5 个int a[MAXN];
int main() {
ios::sync_with_stdio(0);
int n; cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
long long s = 0;
for (int i = 0; i < n; i++) s += a[i];
cout << s << endl;
return0;
}
🔍 代码逐段解析 · overflow.cpp(修复版)
第 3~4 行 —— const int MAXN = 100005:按数据范围上界开数组并留余量,此后合法下标远大于 n 的可能取值。
第 5~7 行 —— 读入 n 与 n 个元素;只要题目保证 n ≤ 10^5,第 7 行的写入就全部落在数组内。
main.cpp: In function 'int main()':
main.cpp:7:18: error: expected ';' before 'cout'
7 | int ans = 0
| ^
main.cpp:12:23: warning: comparison of integer expressions of different signedness: 'int' and 'std::vector<int>::size_type' [-Wsign-compare]
main.cpp:15:9: warning: 'ans' is used uninitialized [-Wuninitialized]
main.cpp:20:1: error: 's' was not declared in this scope
20 | cout << s << endl;
| ^
输出要求:逐条给出"行号 → 类型(error/warning)→ 原因 → 修改方案"。
数据范围:日志不超过 100 行;需在 2 分钟内完成定位(模拟考场节奏)。
🔍 逐条分析
① 第 7 行 error:expected ';' before 'cout' —— int ans = 0 后面少了分号。编译器在读到 cout 时才发现上一句没结束。修法:补 ;。
② 第 12 行 warning:signedness 比较 —— 拿 int i 与 v.size()(无符号)比较。size() 返回无符号类型,一旦 i 为负就会被当成极大的正数。修法:比较时显式强转 (int)v.size(),或把循环变量 i 声明为 size_t。
③ 第 15 行 warning:ans 未初始化 —— 声明时没给初值。修法:long long ans = 0;。
④ 第 20 行 error:'s' was not declared —— 用了未声明的变量 s,或它的声明在 { } 作用域里已经结束。修法:在函数开头声明 long long s = 0;,或检查变量名是否拼错。
#include<bits/stdc++.h>using namespace std;
int main() {
ios::sync_with_stdio(0);
int n; cin >> n;
long long s = 0;
for (int i = 1; i <= n; i++) {
long long x; cin >> x;
s += x;
#ifdef DEBUG
cerr << "[调试] i=" << i << " s=" << s << endl;
#endif
}
cout << s << endl;
return0;
}
写出并解释三条 vim 命令:① 把 10~20 行中所有 int 替换为 long long 并逐个确认 ② 删除第 30 到第 45 行 ③ 把整个文件按 4 空格重新缩进
查看参考答案
① :10,20s/int/long long/gc —— 10,20 限定行范围,s///g 全文替换,c 逐个确认。注意 int 也会匹配到 printf 里的 int,所以 c(确认模式)非常必要。
② :30,45d —— 删除指定行区间。等价做法:跳过去按 16dd(从第 30 行起删 16 行),但计数容易错,用行号区间更安全。
③ gg=G —— gg 到文件首,= 是"按缩进规则重排",G 到文件尾,合起来表示全文自动缩进。
考点:vim 命令行模式下的行范围语法 起,止 命令,以及 g/c 标志位的语义。
C2深度分析
为什么"有符号整数溢出"是未定义行为,而无符号整数溢出是明确定义的?这对竞赛代码有什么实际影响?
查看参考答案
无符号:标准规定其运算按 2^n 取模(模运算语义),因此 unsigned int 溢出后回绕是定义明确的,可以依赖它写代码(例如哈希、位运算)。
有符号:历史上不同机器有不同的整数表示(补码、反码、原码),为兼容它们,标准把有符号溢出定为未定义行为,从而允许编译器假设"a + b 一定不会溢出",并据此做优化(如把 i <= n 的循环变量当作永不回绕)。
实际影响:① 用 int 累加大数据会溢出,结果不可预测 —— 求和必须用 long long;② 有些"哨兵值 1e18"技巧若超过 int 范围也会坏掉;③ 反向遍历 for (int i = n-1; i >= 0; i--) 是安全的(不溢出),但 for (unsigned i = n-1; i >= 0; i--) 会死循环。
启示:提高组选手要养成"看到累加、乘积、计数就用 long long"的条件反射。
C3工具链设计
为多文件小工程写一个 Makefile,要求:支持 make 编译出 prog、make debug 编译调试版、make clean 清理产物。