對於一個未曾實例化的空類別,編譯器不會為其生成任何預設函數;但一旦實例化,編譯器便會按需合成相應的函數,主要包括:預設構造函數、拷貝構造函數、析構函數與賦值運算符。本文將聚焦於「拷貝構造函數」,深入探討其觸發時機、分類(trivial 與 non-trivial)、以及編譯器在何種情況下會合成非平凡的拷貝構造函數。
拷貝構造函數的簽名通常為 ClassName(const ClassName&),當一個對象以另一個同類對象作為初始值時被調用。具體發生在以下三種情況:
Animal a2 = a1;void func(Animal a); func(a1);Animal func() { Animal a; return a; }若類別未聲明拷貝構造函數,且不包含需要特殊處理的成員,編譯器採用「位逐次拷貝」,即按位複製內存內容。這屬於淺拷貝,對於包含裸指針的類別極不安全,會導致雙重釋放或資源洩漏。
相比之下,std::string 等類別提供了顯式拷貝構造函數,會為新對象重新分配內存並複製數據,實現深拷貝,避免雙指針指向同一塊內存。
若類別包含擁有拷貝構造函數的成員對象,編譯器合成的拷貝構造函數會逐一調用各成員的拷貝構造函數,而非簡單位拷貝。
class Animal {
public:
Animal() {}
Animal(const Animal&) { cout << "Animal's Copy Constructor" << endl; }
};
class Dog {
Animal animal; // 含有拷貝構造函數的成員
};
Dog d1;
Dog d2 = d1; // 輸出 "Animal's Copy Constructor"
派生類的合成拷貝構造函數會先調用基類的拷貝構造函數,再處理自身成員。
class Dog : public Animal {
// ...
};
Dog d1;
Dog d2 = d1; // 同樣觸發基類拷貝構造函數
虛函數引入虛表指針。同類型對象間拷貝時,虛表指針指向同一虛表,位逐次拷貝安全,編譯器不會合成拷貝構造函數。
Dog d1; Dog d2 = d1; // vptr 相同,位拷貝即可
但派生類對象初始化基類對象時,基類需重新設定 vptr 指向自身虛表,此時位拷貝失效,編譯器必須合成 non-trivial 拷貝構造函數。
Dog d; Animal a = d; // 必須調整 vptr,合成拷貝構造函數
虛繼承引入虛基類指針。拷貝時需重新設定指向虛基類的指針,位逐次拷貝失效,編譯器合成拷貝構造函數處理指針調整。
編譯器合成 non-trivial 拷貝構造函數的四大場景:
關鍵點:同類型對象間拷貝,即使有虛函數或虛基類,仍採用位逐次拷貝,不合成拷貝構造函數。