C++启蒙课程:第12章  类和动态内存分配

我们将迎来“内存管理大挑战”!掌握“三大铁律”,让你的对象成为健壮的超级英雄,而不是内存泄漏的麻烦制造者!

Leaks

动态内存的陷阱

理解“动态披风”`new`为何会泄漏,以及“清场队”`delete`的必要性。

🧬

复制的战争

区分“共享宝藏”(浅复制)和“独享宝藏”(深复制)的区别。

🎯

高级内存控制

掌握“智能卫士”`nullptr`和“定向传送门”`placement new`。

课时一:动态内存的陷阱与析构函数

(覆盖知识点 12.1 - 12.2.1)

12.1 “动态披风” (内存泄漏)

对象(英雄)在构造时用 `new` 获得“动态披风”(堆内存)。如果析构函数中没有 `delete`,当英雄消失时,披风被留在原地,造成内存泄漏。

内存:
Hero (栈)
Cape (堆)
class Hero { char* cape; public: Hero() { cape = new char[10]; } // 缺少析构函数 ~Hero() }; int main() { Hero h; return 0; // h 消失, cape 泄漏! }

12.1 “清场队” (析构函数)

必须提供显式析构函数 ~Hero(),让“清场队” 🧹 在英雄消失前,先用 `delete` 回收披风。

内存:
Hero (栈)
Cape (堆)
class Hero { char* cape; public: Hero() { cape = new char[10]; } // 12.1 显式析构函数 ~Hero() { delete [] cape; // 清场队回收披风 } };

12.2.1 `delete` vs `delete[]`

`new[]` 建造了“数组大楼”,必须用 `delete[]` 拆除整栋楼。如果错用 `delete`,只会拆除第一个房间,导致内存泄漏。

new char[5] (大楼)
char * str = new char[10]; // 建造大楼 // 错误!只拆 1 个房间 // delete str; // 12.2.1 正确!拆除整栋楼 delete [] str;

课时二:复制的战争:浅复制与深复制

(覆盖知识点 12.3.1, 12.4, 12.8)

12.3.1 “共享的宝藏” (浅复制)

默认复制(浅复制)只复制指针(藏宝图地址),导致两个对象指向“同一块”内存(同一个宝藏)。当一个对象被销毁,宝藏被释放,另一个对象就有了“ dangling pointer” (悬挂指针)。

Hero A
Hero B
宝藏 (0x100)
// 编译器自动生成的复制构造函数 StringBad::StringBad(const StringBad& s) { // 12.3.1 浅复制: 只复制指针 str = s.str; len = s.len; } // 两个对象指向同一块内存!

12.3 “独享的宝藏” (深复制)

必须定义**复制构造函数**。它会 `new` 一块“新内存”,并把内容复制过去。这样两个对象就有了各自“独享的宝藏”。

Hero A
Hero B
宝藏 A (0x100)
...
// 12.3 复制构造函数 (深复制) MyString::MyString(const MyString & other) { len = other.len; // 1. 分配新的内存 str = new char[len + 1]; // 2. 复制内容 strcpy(str, other.str); }

12.4 赋值运算符 (=)

`s3 = s1` (赋值) 和 `MyString s2 = s1;` (初始化) 不同。赋值是发生在两个“已存在”的对象之间,它必须:1. 检查自赋值 2. 释放旧内存 3. 分配新内存 4. 复制内容。

💡 互动:s3 = s1

s1 (new)
[Heap A]
s3 (old)
[Heap B]

📜 代码支撑:

MyString& MyString::operator=(const MyString & other) { // 1. 检查自赋值 if (this == &other) return *this; // 2. 释放 s3 原有的 [Heap B] delete [] str; // 3. 分配新内存 [Heap C] len = other.len; str = new char[len + 1]; // 4. 复制 [Heap A] 的内容到 [Heap C] strcpy(str, other.str); return *this; // 返回 s3 自身 }

课时三:高级内存控制与安全措施

(覆盖知识点 12.2.2, 12.6, 12.7)

成员初始化列表

这是在构造函数中初始化的首选方式。对于 `const` 成员或引用成员,这是**唯一**的方式。

class ConstHolder { const int ID; // const 成员 public: // 12.8 必须使用初始化列表 ConstHolder(int id_val) : ID(id_val) { // ID = id_val; // 错误! } };

12.6 “定向传送门” (定位 new)

`new (address) Type` 允许你在一个“预先分配好的缓冲区”(指定地址)上构造对象,它**不分配新内存**。

Buffer (0x500)
#include // 必须包含 char buffer[100]; // 预分配的缓冲区 // 12.6 定位 new // 在 buffer 的地址上构造一个 Hero Hero* h1 = new (buffer) Hero(); // 注意: 不能 delete h1 // 必须手动调用析构函数 // h1->~Hero();

编程实践与作业

是时候检验你作为“内存管理大师”的实力了!

练习 1:动态数据与析构函数 (12.1)

任务:

编写一个 DynamicData 类,其构造函数使用 `new int` 分配内存。编写必要的构造函数和**析构函数**,以确保内存被正确释放。

点击查看参考答案
#include <iostream> using namespace std; class DynamicData { private: int * ptr; // 指向动态内存 public: DynamicData(int val) { ptr = new int(val); cout << "构造函数:分配内存 at " << ptr << endl; } // 12.1 必须提供显式析构函数 ~DynamicData() { cout << "析构函数:释放内存 at " << ptr << endl; delete ptr; } }; int main() { DynamicData d1(42); return 0; // d1 过期, 自动调用 ~DynamicData() }
练习 2:深复制(复制构造函数与赋值) (12.3, 12.4)

任务:

MyString 类(包含 char* str)实现**复制构造函数**和**赋值运算符**,执行深复制。

点击查看参考答案
#include <cstring> #include <iostream> using namespace std; class MyString { private: char * str; int len; public: MyString(const char * s = "") { len = strlen(s); str = new char[len + 1]; strcpy(str, s); } ~MyString() { delete [] str; } // 12.3 复制构造函数 (深复制) MyString(const MyString & other) { cout << "调用复制构造..." << endl; len = other.len; str = new char[len + 1]; // 分配新内存 strcpy(str, other.str); // 复制内容 } // 12.4 赋值运算符 (深复制) MyString & operator=(const MyString & other) { cout << "调用赋值运算符..." << endl; if (this == &other) return *this; delete [] str; // 释放旧内存 len = other.len; str = new char[len + 1]; // 分配新内存 strcpy(str, other.str); // 复制内容 return *this; } };
练习 3:成员初始化列表 (12.8)

任务:

编写一个 `ConstHolder` 类,它有一个 `const int ID` 成员。演示如何使用**成员初始化列表**来正确初始化它。

点击查看参考答案
#include <iostream> using namespace std; class ConstHolder { private: const int ID; // Const 成员 int * data_ptr; public: // 12.8 必须使用成员初始化列表 ConstHolder(int id_val) : ID(id_val), data_ptr(nullptr) { // ID = id_val; // 错误!不能在函数体中赋值 cout << "ID " << ID << " 对象诞生" << endl; } ~ConstHolder() { delete data_ptr; } }; int main() { ConstHolder c1(101); return 0; }

本章知识点总结与复习 (12.8)

核心概念 解释/功能 关键用法/示例
类与动态内存`new` 分配的内存必须由类管理。char * str;
显式析构函数“清场队”,必须 `delete` 构造函数中 `new` 的内存。~Class() { delete [] ptr; }
复制构造函数在**初始化**时调用,必须执行**深复制**。Class(const Class & obj);
赋值运算符在对**已存在**对象赋值时调用,必须执行深复制。Class & operator=(const Class &);
浅复制 vs. 深复制浅复制只复制指针(危险),深复制复制数据(安全)。str = new char[len + 1];
成员初始化列表在函数体执行前初始化,`const` 成员必须使用。Class() : id(0) { ... }
`nullptr` (C++11)空指针的关键字,比 `0` 更安全。int * ptr = nullptr;
定位 `new`在指定地址构造对象,不分配新内存。new (address) Type;