C++类的构造函数与operator=详解

次浏览

概述

C++ 类的特殊成员函数控制着对象的创建、复制、移动和销毁。理解构造函数(普通构造、拷贝构造、移动构造)和赋值运算符(拷贝赋值、移动赋值)的区别与联系,是掌握 C++ 资源管理的关键。


一、特殊成员函数概览

C++11 之后,一个类有六大特殊成员函数

函数 作用 形式
默认构造函数 无参创建对象 T()
析构函数 销毁对象 ~T()
拷贝构造函数 用左值初始化新对象 T(const T&)
移动构造函数 用右值初始化新对象 T(T&&)
拷贝赋值运算符 用左值赋值给已存在对象 T& operator=(const T&)
移动赋值运算符 用右值赋值给已存在对象 T& operator=(T&&)

二、构造函数家族

2.1 默认构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class String {
private:
    char* data;
    size_t len;

public:
    // 默认构造函数
    String() : data(nullptr), len(0) {
        std::cout << "Default constructor\n";
    }
};

String s1;           // 调用默认构造函数
String s2 = String(); // 调用默认构造函数(不是赋值!)
String s3{};         // 调用默认构造函数(现代风格)

2.2 拷贝构造函数(左值构造)

触发场景:用一个已存在的左值对象来初始化一个新对象

 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
class String {
private:
    char* data;
    size_t len;

public:
    String(const char* str = "") {
        len = strlen(str);
        data = new char[len + 1];
        memcpy(data, str, len + 1);
    }

    ~String() {
        delete[] data;
    }

    // 拷贝构造函数(左值构造)
    String(const String& other) : len(other.len) {
        std::cout << "Copy constructor (lvalue)\n";
        data = new char[len + 1];       // 分配新内存
        memcpy(data, other.data, len + 1); // 深拷贝
    }
};

String s1("hello");
String s2 = s1;      // 拷贝构造:用左值 s1 初始化新对象 s2
String s3(s1);       // 拷贝构造:用左值 s1 初始化新对象 s3
String s4 = String(s1); // 拷贝构造(可能是拷贝消除)

关键点

  • 参数是 const T&(左值引用)
  • 必须深拷贝,否则两个对象指向同一资源,析构时会 double free

2.3 移动构造函数(右值构造)

触发场景:用一个右值来初始化一个新对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class String {
private:
    char* data;
    size_t len;

public:
    // 移动构造函数(右值构造)
    String(String&& other) noexcept : data(other.data), len(other.len) {
        std::cout << "Move constructor (rvalue)\n";
        other.data = nullptr;  // 源对象置空,防止重复释放
        other.len = 0;
    }
};

String s1("hello");
String s2 = std::move(s1);  // 移动构造:s1 被转为右值
String s3 = get_string();   // 移动构造:返回值是右值

// 函数返回值优化
String create_string() {
    String temp("world");
    return temp;  // 可能触发移动构造(或RVO)
}

关键点

  • 参数是 T&&(右值引用)
  • 窃取资源,不分配新内存
  • 将源对象置于"有效但未定义"状态
  • 应标记 noexcept 以利于标准库优化

三、赋值运算符家族

3.1 拷贝赋值运算符

触发场景:用一个左值赋值给一个已存在的对象

 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
class String {
public:
    // 拷贝赋值运算符
    String& operator=(const String& other) {
        std::cout << "Copy assignment\n";
        
        if (this == &other) {  // 自赋值检查
            return *this;
        }
        
        // 释放旧资源
        delete[] data;
        
        // 深拷贝新资源
        len = other.len;
        data = new char[len + 1];
        memcpy(data, other.data, len + 1);
        
        return *this;  // 支持链式赋值
    }
};

String s1("hello");
String s2;
s2 = s1;  // 拷贝赋值:s1 是左值,s2 已存在

3.2 移动赋值运算符

触发场景:用一个右值赋值给一个已存在的对象

 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
class String {
public:
    // 移动赋值运算符
    String& operator=(String&& other) noexcept {
        std::cout << "Move assignment\n";
        
        if (this == &other) {  // 自赋值检查
            return *this;
        }
        
        // 释放旧资源
        delete[] data;
        
        // 窃取资源
        data = other.data;
        len = other.len;
        
        // 源对象置空
        other.data = nullptr;
        other.len = 0;
        
        return *this;
    }
};

