基本資料型別是所有其他資料型別的基礎構件。本章涵蓋數值、整數、浮點數、字元與字串、布林變數、列舉型別、具名常數、陣列,以及自訂型別的使用技巧與常見陷阱。
12.1 數值概論#
- 避免魔術數字(Magic Numbers):不要在程式中直接出現無解釋的數值常數(如
100、47524)。使用具名常數或全域變數取代,好處有三:變更更可靠、變更更容易、程式碼更易讀。 - 硬編碼的 0 和 1 可以接受:用於遞增、遞減或陣列起始索引的
0和1是合理的。經驗法則:程式本體中唯一應出現的字面值只有0和1。 - 預防除以零錯誤:每次使用除法運算子時,都應思考分母是否可能為零,並撰寫防護程式碼。
- 明確進行型別轉換:讓讀者在型別轉換發生時一目了然,例如 C++ 的
(float) i或 VB 的CSng(i)。 - 避免混合型別比較:浮點數與整數的比較幾乎保證不會正確運作,應手動轉換為相同型別再比較。
- 留意編譯器警告:頂尖程式設計師會修正程式碼以消除所有編譯器警告。
12.2 整數#
- 注意整數除法:
7/10在整數運算中通常等於0而非0.7。10 * (7/10)等於0,而非7。解決方法是重新排列運算式,將除法放在最後:(10*7) / 10。 - 注意整數溢位(Integer Overflow):乘法或加法可能產生超出最大整數值的結果。
常見整數型別範圍
有號 8-bit:-128 ~ 127 / 無號 8-bit:0 ~ 255
有號 16-bit:-32,768 ~ 32,767 / 無號 16-bit:0 ~ 65,535
有號 32-bit:-2,147,483,648 ~ 2,147,483,647 / 無號 32-bit:0 ~ 4,294,967,295
有號 64-bit:約 -9.2 x 10^18 ~ 9.2 x 10^18 / 無號 64-bit:0 ~ 約 1.8 x 10^19
預防方式:逐一思考運算式中每個項目可能的最大值,評估結果是否超出範圍。若會超出,改用 64 位元整數或浮點數。
注意中間結果溢位:即使最終結果在範圍內,中間運算結果也可能溢位。例如 1000000 * 1000000 / 1000000 的中間結果需容納 10^12,遠超 32 位元整數上限,導致錯誤結果 -727。
12.3 浮點數#
浮點數的核心問題是:許多十進制小數無法以二進位精確表示。32 位元浮點表示法的 1/3 僅精確到 7 位數(0.33333330)。
- 避免對數量級差異極大的數字做加減法:例如
1,000,000.00 + 0.1可能仍等於1,000,000.00,因為 32 位元沒有足夠的有效位數。解法是先排序再從最小值開始累加。 - 避免浮點數的相等比較:兩條不同的運算路徑得到「應該相等」的數字,實際上往往不相等。例如
0.1累加 10 次不等於1.0。
應使用容差比較函式取代
==:定義可接受的誤差範圍(如ACCEPTABLE_DELTA = 0.00001),當兩值之差的絕對值小於此閾值時視為相等。
- 預期捨入誤差(Rounding Errors):常見對策包括:
- 改用更高精度的型別(如
float改為double) - 改用二進位編碼十進制(BCD)變數,可防止許多捨入錯誤,尤其適用於金額計算
- 改用整數模擬小數:例如以「分」(cents)為單位的整數取代以「元」為單位的浮點數,再建立封裝類別隱藏實作細節
- 改用更高精度的型別(如
- 檢查語言與函式庫的支援:某些語言(如 Visual Basic)提供
Currency等專用型別,若有則應善加利用。
12.4 字元和字串#
- 避免魔術字元與魔術字串:如同魔術數字,不應在程式中散布未解釋的字面字元或字串。應使用具名常數或集中管理的字串資源。
- 注意差一錯誤(Off-by-One Errors):字串的索引操作如同陣列,須留意邊界。
- 及早決定 Unicode 策略:Java 中所有字串皆為 Unicode;C/C++ 需額外處理。應儘早決定是否及何時使用 Unicode。
- 及早決定國際化/在地化策略:考量是否將所有字串存放於外部資源檔,以及是建立各語言的獨立版本還是在執行期決定語言。
- 統一字串型別的轉換策略:在程式內部保持單一格式,僅在輸入輸出時才轉換。
C 語言的字串注意事項#
C++ 的
std::string已消除大部分 C 字串陷阱,但直接使用 C 字串時仍需注意以下事項。
- 區分字串指標與字元陣列:字串操作應使用
strcmp()、strcpy()等函式,而非=運算子。 - 宣告長度為
CONSTANT+1:為 null 結尾字元保留空間,統一以CONSTANT操作字串長度。 - 初始化為 null:宣告時初始化為
{ 0 },或使用calloc()取代malloc()。 - 優先使用字元陣列而非指標,並使用
strncpy()取代strcpy()以限制最大長度。
12.5 布林變數#
布林變數不易誤用,且善加利用能讓程式更清晰。
- 用布林變數記錄程式意圖:將複雜的布林運算式指派給有意義名稱的變數,使
if測試一目了然。例如將(elementIndex < 0) || (MAX_ELEMENTS < elementIndex)指派給finished,比直接嵌入if條件清楚得多。 - 用布林變數簡化複雜測試:將多層條件拆解為數個布林變數,每個代表一個獨立的邏輯概念,可大幅降低閱讀者的心智負擔。
- 必要時自行定義布林型別:若語言沒有內建布林型別(如 C),可用
typedef int BOOLEAN;或enum Boolean { True=1, False=(!True) };自行建立。
12.6 列舉型別#
列舉型別(Enumerated Types) 允許以英文單字描述一組物件的每個成員,是取代「1 代表紅色、2 代表綠色」這類做法的強大替代方案。
| 使用建議 | 說明 |
|---|---|
| 提高可讀性 | if chosenColor = Color_Red 遠比 if chosenColor = 1 清楚,用於函式參數時效果尤其顯著 |
| 提高可靠性 | 編譯器能進行更嚴格的型別檢查,防止 color = Country_England 這類無意義的賦值 |
| 提高可修改性 | 新增列舉值只需修改型別定義並重新編譯 |
| 作為布林的替代方案 | 當 true/false 不夠表達語意時(如需區分 Status_Success、Status_Warning、Status_FatalError),列舉更適合 |
| 檢查無效值 | 在 if 或 case 中使用 else 子句捕捉無效值 |
| 定義 First 和 Last 項目 | 使用 Country_First 和 Country_Last 配合明確數值,方便迴圈遍歷 |
| 保留第一個項目為無效值 | 將映射到 0 的第一個元素設為 InvalidFirst,有助於捕捉未正確初始化的變數 |
對列舉元素指派明確數值時要小心:若值不連續(如 1, 2, 4, 8),迴圈遍歷時會經過無效值(3, 5, 6, 7)。
- 若語言不支援列舉型別:可用類別(class)或全域變數模擬,例如 Java 中建立私有建構子的類別搭配
public static final成員,既可讀又型別安全。
12.7 具名常數#
具名常數(Named Constants) 是值一旦指派就不可變更的變數,用名稱取代數字來參照固定數量(如 MAXIMUM_EMPLOYEES 取代 1000),實現程式的「參數化」與單點控制(Single-Point Control)。
- 在資料宣告中使用具名常數:例如用
LOCAL_NUMBER_LENGTH描述電話號碼長度,而非字面值7。當長度改變時只需修改一處。 - 避免字面值,即使看似「安全」:
For i = 1 To 12中的12雖然「一年不太可能變成 13 個月」,但用NUM_MONTHS_IN_YEAR能消除任何疑慮。更進一步可用列舉型別Month_January To Month_December。
一致使用具名常數:在某處用具名常數、另一處用字面值代表同一實體,是極為危險的做法。修改常數定義時會遺漏硬編碼的字面值,導致隱蔽的缺陷。
- 若語言不支援具名常數:以適當作用域的變數或類別模擬,優先順序為區域 > 類別 > 全域。
12.8 陣列#
| 使用建議 | 說明 |
|---|---|
| 確保索引在陣列邊界內 | 陣列的所有問題歸根結底都與隨機存取有關,最常見的就是越界存取 |
| 考慮使用容器取代陣列 | 集合、堆疊、佇列等循序存取結構較不易出錯,研究顯示能提高軟體可靠性 |
| 檢查陣列端點 | 第一個元素、最後一個元素是否正確存取?有無差一錯誤? |
| 多維陣列的下標順序 | Array[i][j] 和 Array[j][i] 很容易搞混,應仔細確認 |
| 避免索引串擾 | 巢狀迴圈中容易將 Array[j] 寫成 Array[i],使用有意義的索引名稱可降低風險 |
在 C 中使用 ARRAY_LENGTH() 巨集 | #define ARRAY_LENGTH(x) (sizeof(x)/sizeof(x[0])) 可自動計算陣列長度 |
12.9 建立你自己的型別(型別別名)#
自訂型別是語言能賦予你的最強大能力之一,能讓程式更易讀、更易修改,且不需要設計新類別。
| 使用建議 | 說明 |
|---|---|
| 核心概念 | 透過 typedef(C/C++)或等效機制建立語意化型別,日後需改型別時只需修改一行 |
| 資訊隱藏 | 某些語言(如 Ada)支援將型別宣告為 private,真正隱藏底層實作。C++ 的 typedef 僅提供形式上的隱藏 |
| 使用面向問題領域的名稱 | 用 Coordinate、Currency、Age 等名稱,避免 BigInteger、LongString 這類描述電腦資料的名稱 |
| 不要重新定義預定義型別 | 自訂一個名為 Integer 的型別會造成混淆 |
| 為可攜性定義替代型別 | 例如定義 INT32 取代 int,在不同平台上可重新定義以保持一致 |
| 考慮建立類別而非 typedef | 當需要更多控制與彈性時,類別是更好的選擇 |
要點#
- 每種基本資料型別都有各自的使用規則與陷阱,應善用檢核清單確認常見問題。
- 自訂型別能讓程式更易修改且更具自我文件性。
- 當使用
typedef或等效機制建立簡單型別時,應考慮是否需要建立完整的類別來獲得更好的封裝。