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

C++ 對象複製構造函數與賦值運算符重載深度解析:淺拷貝、深拷貝、現代 C++ 最佳實踐

     編輯:Java 相關知識

核心概念:為什麼需要自定義複製/賦值?

當類管理**動態資源** (堆內存、文件句柄、Socket、鎖、智能指針等) 時,編譯器默認生成的**淺拷貝** (逐成員複製指針/句柄值) 會導致:

  • 雙重釋放:兩個對象析構時釋放同一塊內存 → Crash。
  • 資源洩漏:賦值時舊資源未釋放、新資源未正確獲取。
  • 數據競爭/邏輯錯誤:多對象共享可變狀態,一方修改影響另一方。

經典三/五/零法則

代碼演示:從淺拷貝到深拷貝再到零法則

1. 問題代碼:淺拷貝導致雙重釋放

class BadString {
    char* data_;
    size_t len_;
public:
    BadString(const char* s) : len_(strlen(s)) {
        data_ = new char[len_ + 1];
        strcpy(data_, s);
    }
    ~BadString() { delete[] data_; }
    // 編譯器默認生成:淺拷貝構造/賦值 (逐位複製 data_ 指針)
};

void test() {
    BadString a("Hello");
    BadString b = a; // 複製構造:b.data_ == a.data_ (同一地址)
    // 離開作用域:b 析構 delete[] data_ → a.data_ 成懸空指針
    // 再 a 析構 delete[] data_ → **Double Free Crash**
}

2. C++98 風格:手動實現深拷貝 (三法則)

class Cpp98String {
    char* data_;
    size_t len_;
public:
    // 構造
    Cpp98String(const char* s = "") : len_(strlen(s)) {
        data_ = new char[len_ + 1];
        strcpy(data_, s);
    }
    // 析構
    ~Cpp98String() { delete[] data_; }
    
    // 複製構造函數 (深拷貝)
    Cpp98String(const Cpp98String& other) : len_(other.len_) {
        data_ = new char[len_ + 1];
        strcpy(data_, other.data_);
    }
    
    // 賦值運算符 (複製並交換 / Copy-and-Swap 惯用法)
    Cpp98String& operator=(Cpp98String other) { // 參數按值傳遞 → 自動調用複製構造
        swap(*this, other); // 交換內部指針
        return *this;       // other 析構時釋放舊內存
    }
    
    friend void swap(Cpp98String& a, Cpp98String& b) noexcept {
        std::swap(a.data_, b.data_);
        std::swap(a.len_, b.len_);
    }
    
    // 訪問器
    const char* c_str() const { return data_; }
    size_t size() const { return len_; }
};

Copy-and-Swap 優勢:異常安全 (強異常保證)、自動處理自我賦值、代碼復用複製構造邏輯。

3. C++11 移動語義 (五法則)

class Cpp11String {
    char* data_;
    size_t len_;
public:
    // ... 構造/析構/複製構造/複製賦值 同 C++98 ...
    
    // 移動構造函數 (竊取資源)
    Cpp11String(Cpp11String&& other) noexcept : data_(other.data_), len_(other.len_) {
        other.data_ = nullptr;
        other.len_ = 0;
    }
    
    // 移動賦值運算符
    Cpp11String& operator=(Cpp11String&& other) noexcept {
        if (this != &other) {
            delete[] data_;        // 釋放舊資源
            data_ = other.data_;   // 竊取
            len_ = other.len_;
            other.data_ = nullptr;
            other.len_ = 0;
        }
        return *this;
    }
    
    // swap 同前
};

noexcept 關鍵:標準容器 (vector 等) 在擴容移動元素時,**僅當移動操作 noexcept 時才使用移動**,否則退化為複製,嚴重影響性能。

4. 現代 C++ 零法則:std::string / std::vector / 智能指針 (推薦)


// 直接用標準庫,零手工內存管理
class ModernString {
    std::string data_; // 內部自動管理深拷貝、移動、SSO (小字符串優化)
public:
    ModernString(const char* s = "") : data_(s) {}
    // 無需聲明任何特殊成員函數!編譯器默認生成的全都正確。
    const char* c_str() const { return data_.c_str(); }
    size_t size() const { return data_.size(); }
};

