《More Effective C++:35個改善編程與設計的有效方法》
讀書筆記:非必要不提供default constructor
在 C++ 中,默認構造函數(即無需任何參數即可調用的構造函數)是對象“無中生有”的一種方式。它的核心作用是在沒有外部信息輸入時完成對象的初始化。但并非所有類都需要默認構造函數,“非必要不提供”才是更合理的設計原則。
一、默認構造函數的適用邊界
默認構造函數的價值,在于“無外部信息時仍能合理初始化對象”。比如:
- 數值類對象可初始化為 0 或無意義值;
- 指針可初始化為 null;
- 鏈表、哈希表等容器可初始化為空容器。
這些場景中,默認構造函數能確保對象處于“可用狀態”。但更多場景下,對象的初始化依賴外部信息——比如模擬公司設備的類必須有唯一 ID,通信簿字段必須包含人名。這類對象若沒有外部信息,根本無法完成“有意義的初始化”,此時強行提供默認構造函數反而會埋下隱患。
二、缺乏默認構造函數的“限制”與應對
若類不提供默認構造函數,使用時會面臨一些限制,但這些限制并非無法解決,只是需要更謹慎的處理。
1. 數組初始化的挑戰
C++ 中創建對象數組時,默認會調用元素的默認構造函數。因此,缺乏默認構造函數的類無法直接創建數組:
class EquipmentPiece {
public:EquipmentPiece(int IDNumber); // 必須傳入ID,無默認構造函數
};EquipmentPiece pieces[10]; // 錯誤:無法調用構造函數
應對方法有三種:
-
棧上顯式初始化:僅適用于棧數組,通過初始化列表為每個元素傳入參數:
int ids[10] = {1,2,...,10}; EquipmentPiece pieces[] = {EquipmentPiece(ids[0]),EquipmentPiece(ids[1]),...,EquipmentPiece(ids[9]) };
但此方法無法用于堆數組。
-
指針數組:用指針數組替代對象數組,后續通過
new
為每個指針分配帶參數的對象:using PEP = EquipmentPiece*; PEP pieces[10]; // 指針數組無需調用構造函數 for (int i=0; i<10; i++) {pieces[i] = new EquipmentPiece(ids[i]); }
缺點是需手動管理內存(避免泄漏),且額外占用指針的存儲空間。
-
placement new:先分配原始內存,再通過 placement new 在指定內存上構造對象:
// 分配足夠存儲10個對象的原始內存 void* rawMem = operator new[](10 * sizeof(EquipmentPiece)); EquipmentPiece* pieces = static_cast<EquipmentPiece*>(rawMem);// 逐個構造對象(需傳入參數) for (int i=0; i<10; i++) {new (&pieces[i]) EquipmentPiece(ids[i]); }// 手動析構與釋放內存(注意順序) for (int i=9; i>=0; i--) {pieces[i].~EquipmentPiece(); } operator delete[](rawMem);
此方法節省內存,但實現復雜,維護成本高(需手動調用析構函數,且釋放內存的方式特殊)。
2. 與模板容器的兼容性問題
部分模板容器(如早期設計的數組模板)會在內部創建元素類型的數組,此時要求元素類型必須有默認構造函數。例如:
template<class T>
class Array {
public:Array(int size) { data = new T[size]; } // 調用T的默認構造函數
private:T* data;
};
若 T
是缺乏默認構造函數的類(如 EquipmentPiece
),模板實例化會失敗。
不過,隨著模板設計的成熟(如標準庫 vector
),許多現代模板已消除了對默認構造函數的依賴。這一問題的影響正在逐漸減弱。
3. 虛擬基類的協作成本
若類作為虛擬基類且缺乏默認構造函數,所有派生類(無論層級多深)都必須在構造函數中為其傳遞參數。這會增加派生類的設計負擔,但本質上是“強制正確初始化”的合理約束。
三、強行添加默認構造函數的隱患
為了規避上述限制,有些開發者會給“本不需要默認構造函數”的類強行添加,比如給 EquipmentPiece
加一個帶默認參數的構造函數:
class EquipmentPiece {
public:EquipmentPiece(int IDNumber = UNSPECIFIED); // 強行添加默認構造函數
private:static const int UNSPECIFIED = -1; // 無意義的“占位值”
};
這種做法看似解決了使用限制,實則埋下更深的問題:
1. 成員函數復雜化
強行添加的默認構造函數無法保證對象完全初始化(比如 IDNumber
可能為 UNSPECIFIED
)。此時,類的所有成員函數都必須先檢查“對象是否處于有效狀態”,否則可能觸發邏輯錯誤。例如,調用設備操作函數前需先驗證 ID 有效性,這會讓代碼變得臃腫且易錯。
2. 效率降低
額外的有效性檢查會增加運行時開銷(時間成本)和代碼體積(空間成本)。若檢查到無效狀態,還需處理異常(如拋出異常、終止程序),進一步消耗資源。
3. 軟件質量下降
“未完全初始化的對象”本質上是一種“不合法狀態”。允許這種狀態存在,會讓類的行為變得不可預測,增加調試難度,最終降低軟件的可靠性。
四、總結:堅守“非必要不提供”的原則
默認構造函數的核心價值是“在無外部信息時確保對象可合理初始化”。對于需要外部信息才能完成初始化的類(如依賴 ID 的設備、必須包含內容的通信簿),強行添加默認構造函數只會帶來復雜性、低效率和不可靠性。
雖然缺乏默認構造函數會帶來數組初始化、模板兼容等限制,但這些限制可以通過更嚴謹的代碼(如指針數組、placement new)解決,且本質上是“確保對象正確初始化”的合理代價。
因此,設計類時應遵循:能通過默認構造函數完成合理初始化的類,才提供它;否則,堅決不提供。這是保證代碼健壯性和效率的重要原則。