當頭文件修改時,所有包含它的原始檔都需重新編譯——這是著名的「易碎基類問題」。對於大型專案,編譯時間極其可觀。句柄類模式配合前向宣告,能將實作細節隱藏於 .cpp 中,大幅減少不必要的重新編譯。
// head.h
struct A { int i; }; // 完整定義
class B { // 句柄類,包裝 A
A* a;
public:
B(int n = 0);
void show();
~B();
};
// head.cpp
#include "head.h"
#include <iostream.h>
B::B(int n) { a = new A; a->i = n; }
void B::show() { cout << a->i; }
B::~B() { delete a; }
// main.cpp
#include <iostream>
#include "head.h"
int main() { B b(1); b.show(); return 1; }
若修改 struct A (如新增欄位),head.h 變更 → head.cpp、main.cpp 皆需重編,即使 main.cpp 完全不關心 A 的內部結構。
// head.h (修改後)
class B {
struct A; // 前向宣告:不完整型別,僅宣告存在性
A* a; // 指標/引用只需不完整型別
public:
B(int n = 0);
void show();
~B();
};
// head.cpp (修改後)
#include "head.h"
#include <iostream.h>
struct B::A { // 完整定義移至 .cpp,屬於 B 的巢狀類別
int i;
// 新增欄位僅影響 head.cpp
};
B::B(int n) { a = new A; a->i = n; }
void B::show() { cout << a->i; }
B::~B() { delete a; }
// main.cpp (完全不變)
#include <iostream>
#include "head.h"
int main() { B b(1); b.show(); return 1; }
| 場景 | 是否需完整型別定義 |
|---|---|
宣告指標/引用 (A* a, A& a) | 否,前向宣告即可 |
宣告物件實例 (A a) | 是,需知道大小與佈局 |
| 呼叫成員函數/存取成員 | 是,需完整定義 |
sizeof(A) | 是 |
句柄類 B 僅持有 A*,頭文件只需 struct A; 前向宣告。完整定義下沉至 head.cpp,成為 B 的私有巢狀類別 B::A,實現介面與實作完全分離。
head.cpp 重編,main.cpp 及其他使用 B 的檔案零影響更通用的做法是 Pimpl (Pointer to Implementation):
// Widget.h
class Widget {
struct Impl; // 前向宣告
std::unique_ptr<Impl> pImpl; // 智慧指標管理生命週期
public:
Widget();
~Widget(); // 必須在 .cpp 定義,因需完整 Impl 型別
void doSomething();
};
// Widget.cpp
struct Widget::Impl {
int data1, data2;
std::vector<string> items;
// 任意複雜實作細節
};
Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::doSomething() { pImpl->data1++; }
Pimpl 是現代 C++ 實現編譯防火牆的標準模式,廣泛用於 Qt、標準庫實現等大型專案。
掌握前向宣告與 Pimpl,能從架構層面優化建構系統,是資深 C++ 開發者的必備技能。