替你設定的東西命名:指定初始化子
到現在你已經用舊方法建過不少結構了:在一對大括號裡按宣告順序列出值,然後祈禱自己記得哪個位置對應哪個欄位。對一個像 `struct point { int x; int y; };` 的結構而言,那就是 `struct point p = { 3, 4 };`,而你就只能「記住」3 落在 x、4 落在 y。兩個欄位時還好。但有十個欄位、其中還有幾個是同型別時,依位置的初始化就成了一個安靜的陷阱:把兩個值對調,編譯器一聲不吭,因為兩個位置都接受 int。
C99 的指定初始化子讓你用一個前置的點來命名要設定的欄位:`struct point p = { .x = 3, .y = 4 };`。現在這個指派在呼叫點就寫得明明白白,於是讀者(與編譯器)能看清每個值該去哪——而且你可以照任何喜歡的順序撰寫。同樣的想法用 `[索引]` 也適用於陣列:`int days[7] = { [0] = 1, [6] = 1 };` 設定週末那兩個位置,其餘的不動。這不是化妝。把意思說清楚的程式碼,是你能「看見」其臭蟲的程式碼。
有一個行為你應該「刻意」地依賴:任何你沒提到的欄位都會被零初始化,就如同你寫了 `= {0}` 一樣。所以 `struct config c = { .port = 8080 };` 會把其他每個成員都設為零,而非垃圾值。正是這條規則讓指定初始化子能抵抗變更。明年往結構加一個新欄位,每一個既有的 `{ .port = ... }` 都還能編譯,且新欄位是個乾淨的零,而非一顆未初始化的地雷——你到現在已經見過夠多未初始化變數,懂得這值多少。
沒有名字的物件:複合常值
指定初始化子描述的是「放進」物件裡的東西。複合常值則讓你就在需要的地方變出物件本身,不必先為它宣告一個具名變數。語法是括號包住一個型別、後接大括號初始化子:`(struct point){ .x = 3, .y = 4 }`。那個運算式「就是」一個物件——一個真實的物件,有儲存空間、有位址——所以你可以把它直接傳給函式:`draw((struct point){ .x = 3, .y = 4 });`。沒有臨時變數、沒有第二行。
讓複合常值不只是簡寫的關鍵細節,在於結果是個真正的左值(lvalue)——它有一個你能取得的位址。`int *q = &(int){ 42 };` 是一個指向持有 42 的 int 的合法指標,憑空具現出來。在大括號內你可以用上一節同樣的指定初始化子語法,於是這兩個特性扣在一起:`&(struct config){ .port = 8080 }` 給你一個指向完整成形、大半為零的 config 的指標,全程不必為它命名。這是把小型固定物件以位址傳遞的優雅方式。
一個標頭,一條可變的尾巴:彈性陣列成員
現在來看一個解決具體形狀問題的特性:你想要一塊記憶體,容納一個小型標頭、後接數量可變的元素——一個長度,然後是那些位元組;一個封包標頭,然後是它的酬載。天真的答案是兩次配置,一個標頭結構加上它所指向的另一個緩衝區,配上兩次 `free()` 與一個額外的洩漏或懸置機會——忘了第二次 free() 就成了記憶體洩漏。C99 正式化的彈性陣列成員,則把兩者打包進單一次配置。
你把它宣告成一個「沒有大小」的陣列,而且它必須是結構的最後一個成員:`struct buf { size_t len; char data[]; };`。那個空的 `data[]` 對 `sizeof(struct buf)` 毫無貢獻——大小是把陣列當作長度為零來計算的。訣竅在於你怎麼配置:你向 malloc() 索取標頭「加上」你想要的任意數量元素,一次呼叫搞定。`malloc(sizeof *b + n)` 給你一個結構,其 `data` 陣列你可以合法地從索引 0 用到 n-1,而單一次 free() 就釋放整個東西。
struct buf { size_t len; char data[]; }; // data[] adds 0 to sizeof
size_t n = 100;
struct buf *b = malloc(sizeof *b + n); // header + 100 bytes, ONE alloc
if (b == NULL) { return -1; } // always check malloc
b->len = n;
b->data[0] = 'H'; // indices 0..n-1 are valid
/* ... use b->data ... */
free(b); // one free releases both parts誠實的提醒值得直說。彈性成員必須在最後,且結構必須至少還有一個其他成員。你不能做這種結構的「陣列」,也不能把它嵌入另一個結構內——編譯器會不知道每一個有多大。而且你必須自己超額配置:一個純區域變數 `struct buf b;` 給 `data` 的可用元素恰為「零」,所以寫 `b.data[0]` 就是越界存取與未定義行為。這個特性給你的是打包;那段大小計算與 malloc() 的檢查,仍得由你自己做對。
一個實作範例,三者合一
這些特性是朋友,一個小巧的訊息建構器能展示三者朝同一方向使力。我們想要一個函式,它配置一則訊息——一個標頭加上一段可變長的位元組——用具名欄位填好標頭,然後回傳它。走過一遍,留意每個特性各自的價值在哪。
- 宣告型別,把彈性陣列成員放在最後:`struct msg { int kind; size_t len; char body[]; };`。`body[]` 是那條可變尾巴;`sizeof(struct msg)` 只計入 kind 與 len。
- 一次呼叫配置標頭加酬載,然後檢查它:`struct msg *m = malloc(sizeof *m + n); if (m == NULL) return NULL;`——絕不略過檢查 malloc 是否回傳 NULL。
- 直接設定具名欄位:`m->kind = 7; m->len = n;`。(你不能對已配置好的 `*m` 用大括號指定初始化子,但具名欄位的清晰度延續到這些指派上。)
- 透過 `m->body` 填入那條尾巴,索引 0 到 n-1,然後把這單一區塊交回去。呼叫者用一次 free() 釋放它,標頭與酬載一同釋放。
複合常值在哪裡派上用場?在「呼叫點」,當你要不拘禮節地傳入小型固定輸入時。假設某個輔助函式接收一份設定:`send_with((struct opts){ .retries = 3, .verbose = true }, m);`。那個 opts 物件以具名欄位就地建構,恰好活到這次呼叫結束,從不需要名字。三個特性,一個習慣:把意思說清楚、在使用處建構它、並讓配置貼合資料的形狀——一個尾巴會長到合身的結構。
你帶往下一步的東西
退一步看,貫穿的主線很清楚:這些 C99 特性每一個都消除了一類特定的錯誤。指定初始化子殺掉了被悄悄對調的位置引數、以及你忘了歸零的欄位。複合常值移除了用完即丟的具名變數——同時誠實地交給你一條必須尊重的生命期規則,因為它們的區塊範圍生命期正是懸置指標臭蟲藏身之處。彈性陣列成員把兩次配置壓成一次,並讓「標頭加酬載」的佈局成為語言所理解的一等公民。
最後一條連往本級其餘部分的線。當你為彈性陣列成員超額配置時,你稍早認識的對齊與填充仍然適用——尾巴從標頭所需的任何填充之後開始,而若元素要求更強的對齊,你可能需要 alignas 或 aligned_alloc()。而這一切都活在下一篇要精煉的同一台抽象機器裡:複合常值的生命期、指定初始化子的歸零、彈性陣列的邊界,都是標準精確陳述的規則,所以信守它們,最佳化器就回報你。本級的最後一篇——談可變引數巨集、C23 屬性、以及對抽象機器的精煉觀點——正是從這裡接續下去。