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)
};
void Func(MyClass obj); → Func(instance);MyClass Create(); → MyClass obj = Create(); (可能觸發 RVO/NRVO 優化省略拷貝)MyClass b = a;、MyClass b(a);、MyClass b{a};vector.push_back(a)、insert、resize、reallocate 時元素搬移。[obj] { ... } (預設以值捕獲)
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 類型),完全不宣告 五大特殊函數,讓編譯器生成預設版本 | 「不寫最好,用標準庫」 |
#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)、移動、解構 — 全自動正確
};
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(); // 執行期多型決定實際類型
}
void BadFunc(Base b) { } // 衍生類物件傳入 → 只拷貝 Base 部分,衍生成員切掉
void GoodFunc(const Base& b) { } // 參考/指標保持多型
explicit 拷貝建構函數 (C++23 提案 / 部分編譯器擴充)防止隱含轉換:explicit MyClass(const MyClass&); 需顯式 MyClass b(a);,禁止 MyClass b = a;。
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 傳遞 |
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++ 程式碼。