文章

C++拷贝构造与拷贝赋值

拷贝构造用新对象初始化,拷贝赋值给已有对象赋值,二者都复制对象内容,管理资源时需深拷贝防止共享问题。

C++拷贝构造与拷贝赋值

C++ 拷贝构造与拷贝赋值

拷贝构造函数

在 C++ 中,拷贝构造函数(Copy Constructor) 是一种特殊的构造函数,用于使用一个已有对象来初始化一个新对象。它的典型声明形式如下:

1
ClassName(const ClassName& other);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Person {
public:
    string name;

    // 构造函数
    Person(string n) : name(n) {}

    // 拷贝构造函数
    Person(const Person& other) {
        cout << "拷贝构造函数调用" << endl;
        name = other.name;
    }
};

int main() {
    Person p1("Alice");
    Person p2 = p1; // 调用拷贝构造函数
    cout << p2.name << endl;
    return 0;
}
  • 拷贝构造函数的第一个参数必须是“对自身类型的const引用”或“引用”,不能是按值传递,否则会导致无限递归调用(编译失败)

  • 几乎所有正常使用场景下,拷贝构造函数的参数都是 const T&

    • 支持 const 对象复制: 如果不写 const,那 MyClass obj2 = obj1; 中如果 obj1const,就不能调用 MyClass(MyClass&)

      ==C++ 引用匹配规则==

      • const T& 可以绑定到
        • const 左值对象
        • const 左值对象
        • 临时值(右值)
      • T&(非常量引用)只能绑定到
        • const 左值对象
        • 不能绑定 const 对象或临时对象
    • 拷贝操作本质上不应修改源对象: 加上 const 表示“拷贝源是只读的”,更安全、符合语义。
    • 和 STL、标准库兼容性更好: 所有标准容器类、算法等内部实现都假设拷贝构造是 const T&

拷贝构造函数调用时机

  • 用一个对象初始化另一个对象(拷贝初始化):Person p2 = p1;

  • 将对象按值传递给函数时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
public:
    string name;

    Person(string n) : name(n) {}

    Person(const Person& other) {
        cout << "拷贝构造函数被调用" << endl;
        name = other.name;
    }
};

// 当 greet(a) 被调用时,a 会被拷贝一份,传给 p
// 在 greet 里面操作的是 a 的副本,不会影响 a 本身
void greet(Person p) {
    cout << "Hello " << p.name << endl;
}

int main() {
    Person a("Alice");
    greet(a);  // 这里会调用拷贝构造函数
}
  • 从函数按值返回对象时。
1
2
3
4
5
6
7
8
9
Person makePerson() {
    Person temp("Bob");
    return temp;  // 返回对象副本,会调用拷贝构造函数(或优化)
}

int main() {
    // 这个返回值会被拷贝到主函数中的变量 b
    Person b = makePerson();  // 这里也会调用拷贝构造函数(或优化)
}

合成拷贝构造函数

在 C++ 中,合成拷贝构造函数(synthesized copy constructor)是指编译器自动为一个类生成的拷贝构造函数,用于在对象复制时拷贝成员变量。其形式通常如下:

1
ClassName(const ClassName& other);
合成拷贝构造函数的生成条件

