第 6 / 15 部 待錄

基本型別與不尋常型別

《Code Complete 軟體建構之道》Podcast 準備稿:基本資料型別與不尋常資料型別

書名: 軟體建構之道 Code Complete(A Practical Handbook of Software Construction, 2nd Edition) 作者: Steve McConnell 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|Code Complete.浮點數的坑、指標的雷、全域資料的毒 (6/15) 涵蓋範圍: 第12章 基本資料型別 + 第13章 不尋常的資料型別

背景速覽

這集收尾「變數」這一大部,從變數的「使用」與「命名」下沉到變數的「型別」本身。第12章講最日常的基本型別——數值、整數、浮點數、字串、布林、列舉、具名常數、陣列——的陷阱與技巧;第13章講三個比較硬核的主題:結構體、指標全域資料。這集會有不少「踩過才知道痛」的經典坑,很適合用真實 bug 故事來講。

一句話重點

每種基本型別都藏著它特有的地雷——整數會溢位、浮點數不能用等號比較、陣列會越界;而指標和全域資料是兩個最危險的領域,最好的策略不是小心使用,而是用存取常式、用類別把它們包起來、甚至能避就避。

值得討論的重點

1. 整數與浮點數的經典陷阱——數字沒你想的乖

McConnell 列了一串會讓你冒冷汗的坑。整數:7/10 在整數運算等於 0 不是 0.7;中間結果溢位——1000000 * 1000000 / 1000000 最終結果該是一百萬,但中間結果超過 32 位元上限,算出來是 -727。浮點數更陰險:許多十進制小數無法用二進位精確表示,所以浮點數絕對不能用 == 比較——0.1 累加十次不等於 1.0。正解是用容差比較(兩值之差的絕對值小於某個 delta 才算相等)。金額計算尤其危險,建議改用整數(以「分」為單位)或 BCD。這些坑共同的教訓:別假設電腦的算術跟你數學課學的一樣。

2. 魔術數字、列舉、具名常數——把知識放進「資料」而非散在 code 裡

McConnell 反覆強調避免魔術數字:程式本體裡唯一該出現的字面值只有 01(用於遞增、遞減、陣列起始)。其他都該用具名常數。他連「安全」的字面值都不放過——For i = 1 To 12 裡的 12,雖然「一年不太可能變 13 個月」,但用 NUM_MONTHS_IN_YEAR 能消除任何疑慮。列舉型別是個強大工具:if color = Color_Red 遠勝 if color = 1,而且編譯器能做型別檢查擋掉無意義的賦值。具名常數實現了「單點控制」——電話號碼長度要改時只需改一處。最重要的一條警告:一致使用——在某處用具名常數、另一處用字面值代表同一個東西,是極危險的,改常數時會漏掉硬編碼的字面值。

3. 指標——現代程式設計最容易出錯的領域

第13章的指標是重頭戲。McConnell 點破為什麼 Java、C#、VB 乾脆不提供指標——它太容易錯,而且指標錯誤的症狀往往跟成因毫無關聯(可能崩潰、可能算錯、也可能像定時炸彈暫時沒事)。許多安全漏洞(緩衝區溢位)都源自指標誤用。他的策略是「預防為主、早期偵測為輔」,一長串實用招數裡最值得講的幾條:把指標操作隔離到存取常式裡、宣告時立即初始化、配置(new)和釋放(delete)保持在同一作用域對稱、釋放後立即設為 null(讓對懸空指標的寫入產生明確錯誤)、釋放前填垃圾值。還有一條根本性的:如果有其他替代方案,就別用指標。

4. 全域資料——不只危險,而且你有更好的替代品

全域變數這段觀點很鮮明。McConnell 列了全域資料的一堆問題:意外修改(side effect)、別名問題(aliasing,同一變數有兩個名字)、重入性問題(多執行緒共享)、阻礙重用、破壞模組化。但他的態度不是「絕對禁用」,而是「最後手段」——先試區域變數、再試類別變數、最後才考慮全域。最關鍵的解法是用存取常式取代直接存取全域資料:把資料藏在類別裡,外面透過 Get()/Set() 來碰。這帶來集中控制、自動防護(比如在 PushStack() 內建溢位檢查)、資訊隱藏,而且存取常式的抽象層級該對齊問題領域——account = NextAccount(account)node = node.next 更能表達意圖。他的金句:用存取常式能提供全域變數的所有好處,而且更多。

注意事項