String s1("hello");
String s2;
s2 = std::move(s1);  // 移动赋值:s1 转为右值
s2 = get_string();   // 移动赋值:返回值是右值

四、区别与联系

4.1 构造 vs 赋值的根本区别

特性 构造函数 赋值运算符
对象状态 对象不存在 对象已存在
触发时机 初始化时 赋值时
资源管理 无需释放旧资源 必须先释放旧资源
自身检查 不需要 必须检查自赋值
1
2
3
4
5
6
7
String s1("hello");   // 构造:s1 不存在 → 存在
String s2 = s1;       // 拷贝构造:s2 不存在 → 存在

String s3;
s3 = s1;              // 拷贝赋值:s3 已存在,先释放旧资源,再拷贝

s3 = s3;              // 自赋值!赋值运算符必须处理这种情况

4.2 左值 vs 右值的区别

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
String s1("hello");

// 左值:有名字、有地址、可取地址
String& ref = s1;            // s1 是左值
String s2 = s1;              // s1 是左值 → 拷贝构造
s2 = s1;                     // s1 是左值 → 拷贝赋值

// 右值:无名字、临时、不可取地址
String s3 = String("world"); // String("world") 是右值 → 可能移动构造或RVO
String s4 = std::move(s1);   // std::move(s1) 是右值 → 移动构造
s4 = get_string();           // 返回值是右值 → 移动赋值

4.3 完整示例:追踪所有调用

 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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <iostream>
#include <utility>

class Trace {
public:
    int* data;

    // 默认构造
    Trace() : data(new int(0)) {
        std::cout << "Default constructor\n";
    }

    // 带参构造
    Trace(int v) : data(new int(v)) {
        std::cout << "Parameterized constructor: " << v << "\n";
    }

    // 析构函数
    ~Trace() {
        std::cout << "Destructor: " << (data ? *data : -1) << "\n";
        delete data;
    }

    // 拷贝构造(左值构造)
    Trace(const Trace& other) : data(new int(*other.data)) {
        std::cout << "Copy constructor (lvalue): " << *other.data << "\n";
    }

    // 移动构造(右值构造)
    Trace(Trace&& other) noexcept : data(other.data) {
        std::cout << "Move constructor (rvalue): " << *data << "\n";
        other.data = nullptr;
    }

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

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

Trace create_trace(int v) {
    return Trace(v);  // RVO 或移动构造
}

int main() {
    std::cout << "=== 1. 默认构造 ===\n";
    Trace t1;

    std::cout << "\n=== 2. 带参构造 ===\n";
    Trace t2(42);

    std::cout << "\n=== 3. 拷贝构造(左值构造) ===\n";
    Trace t3 = t2;  // 左值 t2 → 拷贝构造

    std::cout << "\n=== 4. 移动构造(右值构造) ===\n";
    Trace t4 = std::move(t2);  // t2 转为右值 → 移动构造

    std::cout << "\n=== 5. 拷贝赋值 ===\n";
    t1 = t3;  // 左值 t3 → 拷贝赋值

    std::cout << "\n=== 6. 移动赋值 ===\n";
    t1 = create_trace(100);  // 返回值是右值 → 移动赋值

    std::cout << "\n=== 7. 析构 ===\n";
    return 0;
}

可能的输出

 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
=== 1. 默认构造 ===
Default constructor

=== 2. 带参构造 ===
Parameterized constructor: 42

=== 3. 拷贝构造(左值构造) ===
Copy constructor (lvalue): 42

=== 4. 移动构造(右值构造) ===
Move constructor (rvalue): 42

=== 5. 拷贝赋值 ===
Copy assignment: 42

=== 6. 移动赋值 ===
Parameterized constructor: 100
Move assignment: 100

=== 7. 析构 ===
Destructor: 100
Destructor: -1
Destructor: 42
Destructor: -1
Destructor: 0

五、Rule of Three / Five / Zero

5.1 Rule of Three(C++03)

如果类需要自定义以下任一函数,则三个都需要自定义

  • 析构函数
  • 拷贝构造函数
  • 拷贝赋值运算符
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Resource {
    int* data;
public:
    ~Resource() { delete data; }
    Resource(const Resource& other) { data = new int(*other.data); }
    Resource& operator=(const Resource& other) {
        delete data;
        data = new int(*other.data);
        return *this;
    }
};

5.2 Rule of Five(C++11)

在 Rule of Three 基础上,增加:

