C++模板的編譯與連線及inline 和 static 的說明
C++的編譯是以.cpp檔案為單位進行。編譯之前存在一個預處理的過程:檔案包含,條件編譯和巨集展開。檔案包含是將include 的標頭檔案中的內容複製到.cpp檔案中。一般介面與實現的分離設計,標頭檔案中通常都是函式和類的宣告。
在編譯的過程中,如果A.cpp檔案中有函式 f() 的呼叫,但是不存在 f() 函式的定義;那麼在編譯f() 函式時,將呼叫語句編譯為外部連結的呼叫指令(call + 經過namemagling 處理之後的函式名稱),得到 A.obj檔案;在連線的過程中,會在其他 .obj 目標檔案中找到函式f()的二進位制程式碼(通過符號匯入表和符號匯出錶快速查詢)地址, 將 A.obj 中的那條外部連結指令替換為實際函式的二進位制地址(編譯之後的目標 .obj檔案都是二進位制檔案)。
對於函式,只有當發生呼叫才會被例項化,才會被編譯為二進位制程式碼。
所以如果分離編譯模板,例如A.cpp 檔案中呼叫一個函式模板 f(T t), 呼叫語句為 f(8); 因為此時在A.cpp檔案中看不到 f(T t) 的定義,所以無法例項化,所以編譯過程同上邊是一樣的,最後 A.obj 目標檔案中呼叫語句被編譯為外部連結呼叫指令,將插入實際例項化的二進位制程式碼的地址的工作,不過編譯器在編譯包含模板模板 f(T t)的定義的原始檔 B.cpp 時,由於只是定義,而沒有發生呼叫,所以將不會例項化f(T t), B.obj 目標檔案中也就得不到 f() 函式的二進位制程式碼! 所以連結器在尋找 f() 的二進位制程式碼時將發生失敗,只能給出一條連線錯誤了。
所以如果B.h檔案中有 f(T t) 的定義,在A.cpp 包含 B.h 的時候,f(T t)定義將隨著B.h 檔案中的所有內容插入到A.cpp當中,於是,在編譯時,A.cpp中的 f(T t) 函式呼叫能夠看到其定義,所以 最終A.obj檔案中將直接包含 f() 函式的二進位制程式碼!如果C.cpp中也包含了B.h, 那麼B.obj檔案中也將包含函式f() 函式的二進位制程式碼,最後連結時,聯結器負責除重。
1、而對於普通函式g(),如果介面與實現都在標頭檔案中B.h中,如果B.h被多個.cpp原始檔包含,那麼連結時將會發生函式g()重複定義的錯誤。此時可將函式g()設定為inline, 便可消除錯誤。
2、對於類的成員方法,一般也是要求介面與實現分離,將成員方法的實現放到 .cpp檔案中;如果在標頭檔案的類內部給出實現,也可以編譯通過(不是好的習慣),因為類內部自帶inline。如果在標頭檔案的類外給出成員方法的定義,必須顯示的設為inline,否則也會發生重複定義的錯誤。
3、而對於函式模板,只要所有的實現程式碼都在都檔案中,無論成員方法的實現是在類內部還是類外,都可以。
實際遇到的問題:
在都檔案A.hpp中實現:
class Temp
{
public:
template<int N>
void func(int n);
template<>
void func<1>(int n)
{
std::cout << " 1 ";
}
template<>
void func<2>(int n)
{
std::cout << " 2 ";
}
};
template<int N>
void Temp::func(int n)
{
}
然後在兩個 b.cpp 和 c.cpp原始檔中包含該標頭檔案。此時木有問題。注意我故意還將主成員模板放在內外定義。
一旦將特化版本拿到模板類的外部實現:
class Temp
{
public:
template<int N>
void func(int n)
{
}
};
template<>
void Temp::func<1>(int n)
{
std::cout << " 1 ";
}
template<>
void Temp::func<2>(int n)
{
std::cout << " 2 ";
}
將會發生特化版本重複定義的錯誤,原因就在於全特化版本已經不含模板引數,其實就是一個普通的非模板方法的,所以必須使用規則2。此時或者在上邊為兩個特化版本新增 inline 關鍵字,也可以通過。
需要注意的是,設為inline的函式不一定真的會被inline(將函式程式碼插入到呼叫處,當然這樣會造成程式碼膨脹),當前的編譯器做法都是inline失敗之後,辦證全域性只有一份函式程式碼,仍然是在連結時去重。
當然發生函式重複定義時,也可以將函式設為static,這樣保證每個編譯單元都有一份獨立的函式程式碼,仍然會造成程式碼膨脹。