⚠️ 整章 C/C++ 色彩很重(指標、strcpy/strncpystructtypedef、巨集算陣列長度……)。現代用 Java/Kotlin/Swift/Go 的聽眾不會直接碰指標,但核心觀念有現代對應:指標的「懸空/別名」問題對應現代的 null safety、可變共享狀態問題;全域資料的毒對應今天的「全域 singleton、可變靜態狀態」。講的時候把 C 的細節快速帶過,把觀念翻譯到現代語境——比如「釋放後設 null」對應「用完的引用要清掉避免記憶體洩漏」、「全域資料要包進存取常式」對應「別到處 import 一個可變的全域 config」。

⚠️ 浮點數比較那條是至今 100% 成立、不分語言的硬核真理——這段一定要講,因為它是最常見、最難 debug 的初級陷阱,每個現代語言的工程師都會踩。別因為「這章很 C」就跳過它。

專家補充

💡 「把知識放進資料而非邏輯」這條(用查表、用列舉、用具名常數取代散落的字面值和 if)其實是一個貫穿全書的大主題,下一集 EP8 的「表格驅動法」就是它的極致版本——把一大堆 if/case 換成一張表。可以預告聽眾:這條線會在陳述式那部爆發。

💡 指標和全域資料這兩章,今天回看是很好的「語言演進史」教材。McConnell 在 2004 年苦口婆心教你怎麼小心使用指標、避免全域資料——而後來的語言設計者乾脆從根上解決問題:Java/C# 拿掉指標、Rust 用所有權系統在編譯期擋掉懸空指標、函數式語言用不可變性消滅全域可變狀態。可以拋這個縱深:很多 2004 年要靠紀律解決的問題,今天靠語言設計就解決了——但理解這些坑為什麼存在,仍然讓你成為更好的工程師。

💡 列舉型別的「保留第一個項目為無效值(InvalidFirst)」這招很精妙——把映射到 0 的第一個元素設為無效,這樣未初始化的變數(預設是 0)一被使用就會被抓出來。這是「進攻式程式設計」(EP4)在型別層面的應用,是個很細但很專業的技巧。

討論問題

🎙️ 開場鉤子:「0.1 + 0.2 等於多少?如果你脫口而出 0.3,那你還沒被浮點數咬過。這集我們來盤點變數型別裡那些『數學課沒教、但會讓你 debug 到天亮』的坑。」

🎙️ 自問自答:「為什麼新一代語言都不給你用指標了?」——因為指標錯誤的症狀跟成因常常八竿子打不著,難 debug 到 Java、C# 的設計者乾脆把它拿掉。這不是退化,是語言設計者替你把雷排掉了。

🎙️ 帶走的一題:「打開你手上的專案,搜一下有沒有那種『一個可變的全域 config 物件,到處被 import、到處被改』——那就是 McConnell 講的全域資料的毒。試著把它包進一個只給 Get、嚴格管 Set 的存取層。」

更大範圍關聯

  • 「知識放進資料」這條線:本集的具名常數與列舉接 EP8 的表格驅動法是同一思想的升級,可在節目裡埋線預告。
  • 語言演進史的活教材:指標與全域資料兩章,對照 Rust 所有權、Java 無指標、函數式不可變性,是觀察「紀律 vs 語言設計」如何解決同一問題的絕佳案例。
  • 回扣資訊隱藏:用存取常式包全域資料,直接呼應 EP2 的資訊隱藏與 EP3 的封裝——又一次證明全書原則的一致性。
  • 時代座標:這兩章是全書「C/C++ 含量」最高的部分,反映 2004 年 C++ 仍是主流系統語言;今天讀它,更像在讀一份「為什麼我們需要更安全的語言」的需求清單。

錄製建議

  • 建議時長:約 25-28 分鐘。配比:整數浮點陷阱(最有共鳴)7 分;魔術數字/列舉/具名常數 6 分;指標 7 分;全域資料與存取常式 7 分。
  • 討論策略:用「0.1 + 0.2」當開場鉤子,浮點數陷阱是全場最有共鳴的點,務必講透容差比較。指標和全域資料這兩段,刻意採取「這章很 C,但觀念能翻譯到你的現代語言」的處理方式——一邊講 2004 年的細節、一邊即時翻譯成 null safety、不可變狀態、避免全域 singleton,讓現代聽眾有共鳴。「語言演進史」那個縱深點是這集最有質感的料,值得展開。
  • 提醒自己:C 字串、struct、typedef 的細節快速帶過,把時間留給浮點數、列舉、指標、全域這四個有現代對應的主題。