1. 引言:对象生命周期与 RAII

C++ 中,对象有明确的生命周期:它被创建,然后在使用结束后被销毁。构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数这六个函数(通常被称为特殊成员函数)就是用来管理这个生命周期的。它们控制着对象的:

  • 创建 (Construction)
  • 销毁 (Destruction)
  • 拷贝 (Copying)
  • 移动 (Moving)

它们是实现 C++ 核心设计哲学 RAII (Resource Acquisition Is Initialization) 的基石。RAII 意味着在对象的构造函数中获取资源(如内存、文件句柄、锁),并在其析构函数中释放资源,从而将资源的生命周期与对象的生命周期绑定在一起,避免资源泄漏。

2. 核心示例:一个简单的 Buffer

我们将使用一个简单的 Buffer 类来贯穿整个讲解。这个类在堆上分配了一块整数数组,因此它需要手动管理内存资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <algorithm> // For std::copy

class Buffer {
private:
int* _ptr;
size_t _size;

public:
// 我们将在这里填充这六个特殊成员函数

void print() const {
if (_ptr) {
for (size_t i = 0; i < _size; ++i) {
std::cout << _ptr[i] << " ";
}
}
std::cout << std::endl;
}
};

3. 构造函数 (Constructor)

  • 目的初始化一个新创建的对象,为其分配所需资源,并建立一个有效的初始状态。
  • 特征:函数名与类名相同,没有返回类型。
  • 何时调用:当一个新对象被创建时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在 Buffer 类中
public:
// 参数化构造函数
explicit Buffer(size_t size) : _ptr(new int[size]), _size(size) {
std::cout << "Constructor called (size=" << size << ")" << std::endl;
// 初始化 buffer 内容,例如全为0
for (size_t i = 0; i < _size; ++i) {
_ptr[i] = 0;
}
}

// 默认构造函数 (创建一个空 Buffer)
Buffer() : _ptr(nullptr), _size(0) {
std::cout << "Default Constructor called" << std::endl;
}
  • 重点:使用 成员初始化列表 (Member Initializer List)(即冒号 : 后面的部分)来初始化成员。这比在构造函数体 {} 内赋值更高效,对于 const 成员或引用成员来说是必须的。

4. 析构函数 (Destructor)

  • 目的:在对象生命周期结束时 清理 对象,释放其占有的资源。
  • 特征:函数名前面有一个波浪号 ~,没有返回类型,也没有参数。
  • 何时调用:当对象离开其作用域、delete一个指向对象的指针时,或程序结束时(对于全局/静态对象)。
1
2
3
4
5
6
7
// 在 Buffer 类中
public:
~Buffer() {
std::cout << "Destructor called (size=" << _size << ")" << std::endl;
delete[] _ptr; // 释放构造函数中分配的内存
_ptr = nullptr; // 好的实践,防止悬挂指针
}
  • 重点:如果你的类被用作基类,并且你希望通过基类指针 delete 派生类对象,那么析构函数 必须 声明为 virtualvirtual ~Buffer()),以防止资源泄漏。

5. 拷贝构造函数 (Copy Constructor)

  • 目的:使用一个 已存在的同类对象创建一个新的对象。这是“拷贝”语义的来源。
  • 特征:参数是此类的一个 const 引用。const 是因为我们不应该修改源对象,引用 & 是为了避免无限递归的拷贝。
  • 何时调用
    1. Buffer b2 = b1;Buffer b2(b1); (初始化)
    2. 函数按值传递对象:void func(Buffer b);
    3. 函数按值返回对象:Buffer func();
1
2
3
4
5
6
7
8
// 在 Buffer 类中
public:
// 拷贝构造函数 (实现深拷贝)
Buffer(const Buffer& other) : _ptr(new int[other._size]), _size(other._size) {
std::cout << "Copy Constructor called" << std::endl;
// 复制源对象 buffer 中的内容,而不是仅仅复制指针
std::copy(other._ptr, other._ptr + other._size, _ptr);
}
  • 重点:如果你不提供,编译器会生成一个默认的拷贝构造函数,它执行 浅拷贝(只复制指针 _ptr 的值,不复制其指向的数据),这会导致两个对象指向同一块内存,从而引发“重复释放”的严重错误。因此,对于管理资源的类,必须 实现深拷贝。

6. 拷贝赋值运算符 (Copy Assignment Operator)

  • 目的:将一个 已存在的对象 的值赋给另一个 已存在的对象
  • 特征:通常返回一个指向当前对象的引用 (*this) 以支持链式赋值 (a = b = c;)。
  • 何时调用b2 = b1; (赋值,此时 b1 和 b2 都已经存在)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在 Buffer 类中
