經典場景:網絡封包、文件格式、共享記憶體中,結構體頭部固定,尾部跟隨**可變長度**的數據區。C89 無標準寫法,常見非標準做法:
char data[0];(GCC/Clang/MSVC 擴展支援)char data[1];(舊代碼常見,分配時 -1)C99 標準化了**柔性數組成員**(Flexible Array Member,FAM),語法:type name[];(無長度),**必須為結構體最後一個成員**,且結構體至少包含一個其他成員。
// 標準 C99 柔性數組
struct Packet {
uint16_t id;
uint32_t timestamp;
uint8_t payload[]; // 柔性數組成員,無長度
};
// 記憶體佈局
// +----------------+----------------+----------------+----------+
// | id (2) | timestamp (4) | padding (2) | payload |
// +----------------+----------------+----------------+----------+
// ↑ struct Packet 起始地址 ↑ payload 起始(緊跟結構體)
關鍵規則:
sizeof(struct Packet) **不包含**柔性數組大小(等同於無該成員)。struct Packet p; 非法),只能動態分配或作為尾部緩衝區。malloc(sizeof(struct Packet) + data_len)。p->payload[i] 完全等價於普通數組。#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
struct Message {
uint32_t seq;
uint16_t type;
char data[]; // 柔性數組
};
void send_message(uint32_t seq, uint16_t type, const char *buf, size_t len) {
// 1. 分配:結構體大小 + 數據長度
struct Message *msg = malloc(sizeof(struct Message) + len);
if (!msg) return; // 處理 OOM
// 2. 填充頭部
msg->seq = seq;
msg->type = type;
// 3. 複製數據到柔性數組
memcpy(msg->data, buf, len);
// 4. 發送(伪代碼)
// socket_send(msg, sizeof(struct Message) + len);
// 5. 釋放
free(msg);
}
int main() {
char payload[] = "Hello, Flexible Array!";
send_message(1, 100, payload, strlen(payload) + 1); // 含結束符
return 0;
}
| 特性 | char data[0]; (GNU 擴展) | char data[]; (C99 FAM) |
|---|---|---|
| 標準合規性 | 非標準(C89/C99/C11 均不允許) | **C99 標準**,C11/C18 繼承 |
| sizeof 結構體 | 通常不包含數組(編譯器相關) | **標準保證不包含** |
| 定義結構體變量 | 部分編譯器允許(大小不含數組) | **禁止**(編譯錯誤) |
| 多柔性數組 | 不允許 | **不允許**(僅最後一個成員) |
| 嵌套結構體 | 行為未定義 | 外層結構體不可包含 FAM 結構體作為非最後成員 |
| 對齊 | 編譯器自行決定 | 柔性數組按其類型對齊,前面隱式填充 |
struct Header { uint32_t magic; uint16_t count; };
struct Item { uint16_t id; uint32_t len; char val[]; };
// 分配整塊:Header + count * (Item + val_len)
// 訪問第 i 個 Item:需手動指針運算,不再依賴編譯器佈局
std::vector<T>、std::string、std::unique_ptr<T[]> 管理變長數據。char data[1]; 並配合 #pragma pack,或用 std::byte + placement new 手動構造。// 反序列化:直接將網絡緩衝區強轉
struct Packet *pkt = (struct Packet *)buffer;
// 直接訪問 pkt->payload,無需 memcpy
// 注意:網絡序轉主機序、對齊檢查、緩衝區邊界檢查必不可少
struct Packet pkt; → **編譯錯誤**(C99 禁止)。malloc(sizeof(struct Packet)) → pkt->data[0] 越界寫入。struct Packet pkts[10]; → **非法**,FAM 結構體不能出現在數組中。struct Outer { int a; struct Packet p; int b; }; // 錯誤:p 非最後成員
struct Outer2 { int a; struct Packet p; }; // 合法,但 Outer2 自身變為 FAM 類型
uint64_t 時,結構體尾部會有隱式填充以滿足 8 字節對齊,malloc 需考慮(通常 sizeof 已包含填充)。C99 **柔性數組成員(type name[];)** 是「結構體尾部變長數據」的**標準、可移植、零開銷**解決方案。徹底取代 data[0]、data[1] 等非標準寫法。核心口訣:**最後一個成員、無長度、sizeof 不含、動態分配、指針訪問**。掌握它,是系統級 C 程式設計的必修課。