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

C++ 物件拷貝深度解析:拷貝建構函數、淺拷貝、深拷貝與三/五法則

     編輯:關於C語言

為什麼需要拷貝建構函數?

C++ 中物件預設以「位元逐位複製」(bitwise copy) 進行拷貝 — 即淺拷貝。對於僅包含純量、std::string、std::vector 等已正確實現拷貝語意的成員類型,預設行為正確。但一旦類別擁有原始指標、檔案描述符、socket、記憶體區塊、鎖等「資源」,預設淺拷貝會導致雙重釋放、資源洩漏、資料競爭等嚴重錯誤。

拷貝建構函數語法與調用時機


class MyClass {
public:
    // 拷貝建構函數 (Copy Constructor)
    MyClass(const MyClass& other);           // 標準形式
    // MyClass(MyClass& other);               // 非 const 參考 (罕見,C++11 前標準庫容器需求)
    // MyClass(const volatile MyClass&);      // volatile 版本
    // MyClass(MyClass&& other) noexcept;     // 移動建構函數 (C++11)
};

自動調用場景

  1. 以值傳遞參數:void Func(MyClass obj); → Func(instance);
  2. 以值回傳:MyClass Create(); → MyClass obj = Create(); (可能觸發 RVO/NRVO 優化省略拷貝)
  3. 用現有物件初始化新物件:MyClass b = a;、MyClass b(a);、MyClass b{a};
  4. 標準容器操作:vector.push_back(a)、insert、resize、reallocate 時元素搬移。
  5. Lambda 捕獲:[obj] { ... } (預設以值捕獲)

淺拷貝 vs 深拷貝:經典案例

淺拷貝 — 預設行為,共享資源指標


class ShallowString {
    char *data;
    size_t len;
public:
    ShallowString(const char *s) : len(strlen(s)) {
        data = new char[len+1];
        strcpy(data, s);
    }
    // 無自訂拷貝建構函數 → 編譯器生成淺拷貝
    ~ShallowString() { delete[] data; }
};

void TestShallow() {
    ShallowString a("Hello");
    ShallowString b = a;   // 淺拷貝:b.data == a.data
    // a, b 離開作用域 → ~ShallowString() 呼叫兩次 delete[] 同一記憶體 → Undefined Behavior (雙重釋放)
}

深拷貝 — 分配新記憶體,複製內容


class DeepString {
    char *data;
    size_t len;
public:
    DeepString(const char *s) : len(strlen(s)) {
        data = new char[len+1];
        strcpy(data, s);
    }
    // 1. 拷貝建構函數
    DeepString(const DeepString& other) : len(other.len) {
        data = new char[len+1];          // 分配新記憶體
        strcpy(data, other.data);        // 複製內容
    }
    // 2. 拷貝指派運算子 (Copy Assignment)
    DeepString& operator=(const DeepString& other) {
        if (this != &other) {            // 自我指派檢查
            delete[] data;               // 釋放舊資源
            len = other.len;
            data = new char[len+1];
            strcpy(data, other.data);
        }
        return *this;
    }
    // 3. 解構函數
    ~DeepString() { delete[] data; }
};

三法則 / 五法則 / 零法則

法則適用版本核心內容口訣
三法則C++98若需自訂解構函數、拷貝建構函數、拷貝指派運算子三者任一,通常三者都需自訂「管一管三」
五法則C++11三法則 + 移動建構函數 + 移動指派運算子「資源擁有者需完整掌控移動語意」
零法則C++11+若類別不直接管理資源 (成員皆為 RAII 類型),完全不宣告 五大特殊函數,讓編譯器生成預設版本「不寫最好,用標準庫」

現代 C++ 最佳實踐:Rule of Zero + RAII


#include <memory>
#include <string>
#include <vector>

