首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ Primer 讀書筆記第 13 章:深度解析拷貝控制與移動語義

     編輯:關於C++

在 C++ 類設計中,拷貝控制是核心議題之一。當我們定義一個類時,需要明確對象在拷貝、移動、賦值和銷毀時的行為。這些行為由五個特殊成員函數控制,統稱為拷貝控制操作:拷貝構造函數、拷貝賦值運算符、移動構造函數、移動賦值運算符、析構函數。掌握何時需要自定義這些操作,是編寫健壯 C++ 代碼的關鍵。

13.1 拷貝、賦值與銷毀

13.1.1 拷貝構造函數

拷貝構造函數的第一個參數必須是自身類類型的引用(通常為 const 引用),且任何額外參數都有默認值。由於拷貝構造函數常被隱式調用(如傳參、返回值),它通常不應聲明為 explicit,否則會阻止隱式拷貝初始化。

class Foo {
public:
    Foo(const Foo&);                 // 標準拷貝構造函數
    Foo(const Foo&, int i = 0);      // 也是拷貝構造函數(額外參數有默認值)
};

合成拷貝構造函數

若未自定義拷貝構造函數,編譯器會合成一個,執行成員級別的淺拷貝(逐個拷貝非 static 成員)。這對於包含指針成員的類極其危險,會導致雙重釋放錯誤。

拷貝初始化 vs 直接初始化

  • 直接初始化:string dots("zhang") —— 直接調用構造函數。
  • 拷貝初始化:string dots = "zhang" —— 先轉換再拷貝,可能產生額外開銷。

拷貝初始化發生於:按值傳參、按值返回、花括號初始化數組/聚合類成員時。編譯器可優化掉拷貝(複製省略),但拷貝構造函數必須存在且可訪問。

13.1.2 拷貝賦值運算符

拷貝賦值運算符重載 operator=,通常接受 const 引用並返回左側對象的引用。合成版本同樣執行淺拷貝。注意區分:Test t2 = t1 調用拷貝構造函數;t1 = t2 調用拷貝賦值運算符。

13.1.3 析構函數

析構函數釋放資源並銷毀非 static 成員。它無參數、無返回值、不可重載。對象銷毀時機:離開作用域、delete 動態對象、臨時對象表達式結束、容器銷毀時銷毀元素。關鍵點:析構函數不直接銷毀成員,而是在成員銷毀之前執行清理工作。

13.1.4 三/五法則

  • 若類需要自定義析構函數,幾乎肯定需要拷貝構造函數和拷貝賦值運算符。
  • 若類需要拷貝構造函數,幾乎肯定需要拷貝賦值運算符。

核心原因通常是資源管理(如動態內存)。若只用合成版本,淺拷貝會導致多個對象共享同一資源,析構時重複釋放。

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;           // 深賦值
    }
};

13.1.5 使用 = default

C++11 允許顯式聲明使用合成版本:Test() = default;。在類內使用 = default 會隱式聲明為 inline 函數。

13.1.6 阻止拷貝

C++11 引入 = delete 刪除函數:Test(const Test&) = delete;。= delete 必須出現在首次聲明處。不可刪除析構函數,否則對象無法銷毀。包含 const 成員或不可拷貝成員的類,合成拷貝賦值運算符會被隱式刪除。

13.2 拷貝控制與資源管理

管理類外資源(如動態內存)的類必須自定義拷貝控制。兩種典型策略:

  • 值語義:每個對象擁有獨立狀態(如 std::string)。拷貝時深拷貝資源。
  • 指針語義:對象共享底層數據(如 std::shared_ptr)。通常用引用計數實現。

13.2.1 類值行為的類

賦值運算符必須處理自賦值、異常安全。推薦「拷貝並交換」惯用法或先拷貝右側資源再釋放左側資源。

Val& operator=(const Val& rhs) {
    auto newp = new (std::nothrow) string(*rhs.ps); // 先拷貝右側
    if (newp) { delete ps; ps = newp; }             // 再更新左側
    return *this;
}

13.2.2 類指針行為的類(引用計數)

核心邏輯:構造時計數置 1;拷貝構造/賦值遞增計數;析構遞減,歸零時釋放資源。賦值運算符必須先遞增右側計數,再遞減左側計數,以正確處理自賦值。

HasPtr& operator=(const HasPtr& rhs) {
    ++*rhs.pUser;                 // 先增右側
    if (--*pUser == 0) { delete pUser; delete ps; } // 再減左側
    ps = rhs.ps; pUser = rhs.pUser;
    return *this;
}

13.3 交換操作

定義 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 析構釋放舊資源
}

13.5 動態內存管理類實例:StrVec

模擬 vector<string> 的簡化實現,展示分配器(allocator)的使用:allocate、construct、destroy、deallocate。擴容時使用 std::move 避免字符串額外拷貝開銷。

13.6 對象移動與右值引用

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 的屬性,防止對右值賦值等誤用。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved