Three 為何物?
所謂的 Three 其實就是 copy constrcutor (複製建構函式) 、copy assignment operator (複製賦值運算子) 和 destructor (解構函式) 。 在介紹 Rule-of-Three 之前,我們回顧一下 C++ Class 的宣告與定義。
class Empty
{
};
類 Empty
十分簡單,我沒有為它賦予任何 data members (資料成員) 或顯式地宣告/定義 member functions (成員函式) 。但事實上,編譯器會在必要時為類 Empty
合成必要的成員函式。
int main()
{
Empty e1 {}; // 建立一個Empty物件
}
由於我沒有為類 Empty
宣告建構函式函式,編譯器自然會為我補充一個 default constructor (預設建構函式) 。
// C++ 概念程式碼
Empty::Empty() {}
當我們需要透過 e1
去構造更多的相同型別物件的時候,編譯器又幫我們做了以下的事情。編譯器會為類 Empty
新增複製建構函式 Empty::Empty(const Empty&)
和複製賦值運算子 Empty& operator=(const Empty& other)
。
事實上就類 Empty
的結構而言,編譯器根本不需要生這兩個函式。因為類 Empty
里根本沒有其他的 class object (類物件) 資料成員。編譯器只需要透過 bitwise copy (位逐次複製) 把記憶體逐一複製便能完成任務。不過我們可以假設編譯器會自動生成所需要的函式。
// C++ 概念程式碼
class Empty
{
public:
Empty(int val) : _val(val) {}
private:
int _val { 0 };
};
Empty::Empty(const Empty& other)
{
_val = other._val;
}
Empty& Empty::operator=(const Empty& other)
{
_val = other._val;
return *this;
}
Empty::~Empty()
{
}
// 執行程式碼
int main()
{
Empty e1 {};
Empty e2 { e1 };
Empty e3;
e3 = e2;
}
為什麼要給 Three 定規則?
我們已經對 Three 有了初步的瞭解,同時編譯器可能會在背後做了很多小動作。所以我們不能完全依賴編譯器的行為。
由於類 Empty
新增了新的資料成員,所以我定義了新的建構函式。同時為了避免記憶體洩漏,我也補上了解構函式 Empty::~Empty()
。
class Empty
{
public:
Empty(int val, char c) : _val(val), _cptr(new char(c)) {}
~Empty()
{
delete _cptr;
}
private:
int _val { 0 };
char* _cptr { nullptr };
};
然後我嘗試對類 Empty
的一些物件進行複製操作。此時編譯器再次幫我新增兩個成員函式。
// 概念程式碼
Empty(const Empty& other)
{
_val = other._val;
_cptr = other._cptr;
}
Empty& operator=(const Empty& other)
{
_val = other._val;
_cptr = other._cptr;
return *this;
}
// 執行程式碼
int main()
{
Empty e1 {"1", "empty"};
Empty e2 { e1 };
Empty e3;
e3 = e2;
}
編譯器把 e1
的成員逐個複製給 e2
。不過遺憾的是,程式結束之前會崩潰。崩潰原因是 Empty::_cptr
被重複釋放。因為它複製的是 Empty::_cptr
這個指標,而非 Empty::_cptr
這個指標指向的地址存放的值。
int main()
{
Empty e1(10, 'h');
auto e2 = e1;
} // creash !!!
所以當 Three 同時存在的時候,為了讓它們都 "安分守己",我們必須給它們頂下規矩,這就是所謂的 Rule of Three 。
Rules
Three 的問題在於,如果一個類裡面有指標型別 (或者需要手動釋放的資源型別) 的資料成員,編譯器的位逐次複製會讓程式變得不可靠。所以我們必須為讓程式變得安全。基於上面的問題,我們有兩個解決方案。要麼這個類的物件是不允許被複製;要麼這個類的物件允許被複製,但我們必須親自重寫這個類的 Three 。
方案一:
class Empty
{
public:
// other user-definfed ctors
~Empty()
{
delete _cptr;
}
Empty(const Empty& other) = delete;
Empty& operator=(const Empty& other) = delete;
private:
// data members
};
方案二:
class Empty
{
public:
// other user-definfed ctors
~Empty()
{
delete _cptr;
}
Empty(const Empty& other)
{
_val = other._val;
_cptr = new char(*other._cptr);
}
Empty& operator=(const Empty& other)
{
if (*this == other)
return *this;
_val = other._val;
if (_cptr != nullptr)
delete _cptr;
_cptr = new char(*other._cptr);
}
private:
// data members
};
啟發
C++ 的物件模型往往不是我們想象中的那麼簡單,編譯器暗地裡會做很多額外的工作。所以我們在管理類物件的資源的時候需要格外小心。