名稱修飾與 ABI 穩定性(name mangling and ABI stability)
C++ 允許你多載函式——好幾個函式可以共用 max 這個名字卻接受不同的引數型別。然而連結器處理的是扁平的符號名稱,沒有型別或多載的概念;它只需要每個函式有一個唯一的名字。名稱修飾(name mangling)是編譯器彌合這道鴻溝的方法:它把函式的命名空間、名稱與完整的引數型別編碼進單一個修飾過的符號,於是 max(int) 與 max(double) 變成連結器能區分的兩個相異、唯一的符號。
具體而言,像 int ns::max(int, int) 這樣的函式可能被修飾成類似 _ZN2ns3maxEii——對命名空間 ns、名稱 max 與兩個 int 參數的編碼。這正是讓多載與命名空間在連結期運作的原因,也是為什麼連結器錯誤或回溯追蹤裡的 C++ 名稱看起來像雜訊(c++filt 之類的工具能把它反修飾回可讀形式)。要從 C++ 原封不動地呼叫 C 函式,你把它們包在 extern "C" 裡,這告訴編譯器使用樸素、未修飾的 C 名稱——這正是 C++ 程式碼連結 C 函式庫的方式。ABI,即應用二進位介面,是編譯後程式碼在二進位層級如何互通的更廣泛契約:引數如何在暫存器中傳遞、堆疊如何佈局、物件與 vtable 如何排列、以及名稱如何修飾。
為何重要:ABI 穩定性是這樣的承諾:分開編譯的程式碼——你的程式與一個預編譯的共享函式庫,或由不同編譯器版本建構的兩半——會在這些二進位慣例上達成一致,並正確地連結與執行。誠實的難處是真實且一再出現的:某平台上的 C ABI 以穩定數十年著稱,但 C++ ABI 脆弱得多——改變一個類別的佈局(增加成員、改變繼承),或混用以不相容編譯器或標準函式庫版本建構的物件,都可能在執行期無聲地破壞記憶體,卻沒有任何編譯錯誤。修飾方案在不同編譯器間也各異,這是為什麼擁有簡單穩定 ABI 的 C,即便在 C++ 程式之間,仍是函式庫邊界的通用語的原因之一。
extern "C" int add(int, int); // 抑制 C++ 名稱修飾,使符號就是樸素的「add」,可連結 C 函式庫或被 C 呼叫
extern "C" 給函式一個未修飾的 C 符號名稱,正是讓 C++ 與 C 彼此連結的橋樑。
ABI 不匹配特別惡劣,因為它通常編譯與連結都沒有錯誤,卻在執行期破壞記憶體:混用兩個對某類別佈局意見相左的目的檔是未定義行為,而非被診斷出的錯誤。這正是為什麼跨二進位邊界混用標準函式庫或編譯器版本如此冒險。