15.2 讓編譯器幫你抓錯:警告參數
查錯最便宜的一步,其實發生在程式執行之前。編譯器在編譯時就看得出很多可疑寫法,只是預設不太愛講話;加上警告參數,它就會變成一位囉唆但可靠的隊友。在終端機編譯時直接加在指令裡:
g++ -O2 -Wall -Wextra -Wshadow main.cpp
指令裡的 -O2 是最佳化參數,本站 OJ 評測時同樣開著它。它除了讓程式跑得更快,還會讓編譯器把整份程式分析得更透徹——有些警告要開著最佳化才抓得到(第一個例子馬上就會看到),所以請把它跟警告參數綁在一起用。用 Code::Blocks 的話,在 Settings → Compiler... 裡把對應的警告項目和最佳化 -O2 一併打勾即可,效果相同。以下逐一介紹四個最值得開的參數——每個都配一段「真的會被抓到」的錯誤程式,警告訊息都是本站編譯器(GNU g++)的實際輸出。
¶-Wall:最基本的一批警告
-Wall 一次打開一大批常用警告。名字雖然叫 all,但它不是「全部警告」,只是「很多」。它抓得到的經典錯誤:使用未初始化的變數、函式忘了 return、= 寫成 ==(15.3 會展示)等。
#include <iostream>
using namespace std;
int main() {
int sum; // 忘記初始化!
for (int i = 1; i <= 10; i++) {
sum += i;
}
cout << sum << '\n';
return 0;
}
編譯訊息(加 -O2 -Wall):
main.cpp: In function ‘int main()’:
main.cpp:5:9: warning: ‘sum’ is used uninitialized [-Wuninitialized]
5 | int sum; // 忘記初始化!
| ^~~
一個重要細節:這條警告要開著最佳化(-O1 以上)才會出現——像這樣「繞進迴圈累加才用到」的未初始化,編譯器要在最佳化的深入分析裡才看得出來,沒開 -O 時它會默默放行。這正是本節開頭把 -O2 一起寫進指令的原因。
驚悚的地方在後頭:這支程式在本站執行還印出 55——正確答案!未初始化變數是未定義行為(上冊 6.8),本機「碰巧」拿到 0 起跑,到了 OJ 就未必。要不是警告,你根本不會發現這顆地雷——這正是 15.4 情境二的預告片。
¶-Wextra:補上 -Wall 沒開的部分
-Wextra 再補一批 -Wall 沒包含的警告(一樣不是「補完就是全部」)。例如它會抓「永遠不可能成立的比較」:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v;
if (v.size() - 1 < 0) { // 想判斷「空的 vector」,但 size() 是無號整數
cout << "empty" << '\n';
}
return 0;
}
編譯訊息(加 -Wall -Wextra):
main.cpp: In function ‘int main()’:
main.cpp:7:22: warning: comparison of unsigned expression in ‘< 0’ is always false [-Wtype-limits]
7 | if (v.size() - 1 < 0) { // 想判斷「空的 vector」,但 size() 是無號整數
| ~~~~~~~~~~~~~^~~
這正是 10.4 的無號整數陷阱:v.size() - 1 環繞成天文數字,永遠不會小於 0,empty 永遠印不出來。只開 -Wall 編譯這支程式是一片安靜——-Wextra 才會出聲。
¶-Wconversion:隱性轉型把值改掉時警告
不同型態之間的隱性轉換(上冊 3.7)常常伴隨「值悄悄變了」。-Wconversion 專抓這件事:
#include <iostream>
using namespace std;
int main() {
long long big = 50000LL * 100000; // 5×10^9:long long 裝得下
int x = big; // 塞回 int:裝不下!
cout << x << '\n';
return 0;
}
編譯訊息(加 -Wall -Wconversion):
main.cpp: In function ‘int main()’:
main.cpp:6:13: warning: conversion from ‘long long int’ to ‘int’ may change value [-Wconversion]
6 | int x = big; // 塞回 int:裝不下!
| ^~~
執行結果:
705032704
5 \times 10^9 塞進 int 直接面目全非。「辛苦用 long long 算完,最後手滑存回 int」是溢位錯誤裡特別冤的一種,-Wconversion 抓它一抓一個準;小數存進整數變數(例如 int half = n / 2.0;)它也會警告。
不過警告參數不是萬靈丹。看這行經典程式碼:
long long INF = 1e18 + 1; // 想要 10^18 + 1
四個警告參數全開,編譯器一聲不吭;執行印出來卻是 1000000000000000000——那個 +1 憑空蒸發了。原因你在 9.6 學過:1e18 是浮點數,在 10^{18} 附近浮點數的格距早已大於 1,+ 1 在浮點世界根本擠不進去;等它轉回 long long 時,值「恰好沒變」(變不見的那步是浮點加法,不是轉型),所以 -Wconversion 也無話可說。教訓:警告是輔助,不是保證——大整數就老老實實用整數運算寫 1000000000000000001LL。
¶-Wshadow:變數遮蔽警告
遮蔽(shadow)指的是:區域變數跟全域變數(或外層變數)同名,於是在區域範圍內「蓋住」了外面那個。上冊 3.8 的變數範圍講過這是合法的——但合法不代表是好主意:
#include <iostream>
using namespace std;
int total = 0; // 全域:想累積所有輸入的和
void add(int x) {
int total = 0; // 手滑又宣告了一個 total
total += x; // 加到的是區域的 total!
}
int main() {
add(3);
add(5);
cout << total << '\n'; // 期望 8
return 0;
}
編譯訊息(加 -Wall -Wshadow):
main.cpp: In function ‘void add(int)’:
main.cpp:7:9: warning: declaration of ‘total’ shadows a global declaration [-Wshadow]
7 | int total = 0; // 手滑又宣告了一個 total
| ^~~~~
main.cpp:4:5: note: shadowed declaration is here
4 | int total = 0; // 全域:想累積所有輸入的和
| ^~~~~
執行結果:
0
add 裡加來加去的都是那個馬上就消失的區域 total,全域的一分錢都沒收到。這種 bug 用眼睛極難看出來——兩個變數長得一模一樣;-Wshadow 一行就點破。競程裡全域變數用得多(大陣列習慣開在全域,上冊第六單元),-Wshadow 特別值得開。
動手試試看:把 15.1 的「最大值減最小值」錯誤程式用 -O2 -Wall -Wextra -Wshadow 重新編譯一次,看看編譯器有沒有話要說。接著故意寫一個忘了 return 的函式(int f() { int x = 1; }),觀察 -Wall 給你的警告。