编译器会在没有用户自定义拷贝构造函数的前提下,自动合成一个拷贝构造函数,前提是:

  • 所有成员变量的拷贝构造函数也都是可访问的;
    • 成员的拷贝构造是 private
    • 成员的拷贝构造是 protected,当前类不是子类或友元
    • 成员类禁止拷贝(被 delete
    • 私有成员类的拷贝构造是 private,但当前类不是它的友元
  • 没有被 = delete 的特殊成员函数影响(例如:移动构造被删除,可能会影响是否合成等);
成员函数写/删/自定义后影响
拷贝构造(const T&)手写后,编译器不再自动生成移动构造
移动构造(T&&)手写后,编译器不再自动生成拷贝构造
删除移动构造拷贝构造仍然可以合成(只要合法)
删除拷贝构造移动构造仍然可以合成(只要合法)
  • 类中没有继承或成员是不可复制的对象。
类型是否可复制说明
std::mutex明确删除了拷贝构造和赋值
自定义类 A(const A&) = delete手动禁用复制
继承自不可复制基类基类拷贝构造被删除或私有
含有以上类为成员或基类传播不可复制性
合成拷贝构造函数的行为
  • 对所有非静态成员变量执行逐个拷贝(member-wise copy)
  • 如果有基类,也会调用基类的拷贝构造函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct A {
    int x;
};

struct B {
    A a;
    int y;
    // 编译器会自动生成:
    // B(const B& other) : a(other.a), y(other.y) {}
};

void test() {
    B b1 = { {42}, 7 };
    B b2 = b1;  // 调用合成拷贝构造函数
}

与默认关键字 = default 的关系

可以显示告诉编译器合成一个:

1
2
3
4
struct C {
    int x;
    C(const C&) = default;  // 明确使用合成版本
};

也可以禁用:

1
2
3
4
struct D {
    int x;
    D(const D&) = delete;  // 禁止拷贝构造
};

注意事项

  • 合成拷贝构造函数是浅拷贝,对指针成员变量只拷贝地址,不复制指针所指内容;
  • 对于资源管理类(比如类中有 new 分配的内存),需要自己定义拷贝构造函数以防止资源泄漏或双重释放
  • 在 C++11 之后,如果自定义了移动构造函数或移动赋值函数,但没有提供拷贝构造,拷贝构造函数可能不会被自动合成。

默认拷贝构造函数

名称定义方式本质行为区别点
合成拷贝构造函数什么都不写浅拷贝编译器隐式生成
默认拷贝构造函数= default 明确指定浅拷贝程序员显式要求编译器合成

自定义拷贝构造函数的典型用途

如果的类中包含指针或管理动态资源(如内存、文件、网络句柄等),就应该自定义拷贝构造函数来实现深拷贝(deep copy)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyArray {
private:
    int* data;
    int size;

public:
    MyArray(int s) : size(s) {
        data = new int[size];
    }

    // 拷贝构造函数 - 深拷贝
    MyArray(const MyArray& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; ++i)
            data[i] = other.data[i];
    }

    ~MyArray() {
        delete[] data;
    }
};

如何避免拷贝构造开销

引用传参

默认传参是「值传递」,会调用拷贝构造函数。但如果用「引用」来传递参数,就不会拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person {
public:
    string name;
    Person(string n) : name(n) {}

    Person(const Person& other) {
        cout << "拷贝构造函数调用" << endl;
        name = other.name;
    }
};

void greet(const Person& p) {  // 使用 const 引用避免拷贝
    cout << "Hello " << p.name << endl;
}

int main() {
    Person a("Alice");
    greet(a);  // 不会调用拷贝构造函数
}

为什么加 const

  • 防止函数修改 a
  • 允许函数接收临时对象(如 greet(Person("Temp")))。
    • C++ 为了安全性,只允许把临时对象绑定到 const T&
    • 不允许绑定到 T&(非常量引用),因为不能修改临时对象。
移动语义(C++11)

如果一个对象是临时的、马上就要销毁了,那就没有必要复制它的资源,而是可以“移动”它的资源到新对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MyArray {
public:
    int* data;
    int size;

    MyArray(int s) : size(s) {
        data = new int[size];
        cout << "构造" << endl;
    }

    // 拷贝构造(深拷贝)
    MyArray(const MyArray& other) {
        cout << "拷贝构造" << endl;
        size = other.size;
        data = new int[size];
        for (int i = 0; i < size; ++i)
            data[i] = other.data[i];
    }

    // 移动构造函数(转移资源)
    MyArray(MyArray&& other) noexcept {
        cout << "移动构造" << endl;
        data = other.data;
        size = other.size;
        other.data = nullptr;  // 防止析构时重复释放
        other.size = 0;
    }

    ~MyArray() {
        delete[] data;
    }
};

MyArray createArray() {
    MyArray temp(10);  // 临时对象
    return temp;       // 会触发移动构造
}

