第12章 对象的“记忆”与“克隆”

当对象拥有自己的“租借记忆”(动态内存)时,简单的复制会引发大灾难。本章将揭示如何正确地“克隆”对象,并管理它们的“遗产”。

环节 1:动态内存与“收房管家”

如果对象在构造时用 `new` “租借”了一块内存,那么在它消亡时,必须有一个“收房管家”——**析构函数**——用 `delete` 把这块内存“归还”给系统,否则就会造成内存泄漏。

class String { private: char* str; // 指向“租借”的内存 public: String(const char* s) { str = new char[strlen(s) + 1]; strcpy(str, s); } // 析构函数: 归还内存 ~String() { delete[] str; } };

内存租借与归还 Guesthouse

模拟 `String` 对象的创建和销毁,观察内存的分配与释放过程。

系统内存 (Heap)

空闲

环节 2:深拷贝 vs. 浅拷贝

默认的复制行为是**浅拷贝**,就像两个人共用一个“秘密日记本”的地址。一个人销毁了日记本,另一个人手里的地址就失效了,再次销毁就会导致程序崩溃。正确的做法是**深拷贝**:为新人复制一本全新的日记本。

克隆实验室 🧬

灾难现场: 浅拷贝

Original: char* str -> @0x123
Clone: char* str -> @0x123
"Diary"

正确方案: 深拷贝

Original: char* str -> @0xABC
Clone: char* str -> @0xDEF
"Diary"
...

环节 3:赋值运算符 —— “搬家工人”

当把一个**已存在的**对象赋给另一个时,需要一个专业的“搬家工人”——重载的赋值运算符。它会先“清理旧房”(释放目标对象的旧内存),再“建造新房”并“复制家具”(深拷贝)。

String& String::operator=(const String& s) { // 1. 检查自我赋值 if (this == &s) return *this; // 2. 清理旧房: 释放旧内存 delete[] str; // 3. 深度复制: 分配新内存并复制 len = s.len; str = new char[len + 1]; strcpy(str, s.str); return *this; }

赋值操作模拟器 🚚

观察执行 `s2 = s1` 时,内存如何一步步地变化。

对象 s1

"Hello"

对象 s2

"World"
> 等待操作...

终点站:编程挑战

练习 1:浅复制的陷阱

任务:

使用一个未实现复制构造函数和赋值运算符的 `StringBad` 类,观察并解释当对象被按值传递或初始化时发生的错误(内存冲突/双重释放)。

预期知识点:

默认复制的行为,析构函数的调用时机。

点击查看参考答案
// main_test.cpp void call_me(StringBad s) { // 浅拷贝在这里发生 std::cout << "函数内执行完毕。\n"; } // s 离开作用域, 其析构函数释放了原始内存 int main() { StringBad original("Original"); // 按值传递, 发生浅拷贝 call_me(original); // 当 main 结束, original 的析构函数 // 会尝试再次释放同一块内存, 导致崩溃! return 0; }
练习 2:实现深度复制

任务:

编写一个 `String` 类,确保其复制构造函数能够正确执行深度复制,从而避免练习 1 中的错误。

预期知识点:

显式复制构造函数,深度复制步骤。

点击查看参考答案
// String 复制构造函数实现 String::String(const String & s) { // 1. 复制非指针成员 len = s.len; // 2. 分配新内存 (深度复制的关键) str = new char[len + 1]; // 3. 复制内容 std::strcpy(str, s.str); }
练习 3:实现赋值运算符

任务:

为 `String` 类实现重载的赋值运算符 `operator=`,以处理对象间的赋值,确保它能处理自我赋值并进行深度复制。

预期知识点:

赋值运算符重载,检查自我赋值,清理旧内存。

点击查看参考答案
// String 赋值运算符实现 String& String::operator=(const String & s) { // 1. 检查自我赋值 if (this == &s) return *this; // 2. 清理旧内存 delete[] str; // 3. 深度复制新数据 len = s.len; str = new char[len + 1]; std::strcpy(str, s.str); // 4. 返回调用对象的引用 return *this; }