// 管理動態數組
class Buffer {
    std::vector data_; // 自動深拷貝、移動、擴容
public:
    Buffer(size_t n = 0) : data_(n) {}
    char* ptr() { return data_.data(); }
    size_t size() const { return data_.size(); }
};

// 管理多態對象 / 共享所有權
class Node {
    std::shared_ptr next_; // 引用計數,自動釋放
    std::unique_ptr data_; // 獨占所有權,移動語義
public:
    // 默認複製/移動/析構全正確
};

複製構造 vs 賦值運算符 關鍵差異

法則版本內容適用場景
三法則C++98若需自定義 析構函數、複製構造函數、賦值運算符 任一,通常三者都需自定義手動管理資源的類
五法則C++11三法則 + 移動構造函數、移動賦值運算符高性能、資源轉移語義
零法則C++11+**優先使用 RAII 容器/智能指針** (vector, string, unique_ptr, shared_ptr),**不聲明** 任何特殊成員函數,編譯器默認生成正確**現代 C++ 首選**,90%+ 場景
特性複製構造函數賦值運算符
調用時機新對象初始化 (T b = a;、參數傳值、返回值)已存在對象賦值 (b = a;)
簽名T(const T&) / T(T&&)T& operator=(const T&) / T& operator=(T&&)
需處理自我賦值無 (新對象)必須檢查 (if (this != &other))
需釋放舊資源無 (新對象無資源)必須釋放 舊資源再獲取新資源
返回值無 (構造函數無返回)*this (引用,支持鏈式賦值)
異常安全構造失敗自動清理需 Copy-and-Swap 或強異常保證

現代 C++ 最佳實踐清單

  1. 零法則優先:成員用 std::string, std::vector, std::unique_ptr, std::shared_ptr, std::optional 等 RAII 類型,**刪除** 手寫的析構/複製/移動函數。
  2. 必須手寫時 (封裝 C API、特殊資源):
    • 聲明 **五個特殊成員函數** (或 = default/= delete)。
    • 移動操作 **必須 noexcept**。
    • 賦值用 **Copy-and-Swap** (複製賦值) / **竊取並清空源** (移動賦值)。
    • 提供 swap 友元函數並 noexcept。
  3. 禁止複製/移動:NonCopyable 基類或直接 = delete:
    class NonCopyable {
        protected: NonCopyable() = default; ~NonCopyable() = default;
        public: NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete;
        };
        // 或
        class MyClass {
            MyClass(const MyClass&) = delete;
            MyClass& operator=(const MyClass&) = delete;
        };
        
  4. Rule of Zero 衍生:若基類遵循零法則,派生類**自動**遵循零法則 (除非引入裸指針/資源)。

常見面試陷阱

代碼問題解析
T a; T b = a;調用複製構造初始化非賦值
T a; T b; b = a;調用賦值運算符先構造 b 再賦值
func(T t) { } func(a);調用複製構造參數傳值
T func() { T t; return t; }C++17 強制 RVO (返回值優化),**不調用** 複製/移動構造強制省略複製
std::vector v; v.push_back(T());C++11 移動構造 (若 noexcept),否則複製構造移動語義觸發
T& operator=(T other) { swap(*this, other); return *this; }Copy-and-Swap 惯用法,參數按值傳遞統一複製/移動異常安全、碼簡潔

總結

C++ 對象複製/賦值的核心在於**資源所有權轉移語義**。**現代 C++ (C++11/14/17/20) 核心準則:零法則優先 (RAII 類型) → 必須手寫時遵循 五法則 (noexcept 移動 + Copy-and-Swap) → 禁用複製用 = delete**。徹底拋棄 C++98 時代的手動 new/delete/深拷貝樣板代碼,擁抱 std::string, std::vector, unique_ptr, shared_ptr,讓編譯器為你生成正確、高效、異常安全的特殊成員函數。

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