int main() {
    MyArray a = createArray();  // 移动构造,而不是拷贝
}

有了移动构造函数,编译器优先选用移动构造:

  • 如果一个类型同时有拷贝构造移动构造,当对象是右值(临时对象、将亡值)*时,编译器会优先调用*移动构造函数

  • 当对象是左值时,调用的还是拷贝构造函数

  • 如果没有定义移动构造函数,或者移动构造不可用(被删除或不可访问),才会调用拷贝构造函数。

移动构造 vs 拷贝构造

特性拷贝构造函数移动构造函数(C++11)
参数类型const T&T&&(右值引用)
复制行为分配新资源并复制把资源“转移”给新对象
性能较慢(复制内容)快(只是转移指针)
触发条件对临时对象初始化新对象优先触发(如果定义了它)

拷贝赋值运算符

拷贝赋值运算符是一个特殊的成员函数,用于定义对象之间通过 = 进行赋值的行为:

1
T& operator=(const T& other);

表示:将 other 的值赋给当前对象(*this),并返回对当前对象的引用。

合成拷贝赋值运算符

如果用户没有显式定义赋值运算符,编译器会自动合成一个,前提是:

  • 所有成员的拷贝赋值运算符也都是可访问的;
  • 没有成员被 =delete
  • 没有某些限制(如成员是不可复制对象);

合成形式:

1
T& operator=(const T& other);

其行为通常是逐成员赋值(浅拷贝),如下所示:

1
2
3
4
5
6
7
8
9
10
11
class A {
public:
    int x;
    std::string y;
};
// 编译器会自动生成如下函数:
A& A::operator=(const A& other) {
    this->x = other.x;
    this->y = other.y;
    return *this;
}

注意:

  • 如果定义了 移动赋值函数、析构函数、拷贝构造函数等,其他特殊成员函数可能不会自动生成(遵循「Rule of Five」)
  • 可以使用 = default 显式要求编译器生成:
1
A& operator=(const A&) = default;

也可以禁止赋值:

1
A& operator=(const A&) = delete;

重载拷贝赋值运算符

可以重载拷贝赋值运算符以实现自定义的资源管理逻辑,例如深拷贝、日志打印、自管理内存等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass {
public:
    int* data;

    MyClass(int val = 0) {
        data = new int(val);
    }

    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {             // 处理自赋值
            delete data;                  // 清除旧资源
            data = new int(*other.data);  // 分配并拷贝新资源
        }
        return *this;
    }

    ~MyClass() {
        delete data;
    }
};
  • 返回类型是 T&(允许链式赋值)
  • 参数必须是 const T&
  • 一定要检查 this != &other(防止自赋值破坏)

对比

拷贝构造函数 VS 拷贝赋值运算符

特性拷贝构造函数拷贝赋值运算符
目的创建一个对象,并用已有对象初始化它将一个已有对象的值赋给另一个已存在的对象
典型签名T(const T& other);T& operator=(const T& other);
返回值类型无返回值返回 T&(支持链式赋值)
调用时机对象定义时: T b = a;对象已存在后赋值: b = a;
对象状态用于构造新对象用于更新已存在对象
可省略调用✅ 是,可被优化掉(RVO/NRVO)❌ 否,赋值行为不可省略
是否可自动合成✅ 是✅ 是
是否会检查自赋值❌ 一般不需处理✅ 推荐处理(if (this != &other))

拷贝构造函数 vs 拷贝赋值运算符 vs 移动构造函数 vs 移动赋值运算符

