cs106l-lecture-5-3-special-member-functions

Course: 斯坦福大学: C++标准库编程 | C++ Standard Library Programming


🎮 CS106L Lecture 5.3 — Special Member Functions

🏛 课程:Stanford CS106L — Standard C++ Programming Language

📍 主题:特殊成员函数(Special Member Functions)

🎯 目标:理解 C++ 自动生成的成员函数机制,掌握拷贝、赋值、析构的语义与“三法则(Rule of Three)”,为后续移动语义与 RAII 奠定基础。


在前几讲中我们学习了:

  • 类与封装
  • 运算符重载
  • const 正确性

本讲正式进入:

“对象的生命周期控制(Object Lifetime Management)。”

也就是 C++ 的核心哲学:

💡 你创建它,就要负责销毁它。


Constructor|构造函数

Destructor|析构函数

Copy Constructor|拷贝构造函数

Copy Assignment|拷贝赋值

Move Constructor|移动构造

Rule of Three / Five|三法则 / 五法则

Deep Copy|深拷贝

Shallow Copy|浅拷贝

RAII|资源获取即初始化


“特殊成员函数是 C++ 的隐形守护者。

它们自动生成,却能决定对象的生死与灵魂。”



C++ 编译器自动为类生成 6 种特殊函数(若用户未定义):

函数名触发时机作用
默认构造函数创建对象时初始化对象
析构函数销毁对象时清理资源
拷贝构造函数用已有对象创建新对象拷贝状态
拷贝赋值运算符用已有对象赋值另一个替换状态
移动构造函数从临时对象“偷取”资源提高效率
移动赋值运算符赋值时“转移”资源避免拷贝成本

📦 示例 1:编译器自动生成的函数

class Demo {
public:
    Demo();                    // 默认构造
    ~Demo();                   // 析构
    Demo(const Demo&);         // 拷贝构造
    Demo& operator=(const Demo&); // 拷贝赋值
    Demo(Demo&&);              // 移动构造 (C++11)
    Demo& operator=(Demo&&);   // 移动赋值 (C++11)
};

🧠 说明:

  • 编译器只在必要时生成这些函数。
  • 一旦你手写其中之一,部分默认版本会被禁止自动生成(详见 Rule of Three/Five)。

C++ 推荐使用初始化列表(initializer list)

它直接初始化成员,而非先默认构造后再赋值。

📦 示例 2:构造函数优化

class Student {
public:
    Student(std::string name, int id)
        : name(name), id(id) {   // ✅ 初始化列表
        std::cout << "Constructed!\n";
    }
private:
    std::string name;
    int id;
};

🧠 为什么要用初始化列表?

  • const / reference 成员,必须使用初始化列表。
  • 避免额外一次默认构造 + 赋值。
  • 更高效、更符合 RAII。

析构函数在对象生命周期结束时被自动调用。

📦 示例 3:析构函数

class FileHandler {
public:
    FileHandler(const std::string& path) {
        file = fopen(path.c_str(), "r");
    }
    ~FileHandler() {   // ✅ 析构释放资源
        if (file) fclose(file);
    }
private:
    FILE* file;
};

🧩 讲解:

  • 析构函数自动调用,不需手动释放。
  • 可防止内存泄漏、文件句柄泄漏。
  • RAII 精神:“资源获取即初始化”。

📦 定义形式:

ClassName(const ClassName& other);

📦 示例 4:拷贝构造

class Buffer {
public:
    Buffer(size_t size)
        : data(new int[size]), size(size) {}
    ~Buffer() { delete[] data; }

    Buffer(const Buffer& other)      // 拷贝构造
        : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);
    }
private:
    int* data;
    size_t size;
};

📤 输出:

Buffer b1(10);
Buffer b2 = b1;  // 调用拷贝构造函数

🧩 讲解:

  • 深拷贝避免“指针双删”问题。
  • 若未定义,编译器执行浅拷贝(仅复制指针值)。

📦 定义形式:

ClassName& operator=(const ClassName& rhs);

📦 示例 5:拷贝赋值