  • 移动构造函数
  • 移动赋值运算符
 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
class Resource {
    int* data;
public:
    ~Resource() { delete data; }
    
    Resource(const Resource& other) : data(new int(*other.data)) {}
    Resource& operator=(const Resource& other) {
        if (this != &other) {
            delete data;
            data = new int(*other.data);
        }
        return *this;
    }
    
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
};

5.3 Rule of Zero(现代 C++ 推荐)

使用智能指针和标准库容器,让编译器自动生成所有函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <memory>
#include <string>

class ModernResource {
    std::unique_ptr<int> data;
    std::string name;
public:
    ModernResource() : data(std::make_unique<int>(0)) {}
    ModernResource(int v, std::string n) 
        : data(std::make_unique<int>(v)), name(std::move(n)) {}
    
    // 不需要自定义任何特殊成员函数!
    // 编译器自动生成的版本完全正确
};

六、编译器自动生成规则

6.1 成员函数的生成规则(C++11)

用户声明 默认构造 析构 拷贝构造 拷贝赋值 移动构造 移动赋值
默认构造
析构
拷贝构造
拷贝赋值
移动构造
移动赋值

6.2 = default 和 = delete

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Widget {
public:
    Widget() = default;                           // 显式要求编译器生成
    Widget(const Widget&) = delete;               // 禁止拷贝构造
    Widget& operator=(const Widget&) = delete;    // 禁止拷贝赋值
    Widget(Widget&&) = default;                   // 显式生成移动构造
    Widget& operator=(Widget&&) = default;        // 显式生成移动赋值
    ~Widget() = default;
};

Widget w1;        // OK
Widget w2 = w1;   // 错误:拷贝构造被删除
Widget w3 = std::move(w1);  // OK:移动构造

七、常见错误与陷阱

7.1 返回局部对象的引用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// ❌ 错误:返回局部变量的引用
const String& bad_func() {
    String local("hello");
    return local;  // 悬空引用!
}

// ✅ 正确:返回值(触发移动或RVO)
String good_func() {
    String local("hello");
    return local;
}

7.2 移动后使用源对象

1
2
3
4
5
String s1("hello");
String s2 = std::move(s1);

// ❌ 危险:s1 处于"有效但未定义"状态
std::cout << s1.data;  // 可能崩溃或输出垃圾

7.3 忘记自赋值检查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ❌ 错误:没有自赋值检查
String& operator=(const String& other) {
    delete[] data;           // 如果 this == &other,data 已被删除!
    data = new char[other.len + 1];
    memcpy(data, other.data, other.len + 1);  // 读取已释放的内存!
    return *this;
}

// ✅ 正确:先检查自赋值
String& operator=(const String& other) {
    if (this == &other) return *this;
    delete[] data;
    data = new char[other.len + 1];
    memcpy(data, other.data, other.len + 1);
    return *this;
}

7.4 copy-and-swap 惯用法

一个优雅的实现方式,同时解决自赋值和异常安全:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class String {
public:
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(len, other.len);
    }

    // 统一的拷贝赋值(通过值传递)
    String& operator=(String other) noexcept {
        swap(other);  // 交换资源
        return *this;
        // other 析构时自动释放旧资源
    }
};

// 使用
String s1("hello");
s1 = String("world");  // 移动构造参数,然后交换
s1 = s1;               // 自赋值安全:拷贝一份,然后交换,没有问题

八、总结对照表

操作 函数签名 触发条件 对象状态 资源处理
拷贝构造 T(const T&) 左值初始化新对象 对象不存在 深拷贝
移动构造 T(T&&) 右值初始化新对象 对象不存在 窃取资源
拷贝赋值 T& operator=(const T&) 左值赋值已存在对象 对象已存在 释放旧+深拷贝
移动赋值 T& operator=(T&&) 右值赋值已存在对象 对象已存在 释放旧+窃取

记忆口诀

  • 构造 = 新生 → 不需要清理旧资源
  • 赋值 = 改造 → 必须先清理旧资源
  • 左值 = 拷贝 → 别人的东西不能抢
  • 右值 = 移动 → 临时的东西可以偷

参考资料

使用 Hugo 构建
主题 StackJimmy 设计