// ✅ 零法則範例:不直接持有原始指標
class NetworkBuffer {
    std::vector buffer_;      // RAII 管理記憶體
    std::string remote_addr_;       // RAII 管理字串
    std::unique_ptr sock_;  // 獨佔語意,不可拷貝 (預設刪除拷貝)
public:
    NetworkBuffer(size_t size, std::string addr)
        : buffer_(size), remote_addr_(std::move(addr)) {}
    // 編譯器自動生成:移動建構/指派、解構
    // 拷貝被刪除 (因 unique_ptr),符合獨佔語意
};

// 若需可拷貝的資源擁有者 → shared_ptr
class SharedBuffer {
    std::shared_ptr> buffer_;
    std::string remote_addr_;
public:
    SharedBuffer(size_t size, std::string addr)
        : buffer_(std::make_shared>(size)), remote_addr_(std::move(addr)) {}
    // 編譯器生成:拷貝 (引用計數+1)、移動、解構 — 全自動正確
};

進階議題

1. 虛擬拷貝 (Virtual Copy Constructor) — 多型拷貝


class Shape {
public:
    virtual ~Shape() = default;
    virtual std::unique_ptr Clone() const = 0; // 純虛擬拷貝
};

class Circle : public Shape {
    double r_;
public:
    Circle(double r) : r_(r) {}
    std::unique_ptr Clone() const override {
        return std::make_unique(*this); // 呼叫衍生類拷貝建構函數
    }
};

void Duplicate(const Shape& s) {
    auto copy = s.Clone(); // 執行期多型決定實際類型
}

2. 切割問題 — 以值傳遞基類參數


void BadFunc(Base b) { }  // 衍生類物件傳入 → 只拷貝 Base 部分,衍生成員切掉
void GoodFunc(const Base& b) { } // 參考/指標保持多型

3. explicit 拷貝建構函數 (C++23 提案 / 部分編譯器擴充)

防止隱含轉換:explicit MyClass(const MyClass&); 需顯式 MyClass b(a);,禁止 MyClass b = a;。

4. std::is_trivially_copyable、std::is_copy_constructible 型別特徵


static_assert(std::is_trivially_copyable_v);      // memcpy 安全
static_assert(std::is_copy_constructible_v); // 可拷貝
static_assert(!std::is_copy_constructible_v>); // 不可拷貝

常見錯誤避坑指南

錯誤後果修正
只寫拷貝建構函數,忘寫拷貝指派a = b 仍為淺拷貝 → 雙重釋放五法則全實作,或改用 RAII 成員 (零法則)
拷貝指派未檢查自我指派a = a 導致資源先釋放再複製同一記憶體 → 崩潰if (this != &other) 或 Copy-and-Swap 慣用語
拷貝建構函數參數非 const無法綁定暫時物件/常數參考,標準容器要求 const T&固定為 const T&
基類解構函數非虛擬透過基類指標刪除衍生物件 → 僅呼叫基類解構,資源洩漏基類 virtual ~Base()
以值傳遞多型參數物件切割,失去衍生資訊改以 const&、shared_ptr、unique_ptr 傳遞

Copy-and-Swap 慣用語:強烈例外安全的指派實作


class DeepString {
    char *data;
    size_t len;
    friend void swap(DeepString& a, DeepString& b) noexcept {
        std::swap(a.data, b.data);
        std::swap(a.len, b.len);
    }
public:
    DeepString& operator=(DeepString other) { // 以值傳遞 → 自動拷貝/移動建構
        swap(*this, other);                   // 交換資源
        return *this;                         // other 解構釋放舊資源
    }
};

優點:自我指派安全、強烈例外安全 (拷貝失敗不改變 *this)、程式碼簡潔、統一拷貝/移動路徑。

結語

物件拷貝是 C++ 資源管理的核心戰場。現代 C++ 核心哲學:盡量遵循零法則,用標準庫 RAII 類型 (string, vector, unique_ptr, shared_ptr) 替代原始指標;必須自管資源時,完整實作五法則並採用 Copy-and-Swap;多型層級提供虛擬 Clone()。內化這些模式,即能寫出無記憶體洩漏、無雙重釋放、例外安全、效能最佳的 C++ 程式碼。

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