Skip to content

13. Copy Control

Copy control 不止管理 copy, 还管理 move 和 destroy。

Copy control 专门指负责对象资源管理与生命周期的五个特殊成员函数:copy constructor, move constructor, copy-assignment operator, move-assignment operator, 和 destructor。

The Copy Constructor

什么是 Copy Constructor?

Copy constructor 是一种构造函数,用同类型的另一个对象来初始化新对象。

copy constructor 的第一个参数必须是对同类对象的引用:

ClassName(const ClassName &);

什么时候会用到 copy initialization?

  1. 用一个对象初始化另一个对象

    string s1 = "hello";  // 如果是 direct initialization:string s1("hello");
    
  2. 形参不是引用,会触发 copy initialization

    void foo(string s) { }  // s 不是引用 → 必须进行 copy initialization
    
    int main() {
        string a = "hi";
        foo(a);  // copy initialization(a 用来初始化 s)
    }
    
  3. 返回一个对象时(不是引用或指针),也需要用 copy constructor 构造返回值对象

    string bar() {
        string s = "world";
        return s;   // copy initialization 用 s 初始化返回值对象
    }
    
  4. 大多数 STL 容器操作插入已有对象时

    vector<string> v;
    string a = "hi";
    
    v.push_back(a);           // copy constructor
    v.insert(v.begin(), a);   // copy constructor
    
  5. 创建临时对象来满足参数类型

    void f(string);
    
    f("hello");
    

什么时候会隐式生成 copy constructor?

如果类的所有成员都可拷贝,编译器自动生成一个 memberwise 的拷贝构造函数,它的行为是:按类中成员出现的顺序,成员逐个拷贝,每个非 static 成员用它自己的拷贝规则。

即使编译器可能跳过 copy/move constructor,copy/move constructor 本身仍必须合法(存在并且可访问)。

什么时候不会自动生成?

如果某个成员不可拷贝,整个类的拷贝构造函数会被禁止。

explicit 构造函数禁止隐式转换。

Exercise 13.5: Implement the copy constructor
class HasPtr {
public:
    // 构造函数
    HasPtr(const std::string &s = std::string())
        : ps(new std::string(s)), i(0) { }

    // copy constructor
    HasPtr(const HasPtr &rhs)
        : ps(new std::string(*rhs.ps)), // deep copy
        i(rhs.i)
    { }

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

The Copy-Assignment Operator

Copy-assignment operator 是对象赋值时调用的成员函数,默认按成员逐个赋值,通常返回左操作数的引用以支持链式赋值。

class Foo {
public:
    Foo& operator=(const Foo&);
};

如果没有自己定义,编译器会合成一个 synthesized copy-assignment operator,逐个成员赋值,类似 copy constructor,但作用对象是已存在对象。

The Destructor

构造函数分配或初始化资源,析构函数负责销毁对象释放资源。析构函数没有参数,也没有返回值。

class Foo { 
public:  
    ~Foo(); 
};
  • 没写析构函数 → 编译器合成默认析构函数。
  • 析构顺序:先执行函数体,然后按与初始化相反顺序销毁成员。

析构函数什么时候被调用?

很多普通类型(局部变量、STL 对象、智能指针)内部自己管理内存(RAII),比如:

{
    std::string s = "hello";  // 局部变量
} // 离开作用域 → 自动调用 s 的析构函数

但是对于手动管理资源的类,如果不写析构函数,这些资源不会自动释放,就会泄漏:

class HasPtr {
    std::string *ps;
public:
    HasPtr(const std::string &s = "") : ps(new std::string(s)) {}
    ~HasPtr() { delete ps; }  // 必须写析构函数释放内存
};

注意

  1. 引用离开作用域不会触发析构函数。 举例:

    {
        std::string s = "hello";  // 实际对象
        std::string &r = s;       // r 是 s 的引用
    } // r 离开作用域
    

    • r 离开作用域时,只是引用变量被销毁(其实引用本身只是别名,没有内存需要释放)
    • s 的析构函数会在作用域结束时自动调用
  2. 指针变量(除了智能指针以外)离开作用域,只销毁指针本身(即存储地址的内存),指针指向的对象的析构函数不会自动调用。

    {
        std::string *p = new std::string("world");
    } // p 离开作用域
    
    {
        std::string *p = new std::string("world");
        delete p; // 手动触发 *p 的析构函数
    }
    

Exercise 13.3
#include <iostream>
struct X {
    X() { std::cout << "X()" << std::endl; }
    X(const X&) { std::cout << "X(const X&)" << std::endl; }
    X& operator=(const X&) { 
        std::cout << "operator=" << std::endl; 
        return *this; 
    }
    ~X() { std::cout << "~X()" << std::endl; }
};

int main() {
    X a;          // X()
    X b = a;      // X(const X&)
    X c;          
    c = b;        // operator=

    X *p = new X; // X()
    delete p;     // ~X()

    X arr[2];     // X() X()
}

The Rule of Three/Five

1. Classes That Need Destructors Need Copy and Assignment

这段代码有什么问题?

class HasPtr {
public:
    HasPtr(const std::string &s = std::string())
        : ps(new std::string(s)), i(0) { }
    ~HasPtr() { delete ps; }  // WRONG: 需要 copy constructor 和 copy-assignment operator
private:
    std::string *ps;
    int i;
};

没有自定义的 copy constructer 和 copy assignment operator.

编译器会生成 synthesized versions. 这样会有什么问题?

编译器默认生成的拷贝构造函数做的事情等价于:

HasPtr(const HasPtr &orig) 
    : ps(orig.ps), i(orig.i) { }
注意:ps 只是拷贝了指针的值,并没有拷贝 std::string 的内容。

如果你这样使用:

HasPtr a("hello");
HasPtr b = a;  // 调用合成拷贝构造函数
  • 现在 a.psb.ps 都指向同一块内存。
  • a 被销毁时,delete ps 会释放这块内存。
  • b 被销毁时,又会 delete ps → 导致重复释放(double free)。

Rule of Three 第一条:"Classes That Need Destructors Need Copy and Assignment"。 如果类自己管理资源(如 new/delete),必须自己写 copy constructor(实现深拷贝)和 copy assignment operator。

2. Classes That Need Copy Need Assignment, and Vice Versa

如果一个类需要拷贝构造函数,它几乎也一定需要拷贝赋值运算符。反之亦然。

但是需要拷贝构造或拷贝赋值,不一定需要析构函数。

3. Deleted 函数

编译器会为你自动生成 copy-control 成员,但如果类中某个成员无法被拷贝/赋值/销毁,编译器就会把对应的函数标记为 = deleted,来避免产生危险对象。

Deleted 的原因通常是:

  • 成员不可访问(private destructor)
  • 成员有 deleted 函数(deleted copy ctor / deleted operator=)
  • const 或 reference 成员导致默认赋值不可实现

总结:如果类需要用户定义的析构函数、拷贝构造函数或拷贝赋值运算符,几乎肯定需要三者都定义。

🚧 Copy Control and Resource Management