源自酷殼網陳皓經典文章「C語言結構體裡的成員數組和指針」,這道題目以極少代碼考察對 C 語言**內存佈局、指針運算、編譯器行為、未定義行為**的極致理解。本文逐行拆解,還原大神思維路徑。
#include <stdio.h>
struct str {
int len;
char s[0]; // 零長度數組(柔性數組前身)
};
struct foo {
struct str *a;
};
int main(int argc, char** argv) {
struct foo f = {0}; // 全部成員初始化為 0 / NULL
if (f.a->s) { // 第 14 行:條件判斷
printf(f.a->s); // 第 15 行:這裡崩潰!
}
return 0;
}
運行結果: 程式在 第 15 行 printf 處崩潰,而非第 14 行 if 判斷處。
struct str 中 len 佔 4 字節,s[0] 佔 0 字節,**s 的地址等於結構體起始地址 + 4(即緊跟在 len 之後)。**char s[],不佔用結構體大小,用於動態分配變長結構體(如 malloc(sizeof(struct str) + n))。if (ptr) 僅檢查指針值是否為 NULL,**不會訪問指向的內存**;printf(ptr) 則會**嘗試訪問內存**尋找字串結尾。struct foo f = {0};
// f.a = NULL (地址 0x0)
// 結構體佈局(假設 32 位):
// f 地址: 0xXXXX
// f.a 地址: 0xXXXX, 值: 0x0
if (f.a->s)f.a 值為 0x0 (NULL)。->s 相當於 (*(f.a)).s。編譯器計算 s 相對於 struct str 的偏移量:offsetof(struct str, s) = 4。f.a + 4 = 0x0 + 4 = 0x4。**這只是一個地址值計算,不解引用,不訪問內存!**if (0x4) 條件成立(非 NULL),進入 if 體。printf(f.a->s)0x4。printf 要求 const char *,將 0x4 當作字串首地址。0x4 讀取內存,尋找 '\0'。0x4 屬於作業系統保留的低位內存區(通常前 64KB 不可訪問),觸發**頁錯誤 -> 段錯誤 -> 程式崩潰**。(gdb) p &f
$1 = (struct foo *) 0xbffff180
(gdb) p &f.a
$2 = (struct str **) 0xbffff180
(gdb) p f.a
$3 = (struct str *) 0x0
(gdb) p &f.a->s // 計算 s 的地址
$4 = (char *) 0x4 // 0x0 + 偏移 4
(gdb) p f.a->s
Cannot access memory at address 0x4 // 訪問崩潰
| 維度 | 初學者思維 | 大神思維 |
|---|---|---|
| 結構體 | 抽象容器 | **內存塊 + 偏移量映射表** |
| 指針運算 | 魔法符號 | **地址算術 + 類型解釋** |
-> 操作 | 取值 | **基址 + 偏移 -> 目標地址**(是否解引用取決於上下文) |
| 條件判斷 | 檢查內容 | **檢查指針值(地址)本身** |
| 崩潰定位 | 看報錯行號 | **區分「地址計算」與「內存訪問」邊界** |
零長度數組 s[0] 是 GCC 擴展,C99 標準化為柔性數組成員:
struct str {
int len;
char s[]; // 標準寫法,無大小
};
正確使用姿勢:
// 動態分配
struct str *p = malloc(sizeof(struct str) + 100); // 額外 100 字節給 s
p->len = 100;
strcpy(p->s, "safe string"); // 安全訪問
free(p);
這道題的精髓在於:**f.a->s 在條件判斷中只是「計算地址 0x4」,在 printf 中才發起「訪問地址 0x4」**。大神之所以強,是因為他們腦中建立了「源碼 -> AST -> IR -> 機器碼 -> 內存佈局 -> 硬體行為」的完整映射鏈。對 C 語言工程師而言,內存模型是通往大神之路的唯一基石。