Buffer& operator=(const Buffer& rhs) {
    if (this == &rhs) return *this; // ✅ 自赋值保护
    delete[] data;                  // 清理旧资源
    data = new int[rhs.size];
    size = rhs.size;
    std::copy(rhs.data, rhs.data + size, data);
    return *this;
}

🧩 讲解:

  • 拷贝赋值发生在“已有对象被重新赋值”时。
  • 要谨慎处理资源释放与自赋值问题。

📦 示意图:

类型描述问题
浅拷贝仅复制指针地址双重释放(double delete)
深拷贝复制新内存与数据安全但稍慢

📦 错误示例:浅拷贝导致崩溃

Buffer b1(5);
Buffer b2 = b1; // ❌ 浅拷贝,b1/b2.data 指向同一内存

当对象析构时,会释放同一块内存两次 → 程序崩溃!

🧠 结论:

“当类管理资源(堆内存、文件、句柄)时,

必须自定义拷贝构造与赋值函数。”


“如果你定义了以下任意一个,你几乎一定要定义另外两个。”

函数作用
拷贝构造函数创建副本
拷贝赋值运算符替换现有对象
析构函数释放资源

📦 Rule of Three 示例:

class Resource {
public:
    Resource(size_t size);
    Resource(const Resource& rhs);
    Resource& operator=(const Resource& rhs);
    ~Resource();
};

🧠 核心思想:

  • 拥有资源的类(如动态内存)必须正确管理生命周期。
  • 避免浅拷贝引发资源冲突。

C++11 引入“移动语义”后,新增两个特殊函数:

函数作用
移动构造函数从临时对象“偷取”资源
移动赋值运算符转移资源所有权

📦 示例 6:Rule of Five

class Buffer {
public:
    Buffer(size_t size);
    ~Buffer();
    Buffer(const Buffer& rhs);
    Buffer& operator=(const Buffer& rhs);
    Buffer(Buffer&& rhs) noexcept;            // ✅ 移动构造
    Buffer& operator=(Buffer&& rhs) noexcept; // ✅ 移动赋值
};

🧩 讲解:

  • 移动构造能避免深拷贝带来的性能损耗。
  • 使用 std::move() 将左值转换为右值引用。

C++11 支持显式声明默认或删除特殊函数:

📦 示例 7:default / delete

class Example {
public:
    Example() = default;                    // 显式请求默认构造
    Example(const Example&) = delete;       // 禁止拷贝
    Example& operator=(const Example&) = delete;
};

🧠 应用场景:

  • 单例模式(禁止拷贝)
  • 文件句柄类(禁止多实例)
  • 简单 POD 类型(直接用 = default

编译器会自动优化:

return largeObject;

避免临时拷贝(自动触发移动构造或直接构造)。

📦 示例 8:RVO 示例

Buffer makeBuffer() {
    Buffer temp(10);
    return temp; // ✅ 无拷贝发生(RVO)
}

🧠 结论:

“现代编译器 + 移动语义 + RVO,让 C++ 性能更接近底层极限。”


“造三改五删多余;

拷贝防双删,移动省功夫。”


练习 1:手写三法则类

实现一个 StringBuffer,包含构造、析构、拷贝构造、赋值。

练习 2:观察浅拷贝崩溃

用动态数组验证“两个对象共享同一内存”的问题。

练习 3:移动优化

Buffer 添加移动构造与赋值,打印调用顺序。

练习 4:RVO 实验

返回大型对象并比较优化前后的性能。


  • 若类不拥有资源(仅基本类型),无需自定义特殊函数。
  • 若类管理资源(new/delete、文件、socket),必须定义“三法则”。
  • C++11 及之后:推荐实现“五法则”。
  • 若类不允许复制,应显式 delete 拷贝构造和赋值函数。
  • 始终使用初始化列表、RAII 模式与智能指针。
  • 避免在析构中抛异常。

函数类型定义形式调用场景注意事项
默认构造T()创建对象=default
析构函数~T()对象销毁释放资源
拷贝构造T(const T&)新对象初始化深拷贝资源
拷贝赋值operator=赋值给现有对象自赋值检测
移动构造T(T&&)从临时对象转移noexcept 推荐
移动赋值operator=(T&&)转移资源清理旧资源

“掌握特殊成员函数,就是掌握对象生命循环。”