類別物件的複製是 C++ 核心語意之一。內建型別逐位複製即可,但類別擁有成員變數、動態資源、不變式,複製語意必須由複製建構子 定義。本文詳細說明預設行為、自訂實作、深淺複製抉擇與現代最佳實踐。
class MyClass {
public:
// 標準形式:const 參考
MyClass(const MyClass& other);
// 罕見非 const 形式 (舊代碼/特殊需求)
MyClass(MyClass& other);
};
const,表示不修改來源物件若類別未宣告任何複製建構子,編譯器自動產生一個,執行逐成員複製:
std::string 等擁有正確複製語意的型別class Point {
int x, y; // 內建型別,逐位複製即正確
public:
Point(int x_, int y_) : x(x_), y(y_) {}
// 編譯器產生的複製建構子完美運作
};
| 情況 | 問題 | 解決方向 |
|---|---|---|
| 擁有原始指標成員 | 預設淺複製 → 雙重釋放/記憶體洩漏 | 深複製 (配置新記憶體複製內容) 或 共享擁有權 (引用計數/智慧指標) |
| 擁有獨佔資源控制代碼 | 複製後雙方關閉同一資源 | 禁止複製 (=delete) 或 移動語意轉移擁有權 |
| 需維護不變式 | 逐成員複製可能破壞類別約束 | 自訂邏輯驗證/修正 |
| 基類為多型/虛解構子 | 切片問題 | 禁止複製,改用 clone() 虛函數 |
class StringBuffer {
char* data;
size_t len;
public:
StringBuffer(const char* s = "") {
len = strlen(s);
data = new char[len + 1];
memcpy(data, s, len + 1);
}
// 複製建構子:深複製
StringBuffer(const StringBuffer& other) {
len = other.len;
data = new char[len + 1];
memcpy(data, other.data, len + 1);
printf("複製建構子被呼叫\n");
}
// 複製指派運算子 (三大法則)
StringBuffer& operator=(const StringBuffer& other) {
if (this != &other) { // 自我指派檢查
delete[] data;
len = other.len;
data = new char[len + 1];
memcpy(data, other.data, len + 1);
}
return *this;
}
~StringBuffer() { delete[] data; }
};
若類別管理資源,需宣告五個特殊成員函數:
class ResourceHolder {
Resource* ptr;
public:
ResourceHolder() : ptr(new Resource) {}
~ResourceHolder() { delete ptr; }
// 複製建構子
ResourceHolder(const ResourceHolder& other) : ptr(new Resource(*other.ptr)) {}
// 複製指派
ResourceHolder& operator=(const ResourceHolder& other) {
if (this != &other) { *ptr = *other.ptr; }
return *this;
}
// 移動建構子
ResourceHolder(ResourceHolder&& other) noexcept : ptr(other.ptr) { other.ptr = nullptr; }
// 移動指派
ResourceHolder& operator=(ResourceHolder&& other) noexcept {
if (this != &other) { delete ptr; ptr = other.ptr; other.ptr = nullptr; }
return *this;
}
};
盡可能不寫任何特殊成員函數,改用 RAII 型別封裝資源:
class ModernClass {
std::unique_ptr ptr; // 獨佔擁有權,不可複製,可移動
std::shared_ptr shared; // 共享擁有權,可複製
std::vector buffer; // 自動管理記憶體
std::string name; // 自動管理字串
// 編譯器自動產生正確的複製/移動/解構行為
};
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 可保留移動語意
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
| 錯誤 | 徵兆 | 檢測方法 |
|---|---|---|
| 淺複製導致雙重 free | 程式在解構時崩潰 (double free) | Valgrind/AddressSanitizer、啟用 CRT Debug Heap |
| 忘記複製指派運算子 | a = b 後資源洩漏/雙重釋放 | 靜態分析 (Cppcheck, Clang-Tidy) 警告 |
| 基類複製建構子非虛擬 | 多型物件切片 | Code Review、單元測試驗證型別完整性 |
| 自我指派未檢查 | 指派給自己時資源提前釋放 | 單元測試 obj = obj |
複製建構子定義了類別的值語意。現代 C++ 強調以 RAII 替代手動資源管理,讓編譯器自動合成正確操作 (Rule of Zero)。若必須手寫,嚴格遵循 Rule of Five,善用 = default/= delete 表達意圖,並以 sanitizer 驗證記憶體安全。