編譯器默認生成的c++類六大成員函數
編譯器默認生成的六大成員函數
當你定義一個空類時,例如:
class Empty {};
如果代碼中沒有顯式定義任何成員函數,C++編譯器會在需要時(例如,代碼中實際調用了這些函數)為你自動生成以下六個特殊成員函數(Special Member Functions):
-
默認構造函數 (Default Constructor): Empty();
- 用于創建對象,當沒有提供任何初始化參數時被調用。
- 例如:Empty e1;
-
析構函數 (Destructor): ~Empty();
- 用于對象銷毀,在對象生命周期結束時被調用。
- 它負責清理資源,雖然對于空類來說沒什么可清理的。
-
拷貝構造函數 (Copy Constructor): Empty(const Empty& other);
- 用于從一個同類對象創建新對象。
- 例如:Empty e2(e1); 或 Empty e3 = e1;
-
拷貝賦值運算符 (Copy Assignment Operator): Empty& operator=(const Empty& other);
- 用于將一個已存在的同類對象的值賦給另一個已存在的對象。
- 例如:e2 = e1;
-
移動構造函數 (Move Constructor) (C++11及以后): Empty(Empty&& other) noexcept;
- 用于從一個右值(通常是臨時對象)“竊取”其資源來創建新對象,避免不必要的拷貝。
- 例如:Empty e4(std::move(e1));
-
移動賦值運算符 (Move Assignment Operator) (C++11及以后): Empty&operator=(Empty&& other) noexcept;
- 用于將一個右值對象的資源“竊取”并賦給一個已存在的對象。
- 例如:e3 = std::move(e2);
#include <iostream>
#include <utility>class Empty {};int main() {std::cout << "創建一個默認對象 e1..." << std::endl;Empty e1;std::cout << "使用拷貝構造函數創建 e2..." << std::endl;Empty e2(e1);std::cout << "使用拷貝賦值運算符..." << std::endl;Empty e3;e3 = e2;std::cout << "使用移動構造函數創建 e4..." << std::endl;Empty e4(std::move(e1));std::cout << "使用移動賦值運算符..." << std::endl;e3 = std::move(e2);std::cout << "程序結束,對象將被銷毀。" << std::endl;return0;
}
這段代碼可以成功編譯和運行,這雄辯地證明了,即使我們沒有為 Empty 類編寫任何一個函數,編譯器也已經為我們提供了所有必要的“基礎設施”來完成對象的創建、拷貝、移動和銷毀。
類的六大成員函數生成規則及“三/五/零法則”
了解了“有什么”之后,一個優秀的工程師還應該了解“為什么有”和“什么時候沒有”。
生成規則
編譯器并非無腦生成這些函數。它的行為遵循一套精密的規則:
-
只在需要時生成:這些函數只在它們被ODR-used(One Definition
Rule-used,可以簡單理解為“被實際調用”)時,編譯器才會去定義它們。 -
用戶優先:如果你顯式聲明了任何一個特殊成員函數(即使是用 =delete 或 =default),編譯器就不會再為該函數生成默認版本。
-
復雜的關聯規則:聲明一個函數會“抑制”其他函數的自動生成。這正是“三/五法則”的核心。
- 重要:一旦你聲明了自定義的析構函數、拷貝構造或拷貝賦值,編譯器將不會自動生成移動構造和移動賦值函數。這是因為自定義的析-構/拷貝行為可能與默認的移動行為不兼容。
- 反之,聲明了移動操作,也會影響拷貝操作的自動生成。
- 三法則 (Rule of Three): (C++03) 如果你顯式聲明了析構函數、拷貝構造函數或拷貝賦值運算符中的任何一個,通常意味著你需要同時管理這三者,因為類內可能含有需要深度拷貝的資源(如裸指針)。此時,編譯器將不再自動生成它認為你可能需要自己實現的拷貝操作。
- 五法則 (Rule of Five): (C++11) 這是“三法則”的擴展。如果你聲明了上述三者中的任何一個,或者聲明了移動構造函數或移動賦值運算符,編譯器將認為你對資源管理有特殊意圖。
現代C++的智慧:“零法則” (Rule of Zero)
三/五法則”是C++開發者必須掌握的知識,但它們也反映了一種更底層的設計問題:手動資源管理。現代C++推崇**“零法則” (Rule of Zero)**。
核心思想:設計你的類,使其不需要編寫任何自定義的析構、拷貝/移動構造和賦值函數。將所有資源的所有權交給專門的資源管理類(如 std::string, std::vector, std::unique_ptr, std::shared_ptr)。
當你遵循“零法則”時,你的類(即使非空)也能像空類一樣,讓編譯器為其生成正確、高效的特殊成員函數。這些標準庫組件本身已經完美地實現了“五法則”,你的類只需組合它們,就能自動獲得正確的資源管理行為。這使得代碼更簡潔、更安全、更易于維護。
一個有趣的延伸:sizeof(Empty) 等于多少?
與空類相關的另一個高頻面試題是:sizeof(Empty) 的結果是多少?
答案不是0,而是1。
為什么是1?
C++標準規定,任何兩個不同的對象在內存中都必須有不同的地址。如果一個空類的大小為0,那么當你創建一個該類的數組時:
Empty arr[10];
&arr[0] 和 &arr[1] 的地址將會相同,這將導致指針算術和數組索引徹底失效。為了保證對象標識(identity)的唯一性,編譯器會為空類“填充”一個字節。這個字節不存儲任何有用的數據,僅僅是為了“占位”。
空基類優化 (Empty Base Optimization, EBO)
class BaseEmpty {}; // sizeof(BaseEmpty) == 1class Derived : public BaseEmpty {int x; // sizeof(int) == 4
};// sizeof(Derived) 通常是 4,而不是 1 + 4 = 5
在這種情況下,編譯器可以將空基類的“1字節”與派生類的成員(如 x)的數據存儲在相同的地址,或者說,讓空基類不占用任何額外的空間。這樣既滿足了“不同對象地址不同”的原則,又避免了內存浪費。這是C++零開銷原則 Zero-overhead principle的一個體現。
參考文獻:
面試官問“空類里有什么”?別再答“什么都沒有”了