# How to C in 2016 這篇翻譯注重語法的部分,工具的使用細節省略不少。 有興趣深究工具設定的話移駕原文:https://matt.sh/howto-c # 前置作業 C99標準 (C99意指「1999年版的C語言標準」。C11則是指「2011年版的C語言標準」,所以11比99新) - clang, default - clang預設使用一個擴充版的C11 (GNU C11 mode),所以使用現代化功能不需要額外的選項。 - 如果你想用標準版的C11,你需要指定參數`-std=c11`。如果你想用標準版C99,使用`-std=c99`。 - clang編譯原始碼的速度比gcc快 - gcc需要明確指定`-std=c99`或`-std=c11` - gcc建置原始碼比clang慢,但*有時候*能產生較快的程式碼。效能比較與回歸測試都重要。 - gcc 5 預設使用GNU C11 mode (與clang相同),但如果你想要標準版的C11或C99,還是要指定`-std=c99`或`-std=c11` 最佳化 - -O2, -O3 - 通常你想要`-O2`,但偶爾想要`-O3`。在這兩個層級下測試(並跨編譯器)然後保留性能最好的二進位檔。 - -Os - 如果你在意的是快取效率那就用`-Os` 警告 - `-Wall -Wextra -pedantic` - 比較新的編譯器已經改用`-Wpedantic`取代`-pedantic`,但保持向下相容還是接受舊的。 - 測試期間在所有平台上都應該加入`-Werror`和`-Wshadow` - 因為不同的平台、編譯器與函式庫會產生不同的警告,在部署程式碼時使用`-Werror`可能會有點棘手。你大概不會想殺死使用者的整個建置,只因為他的GCC版本在你未曾見過的平台上會產生全新且美妙的抱怨。 - 額外的神奇選項`-Wstrict-overflow` `-fno-strict-aliasing` - 指定`-fno-strict-aliasing`,或確保只以與物件相同型別的方式存取他們。因為許多既存的C語言程式碼常常有跨型別的別名(alias),在你無法完全掌控所有原始碼時使用`-fno-strict-aliasing`是一個較為保險的作法。 - 截至目前為止,Clang會對某些合法的語法做出警告,所以你應該加入`-Wno-missing-field-initializers` - GCC在4.7.0版已經修正了這個不必要的警告 建置 - 編譯單位 - 建置C專案最常用的方式是將每份原始碼拆解成一個個obj檔,最後再link起來。這程序對漸進式開發(incremental development)非常有用,但對性能最佳化並非最優解。你的編譯器無法偵測到潛在的跨檔案最佳化。 - 連結期最佳化 Link Time Optimization (LTO) - LTO解決了源碼分析與跨編譯單位最佳化的問題。藉由在obj檔附註中介碼,需要源碼的最佳化就能在連結期跨過編譯單位了。 - LTO會顯著減慢連結速度,如果你的專案含有複數個不互相依賴的target (.a, .so, .dylib, 測試用的可執行檔,應用程式),`make -j`會有用。 - 截至2016年,clang與gcc的發行版都支援LTO,只要在obj檔編譯與最終連結時加上`-flto`命令列選項。 - LTO仍需小心使用。有時候若你的程式存在沒有被直接使用,而是供外部函式庫使用的程式碼,LTO會認為這些程式碼因為沒用到或無法觸及而不需要出現在最終編譯結果中。 機器架構 - `-march=native` - 給予編譯器使用你CPU所有功能的權限 - 再次聲明,效能測試與回歸測試都很重要(然後比較多個編譯器與不同版本的結果)。確保任何啟用的最佳化不會有不良的副作用。 - `-msse2` and `-msse4.2` may be useful if you need to target not-your-build-machine features. *譯註:這個建議也給太細了吧!? 這簡單來說因為`native`會按照你的CPU決定使用的指令集,但如果執行檔要丟到的機器上不見得支援,或者就是有該版本的SSE,這時就得手動設定。然而這個建議太進階了而且很多程式根本不需要SIMD,真碰到這種需求再來研究吧。* # 撰寫程式碼 ## 型別 如果你發現你還在將 `char` `int` `short` `long` `unsinged` 輸入進你的新程式碼,就代表你做錯啦。 對於現代化的程式,你應該`#include <stdint.h>`,然後使用*標準型別*。(請搜尋stdint.h以獲取更多細節) 常用的標準型別有以下: - `int8_t`, `int16_t`, `int32_t`, `int64_t` —有號整數 - `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` —無號整數 - `float` —標準32位元浮點數 - `double` —標準64位元浮點數 注意:我們不再使用`char`了。`char`其實在C語言裡被錯誤命名然後誤用。 開發者常常濫用`char`代表「位元組」,即便在進行無號位元組操作。使用`uint8_t`會更加清晰地表示一個無號八位元組數值,以及`uint8_t *`表示一系列的無號八位元組數值。 ### 特殊標準型別 除了標準的固定位寬型別(例如`uint16_t` `int32_t`),我們還有 **fast** 與 **least** 型別,一樣定義在stdint.h。 Fast型別: - `int_fast8_t`, `int_fast16_t`, `int_fast32_t`, `int_fast64_t` —有號整數 - `uint_fast8_t`, `uint_fast16_t`, `uint_fast32_t`, `uint_fast64_t` —無號整數 Fast型別不保證型別具體有幾個位元,但保證有`x`位元的最小值。如果在目標平台上使用較大位寬的型別能有更好的支援時,Fast型別就會使用之。 例如在某些64位元的系統上,當你要求`uint_fast16_t`,你實際會得到`uint64_t`。因為符合字組(word)大小的操作會比以32位元的一半操作來得更快。 然而不是所有系統都遵循Fast型別指引,例如OS X的Fast型別就只定義成跟他們字面上的大小一樣。 Fast型別可以用作自我說明的程式碼。如果你知道你的計數器只需要16位元,但你傾向使用64位元整數,因為在你的平台上會更快,那麼`uint_fast16_t`就有用了。在64位元Linux平台,`uint_fast16_t`給你快速的64位元整數,同時在程式碼的層級上保持一個「在此只需要16位元」的行內文件。 Fast型別需要注意一點:它會影響某些測資。如果你需要測試儲存寬度,`uint_fast16_t`在某些平台(例如OS X)上是16位元,然而其他平台(例如Linux)是64位元。這可能會增加你所需要測試的平台數量。 Fast型別確實帶來了與`int`一樣,在跨平台時沒有標準尺寸的不確定性,但使用Fast型別你可以將不確定性限制在一個已知的安全範圍內 (計數、有邊界檢查條件的變數等等) Least型別: - `int_least8_t`, `int_least16_t`, `int_least32_t`, `int_least64_t` —有號整數 - `uint_least8_t`, `uint_least16_t`, `uint_least32_t`, `uint_least64_t` —無號整數 Least型別提供你想要的型別中最*緊湊*的位元數。 實務上Least型別就恰好定義為標準的固定位寬型別,因為這些型別就剛好提供你所要求的最小位元數。 ### 放棄`int`,或我選擇死亡? 一些讀者指出他們真的很愛`int`。我必須指出技術上*不可能*在型別大小會變動的狀況下正確地編寫程式。 也請看看inttypes.h的RATIONALE章節中寫的為何使用非固定位寬型別是不安全的。如果你真的聰明到足以概念化`int`在某些平台上是16位元,且在另外的平台上是32位元,並且你的開發自始自終都測試了所有用到`int`之處的16位元與32位元的邊界條件,那麼就盡情使用`int`吧。 至於其餘無法在撰寫Fizzbuzz時於腦海中掌控整個多層級決策樹平台規格架構的我們,可以使用固定位寬型別並自動獲得更多正確的程式碼,並有較少概念上的麻煩與測試上的負擔。 或者在規格中簡潔地說明:ISO C標準整數升階(integer promotion)規則會意外導致無聲無息的變更。 祝你好運。 ### 拒用 `char` 的例外 在2016年唯一可接受使用`char`的狀況是如果有既存的API要求使用(例如`strncat`、`printf`),或者你在初始化唯讀的字串(例如`const char *hello = "hello";`),因為字串字面值(string literal)`"hello"`的型別依舊是`char []`。 在C11我們有了原生的Unicode支援,且UTF-8字串字面值的型別依舊是`char []`,即使是包含多位元組字元:`const char *abcgrr = u8"abc😬哈哈";` ### 拒用 `int` `long` 等等的例外 如果你使用的函數依舊在用原生型別的回傳值或參數,那就照著規格用。 ### 符號性(signedness) 你不該在任何時候將`unsinged`輸入進你的程式碼。我們現在可以撰寫不含會妨礙可讀性的醜陋傳統C多詞型別的程式碼。誰會想在有了`uint64_t`後繼續用`unsigned long long int`啊? `stdint.h`的型別更加明確、意義上更加準確、更好地傳達你的意圖,而且對可讀性與用法更加緊湊。 ### 將指標當作整數 但是你會說「我需要將指標轉成`long`進行骯髒的指標運算!」 你也許會這麼說,但是你錯了。 正確的指標運算型別是`uintptr_t` (定義在stdint.h),而也好用的`ptrdiff_t`定義在stddef.h 不要用: ```c long diff = (long)ptrOld - (long)ptrNew; ``` 改用: ```c ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew; ``` 另外: ```c printf("%p is unaligned by %" PRIuPTR " bytes.\n", (void *)p, (uintptr_t)somePtr & (sizeof(void *) - 1) ); ``` ### 依存系統的型別 你繼續爭論道「我想要在32位元平台上32位元長,而且在64位元平台上64位元長!」 如果我們無視你藉由使用兩個依存平台而不同的大小*存心*引入難以理解的代碼的想法,你仍然不該想用`long`表示依存系統的型別。 在這些狀況,你應該使用`intptr_t`—能夠承載你所用的平台指標(pointer)數值的型別。 在現代32位元平台上,`intptr_t`是`int32_t` 在現代64位元平台上,`intptr_t`是`int64_t` `intptr_t`也有無號的`uintptr_t` 記錄指標位移量則有恰如其名的`ptrdiff_t`,是適合儲存指標相減數值的型別。 ### 最大數值容器 你需要一個在你系統上能承載會用到的最大整數的型別嗎? 人們傾向使用最大的已知型別,例如將較小的無號型別轉成`uint64_t`。但其實有技術上更加正確的方式保證能裝下其他數值。 任何整數的最安全容器是`intmax_t` (或者`uintmax_t`)。你可以指派或將任何有號整數轉型到`intmax_t`而不用擔心丟失精度。同理`uintmax_t`。 ### 其他型別 最廣為使用的依存系統型別是`size_t`,由stddef.h提供。 `size_t`基本上是指「能夠承載最大陣列索引值得整數」,這也意味著它能承載著你的程式中最大的記憶體位移量。 實際使用上,`size_t`是`sizeof`運算子的回傳值。 在現代的平台上`size_t`幾乎定義成與`uintptr_t`一樣,所以在32位元平台`size_t`是`uint32_t`,在64位元平台`size_t`是`uint64_t`。 也有`ssize_t`代表有號的`size_t`,用在某些函式庫中作為回傳值,以在錯誤發生時回傳`-1`。附註:`ssize_t`是POSIX標準,無法套用在Windows介面上。 所以應該在函數參數將`size_t`作為能依存任意平台的型別使用嗎?技術上`size_t`是`sizeof`的回傳值,所以任何接受表示位元組數的大小值的函數都允許使用`size_t`。 其他用法包括:`size_t`用於`malloc()`的參數、`ssize_t`是`read()`和`write()`的回傳型別(除了Windows因為沒有`ssize_t`,回傳值只是普通的`int`) ## 印出型別 你不該在印出數值時做轉型。 總是使用inttypes.h所定義的type specifier 包括但不限於: - `size_t` - `%zu` - `ssize_t` - `%zd` - `ptrdiff_t` - `%td` - 指標的值 - `%p` (在現代編譯器中會印出16進位數值) - `int64_t` - `"%" PRId64` - `uint64_t` - `"%" PRIu64` - 64位元型別應當只以`PRI[udixXo]64`風格的巨集印出 - 為什麼呢? - 在某些平台上64位元數值是`long`,但其他平台卻是`long long`。使用上述的巨集可正確地跨平台。 - 如果不使用這些格式巨集,實際上不可能指定一個正確的跨平台格式字串。因為這些型別會在你控制之外發生變化(並且記住,在印出前轉型並不安全) - `intptr_t` — `"%" PRIdPTR` - `uintptr_t` — `"%" PRIuPTR` - `intmax_t` — `"%" PRIdMAX` - `uintmax_t` — `"%" PRIuMAX` `PRI*`格式有一點要注意的是他們是巨集,且巨集是在字串外展開的。所以你不能寫成: ```c printf("Local number: %PRIdPTR\n\n", someIntPtr); ``` 而是 ```c printf("Local number: %" PRIdPTR "\n\n", someIntPtr); ``` 注意你要將`%`放在字串裡,但`PRI*`要放在字串外。最終預處理器會將相鄰的字串連接在一起。 ## C99允許變數宣告在任何地方 所以不要這麼做: ```c void test(uint8_t input) { uint32_t b; if (input > 3) { return; } b = input; } ``` 而是: ```c void test(uint8_t input) { if (input > 3) { return; } uint32_t b = input; } ``` 警告:如果你有小巧的迴圈,那麼測試你放置初始化的地方。有時候四散的宣告會導致意外的減速。對於常規的 non-fast-path代碼(在世上佔絕大多數),最好是越清楚越好,將型別宣告在初始化旁會大大提升可讀性。 ## C99允許在`for`迴圈行內宣告計數器 所以不要這麼做: ```c uint32_t i; for (i = 0; i < 10; i++) ``` 而是: ```c for (uint32_t i = 0; i < 10; i++) ``` 除非你需要在離開迴圈後保留你的計數器值。顯然這不能將計數器宣告在迴圈的範疇內。 ## 現代編譯器支援`#pragma once` 所以不要這麼做: ```c #ifndef PROJECT_HEADERNAME #define PROJECT_HEADERNAME #endif /* PROJECT_HEADERNAME */ ``` 而是: ```c #pragma once ``` `#pragma once`會告訴編譯器只要引入這個標頭檔一次就好。你不再需要三行來保護標頭檔。 這個pragma現今在各平台上[受各種編譯器支援](https://zh.wikipedia.org/wiki/Pragma_once#%E7%BC%96%E8%AF%91%E5%99%A8%E6%94%AF%E6%8C%81),因此非常推薦以它取代傳統的具名保護方式。 ## C允許靜態初始化自動配置的陣列 所以不要這麼做: ```c uint32_t numbers[64]; memset(numbers, 0, sizeof(numbers)); ``` 而是: ```c uint32_t numbers[64] = {0}; ``` ## C允許靜態初始化自動配置的結構體 所以不要這麼做: ```c struct thing { uint64_t index; uint32_t counter; }; struct thing localThing; void initThing(void) { memset(&localThing, 0, sizeof(localThing)); } ``` 而是: ```c struct thing { uint64_t index; uint32_t counter; }; struct thing localThing = {0}; ``` **特別注意**:如果你的結構體中含有padding,以`{0}`初始化的作法不會將這些padding也設為零。例如上述例子在`counter`後就有4byte的padding (64位元平台)。如果需要整個歸零,還是得用`memset()`。 如果你需要重新初始化已經配置的結構體,宣告一個全域的零值結構體供後續指派: ```c struct thing { uint64_t index; uint32_t counter; }; static const struct thing localThingNull = {0}; struct thing localThing = {.counter = 3}; localThing = localThingNull; ``` 如果你使用C99或更新的環境,你可以使用compound literals,而不必弄個零值結構體。 compound literals讓你的編譯器自動產生一個暫時的匿名結構體,然後把值複製到目標上: ```c localThing = (struct thing){0}; ``` ## C99加入的可變長度陣列 所以不要這麼做 (如果你知道陣列很小或你只是要快速測試某些東西): ```c uintmax_t arrayLength = strtoumax(argv[1], NULL, 10); void *array[]; array = malloc(sizeof(*array) * arrayLength); /* 用完時記得free(array) */ ``` 而是: ```c uintmax_t arrayLength = strtoumax(argv[1], NULL, 10); void *array[arrayLength]; /* 不需要free了 */ ``` **重要**:可變長度陣列通常使用堆疊空間,如同普通陣列。如果你不會建立一個300萬元素的普通陣列,那也請勿以此在執行期間建立300萬元素大小的可變長度陣列。這些不是可擴充的Python/Ruby list。如果你指定了一個對於你的堆疊來說太大的執行期陣列長度,你的程式會做出糟糕的事情(當掉、造成安全問題)。可變長度陣列對於小型、單一用途的狀況來說很方便,但不該在產品軟體的規模上依賴它。如果你有時需要3個元素的陣列,但其他時候會需要300萬元素的陣列,那顯然是不該使用可變長度陣列功能。 以防在實際狀況中遇到可變長度陣列,了解它是好的(或者拿來做一些簡單的一次性測試)。但它幾乎可以被視為危險的Anti-pattern,因為你會因為僅僅只是忘記檢查元素數量而搞炸你的程式,或是在某些奇怪的環境中因為沒有足夠堆疊空間的而當掉。 附註:你必須確定陣列長度在此狀況下是合理的數值(換句話說少於幾KB,在某些奇怪的平台上你的堆疊最大就只有4KB你知道嗎!!)。你不能在堆疊上配置*巨大*的陣列(幾百萬元素),但如果你知道你有個有限的數量,使用C99可變長度陣列會比用`malloc`跟堆積要空間簡單許多。 再次附註:上述範例沒有檢查使用者的輸入,所以使用者可以藉由配置超大可變長度陣列輕易殺死你的程式。有些人甚至把可變長度陣列稱作Anti-pattern,但如果你緊盯邊界,在某些狀況下可以帶來小小的勝利。 ## C99允許附註**non-overlapping**指標參數 譯註:這裡省略所有作者寫的東西,因為就是在介紹`restrict`關鍵字。 `restrict`關鍵字是少數C有C\++沒有的東西…其實有,但在C\++由各家編譯器用`__restrict__`或類似的關鍵字實現。但C\++標準確實是沒有。 `restrict`關鍵字用於指標,告訴編譯器它所修飾的指標在其生命週期內指向物件的值不會被修改,從而得以省下一些讀寫記憶體的動作實現最佳化。整體來說用處會讓人聯想到`volatile`,但揮發性相較之下常用得多了。 ## 參數型別 如果函數接受任意的輸入資料與長度,那麼就別限制參數的型別。 所以不要這麼做: ```c void processAddBytesOverflow(uint8_t *bytes, uint32_t len) { for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } } ``` 而是: ```c void processAddBytesOverflow(void *input, uint32_t len) { uint8_t *bytes = input; for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } } ``` 函數的輸入型別描述了你的代碼的*介面*,而非具體對參數做了什麼。上述程式碼的介面代表「接受一個位元組陣列與長度」,所以你不會只想限制在`uint8_t`型別的位元組流。也許你的使用者甚至想傳入一個老式的`char *`,或是其他東西。 藉由將你的輸入型別宣告為`void *`,然後重新將其轉型為在你的程式的實際型別,你就能避免使用你的函數的人思考你的函式庫內部的抽象化。 部分讀者指出這例子會有記憶體對齊(alignment)的問題,但我們一次只存取一個位元組,所以一切都沒事。如果我們將輸入轉型成更寬的型別,我們就需要注意對齊問題了。譯註:接下來這段太進階了有點失焦,絕對不是我看不懂喔啾咪 ## 回傳參數型態 C99給了我們`stdbool.h`之力,定義了`true = 1`及`false = 0` 對於成功/失敗的回傳值,函數應當回傳`true`或`false`,而不是一個`int32_t`的回傳值,手動指定`1`或`0` (或者更糟糕的 1/-1?0成功1失敗?0成功-1失敗?) 如果一個函數改變輸入參數會做到無效化的程度,不要回傳更改後的指標,你的整個API應該在會無效化輸入的地方強迫使用「指標的指標」作為參數。以「對於某些呼叫,以回傳值無效化輸入」的方式寫程式在大規模應用上太容易出錯了。 所以不要這麼做: ```c void *growthOptional(void *grow, size_t currentLen, size_t newLen) { if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /* resize success */ grow = newGrow; } else { /* resize failed, free existing and signal failure through NULL */ free(grow); grow = NULL; } } return grow; } ``` 而是: ```c /* Return value: * - 'true' if newLen > currentLen and attempted to grow * - 'true' does not signify success here, the success is still in '*_grow' * - 'false' if newLen <= currentLen */ bool growthOptional(void **_grow, size_t currentLen, size_t newLen) { void *grow = *_grow; if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /* resize success */ *_grow = newGrow; return true; } /* resize failure */ free(grow); *_grow = NULL; /* for this function, * 'true' doesn't mean success, it means 'attempted grow' */ return true; } return false; } ``` 或者這麼做更棒: ```c typedef enum growthResult { GROWTH_RESULT_SUCCESS = 1, GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY, GROWTH_RESULT_FAILURE_ALLOCATION_FAILED } growthResult; growthResult growthOptional(void **_grow, size_t currentLen, size_t newLen) { void *grow = *_grow; if (newLen > currentLen) { void *newGrow = realloc(grow, newLen); if (newGrow) { /* resize success */ *_grow = newGrow; return GROWTH_RESULT_SUCCESS; } /* resize failure, don't remove data because we can signal error */ return GROWTH_RESULT_FAILURE_ALLOCATION_FAILED; } return GROWTH_RESULT_FAILURE_GROW_NOT_NECESSARY; } ``` ## 代碼格式化 Coding Style 既非常重要,同時也毫無價值。 如果你的專案有50頁的撰寫風格指引,沒有人會幫妳的。但如果你的程式碼根本無法閱讀,更沒有人*會想*幫你。 在此提供的解法就是總是使用自動代碼格式化工具。 截至2016年,唯一能用的C代碼格式化工具是clang-format。clang-format在任何C語言格式化工具之中擁有最棒的預設值,而且仍在活躍發展中。 *譯註:接下來就在介紹clang-format的設定了,自己看原文吧* ## 可讀性 *寫作速度似乎在此開始慢了下來…* ### 註解 代碼文件中邏輯自含的部分 ### 檔案結構 盡量限制一個檔案最多1000行 (在非常糟的狀況下,1500行)。如果你的測試程式嵌在原始檔裡(測試靜態函數等等),根據需要調整一下。 ## 其他想法 ### 避免使用`malloc()` 你應該使用`calloc()`。獲取初始化為零的記憶體並沒有性能懲罰。如果你不喜歡`calloc(object count, size per object)`的參數,你也可以自己包裝`#define mycalloc(N) calloc(1, N)` 讀者在此評論了一些東西: - `calloc`在**大量**配置時*確實*會影響效能 - `calloc`在奇怪的平台上*確實*會影響效能 (例如小型嵌入式系統、遊戲主機、30年老硬體…) *譯註:嵌入式系統沒OS沒映射,哪來的全0的記憶體?連靜態變數歸零都要自己來。* - 將`calloc`包裝起來不全然是個好主意。 - 避免使用`malloc()`的一個好理由是它無法檢查整數溢位,從而導致安全風險 - `calloc()`會導致Valgrind的讀取未初始化記憶體警告失效,因為它會將記憶體自動初始化為0 這些都是好觀點,也是為何我們總是要在不同編譯器、平台、作業系統與硬體上做速度的性能測試與回歸測試。 直接使用`calloc()`的好處之一是,不像`malloc()`,它可以檢查整數溢位,因為它會將引數相乘獲得最終配置大小。 沒有建議可以適用所有狀況。但給一個完美通用的建議會搞得像是在讀一本語言的規格書。 以下是關於`calloc()`如何給你乾淨記憶體的參考,歡迎參閱這些美妙的文章: - [Benchmarking fun with calloc() and zero pages (2007)](https://blogs.fau.de/hager/archives/825) - [Copy-on-write in virtual memory management](https://en.wikipedia.org/wiki/Copy-on-write#Copy-on-write_in_virtual_memory_management) 我依然支持我的「在2016年的大部分場景中總是使用`calloc()`」建議(假定x64平台、人類規模但不含人類基因規模的資料)。任何偏離「預期」的情況都會將我們拖入 domain knoledge 的深淵,這是我們今天不打算談論的話題。 次要附註:透過 `calloc()` 傳遞給你的預先歸零的記憶體是一次性的。如果 `realloc()` 以 `calloc()` 配置的記憶體,新長出的區域就不會歸零了 *(譯註:realloc確實不保證新區域會歸零)*,而是填滿正常來說kernel會分配給你的未初始化內容。你必須手動`memset()`擴充的區域。 ### 避免使用`memset()` 當你可以靜態初始化結構體(或陣列)為0,或能以compound literals重設時,避免使用`memset()` 雖然在應付有padding的結構體時,使用`memset()`是你唯一能全部清零的辦法。 # 延伸閱讀 * [Fixed width integer types (since C99)](http://en.cppreference.com/w/c/types/integer) * 蘋果電腦的 [Making Code 64-Bit Clean](https://developer.apple.com/library/mac/documentation/Darwin/Conceptual/64bitPorting/MakingCode64-BitClean/MakingCode64-BitClean.html) * ~~[sizes of C types across architectures](https://www.securecoding.cert.org/confluence/pages/viewpage.action?pageId=4374)~~ *譯註:頁面已死* - 除非將每一行你寫的程式碼化為表格記在腦海中,否則你應該使用明確定義的整數寬度,並且永不使用char/short/int/long等內建儲存型別。 * [size_t and ptrdiff_t](http://www.viva64.com/en/a/0050/) * [Secure Coding](https://www.securecoding.cert.org/confluence/display/c/SEI+CERT+C+Coding+Standard) 如果你真的想完美地寫每個東西,只要記住他們的數千個簡單範例。 * ~~[Modern C](http://icube-icps.unistra.fr/img_auth.php/d/db/ModernC.pdf) by Jens Gustedt at Inria.~~ *譯註:頁面已死* * ~~想要深入了解C11的Unicode支援:[Understanding Character/String Literals in C/C++](http://www.dotslashzero.net/2014/08/22/understanding-characterstring-literals-in-cc/)~~ *譯註:頁面已死* # 結語 大規模撰寫正確的程式碼基本上不可能。我們需要擔心多種作業系統、執行環境、函式庫與硬體平台,甚至還沒考慮到像是記憶體或 block device 的隨機位元翻轉,以未知機率欺騙著我們。 我們能做的最好的事便是撰寫簡單、好理解的程式碼,盡量減少間接引用與無文件的魔法。 *譯註:這裡的間接引用(indirection)我不知道是不是[這個意思](https://en.wikipedia.org/wiki/Indirection)* -[Matt](mailto:matt@matt.sh) — [@mattsta](https://twitter.com/mattsta) — [☁mattsta](https://github.com/mattsta)