已定義符號與未定義符號(defined vs undefined symbols)
你的程式碼跨檔共享的每個名稱,像函式或全域變數,從連結器的角度看,不是被提供就是被需要。已定義符號是其實際本體存在於這個目的檔中的符號:函式真正的機器指令在這裡,或變數的儲存空間在這裡。未定義符號(又稱 external)是這個檔案使用卻不包含的名稱:一個保證那東西存在於某處的承諾,並留有一個空白,待找到後填入它的位址。
具體來說,當你寫了 add 函式並把它的程式碼放在 utils.c 裡,utils.o 就有一個名為 add 的已定義符號。當 main.c 只是呼叫 add 而沒定義它,main.o 就有一個名為 add 的未定義符號。編譯器單就此事完全沒意見,因為每個編譯單元都是孤立編譯的,而一個宣告(標頭檔中的函式原型)就足以承諾 add 存在。把需要的檔案與提供的檔案實際配對的工作,被延後到連結器。
這種切分是各別檔案如何協作的核心。連結本質上就是把每個未定義符號與恰好一個同名的已定義符號配對、然後用真實位址填補空白(重定位)的工作。由此直接導出兩個可預料的失敗:若某個未定義符號在任何地方都沒有相符的定義,你會得到「undefined reference」;若某符號同時在兩處被定義,連結器無法選擇,會回報「multiple definition」。初學者常見的失誤是把變數的定義(而不只是宣告)放進好幾個檔案都引入的標頭檔,這恰恰製造了那種重複定義。
// utils.c 已定義:add 的真正程式碼在這裡 int add(int a, int b) { return a + b; } // main.c 未定義:main 想要 add,但不提供它 int add(int, int); // 一個宣告(承諾),不是定義 int main(void) { return add(2, 3); }
main.o 留下 add 未定義;utils.o 定義它;連結器把兩者配對。
宣告(承諾某名稱存在)不是定義(那個實際的東西);把真正的變數定義放進被許多檔案引入的標頭檔,會在連結時造成「multiple definition」。