Mudo C++網路庫第十章學習筆記

self發表於2018-10-24

C++編譯連結精要

  • C++語言的三大約束: 與C相容, 零開銷(zero overhead)原則, 值語義;
    • 相容C語言的編譯模型與執行模型, 也就是鎖能直接使用C語言的標頭檔案和庫;
  • 標頭檔案包含具有傳遞性, 引入不必要的依賴;
  • 標頭檔案是在編譯時使用, 動態庫檔案是在執行時使用, 二者的時間差可能帶來不匹配, 導致二進位制相容性方面的問題;

C++的編譯模型

  • C++ 繼承了單遍編譯;
    • 編譯器只能根據目前看到的程式碼做出決策, 讀到後面的程式碼也不會影響前面做出的決定;
    • 這特別影響名字查詢(name lookup)和函式過載決議;
  • 使用前向宣告來減少編譯期依賴;

C++連結(linking)

  • C++與靜態連結;
  • C++與動態連結;
  • 傳統的是one-pass連結器 -- 越基礎的庫越是放到後面;
  • C++在C的連結模型上主要增加了兩項內容:
    • vague lingkage, 即同一個符號有多份不衝突的定義;
  • 現在的編譯器聰明到可以自動判斷一個函式是否適合inline, 因此inline關鍵字在原始檔中往往不是必需的;
  • 現在的C++編譯器採用重複程式碼消除的辦法來避免重複定義(multiple definition), 其餘的則丟棄(vague linkage);
  • 編譯器如何處理inline函式中的static變數?

模板

  • C++模板包括函式模板和類别範本:
    • 函式定義, 包括具現化後的函式模板、類别範本的成員函式、類别範本的靜態成員函式;
    • 變數定義, 包括函式模板的靜態資料變數、類别範本的靜態資料成員、類别範本的全域性物件;
  • 模板編譯連結的不同之處在於, 以上具有external linkage的物件通常會在多個編譯單元被定義;
    • 連結器必須進行重複程式碼消除, 才能正確生成可執行檔案;
  • 模板的定義要放到標頭檔案中, 否則會有編譯錯誤(連結錯誤);

虛擬函式

  • 在現在的C++實現中, 虛擬函式的動態呼叫(動態繫結, 執行期決議)是通過虛擬函式表(vtable)進行的, 每個多型class都應該有一份vtable;
  • 定義或繼承了虛擬函式的物件中會有一個隱含成員: 指向vtable的指標, 即vptr;
  • 在構造和析構物件的時候, 編譯器生成的程式碼會修改這個vptr成員, 這就要用到vtable的定義(使用其地址);
  • 我們有時看到的連結錯誤不是抱怨找不到某個虛擬函式的定義, 而是找不到虛擬函式表的定義;
  • 一個多型class的vtable應該恰好被某一個目標檔案定義, 這樣連結就不會有錯;
    • 但是C++編譯器有時無法判斷是否應該在當前編譯單元生成vtable定義, 為了保險起見, 只能每個編譯單元都生成vtable, 交給連結器去消除重複資料;
    • 有時我們不希望vtable導致目標檔案膨脹, 可以在標頭檔案的calss定義中宣告out-line虛擬函式;