public:
Buffer& operator=(const Buffer& other) {
std::cout << "Copy Assignment Operator called" << std::endl;

// 1. 检查自赋值 (非常重要!)
if (this == &other) {
return *this;
}

// 2. 释放当前对象已有的资源
delete[] _ptr;

// 3. 分配新资源并从源对象拷贝数据 (深拷贝)
_size = other._size;
_ptr = new int[other._size];
std::copy(other._ptr, other._ptr + other._size, _ptr);

// 4. 返回 *this
return *this;
}
  • 重点:自赋值检查 (if (this == &other)) 是防止在 b1 = b1; 这种情况下,提前 delete 掉自己的资源导致后续无法拷贝。

7. “三之法则” (The Rule of Three)

这是一个经典的 C++ 设计准则:
如果你需要显式声明析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,那么你几乎肯定需要将这三个都声明。
我们的 Buffer 类就是完美范例:因为它需要自定义析构函数来释放内存,所以它也必须自定义拷贝操作来实现深拷贝。


8. 移动构造函数 (Move Constructor) - C++11

  • 目的:从一个临时对象(右值,如函数返回值)“窃取”或“转移” 资源来构造一个新对象。这比深拷贝高效得多,因为它避免了内存的重新分配和数据的复制。
  • 特征:参数是一个非 const 的右值引用 (&&)。
  • 何时调用:当用一个右值初始化对象时,例如 Buffer b(create_buffer());Buffer b(std::move(another_buffer));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在 Buffer 类中
public:
// 移动构造函数
Buffer(Buffer&& other) noexcept
: _ptr(other._ptr), _size(other._size) {
std::cout << "Move Constructor called" << std::endl;

// 1. "窃取"资源
// 已经通过初始化列表完成

// 2. 将源对象置于一个有效的、可析构的状态
// 这是关键!防止源对象的析构函数释放我们刚刚"窃取"的资源
other._ptr = nullptr;
other._size = 0;
}
  • 重点:移动构造函数应该是 noexcept 的,这向编译器承诺它不会抛出异常,使得标准库容器(如 std::vector)在需要重新分配内存时可以安全地使用移动而非拷贝,从而获得巨大性能提升。

9. 移动赋值运算符 (Move Assignment Operator) - C++11

  • 目的:将一个临时对象(右值)的资源 “窃取” 给一个已存在的对象。
  • 特征:参数是右值引用 &&,返回 *this
  • 何时调用:当将一个右值赋给一个已存在的对象时,例如 b = create_buffer();b = std::move(another_buffer);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 在 Buffer 类中
public:
Buffer& operator=(Buffer&& other) noexcept {
std::cout << "Move Assignment Operator called" << std::endl;

if (this == &other) { // 检查自移动
return *this;
}

// 1. 释放当前对象的资源
delete[] _ptr;

// 2. "窃取"源对象的资源
_ptr = other._ptr;
_size = other._size;

// 3. 将源对象置于有效、可析构的状态
other._ptr = nullptr;
other._size = 0;

return *this;
}

10. “五之法则” (The Rule of Five) 与 “零之法则” (The Rule of Zero)

  • 五之法则 (Rule of Five):C++11 后的扩展。如果一个类需要自定义析构、拷贝或移动操作中的任何一个,那么它可能需要全部五个。

  • 零之法则 (Rule of Zero) - 现代 C++ 最佳实践
    你的类应该只负责业务逻辑,而将资源管理委托给专门的 RAII 类(如 std::vector, std::string, std::unique_ptr, std::shared_ptr)。
    如果遵循此法则,你的类通常 不需要 手动编写任何特殊成员函数,编译器自动生成的版本会做正确的事。

    使用零之法则重写 Buffer 类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <vector>

    class ModernBuffer {
    private:
    std::vector<int> _data; // std::vector 内部已经正确实现了所有五个特殊函数!
    public:
    ModernBuffer(size_t size) : _data(size) {} // 简洁
    // 不需要手动写析构、拷贝、移动函数!
    };

11. 总结

函数名称 签名示例 目的 触发示例 (b1, b2 已存在)
构造函数 Buffer(size_t size); 创建并初始化新对象 Buffer b1(10);
析构函数 ~Buffer(); 销毁对象,释放资源 (自动调用) }delete ptr;
拷贝构造函数 Buffer(const Buffer& other); other 创建一个 新对象 Buffer b3 = b1;
拷贝赋值运算符 Buffer& operator=(const Buffer& other); other 赋给一个 已存在对象 b2 = b1;
移动构造函数 (C++11) Buffer(Buffer&& other) noexcept; 从临时对象 other 窃取资源 创建新对象 Buffer b3 = create_buffer();
移动赋值运算符 (C++11) Buffer& operator=(Buffer&& other) noexcept; 从临时对象 other 窃取资源 给已存在对象 b2 = std::move(b1);