Effective C++ 条款12:复制对象时应确保复制"对象内的所有成员变量"及"所有 base class 成分"

复制对象时务必确保复制了对象内的所有成员变量以及所有基类成分。不要尝试以某个 copying 函数实现另一个 copying 函数,应将共同机能放进第三个函数中,并由两个 copying 函数共同调用。

一、问题的引入

在 C++ 中,当我们自定义拷贝构造函数或赋值运算符时,很容易遗漏某些成员变量,尤其是:

  • 新增成员变量后忘记更新 copying 函数
  • 继承体系中忘记复制基类成分
  • 试图在一个 copying 函数中调用另一个 copying 函数

一个典型的错误示例

class Customer {
public:
    Customer(const std::string& name, const std::string& address)
        : name_(name), address_(address) {}
    
    // 拷贝构造函数
    Customer(const Customer& rhs)
        : name_(rhs.name_) {  // 糟糕!遗漏了 address_
    }
    
    // 赋值运算符
    Customer& operator=(const Customer& rhs) {
        name_ = rhs.name_;
        // 糟糕!又遗漏了 address_
        return *this;
    }

private:
    std::string name_;
    std::string address_;  // 新增的成员,忘记在 copying 函数中处理
};

当新增 address_ 成员时,如果忘记更新拷贝构造函数和赋值运算符,编译器不会报错,但对象复制将不完整!

二、遗漏成员变量的危害

问题类型 后果
遗漏内置类型成员 新对象包含未初始化的垃圾值
遗漏指针成员 悬空指针或双重释放
遗漏类类型成员 默认构造而非复制,状态不一致
遗漏基类成分 基类部分被默认构造,子类逻辑被破坏
Customer c1("Alice", "Beijing");
Customer c2(c1);  // c2 的 address_ 是空字符串!

三、继承体系中的陷阱

继承场景下的 copying 函数更容易出错:

class Person {
public:
    Person(const std::string& name, int age)
        : name_(name), age_(age) {}
    
    Person(const Person& rhs)
        : name_(rhs.name_), age_(rhs.age_) {}
    
    Person& operator=(const Person& rhs) {
        name_ = rhs.name_;
        age_ = rhs.age_;
        return *this;
    }

protected:
    std::string name_;
    int age_;
};

class Student : public Person {
public:
    Student(const std::string& name, int age, const std::string& school)
        : Person(name, age), school_(school) {}
    
    // 错误的拷贝构造函数!
    Student(const Student& rhs)
        : school_(rhs.school_) {  // 糟糕!没有复制 Person 部分
    }
    
    // 错误的赋值运算符!
    Student& operator=(const Student& rhs) {
        school_ = rhs.school_;    // 糟糕!没有赋值 Person 部分
        return *this;
    }

private:
    std::string school_;
};

问题分析

Student s1("Bob", 20, "Tsinghua");
Student s2(s1);  // s2 的 name_ 和 age_ 未定义!

派生类的 copying 函数必须显式调用基类的 copying 函数,否则基类部分会被默认构造。

正确的实现

class Student : public Person {
public:
    Student(const Student& rhs)
        : Person(rhs),           // 调用基类的拷贝构造函数
          school_(rhs.school_) {
    }
    
    Student& operator=(const Student& rhs) {
        Person::operator=(rhs);  // 调用基类的赋值运算符
        school_ = rhs.school_;
        return *this;
    }

private:
    std::string school_;
};

四、常见错误:在 copying 函数中互相调用

有些开发者试图减少代码重复,在拷贝构造函数中调用赋值运算符,或反之:

错误做法1:在拷贝构造函数中调用赋值运算符

class Widget {
public:
    Widget(const Widget& rhs) {
        *this = rhs;  // 危险!成员可能未初始化
    }
};

问题:拷贝构造函数执行时,对象尚未完全构造。赋值运算符可能假设所有成员已初始化,导致未定义行为。

错误做法2:在赋值运算符中调用拷贝构造函数

