語法書 / AA 競程語法書 下冊 / 第十單元 / size() 與無號整數的陷阱

10.4 size() 與無號整數的陷阱

v.size() 這麼常用,偏偏藏著本單元最陰的一顆地雷:它回傳的不是 int,而是無號整數(unsigned)——上冊 2.3 的補充提過:只能存 0 和正數、不能存負數的整數型態(在本站環境是 64 位元的無號整數)。

不能存負數,那 0 再減 1 會怎樣?答案是環繞(上冊 2.9 溢位的環繞行為,這次是往下繞):直接繞到這個型態能表示的最大值。看實驗:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> v;                     // 空的:size() 是 0
    cout << v.size() - 1 << '\n';      // 猜猜印什麼?
    return 0;
}

執行結果:

18446744073709551615

不是 -1,是一個 20 位的天文數字。更陰的是混合運算intsize() 這種無號整數同場比較或運算時,int 那邊會先被轉成無號(上冊 3.7 隱性轉換家族的成員;這是「int 對上容器 size() 的型態」這個組合的規則,不是所有 signed/unsigned 混用都如此)再處理——所以連 i <= v.size() - 1 這種看起來無辜的條件式都會中招。

這顆地雷不是 vector 專屬——所有容器的 size() 都回傳無號整數。「size() 一到手就轉 int」,養成這個習慣一勞永逸。這樣轉之所以安全,是因為競賽題都有明確的資料量上限、遠小於 int 的極限;沒有這種保證的場合(競程裡很少見)才需要保留無號型態原樣處理。

這個習慣在倒著走時特別重要——起點 size() - 1 的減法躲不掉:

for (int i = (int)v.size() - 1; i >= 0; i--) {
    cout << v[i] << '\n';
}

空 vector 時起點是 -1,條件 i >= 0 第一次就不成立、一圈都不跑——行為正確。那不寫 (int) 會怎樣?動手一試,看起來沒事:天文數字指派給 int 變數時被環繞塞了回去,剛好得到 -1。但「實驗沒事」和「保證沒事」是兩回事——要到 C++20,標準才保證這個結果;在 C++17 及更早的標準裡,塞不下時的結果是由各家編譯器自己決定的,只是常見的編譯器剛好都選了環繞。APCS 檢定目前用的正是 C++17,比賽環境也不一定跟得上新標準——依賴這種「剛好」,哪天換個環境就可能翻車。真正的分水嶺是 i 的型態——int i 能把無號值接住;要是讓 i 自己是無號型態(例如用 12.2auto 去接),i >= 0 就永遠成立,迴圈停不下來、直接越界。(int) 轉型是把「我要的是 int」寫明白,跟「一到手就轉 int」的習慣一致——不管環境是 C++17 還是 C++20 都安全,不必靠環繞救援。