在 C 語言開發中,我們常遇到需要儲存「不定長度資料」的結構體,例如網路封包、訊息佇列、資料庫記錄等。傳統做法是在結構體中宣告一個固定大小的大陣列,但這會造成記憶體浪費或長度不足的風險。C99 標準引入了「柔性數組成員」,但在此之前,char data[0](或 char data[1])是實現變長結構體的經典技巧,至今仍廣泛存在於 Linux 核心、嵌入式系統及高效能網路程式庫中。
理解 data[0] 之前,必須先掌握結構體的記憶體佈局規則。編譯器為了 CPU 存取效率,會依據成員型別的對齊需求插入填充位元組:
常見型別在 32/64 位元系統的對齊值:
| 型別 | 32-bit x86 | 64-bit Linux |
|---|---|---|
| char | 1 | 1 |
| short | 2 | 2 |
| int / float | 4 | 4 |
| double | Win:8 / Linux:4 | 8 |
| pointer | 4 | 8 |
#include <stdio.h>
struct s1 {
char ch;
char *ptr;
union {
short a, b;
unsigned int c:2, d:1;
};
struct s1 *next;
};
int main() { printf("%zu\n", sizeof(struct s1)); return 0; }
上述結構體大小會因對齊而非單純加總,實際執行可驗證。
char data[0] 的核心原理data[0] 不佔用結構體大小(sizeof 不計入),但提供一個合法的位址標記,指向結構體緊鄰的記憶體區域。配合動態記憶體分配,即可實現「結構體 + 變長資料」的一體化管理:
struct MyData {
int nLen;
char data[0]; // C99 建議寫作 char data[];data[0] 為 GNU 擴展
};
// 分配:結構體大小 + 實際資料長度
struct MyData *p = malloc(sizeof(struct MyData) + data_len);
memcpy(p->data, src_buf, data_len);
typedef struct {
int head;
int size; // 整包總長
char reply;
char data[0]; // 承載資料起始位置
} packet_t;
packet_t *cmd = malloc(sizeof(packet_t) + payload_len);
cmd->size = sizeof(packet_t) + payload_len;
memcpy(cmd->data, payload, payload_len);
sizeof vs offsetof 的陷阱一個真實專案血淚教訓:開發者試圖用 offsetof(packet, data) 計算資料長度,卻忽略了結構體對齊導致的差異。
// 錯誤示範
#define offsetof(type, member) ((size_t)&(((type *)0)->member))
size_t packet_data_len(packet_t *p) {
return p->size - offsetof(packet_t, data); // 危險!
}
問題根源:sizeof(packet_t) 包含結尾填充(例如 char reply 後補 3 位元組對齊),但 offsetof 計算的是成員相對結構體起始的「邏輯偏移」,不包含結尾填充。導致計算出的資料長度比實際多出填充位元組數,引發 memcpy 越界、Segment Fault。
正確做法:額外增加 data_len 欄位,或確保結構體以柔性數組結尾且不依賴 offsetof 計算長度。
C99 標準化了 char data[](不指定大小),語意更清晰,編譯器保證其位於結構體末尾且不佔用大小。現代程式碼建議採用:
struct MyData {
int nLen;
char data[]; // 標準柔性數組
};
GCC/Clang 同時支援 data[0] 與 data[],MSVC 需較新版本支援 C99 特性。
data[0] 是零長度陣列技巧,利用結構體記憶體佈局特性實現變長資料。malloc(sizeof(struct) + len) 分配,ptr->data 直接操作資料區。sizeof(struct) 含尾部填充,offsetof 不含,兩者不可混用於長度計算。char data[],相容性更佳。