工程專案中標頭檔案的使用規則

  • C++還無法擺脫標頭檔案和預處理, 因此我們要深入理解可能存在的陷阱;
  • 一旦為了使用某個struct或者某個庫函式而包含了一個標頭檔案, 那麼這個標頭檔案中定義的其他名字(struct, 函式, 巨集)也被引入當前編譯單元, 可能製造麻煩;
  • 標頭檔案的害處:
    • 傳遞性, 標頭檔案可以再包含其他標頭檔案;
      • 合理地組織原始碼, 減少開發時rebuild的成本是每個稍具規模專案的必做功課;
    • 順序性, 一個原始檔可以包含多個標頭檔案, 但可能會造成程式的語義跟標頭檔案包含的順序有關, 也跟是否包含某一個標頭檔案有關;
    • 差異性, 內容差異造成不同原始檔看到的標頭檔案不一致, 時間差異造成標頭檔案與庫檔案內容不一致;
  • 現代的程式語言, 模組化做得比較好:
    • 對於解釋型語言, import的時候直接把對應模組的原始檔解析(parse)一遍(不再是簡單地把原始檔包含進來);
    • 對於編譯型語言, 編譯出來的目標檔案(例如Java的.class檔案)裡直接包含了足夠的後設資料, import的時候只需要讀目標檔案的內容, 不需要讀原始檔;
    • 這兩種做法都避免了宣告與定義不一致的問題, 因為在這些語言裡宣告與定義是一體的;
    • 同時這種import手法也不會引入不想要的名字, 大大簡化了名字查詢的負擔(無論是人腦還是編譯器);
    • 也不用擔心import的順序不同造成程式碼功能變化;
  • 標頭檔案的使用規則:
    • 幾乎每個C++程式設計都會涉及到標頭檔案的組織;
    • 將標頭檔案的編譯依賴降至最小;
    • 將定義式之間的依賴關係降至最小, 避免迴圈依賴;
    • 讓class名字、標頭檔案名字、原始檔名字直接相關 -- 這樣方便原始碼的定位;
    • 令標頭檔案自給自足;
    • 總是在標頭檔案內寫內部#include guard(護套), 不要在原始檔寫外部護套;
    • include guard用的巨集的名字應該包含檔案的路徑全名(從版本管理器的角度), 必要的話還要加上專案名稱;

    • 如果編寫程式庫, 那麼公開的標頭檔案應該表達模組的介面, 必要的時候可以把實現細節放到內部標頭檔案中;

工程專案中庫檔案的組織原則

  • Linux的共享庫(shared library)比Windows的動態連結庫在C++程式設計方面要好用得多, 對應用程式來說基本可算是透明的, 跟使用靜態庫無區別;
    • 一致的記憶體管理, Linux動態庫與應用程式共享同一個heap, 因此動態庫分配的記憶體可以交給應用程式去釋放, 反之亦可;
    • 一致的初始化, 動態庫裡的靜態物件(全域性物件、namespace級的物件等等)的初始化和程式其他地方的靜態物件一樣, 不用特別區分物件的位置;
    • 在動態庫的介面中可以放心地使用class、STL、boost(如果版本相同);
    • 沒有dllimport/dllexport的累贅, 直接include標頭檔案就能使用;
    • DLL Hell的問題也小得多, 因為Linux允許多個版本的動態庫並存, 而且每個符號可以有多個版本;
    • 動態庫(.so), 靜態庫(.a), 原始碼庫(.cc);
  • 動態庫比靜態庫節省磁碟空間和記憶體空間, 並且具備動態更新的能力(可以hot fix bug), 似乎動態庫應該是目前的首選;
動態庫是有害的
  • 新的庫會破壞二進位制相容性;
  • 靜態庫也好不到哪兒去, 靜態庫相比動態庫主要有幾點好處:
    • 依賴管理在編譯器決定, 不用擔心日後它用的庫會變, 同理, 除錯core dump不會遇到庫更新導致debug符號失效的情況;
    • 執行速度可能更快, 因為沒有PLT(過程查詢表), 函式呼叫的開銷更小;
    • 釋出方便, 只要把單個可執行檔案拷貝到模板機器上;
  • 靜態庫的一個小缺點是連結比動態庫慢, 有的公司甚至專門開發針對大型C++程式的連結器;

原始碼編譯是王道

  • 每個應用程式自己選擇要用到的庫, 並自行編譯為單個可執行檔案;
  • 最好能和原始碼版本工具配合, 讓應用程式只需要指定用那個庫, build工具能自動幫我們check out庫的原始碼;
  • 在目前看到的開源build工具裡, 最接近這一點的是Chromium的gyp和騰訊的typhoon-blade, 其他如SCons, CMake, Premake, Waf等工具;

相關文章