C 與 C++ 中整數解析的危險性

將字串解析為整數似乎是程式設計中最基本的運作之一。在大多數現代語言中,這只需一行程式碼:Python 中的 int("123") 或 JavaScript 中的 Number("123")。然而,在 C 與 C++ 中,這項簡單的任務揭示了未定義行為、靜默溢位以及違反直覺的 API 設計等雷區。

若要將一個解析器視為「正確」,它理想上應滿足三個標準:儲存的數值必須與字串中的視覺表示一致、必須消耗整個字串(沒有尾隨垃圾內容),且任何未能滿足這些標準的情況都必須能明確地作為錯誤回報。

正如我們將看到的,C 標準函式庫在滿足這些要求方面表現掙扎。

C 標準函式的失敗

在 C 中嘗試進行整數解析主要有三種方式,每種都帶有顯著的限制。

1. atol():危險的遺產

atol() 或許是最具問題的。它無法提供任何檢測錯誤的方法。如果你傳入一個不是數字的字串(例如 "timmy"),它會回傳 0。如果你傳入一個以數字開頭但包含垃圾內容的字串(例如 "123timmy"),它會回傳 123

更令人擔憂的是,C 與 POSIX 標準規定,如果數值無法表示,其行為是未定義的。從理論上來說,這意味著當面對不受信任的輸入時,atol() 可能會做出任何行為——包括導致程式崩潰。

2. strtol():唯一的可行 C 選項(針對有符號整數)

strtol() 顯著地更強健,因為它提供了一個指向解析停止位置的指標 (endptr),並在範圍錯誤時設置 errno。然而,正確使用它需要大量的樣板程式碼:

bool parse_long(const char* in, long* out)
{
  if (!*in) return false; // Detect empty string

  char* endp = NULL;
  errno = 0;
  *out = strtol(in, &endp, 0);

  if (errno) return false; // Range error
  if (*endp) return false; // Trailing garbage
  return true;
}

3. strtoul()sscanf():無符號數的陷阱

雖然 strtol() 適用於有符號數,但 strtoul() (unsigned long) 與 sscanf() 在許多使用場景下從根本上是有問題的的。主要問題在於 strtoul() 允許前導負號。如果你傳入 -1strtoul(),它不會回傳錯誤;它會執行回繞 (wrap-around) 並回傳 ULONG_MAX (例如在 64 位元系統上為 $2^{64}-1$)。

這造成了歧義:如果你收到 ULONG_MAX 作為結果,你無法得知輸入究竟是最大的無符號長整數、字串 "-1",還是超出範圍而溢位的數值。這使得使用標準 C 函式來可靠地解析 64 位元旗標欄位或大型無符號識別碼變得不可能。

C++ 的觀點

C++ 試圖封裝這些 C 函式,但通常也繼承了它們的缺陷。std::stoul()std::istringstream 的行為與其 C 對應版本相似,通常允許透過回繞將負號解析為無符號類型。

現代解決方案:std::from_chars

在 C++17 中引入的 std::from_chars 是第一個行為直覺的標準函式庫函式。它不允許無符號類型使用負號,並提供明確的錯誤碼 (std::errc) 與指向解析序列末端的指標。

template<typename T>
std::optional<T> parse_int(std::string_view sv)
{
  T i;
  const auto [ptr, ec] = std::from_chars(sv.begin(), sv.end(), i);
  if (ec != std::errc()) return {};
  if (ptr != sv.end()) return {};
  return i;
}

社群洞察與規避方法

圍繞此主題的技術討論凸顯了 C 社群的分歧。有些人認為 C 標準函式庫是一個「遺產堆疊」,而經驗豐富的 C 程式設計師只需從頭開始編寫自己的解析邏輯,以確保效能與正確性。

對於必須使用無符號長整數的 C 程式設計師,一個建議的規避方法是先使用 strtol() 解析字串以檢查負號,然後再使用 strtoul() 進行實際轉換:

"Exclude negative numbers, including off scale negative... [then] plain sailing strtoul() from now on."

其他貢獻者指出了進一步的陷阱,例如「八進制陷阱」,即除非明確將基數設置為 10,否則前導零會被自動解釋為八進制指示符。這對於期望標準十進制解析的使用者來說,往往是意料之外的行為。

結論

C 中的整數解析是一個經典案例,說明了早期的設計決ใด於優先考慮簡單性與最小開銷,而非安全性與錯誤回報,如何會造成長期的技術債。對於在 C 中工作的開發者來說,最安全的做法通常是 คือ避免使用標準函式庫的 strto... 系列針對無符號類型,並改為實作自定義解析器,或使用一個會明確檢查負號與空字串的強健封裝函式。

Sources