class Widget {
public:
    Widget& operator=(const Widget& rhs) {
        if (this != &rhs) {
            Widget temp(rhs);  // 这没问题,但...
            *this = temp;      // 递归调用自己!无限循环!
        }
        return *this;
    }
};

问题*this = temp 会再次调用 operator=,导致无限递归。

正确做法:提取共同代码到私有初始化函数

class Customer {
public:
    Customer(const std::string& name, const std::string& address)
        : name_(name), address_(address) {}
    
    Customer(const Customer& rhs) {
        initFrom(rhs);
    }
    
    Customer& operator=(const Customer& rhs) {
        if (this != &rhs) {
            initFrom(rhs);
        }
        return *this;
    }

private:
    std::string name_;
    std::string address_;
    
    void initFrom(const Customer& rhs) {
        name_ = rhs.name_;
        address_ = rhs.address_;
    }
};

更好的做法是使用 copy-and-swap 惯用法(参见条款11),它可以自然避免代码重复。

五、现代 C++ 的解决方案

方案1:使用默认生成的 copying 函数

如果类只包含可以正确复制的成员,让编译器生成默认版本:

class ModernCustomer {
public:
    ModernCustomer(const std::string& name, const std::string& address)
        : name_(name), address_(address) {}
    
    // 编译器生成的拷贝构造函数和赋值运算符完全正确
    ModernCustomer(const ModernCustomer&) = default;
    ModernCustomer& operator=(const ModernCustomer&) = default;

private:
    std::string name_;
    std::string address_;
};

方案2:使用智能指针管理资源

#include <memory>

class Resource {
public:
    void doSomething() {}
};

class SmartWidget {
public:
    SmartWidget() : resource_(std::make_shared<Resource>()) {}
    
    // 编译器生成的 copying 函数自动处理 shared_ptr 的引用计数
    SmartWidget(const SmartWidget&) = default;
    SmartWidget& operator=(const SmartWidget&) = default;

private:
    std::shared_ptr<Resource> resource_;
};

方案3:使用 copy-and-swap(推荐)

class SafeWidget {
public:
    SafeWidget(const std::string& name, int* data, size_t size)
        : name_(name), data_(new int[size]), size_(size) {
        std::copy(data, data + size, data_);
    }
    
    ~SafeWidget() { delete[] data_; }
    
    // 拷贝构造函数
    SafeWidget(const SafeWidget& rhs)
        : name_(rhs.name_), data_(new int[rhs.size_]), size_(rhs.size_) {
        std::copy(rhs.data_, rhs.data_ + rhs.size_, data_);
    }
    
    // 赋值运算符 - copy-and-swap
    SafeWidget& operator=(SafeWidget rhs) {  // 按值传递
        swap(rhs);
        return *this;
    }
    
    void swap(SafeWidget& other) noexcept {
        using std::swap;
        swap(name_, other.name_);
        swap(data_, other.data_);
        swap(size_, other.size_);
    }

private:
    std::string name_;
    int* data_;
    size_t size_;
};

六、实际应用场景

场景1:深拷贝的矩阵类

class Matrix {
public:
    Matrix(size_t rows, size_t cols)
        : rows_(rows), cols_(cols), data_(new double[rows * cols]) {}
    
    // 正确的拷贝构造函数 - 深拷贝
    Matrix(const Matrix& rhs)
        : rows_(rhs.rows_), cols_(rhs.cols_), 
          data_(new double[rhs.rows_ * rhs.cols_]) {
        std::copy(rhs.data_, rhs.data_ + rows_ * cols_, data_);
    }
    
    // 正确的赋值运算符
    Matrix& operator=(const Matrix& rhs) {
        if (this != &rhs) {
            // 使用 copy-and-swap 技术
            Matrix temp(rhs);
            swap(temp);
        }
        return *this;
    }
    
    ~Matrix() { delete[] data_; }
    
