許多 VC++ 使用者遇過 LNK2005: symbol already defined 和 LNK1169: one or more multiply defined symbols found 鏈接錯誤,常發生於使用第三方庫時。本文從編譯鏈接原理出發,徹底解析成因與解決之道。
.obj)。編譯器將全域符號分為強/弱:
extern) 非強非弱。鏈接器處理多重定義規則:
故多目標文件不可重複定義同名函數/已初始化全域變數,否則必現 LNK2005/1169。但有時自查無重定義卻報錯,何解?
ANSI C 標準函數分散於多目標文件,直接提供極不便。C 提供靜態庫 (.lib) 打包機制:鏈接時指定庫文件,鏈接器自動尋找應用程式真正用到的目標模組並拷貝參與建構。
鏈接器按命令列順序左至右掃描,維護三集合:
初始 E、U、D 均空。處理每個輸入文件 f:
f 為目標文件 → 加入 E,其未解析符號入 U,已定義符號入 D,處理下一個。f 為庫文件 → 嘗試用 U 中符號匹配 f 內各目標模組。若模組 m 定義了 U 中符號 → m 加入 E,其未解析符號入 U,已定義符號入 D。重複至達不動點 (U、D 不再變化)。未加入 E 的模組丟棄,處理下一輸入文件。VC 編譯器 cl.exe 相關選項:
| 選項 | 對應庫 | 定義宏 |
|---|---|---|
| /ML (預設) | 單執行緒靜態 libc.lib | — |
| /MT | 多執行緒靜態 libcmt.lib | _MT |
| /MD | 多執行緒 DLL msvcrt.lib (DLL: msvcrt.dll) | _MT, _DLL |
| /MLd | 調試版單執行緒靜態 libcd.lib | _DEBUG |
| /MTd | 調試版多執行緒靜態 libcmtd.lib | _MT, _DEBUG |
| /MDd | 調試版多執行緒 DLL msvcrtd.lib (DLL: msvcrtd.dll) | _MT, _DLL, _DEBUG |
編譯器在目標文件專門區域存放「預設庫」資訊,指導鏈接器掃描時加入該庫。實驗驗證:
/* main.c */ int main() { return 0; }
cl /c main.c # 相當於 cl /c /ML main.c
// 用文本編輯器開 main.obj 搜尋 "defaultlib"
# 看到:"-defaultlib:LIBC -defaultlib:OLDNAMES"
目標文件指定單執行緒靜態庫 libc.lib 與相容庫 oldnames.lib。
/* mylib.c */
#include <stdio.h>
void foo() { printf("%s", "I am from mylib!\n"); }
cl /c /MLd mylib.c # 指定 libcd.lib
lib /OUT:my.lib mylib.obj # 打包庫
/* main.c */
void foo();
int main() { foo(); return 0; }
cl /c main.c # 預設 libc.lib
link main.obj my.lib # 成功,僅警告 LNK4098
分析:掃描 main.obj → E={main.obj}, U={foo}, D={main}, 輸入列表加 libc.lib。掃描 my.lib → foo 解析,printf 入 U,輸入列表加 libcd.lib。掃描 libc.lib → printf 解析,U 空。掃描 libcd.lib → U 空,丟棄所有模組。成功。
/* mylib.c 修改版 */
#include <stdlib.h>
void foo() { _malloc_dbg(1, _NORMAL_BLOCK, __FILE__, __LINE__); } // 非 ANSI,需 _DEBUG
cl /c /MLd mylib.c
lib /OUT:my.lib mylib.obj
link main.obj my.lib # 大量 LNK2005 + LNK1169
分析:_malloc_dbg 僅存在調試版庫。掃描 libc.lib 時無模組定義它,但 crt0.obj 等初始化模組仍加入 E,帶入 malloc 等符號入 D。最後掃描 libcd.lib → dbgheap.obj 定義 _malloc_dbg 加入 E,同時定義 malloc 等同名符號 → 與 D 中符號衝突 → 鏈接器中斷報錯。
/MLd 或全用 /ML) 重編打包。dumpbin /DIRECTIVES my.lib 查看庫模組指定的預設庫 (輸出中 -defaultlib:XXXX),再用匹配選項編譯自有程式。核心原則:確保所有參與鏈接的目標文件/庫指定一致的預設運行時庫版本。