2Effective C++(第3版)

构造/析构/赋值

对象生命周期

拷贝构造赋值操作符资源管理RAII

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章:构造/析构/赋值(Constructors, Destructors, and Assignment Operators)

一、导读

1.1 本章学习目标

第2章深入讲解 C++ 对象生命周期的核心:构造函数、析构函数和赋值操作符。Meyers 在此给出了许多经典的建议,帮助读者避免常见的陷阱。

本章核心学习目标:

  • 掌握拷贝构造和拷贝赋值的区别:什么时候调用哪个?
  • 理解 Rule of Three/Five/Zero:什么时候需要自定义这些函数?
  • 掌握移动语义:C++11 的移动构造和移动赋值
  • 理解虚析构函数的必要性:为什么基类需要虚析构?
  • 掌握异常安全的构造函数:构造失败时如何清理资源?

1.2 为什么本章如此重要

对象的创建和销毁是 C++ 中最基本的操作。错误的拷贝语义会导致资源泄漏、双重释放等严重问题。理解这些概念是写出正确 C++ 代码的基础。

1.3 核心问题引导

拷贝构造和拷贝赋值在什么时候被调用?

为什么含有指针成员的类通常需要自定义拷贝操作?

移动语义如何避免不必要的拷贝?


二、核心概念详解

2.1 拷贝构造 vs 拷贝赋值

cpp
class Widget {
    std::string name;
    int* data;
    size_t size;
public:
    // 拷贝构造: 从另一个对象创建新对象
    Widget(const Widget& other)
        : name(other.name), size(other.size) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    
    // 拷贝赋值: 将另一个对象的值赋给已有对象
    Widget& operator=(const Widget& other) {
        if (this != &other) {  // 自赋值检查
            delete[] data;     // 释放旧资源
            name = other.name;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
};

Widget w1("hello");
Widget w2(w1);      // 拷贝构造
Widget w3 = w1;     // 拷贝构造(不是赋值!)
w3 = w1;            // 拷贝赋值

2.2 Rule of Three / Five / Zero

cpp
// Rule of Three: 如果自定义了以下之一,通常需要自定义全部三个
// 1. 析构函数
// 2. 拷贝构造函数
// 3. 拷贝赋值操作符

// Rule of Five (C++11): 加上移动语义
// 4. 移动构造函数
// 5. 移动赋值操作符

// Rule of Zero: 最好的做法是不需要自定义
// 使用智能指针和标准容器管理资源
class ModernWidget {
    std::string name;                    // 自动管理
    std::vector<int> data;               // 自动管理
    std::unique_ptr<Resource> resource;  // 自动管理
    // 不需要自定义任何特殊成员函数!
};

// 使用智能指针实现深拷贝
class SafeWidget {
    std::unique_ptr<int[]> data;
    size_t size;
public:
    SafeWidget(size_t n) : data(new int[n]()), size(n) {}
    
    // 拷贝构造
    SafeWidget(const SafeWidget& other)
        : data(new int[other.size]), size(other.size) {
        std::copy(other.data.get(), other.data.get() + size, data.get());
    }
    
    // 移动构造
    SafeWidget(SafeWidget&& other) noexcept
        : data(std::move(other.data)), size(other.size) {
        other.size = 0;
    }
};

2.3 虚析构函数

cpp
// 错误: 基类没有虚析构
class Base {
    std::string name;
public:
    Base(std::string n) : name(n) {}
    ~Base() { /* 清理 Base 资源 */ }
};

class Derived : public Base {
    int* data;
public:
    Derived(std::string n, int size) : Base(n), data(new int[size]) {}
    ~Derived() { delete[] data; /* 清理 Derived 资源 */ }
};

Base* ptr = new Derived("hello", 100);
delete ptr;  // 只调用 ~Base(),不调用 ~Derived()!
             // data 泄漏!

// 正确: 基类使用虚析构
class GoodBase {
    std::string name;
public:
    GoodBase(std::string n) : name(n) {}
    virtual ~GoodBase() = default;  // 虚析构
};

class GoodDerived : public GoodBase {
    int* data;
public:
    GoodDerived(std::string n, int size) : GoodBase(n), data(new int[size]) {}
    ~GoodDerived() override { delete[] data; }
};

GoodBase* ptr2 = new GoodDerived("hello", 100);
delete ptr2;  // 正确调用 ~GoodDerived() 然后 ~GoodBase()

2.4 移动语义

cpp
// 移动构造: 窃取资源而非复制
class Buffer {
    std::unique_ptr<int[]> data;
    size_t size;
public:
    Buffer(size_t n) : data(new int[n]), size(n) {}
    
    // 移动构造
    Buffer(Buffer&& other) noexcept
        : data(std::move(other.data)), size(other.size) {
        other.size = 0;
        // other.data 现在是 nullptr
    }
    
    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
            size = other.size;
            other.size = 0;
        }
        return *this;
    }
};

Buffer createBuffer() {
    Buffer b(1000000);
    return b;  // 移动构造(或 NRVO 直接构造)
}

Buffer b1(100);
Buffer b2 = std::move(b1);  // b1 被移动,b1.data 变为 nullptr
// b1 现在处于"有效但未指定"状态

三、实战案例

3.1 案例一:实现异常安全的赋值操作符

cpp
class Image {
    std::string filename;
    std::unique_ptr<char[]> pixels;
    size_t width, height;
    
    void swap(Image& other) noexcept {
        using std::swap;
        swap(filename, other.filename);
        swap(pixels, other.pixels);
        swap(width, other.width);
        swap(height, other.height);
    }
public:
    Image(std::string fn, size_t w, size_t h)
        : filename(std::move(fn)), pixels(new char[w * h]()), 
          width(w), height(h) {}
    
    // 异常安全的赋值: copy-and-swap 惯用法
    Image& operator=(Image other) {  // 按值传递(触发拷贝/移动构造)
        swap(other);  // 交换资源
        return *this;
        // other 在函数结束时析构,释放旧资源
    }
};

// 为什么这是异常安全的?
// 1. 参数 other 通过值传递,在函数体外完成拷贝
//    如果拷贝抛出异常,当前对象不受影响
// 2. swap 不抛出异常(noexcept)
// 3. 旧资源在 other 析构时自动释放

四、本章小结

拷贝构造创建新对象,拷贝赋值修改已有对象

Rule of Zero:优先使用智能指针和标准容器,避免自定义特殊成员函数

虚析构是基类的必备,否则通过基类指针删除派生类对象会导致资源泄漏

移动语义通过窃取资源避免不必要的拷贝,大幅提升性能


五、思考与练习

思考题

为什么移动操作应该标记为 noexcept?

copy-and-swap 惯用法为什么是异常安全的?

编程练习

实现一个字符串类,包含完整的 Rule of Five 成员函数。

实现一个智能指针类(类似 unique_ptr),支持移动语义。


2/9构造/析
0%