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|资源获取即初始化
🎮 30 秒课程摘要
“特殊成员函数是 C++ 的隐形守护者。
它们自动生成,却能决定对象的生死与灵魂。”
📚 核心内容讲解
🧩 1️⃣ 什么是特殊成员函数?
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)。
🧩 2️⃣ 构造函数与初始化列表
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️⃣ 析构函数(Destructor)
析构函数在对象生命周期结束时被自动调用。
📦 示例 3:析构函数
class FileHandler {
public:
FileHandler(const std::string& path) {
file = fopen(path.c_str(), "r");
}
~FileHandler() { // ✅ 析构释放资源
if (file) fclose(file);
}
private:
FILE* file;
};🧩 讲解:
- 析构函数自动调用,不需手动释放。
- 可防止内存泄漏、文件句柄泄漏。
- RAII 精神:“资源获取即初始化”。
🧩 4️⃣ 拷贝构造函数(Copy Constructor)
📦 定义形式:
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; // 调用拷贝构造函数
🧩 讲解:
- 深拷贝避免“指针双删”问题。
- 若未定义,编译器执行浅拷贝(仅复制指针值)。
🧩 5️⃣ 拷贝赋值运算符(Copy Assignment Operator)
📦 定义形式:
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;
}🧩 讲解:
- 拷贝赋值发生在“已有对象被重新赋值”时。
- 要谨慎处理资源释放与自赋值问题。
🧩 6️⃣ 深拷贝 vs 浅拷贝
📦 示意图:
| 类型 | 描述 | 问题 |
|---|---|---|
| 浅拷贝 | 仅复制指针地址 | 双重释放(double delete) |
| 深拷贝 | 复制新内存与数据 | 安全但稍慢 |
📦 错误示例:浅拷贝导致崩溃
Buffer b1(5);
Buffer b2 = b1; // ❌ 浅拷贝,b1/b2.data 指向同一内存
当对象析构时,会释放同一块内存两次 → 程序崩溃!
🧠 结论:
“当类管理资源(堆内存、文件、句柄)时,
必须自定义拷贝构造与赋值函数。”
🧩 7️⃣ 三法则(Rule of Three)
“如果你定义了以下任意一个,你几乎一定要定义另外两个。”
| 函数 | 作用 |
|---|---|
| 拷贝构造函数 | 创建副本 |
| 拷贝赋值运算符 | 替换现有对象 |
| 析构函数 | 释放资源 |
📦 Rule of Three 示例:
class Resource {
public:
Resource(size_t size);
Resource(const Resource& rhs);
Resource& operator=(const Resource& rhs);
~Resource();
};🧠 核心思想:
- 拥有资源的类(如动态内存)必须正确管理生命周期。
- 避免浅拷贝引发资源冲突。
🧩 8️⃣ 五法则(Rule of Five)
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()将左值转换为右值引用。
🧩 9️⃣ 默认与删除
C++11 支持显式声明默认或删除特殊函数:
📦 示例 7:default / delete
class Example {
public:
Example() = default; // 显式请求默认构造
Example(const Example&) = delete; // 禁止拷贝
Example& operator=(const Example&) = delete;
};🧠 应用场景:
- 单例模式(禁止拷贝)
- 文件句柄类(禁止多实例)
- 简单 POD 类型(直接用
= default)
🧩 🔟 返回值优化(RVO)
编译器会自动优化:
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&&) | 转移资源 | 清理旧资源 |
“掌握特殊成员函数,就是掌握对象生命循环。”