語法書 / AA 競程語法書 下冊 / 第十五單元 / 讓編譯器幫你抓錯:警告參數

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 環繞成天文數字,永遠不會小於 0empty 永遠印不出來。只開 -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 給你的警告。