在 C++ 類設計中,拷貝控制是核心議題之一。當我們定義一個類時,需要明確對象在拷貝、移動、賦值和銷毀時的行為。這些行為由五個特殊成員函數控制,統稱為拷貝控制操作:拷貝構造函數、拷貝賦值運算符、移動構造函數、移動賦值運算符、析構函數。掌握何時需要自定義這些操作,是編寫健壯 C++ 代碼的關鍵。
拷貝構造函數的第一個參數必須是自身類類型的引用(通常為 const 引用),且任何額外參數都有默認值。由於拷貝構造函數常被隱式調用(如傳參、返回值),它通常不應聲明為 explicit,否則會阻止隱式拷貝初始化。
class Foo {
public:
Foo(const Foo&); // 標準拷貝構造函數
Foo(const Foo&, int i = 0); // 也是拷貝構造函數(額外參數有默認值)
};
若未自定義拷貝構造函數,編譯器會合成一個,執行成員級別的淺拷貝(逐個拷貝非 static 成員)。這對於包含指針成員的類極其危險,會導致雙重釋放錯誤。
string dots("zhang") —— 直接調用構造函數。string dots = "zhang" —— 先轉換再拷貝,可能產生額外開銷。拷貝初始化發生於:按值傳參、按值返回、花括號初始化數組/聚合類成員時。編譯器可優化掉拷貝(複製省略),但拷貝構造函數必須存在且可訪問。
拷貝賦值運算符重載 operator=,通常接受 const 引用並返回左側對象的引用。合成版本同樣執行淺拷貝。注意區分:Test t2 = t1 調用拷貝構造函數;t1 = t2 調用拷貝賦值運算符。
析構函數釋放資源並銷毀非 static 成員。它無參數、無返回值、不可重載。對象銷毀時機:離開作用域、delete 動態對象、臨時對象表達式結束、容器銷毀時銷毀元素。關鍵點:析構函數不直接銷毀成員,而是在成員銷毀之前執行清理工作。
核心原因通常是資源管理(如動態內存)。若只用合成版本,淺拷貝會導致多個對象共享同一資源,析構時重複釋放。
class Test {
int* _pa;
public:
Test() : _pa(new int(10)) {}
~Test() { delete _pa; } // 必須自定義析構
Test(const Test& t) : _pa(new int(*t._pa)) {} // 深拷貝
Test& operator=(const Test& t) {
*_pa = *t._pa; return *this; // 深賦值
}
};
= defaultC++11 允許顯式聲明使用合成版本:Test() = default;。在類內使用 = default 會隱式聲明為 inline 函數。
C++11 引入 = delete 刪除函數:Test(const Test&) = delete;。= delete 必須出現在首次聲明處。不可刪除析構函數,否則對象無法銷毀。包含 const 成員或不可拷貝成員的類,合成拷貝賦值運算符會被隱式刪除。
管理類外資源(如動態內存)的類必須自定義拷貝控制。兩種典型策略:
std::string)。拷貝時深拷貝資源。std::shared_ptr)。通常用引用計數實現。賦值運算符必須處理自賦值、異常安全。推薦「拷貝並交換」惯用法或先拷貝右側資源再釋放左側資源。
Val& operator=(const Val& rhs) {
auto newp = new (std::nothrow) string(*rhs.ps); // 先拷貝右側
if (newp) { delete ps; ps = newp; } // 再更新左側
return *this;
}
核心邏輯:構造時計數置 1;拷貝構造/賦值遞增計數;析構遞減,歸零時釋放資源。賦值運算符必須先遞增右側計數,再遞減左側計數,以正確處理自賦值。
HasPtr& operator=(const HasPtr& rhs) {
++*rhs.pUser; // 先增右側
if (--*pUser == 0) { delete pUser; delete ps; } // 再減左側
ps = rhs.ps; pUser = rhs.pUser;
return *this;
}
定義 swap 對於與標準算法(sort、unique)配合的類至關重要。自定義 swap 應交換內部指針而非重新分配內存。注意:swap 內部應調用未限定的 swap(配合 using std::swap;),以便 ADL 找到用戶定義版本。
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
// ...
};
inline void swap(HasPtr& lhs, HasPtr& rhs) {
using std::swap;
swap(lhs.ps, rhs.ps);
swap(lhs.pUser, rhs.pUser);
}
在賦值運算符中使用 swap(拷貝並交換惯用法)可自動處理自賦值且異常安全:
HasPtr& operator=(HasPtr rhs) { // 形參按值傳遞(觸發拷貝)
swap(*this, rhs); // 交換資源
return *this; // rhs 析構釋放舊資源
}
模擬 vector<string> 的簡化實現,展示分配器(allocator)的使用:allocate、construct、destroy、deallocate。擴容時使用 std::move 避免字符串額外拷貝開銷。
C++11 引入右值引用(T&&),綁定到即將銷毀的臨時對象,允許「偷取」資源而非拷貝。std::move 將左值轉為右值引用,標誌資源可被移動。
nullptr)。std::move(*this))。noexcept:標準容器(如 vector)僅在移動操作不拋出異常時才使用移動優化,否則退回拷貝。StrVec(StrVec&& s) noexcept
: elements(s.elements), first_free(s.first_free), cap(s.cap) {
s.elements = s.first_free = s.cap = nullptr; // 來源置空
}
StrVec& operator=(StrVec&& rhs) noexcept {
if (this != &rhs) {
free();
elements = rhs.elements; first_free = rhs.first_free; cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
編譯器不總是合成移動操作。條件:類未聲明任何拷貝控制成員,且所有非 static 成員可移動。若類定義了移動操作,合成拷貝操作會被刪除。更新的五法則:若定義了任一拷貝/移動操作,通常應定義全部五個。
std::make_move_iterator 將普通迭代器轉為移動迭代器,解引用產生右值引用,配合 uninitialized_copy 實現高效移動。
可為成員函數提供左值/右值重載版本(如 push_back(const T&) 和 push_back(T&&))。C++11 引入引用限定符(& / &&)限定 this 的屬性,防止對右值賦值等誤用。