在頭文件 (`.h`) 中定義 (定義 = 分配儲存空間) 全域變數,會導致多檔案包含時出現連結器錯誤 `duplicate symbol`。本文解析原因、展示預處理過程、說明 `include guard` 為何失效,並給出正確的「宣告/定義分離」模式。
/* t1.h */
#ifndef T1_H
#define T1_H
int a = 0; /* 定義!分配儲存空間 */
#endif
/* t1.c */ #include "t1.h" #include "t2.h" int main(){ return 0; }
/* t2.h */ #include "t1.h"
/* t2.c */ #include "t2.h"
編譯 `t1.c`、`t2.c` 各產生 `.o`,連結時發現兩個 `a` 定義 → `ld: duplicate symbol`。
`#ifndef` 僅在單一編譯單元 (translation unit = 一個 `.c` + 其包含的所有 `.h`) 內防止重複展開。
`t1.c` 編譯單元展開一次 `int a = 0;` → `t1.o` 含定義。
`t2.c` 編譯單元獨立展開一次 `int a = 0;` → `t2.o` 含定義。
連結器合併兩 `.o` 時發現兩個同名定義 → 錯誤。
$ gcc -E t1.c
...
int a = 0; /* 來自 t1.h */
$ gcc -E t2.c
...
int a = 0; /* 來自 t1.h → t2.h → t2.c */
兩個編譯單元各自完整展開了定義。
/* globals.h */
#ifndef GLOBALS_H
#define GLOBALS_H
extern int g_counter; /* 宣告:告知編譯器型別與名稱,不分配空間 */
extern const char *g_version;
#endif
/* globals.c */
#include "globals.h"
int g_counter = 0; /* 定義:分配空間、初始化 */
const char *g_version = "1.0";
/* other.c */
#include "globals.h"
void foo() { g_counter++; }
若專案架構強制要求頭文件定義 (如某些嵌入式專案),可採用:
static:每編譯單元擁有獨立副本 (不共享),適合檔案內部常數。inline (C99) / `static inline`:函數定義在頭文件,避免重複符號。#ifdef DEFINE_GLOBALS
#define EXTERN
#else
#define EXTERN extern
#endif
EXTERN int global1;
僅在一個 `.c` 定義 `DEFINE_GLOBALS` 再包含此頭文件。頭文件只放宣告 (`extern`)、定義放 `.c`** 是 C 語言模組化的鐵律。理解「編譯單元獨立性」與「連結器合併符號」機制,即可徹底避開 `duplicate symbol` 坑。