    void swap(Matrix& other) noexcept {
        using std::swap;
        swap(rows_, other.rows_);
        swap(cols_, other.cols_);
        swap(data_, other.data_);
    }

private:
    size_t rows_;
    size_t cols_;
    double* data_;
};

场景2:继承体系中的完整复制

class Animal {
public:
    Animal(const std::string& species, int age)
        : species_(species), age_(age) {}
    
    Animal(const Animal& rhs) = default;
    Animal& operator=(const Animal& rhs) = default;
    virtual ~Animal() = default;

protected:
    std::string species_;
    int age_;
};

class Dog : public Animal {
public:
    Dog(const std::string& name, int age, const std::string& breed)
        : Animal("Canis lupus familiaris", age), name_(name), breed_(breed) {}
    
    // 正确的拷贝构造函数
    Dog(const Dog& rhs)
        : Animal(rhs),      // 必须显式调用基类拷贝构造函数
          name_(rhs.name_),
          breed_(rhs.breed_) {
    }
    
    // 正确的赋值运算符
    Dog& operator=(const Dog& rhs) {
        if (this != &rhs) {
            Animal::operator=(rhs);  // 必须显式调用基类赋值运算符
            name_ = rhs.name_;
            breed_ = rhs.breed_;
        }
        return *this;
    }

private:
    std::string name_;
    std::string breed_;
};

场景3:包含多个成员的复杂类

class Configuration {
public:
    Configuration() = default;
    
    Configuration(const Configuration& rhs)
        : host_(rhs.host_),
          port_(rhs.port_),
          timeout_(rhs.timeout_),
          retry_count_(rhs.retry_count_),
          ssl_enabled_(rhs.ssl_enabled_),
          headers_(rhs.headers_) {  // 别忘了任何一个成员!
    }
    
    Configuration& operator=(const Configuration& rhs) {
        if (this != &rhs) {
            host_ = rhs.host_;
            port_ = rhs.port_;
            timeout_ = rhs.timeout_;
            retry_count_ = rhs.retry_count_;
            ssl_enabled_ = rhs.ssl_enabled_;
            headers_ = rhs.headers_;
        }
        return *this;
    }

private:
    std::string host_;
    int port_;
    int timeout_;
    int retry_count_;
    bool ssl_enabled_;
    std::map<std::string, std::string> headers_;
};

七、最佳实践检查清单

当你实现 copying 函数时,请检查:

  • 是否复制了所有成员变量?
  • 是否在派生类的 copying 函数中调用了基类的 copying 函数?
  • 赋值运算符是否处理了自我赋值?
  • 是否考虑使用 = default 让编译器生成?
  • 是否考虑使用 copy-and-swap 惯用法?
  • 新增成员变量时是否更新了 copying 函数?

八、编译器警告与工具

一些编译器和静态分析工具可以帮助发现问题:

// GCC/Clang 可以使用 -Weffc++ 警告
// 例如:
// warning: 'class Student' should explicitly initialize the 
//          base class 'Person' in the copy constructor

现代 IDE(如 CLion、Visual Studio)也会在 copying 函数不完整时给出提示。

九、总结

请记住:

  • copying 函数应确保复制对象内的所有成员变量和所有基类成分
  • 不要在拷贝构造函数中调用赋值运算符,也不要在赋值运算符中调用拷贝构造函数
  • 将共同机能放进第三个私有初始化函数中,或使用 copy-and-swap 技术
  • 新增成员变量时,务必更新所有 copying 函数
  • 考虑使用编译器生成的默认 copying 函数,或使用现代 C++ 特性简化实现

复制对象看似简单,但遗漏成员变量或基类成分的 bug 往往隐蔽且难以调试。养成检查所有成员的习惯,并优先考虑让编译器生成或采用 copy-and-swap 等安全惯用法,可以大大减少这类错误。


参考阅读:

  • 《Effective C++》第3版,Scott Meyers,条款12
  • 《C++ Primer》关于拷贝控制(copy control)的章节
  • C++ Core Guidelines: C.21, C.80, C.81

更多推荐