對於未顯式宣告特定函數的類別,編譯器會隱式生成以下成員函數:
class Empty {
public:
Empty(); // 預設建構函數
Empty(const Empty&); // 複製建構函數
~Empty(); // 解構函數
Empty& operator=(const Empty&); // 複製賦值運算子
Empty* operator&(); // 取址運算子 (non-const)
const Empty* operator&() const; // 取址運算子 (const)
};
class StringBad {
private:
char* str;
int len;
public:
StringBad(const char* s);
StringBad();
~StringBad();
};
StringBad::StringBad(const char* s) {
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
}
StringBad::StringBad() { len = 4; str = new char[4]; }
StringBad::~StringBad() { delete[] str; }
執行 StringBad sailor = sports; 時,編譯器生成的隱式複製建構函數執行「成員逐一複製」(淺複製):
sailor.str = sports.str; // 僅複製指標值!
導致兩物件指向同一記憶體區塊。先被解構的物件釋放記憶體,另一物件的指標成為懸空指標,後續存取導致未定義行為。
knot = headline1; 呼叫隱式賦值運算子,同樣僅複製指標,造成雙重釋放或記憶體洩漏。
凡類別含 new 初始化的指標成員,必須顯式定義:
兩者皆需實作深度複製:配置新記憶體並複製資料內容,而非指標值。並遵循「三大法則」(C++98)或「五大法則」(C++11 含移動語意)。
類別 X 的建構函數若第一參數為下列之一,且無其他參數或其餘參數有預設值,即為複製建構函數:
X&const X&volatile X&const volatile X&X::X(const X&); // 是
X::X(X&, int = 1); // 是(第二參數有預設值)
X::X(X); // 否(傳值參數會無限遞迴)
X::X(X&, int, int=2); // 否(第二參數無預設值)
類別可同時擁有 X(const X&) 與 X(X&)。但若僅提供 X(X&),則無法以 const X 或 volatile X 物件進行複製初始化。
struct X {
template<typename T> X(const T&); // 非複製建構函數
template<typename T> X& operator=(const T&); // 非複製賦值
};
原因:樣板不改變語言規則。當程式需要複製建構函數而使用者未宣告,編譯器依然會自動生成。此規則同樣適用於賦值運算子。參考《Effective C++ 3/e》條款 45。
#include <iostream>
#include <string>
struct X {
template<typename T>
X(T&) { std::cout << "This is ctor." << std::endl; }
template<typename T>
X& operator=(T&) { std::cout << "This is ctor." << std::endl; }
};
void main() {
X a(5); // 呼叫樣板建構函數
X b(10.5); // 呼叫樣板建構函數
X c = a; // 呼叫編譯器生成的複製建構函數(非樣板!)
c = b; // 呼叫編譯器生成的複製賦值運算子(非樣板!)
}
輸出結果:
This is ctor. // a(5)
This is ctor. // b(10.5)
This is ctor. // c = a (編譯器生成的複製建構函數預設輸出相同訊息?實際上不會輸出此訊息)
This is ctor. // c = b (編譯器生成的複製賦值運算子同理)
修正說明:編譯器生成的複製建構函數與賦值運算子不會輸出 "This is ctor."。實際輸出僅前兩行。樣板函數不會被選為複製操作,編譯器會另行合成正確簽名的函數。