特性拷贝构造函数 T(const T&)拷贝赋值运算符 T& operator=(const T&)移动构造函数 T(T&&)移动赋值运算符 T& operator=(T&&)
目的用已有对象初始化新对象将已有对象的值赋给已存在对象用将亡对象初始化新对象将将亡对象的值赋给已存在对象
参数类型const T&(左值引用)const T&T&&(右值引用)T&&
何时调用定义新对象时传入左值已存在对象被赋值左值定义新对象时传入右值已存在对象被赋值右值
对象状态正在构造已构造正在构造已构造
是否涉及资源拷贝✅ 是(深/浅拷贝)✅ 是❌ 通常“掏走资源”❌ 通常“掏走资源”
是否有返回值❌ 无✅ 有(返回 *this❌ 无✅ 有(返回 *this
典型用途复制传参、返回值优化失败时覆盖已有对象(如 a = b;避免不必要的复制优化临时对象赋值(如 a = std::move(b);
是否自动生成✅ 是(符合条件时)✅ 是✅ 是(符合条件时)✅ 是(符合条件时)
是否可删除/默认声明✅ 支持 =delete / =default✅ 支持✅ 支持✅ 支持
常用策略Rule of 5Rule of 5Rule of 5Rule of 5

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <iostream>

struct T {
    int* data;

    // 构造
    T(int val = 0) {
        data = new int(val);
        std::cout << "Default Constructor\n";
    }

    // 拷贝构造
    T(const T& other) {
        data = new int(*other.data);
        std::cout << "Copy Constructor\n";
    }

    // 拷贝赋值
    T& operator=(const T& other) {
        if (this != &other) {
            delete data;
            data = new int(*other.data);
        }
        std::cout << "Copy Assignment\n";
        return *this;
    }

    // 移动构造
    T(T&& other) noexcept {
        data = other.data;
        other.data = nullptr;
        std::cout << "Move Constructor\n";
    }

    // 移动赋值
    T& operator=(T&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        std::cout << "Move Assignment\n";
        return *this;
    }

    ~T() {
        delete data;
        std::cout << "Destructor\n";
    }
};

int main() {
    T a(10);
    T b = a;              // Copy Constructor
    T c;
    c = a;                // Copy Assignment
    T d = std::move(a);   // Move Constructor
    T e;
    e = std::move(b);     // Move Assignment
}

初始化类型

按语义分类(标准定义的初始化类型)

初始化方式简介说明示例
默认初始化default-initialization用于未显式初始化的变量(如类成员或局部变量),是否被初始化取决于类型和上下文int x;MyClass obj;
值初始化value-initialization初始化为“零”或调用默认构造函数,常用于 T obj{};T obj = T();int x{};T obj = T();
拷贝初始化copy-initialization使用 = 语法进行初始化,允许调用拷贝构造、隐式类型转换等T obj = other;
直接初始化direct-initialization使用括号语法初始化,优先匹配构造函数T obj(arg);
列表初始化list-initialization使用花括号 {},可以分为“直接列表初始化”和“拷贝列表初始化”T obj{arg};T obj = {arg};
聚合初始化aggregate-initialization针对聚合类型,按成员顺序使用 {} 初始化,无需构造函数Point p = {1, 2};
零初始化zero-initialization所有字节置为 0,仅适用于静态存储对象或 value-init 时被用作子步骤static int x;
引用绑定初始化reference binding初始化引用,可能涉及临时对象绑定const T& ref = value;

按语法分类(写法维度)

写法所属初始化类型(可能)
T obj;默认初始化(局部变量)或零初始化(静态)
T obj = value;拷贝初始化
T obj(value);直接初始化
T obj{};值初始化 / 直接列表初始化
T obj = {};值初始化 / 拷贝列表初始化
T obj = T();值初始化(经典 idiom)
T obj = {a, b};拷贝列表初始化
T obj{a, b};直接列表初始化
T arr[] = {1, 2, 3};聚合初始化(数组)
MyStruct s = {1, 2};聚合初始化(聚合类)

实用对比重点

关键点直接初始化 (T obj(arg))拷贝初始化 (T obj = arg)列表初始化 ({})
是否调用构造函数✅(更严格)
是否支持隐式转换视情况而定
是否允许 narrowing❌ 会报错(如 {3.14}int
是否支持 explicit 构造函数直接 {} 可以,= 不行
1
2
3
4
5
6
7
8
9
10
11
12
struct A {
    A(int) {}
    explicit A(double) {}
};

A a1 = 1;      // 拷贝初始化,调用 A(int)
A a2(1);       // 直接初始化,调用 A(int)
A a3 = 1.5;    // 拷贝初始化,调用 A(double),但不能用 explicit!
A a4(1.5);     // 直接初始化,explicit 构造函数可用

A a5 = {1};    // 拷贝列表初始化(不能用 explicit)
A a6{1};       // 直接列表初始化(可用 explicit)

拷贝初始化

拷贝初始化的语法形式

1
T obj = expr;
  • T 是目标对象类型
  • expr 是用于初始化的表达式,可以是 T 类型的对象、其他类型的值、临时对象等

拷贝初始化的工作机制

  • 编译器会尝试使用 expr 通过隐式转换生成一个类型为 T 的临时对象(或直接就是 T 类型对象)
  • 然后用这个临时对象调用 T 的拷贝构造函数(或移动构造函数)来初始化 obj
  • 编译器允许通过隐式类型转换构造 T 对象,所以 expr 不必是 T 类型
  • 现代编译器通常会进行复制省略(Copy Elision),优化掉临时对象,直接初始化 obj,避免调用拷贝构造

拷贝初始化与直接初始化的区别

初始化方式语法示例是否允许调用 explicit 构造函数是否调用拷贝构造函数
拷贝初始化T obj = expr;不允许通常会调用,但可能被省略
直接初始化T obj(expr);允许不调用拷贝构造,直接调用对应构造函数
列表初始化T obj{expr};允许不调用拷贝构造,调用对应构造函数

拷贝初始化发生的时机

  • 使用 = 定义变量
  • 将一个对象作为实参传递给一个非引用类型的形参
  • 从一个返回类型为非引用类型的函数返回一个对象
  • 用花括号列表初始化一个数组中的元素或一个聚合类中的成员,也是拷贝初始化

拷贝初始化为啥不能用explict?

  • 本质原因:为了防止意外调用显式构造函数。
  • explicit 的含义本身就是“不允许隐式调用”。而拷贝初始化(T obj = arg;是隐式初始化语法
  • 直接初始化直接列表初始化 是“显式语法”,允许调用 explicit 构造函数。

拷贝初始化一定会调用拷贝构造函数吗?

虽然名字叫“拷贝初始化”,但 “拷贝初始化”并不总是调用拷贝构造函数,它只是语法形式为 T obj = something; 的一种初始化方式,实际调用什么构造函数、是否优化构造,全看上下文

可能的实际行为(几个典型场景)

场景是否调用拷贝构造函数说明
T obj = otherT;otherTT 类型)✅ 是,调用拷贝构造最常见的拷贝构造使用场景
T obj = value;value 是其他类型)❌ 否,可能调用转换构造会寻找 T::T(U) 形式的构造函数(可能隐式转换)
T obj = T(123);❌ 否,可能被优化编译器常常应用 复制省略(copy elision)
T obj = funcReturningT();❌ 否,可能被优化如果启用了 NRVO(返回值优化),拷贝构造会被省略
T obj = {123};(列表初始化)❌ 否,使用花括号构造这是拷贝列表初始化,不调用拷贝构造

交换操作

swap 函数应该调用 swap,而不是 std::swap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <string>
#include <utility> // for std::swap

// 自定义类 HasPtr
class HasPtr {
    // 声明 swap 为友元函数,使其可以访问私有成员
    friend void swap(HasPtr&, HasPtr&);
private:
    std::string *ps;
    int i;
};

// 自定义 swap(HasPtr&, HasPtr&),用于高效交换 HasPtr 成员
inline void swap(HasPtr& lhs, HasPtr& rhs) {
    using std::swap;
    // 调用标准库 swap 交换指针和 int
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
}

// 包含 HasPtr 成员的类 Foo
class Foo {
public:
    HasPtr h;
};

// swap1: 强制使用 std::swap
void swap1(Foo &lhs, Foo &rhs) {
    // 这里直接写 std::swap,不允许 ADL 查找用户自定义 swap 函数
    // 最终会调用 std::swap(lhs, rhs),即执行拷贝构造 + 析构(可能效率较低)
    std::swap(lhs, rhs);
}

// swap2: 推荐方式,支持 ADL 查找
void swap2(Foo &lhs, Foo &rhs) {
    using std::swap;
    // 此处 swap(lhs, rhs) 会触发 ADL:
    // 如果用户为 Foo 或其成员类型(如 HasPtr)定义了 swap,则优先使用这些版本
    // 否则才退回使用 std::swap
    swap(lhs, rhs);
}

// ✅ 推荐使用 swap2 的写法,因为它支持 ADL,可以调用用户为 Foo 或其成员提供的自定义 swap 函数
// ❌ 不推荐 swap1 的写法,在有自定义 swap 的类中会丢失优化机会

拷贝并交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

class HasPtr {
public:
    HasPtr &operator=(HasPtr);

    friend void swap(HasPtr &, HasPtr &);

private:
    std::string *ps;
    int i;
};

inline void swap(HasPtr &lhs, HasPtr &rhs) {
    using std::swap;
    swap(lhs.ps, rhs.ps); // 交换指针
    swap(lhs.i, rhs.i);   // 交换值
}

HasPtr &HasPtr::operator=(HasPtr rhs) {
    swap(*this, rhs);
    return *this;
}

假设有:

1
2
3
HasPtr a;
HasPtr b;
a = b;  // 会发生什么?

流程如下:

  1. b 被值传递为参数 rhs,调用拷贝构造函数生成临时副本 rhs
  2. swap(*this, rhs):即交换 arhs 的资源。
  3. rhs 被销毁,原本 a 的旧资源被释放。
  4. a 拥有了 b 的内容(副本)。

这个技巧的核心思路是:

  1. 参数按值传递:调用这个函数时,会自动对传入参数 rhs 调用一次拷贝构造函数(产生一个副本 rhs)。
  2. 交换资源:使用 swap(*this, rhs),交换当前对象的资源和副本对象的资源。
  3. 结束后 rhs 离开作用域,被析构,它原本持有的(旧的)资源会被自动释放。

这样,对象 *this 拥有了新内容,旧内容随着 rhs 的销毁而安全释放。

为什么这样写?

  • 异常安全

    • 任何异常都发生在拷贝阶段(参数传进来前就已经拷贝好了),

    • 如果失败,原对象不变,保证强异常安全保证(Strong Exception Guarantee)。

  • 代码复用
    • 只需要定义一个 swap 和拷贝构造函数,就能实现安全的赋值逻辑,无需重复资源释放和分配代码。
  • 自给自足,简洁优雅

    • 避免手动检查自赋值 if (this != &rhs)

    • 避免资源泄露和中间状态出错。

    • 使用标准的 swap 机制管理所有资源交接。

对比传统写法

传统写法:手动释放并复制资源

1
2
3
4
5
6
7
8
HasPtr& HasPtr::operator=(const HasPtr& rhs) {
    if (this != &rhs) {              // 处理自赋值
        delete ps;                   // 释放原资源
        ps = new std::string(*rhs.ps); // 分配新资源
        i = rhs.i;                   // 拷贝数据成员
    }
    return *this;
}

特点:

问题点说明
❌ 冗长要显式写释放、分配逻辑
❌ 容易出错少写 if (this != &rhs) 就炸了
❌ 异常不安全new 抛异常后,原资源已释放,程序处于不一致状态
✅ 性能略优避免一次副本(copy-and-swap 多了一次构造)
对比项传统写法copy-and-swap
代码简洁性❌ 需要写释放、分配、拷贝✅ 只需要一行 swap
异常安全❌ 不安全,容易资源泄露✅ 拷贝失败不影响原对象
自赋值处理❌ 需要显式判断✅ swap 不怕自赋值
可维护性❌ 难维护✅ 高内聚低耦合
依赖无额外要求需要定义 swap
性能✅ 少一次构造❌ 多一次副本(但可由编译器优化)
本文由作者按照 CC BY 4.0 进行授权