頭文件變動 → 所有引用者強制重編。Chrome 級項目一次全量編譯可耗時數小時。掌握編譯依賴最小化技巧,是資深工程師必備技能。
// 原始 old.h - 5 個 #include
#include "file.h" // 基類,必須保留
#include "db.h" // 基類,必須保留
#include "cx.h" // 成員類型,必須保留
#include "deduce.h" // 成員類型,必須保留
#include "error.h" // 僅作參數/返回值 → 可前向聲明
// 優化後
#include "file.h"
#include "db.h"
#include "cx.h"
#include "deduce.h"
class error; // 前向聲明
// 標準庫優化
#include // 替代 /,僅含流類型前向聲明
// 內部實現用 時再在 .cpp 中包含
將**所有私有成員**移至不透明的實現類,頭文件僅保留一個指針:
// old.h
class Old {
public:
Old(const Cx&);
// ... 公有介面 ...
private:
class Impl; // 前向聲明
Impl* pImpl; // 唯一私有成員
};
// old.cpp
struct Old::Impl {
std::list cxList;
Deduce deduce;
// 所有私有細節在此,任意變更不影響客戶端編譯
};
Old::Old(const Cx& c) : pImpl(new Impl) {}
Old::~Old() { delete pImpl; } // 需在 .cpp 定義析構函數
| 優點 | 缺點 |
|---|---|
| 徹底切斷編譯依賴,頭文件極度精簡 | 堆分配/釋放開銷,可能產生內存碎片 |
| 二進制兼容性強 (ABI 穩定),庫升級無需重編客戶端 | 每次成員訪問多一次間接引用 (pImpl->member) |
| 隱藏實現細節,封裝性極佳 | 調試時需額外解引用查看成員 |
class Old : public File, private Db { // 私有繼承 Db 是何意圖?
// ...
};
私有繼承通常為「is-implemented-in-terms-of」,**組合更清晰、編譯依賴更弱**。
// old.h
#include "file.h" // 公有繼承保留 (有虛函數需 override)
class Db; // 前向聲明
class Cx;
class Error;
class Old : public File {
public:
Old(const Cx&);
Db getDb(int, char*);
// ...
private:
class Impl;
Impl* pImpl;
};
// old.cpp
struct Old::Impl {
Db db; // 組合替代私有繼承
std::list cxList;
Deduce deduce;
};
| 指標 | 重構前 | 重構後 |
|---|---|---|
| 頭文件 #include 數 | 7+ (含遞歸展開) | 2 (file.h + iosfwd) |
| 修改私有成員觸發重編 | 所有客戶端 | **零客戶端** |
| 修改 Db 類觸發重編 | 所有客戶端 | **零客戶端** (僅 old.cpp 重編) |
| 二進制兼容性 | 極脆弱 | 強 (Pimpl 隔離) |
iosfwd、memory 等前向聲明頭**