Ch 25: RAII 和内存管理
- 掌握 RAII 模式的核心思想和实现原理
- 理解栈和堆的区别以及内存布局
- 学会实现 RAII 资源管理类
- 掌握常见内存问题的识别和避免方法
- 理解 C++ 内存模型和生命周期规则
25.1 Python 垃圾回收 vs C++ RAII
Section titled “25.1 Python 垃圾回收 vs C++ RAII”Python 垃圾回收机制
Section titled “Python 垃圾回收机制”# Python - 依赖垃圾回收器自动管理内存def process(): data = load_data() # 创建大对象 result = heavy_computation(data) return result# data 在函数结束后变成垃圾,但 GC 回收时机不确定# 可能在函数返回后立即回收,也可能延迟数秒Python GC 的特点:
- 引用计数为主 + 标记-清除为辅
- 对象引用计数归零时立即回收(基本确定)
- 循环引用需要标记-清除GC处理(延迟)
- 无法控制回收时机
C++ RAII:资源获取即初始化
Section titled “C++ RAII:资源获取即初始化”// C++ - RAII:构造函数获取资源,析构函数释放#include <fstream>#include <stdexcept>
class FileHandler {public: explicit FileHandler(const std::string& filename) : file_(filename, std::ios::in | std::ios::out) { if (!file_.is_open()) { throw std::runtime_error("Cannot open file: " + filename); } }
// 析构函数:对象销毁时自动调用 ~FileHandler() { if (file_.is_open()) { file_.close(); std::cout << "File automatically closed\n"; } }
// 不可拷贝,不可移动 FileHandler(const FileHandler&) = delete; FileHandler& operator=(const FileHandler&) = delete; FileHandler(FileHandler&&) = delete; FileHandler& operator=(FileHandler&&) = delete;
void write(const std::string& data) { file_ << data << std::endl; }
std::string read() { std::string line; std::getline(file_, line); return line; }
private: mutable std::fstream file_; // mutable 允许在 const 方法中操作};
void process_file() { FileHandler handler("data.txt"); // 构造时打开文件 handler.write("Hello, RAII!");
// 使用文件... // 即使发生异常,handler 离开作用域时也会自动关闭
} // handler 自动析构,文件保证关闭RAII 的核心优势:
- 确定性:资源在析构时立即释放
- 异常安全:异常传播时栈上对象仍会析构
- 封装性:资源管理逻辑隐藏在类内部
- 无泄漏:所有路径都能保证资源释放
RAII vs Python with 语句
Section titled “RAII vs Python with 语句”# Python 的 with 语句也是一种 RAII 模式with open("data.txt") as f: data = f.read()# 文件自动关闭,与 C++ 析构函数机制类似
# 但 Python 的 GC 是不确定的del f # 删除引用,但文件可能还没关闭(GC 未运行)// C++ 的 RAII 有确定性析构{ std::ofstream f("data.txt"); f << "Hello";} // f 立即析构,文件立即关闭,无需 GC25.2 栈和堆详解
Section titled “25.2 栈和堆详解”+------------------+ 高地址| Stack | ← 本地变量、函数参数| (向下增长) |+------------------+| ↓ || || ↑ || |+------------------+| Heap | ← 动态分配内存| (向上增长) |+------------------+| BSS | ← 未初始化全局/静态变量+------------------+| Data | ← 已初始化全局/静态变量+------------------+| Text | ← 代码段(只读)+------------------+ 低地址#include <iostream>#include <vector>#include <string>
void stack_examples() { // 基本类型 - 栈上分配 int x = 42; double pi = 3.14159; char c = 'A'; bool flag = true;
// 数组 - 栈上分配(编译时大小固定) int arr[100]; // 400 字节,栈上 double matrix[10][10]; // 800 字节,栈上
// std::string(小字符串优化) std::string s1 = "Hello"; // 通常栈上(小字符串优化) std::string s2 = "This is a very long string that exceeds SSO..."; // s2 可能分配在堆上(大字符串)
// std::vector - 栈上对象,堆上数据 std::vector<int> v(1000); // v 本身在栈上(控制块) // v 的数据在堆上(1000 * 4 = 4000 字节)
// 栈帧示例 // main() 调用 func() 时: // 1. func() 的返回地址压栈 // 2. func() 的参数压栈 // 3. func() 的局部变量在栈上分配 // 4. func() 返回时,栈帧弹出,变量立即销毁}
int main() { stack_examples(); std::cout << "Stack frame destroyed\n";
// 栈大小有限(通常 1-8 MB) // 避免在栈上分配大数组 // int big_array[10000000]; // 40 MB,可能栈溢出! return 0;}#include <memory>#include <vector>
void heap_examples() { // ❌ 危险:手动管理(永远避免) int* raw_ptr = new int(42); std::cout << *raw_ptr << "\n"; delete raw_ptr; // 必须手动释放 raw_ptr = nullptr; // 避免悬垂指针
// ❌ 危险:数组手动管理 int* arr = new int[1000]; // ... 使用数组 delete[] arr; // 必须用 delete[],不能用 delete
// ❌ 危险:容易忘记释放 void function_with_leak() { int* p = new int(100); if (some_condition()) { return; // 内存泄漏!delete 被跳过 } delete p; }
// ✅ 安全:智能指针 auto sp = std::make_shared<int>(42); // shared_ptr auto up = std::make_unique<int>(42); // unique_ptr
// ✅ 安全:容器通常在栈上 auto v = std::vector<int>{1, 2, 3}; // 栈上的 vector 对象 auto s = std::string("Hello"); // 栈上的 string 对象}
// unique_ptr 用于独占所有权void unique_ptr_example() { auto up = std::make_unique<int[]>(100); // 动态数组 up[0] = 1; up[1] = 2; // 离开作用域自动 delete[]
// 自定义删除器 auto deleter = [](FILE* f) { if (f) fclose(f); }; std::unique_ptr<FILE, decltype(deleter)> fp( fopen("data.txt", "r"), deleter);}
// shared_ptr 用于共享所有权void shared_ptr_example() { auto sp1 = std::make_shared<int>(42); { auto sp2 = sp1; // 引用计数从 1 变为 2 std::cout << *sp2 << "\n"; } // sp2 销毁,引用计数变为 1 std::cout << *sp1 << "\n";} // sp1 销毁,引用计数变为 0,内存释放#include <iostream>
// 静态存储期:整个程序运行期间存在int global_var = 42; // 全局变量static int static_var = 100; // 内部链接静态变量
class MyClass {public: static int class_static; // 类静态成员};int MyClass::class_static = 0; // 类外定义
void function_static_example() { static int call_count = 0; // 函数内静态变量 ++call_count; // 只初始化一次 std::cout << "Called " << call_count << " times\n";}
int main() { // 静态变量在程序启动时分配,程序结束时释放 // 位于 Data 段(BSS 用于未初始化) function_static_example(); // Called 1 times function_static_example(); // Called 2 times function_static_example(); // Called 3 times}25.3 实现 RAII 资源管理类
Section titled “25.3 实现 RAII 资源管理类”RAII 锁(互斥锁保护)
Section titled “RAII 锁(互斥锁保护)”#include <mutex>#include <thread>#include <iostream>
// RAII 锁包装器class LockGuard {public: explicit LockGuard(std::mutex& mtx) : mutex_(mtx) { mutex_.lock(); std::cout << "Locked\n"; }
~LockGuard() { mutex_.unlock(); std::cout << "Unlocked\n"; }
// 禁止拷贝和移动 LockGuard(const LockGuard&) = delete; LockGuard& operator=(const LockGuard&) = delete; LockGuard(LockGuard&&) = delete; LockGuard& operator=(LockGuard&&) = delete;
private: std::mutex& mutex_;};
// 使用示例std::mutex cout_mutex;int shared_data = 0;
void critical_worker(int id) { for (int i = 0; i < 3; ++i) { LockGuard lock(cout_mutex); // 自动加锁 std::cout << "Worker " << id << ": " << ++shared_data << "\n"; } // 离开作用域自动解锁}
int main() { std::thread t1(critical_worker, 1); std::thread t2(critical_worker, 2); t1.join(); t2.join(); // RAII 保证即使异常也会解锁}C++11 std::lock_guard
Section titled “C++11 std::lock_guard”#include <mutex>
std::mutex mtx;
// 推荐:使用标准库 lock_guardvoid standard_lock_guard() { std::lock_guard<std::mutex> lock(mtx); // 自动加锁 // 临界区代码} // 离开作用域自动解锁
// C++17 std::scoped_lock(支持多个互斥锁)void multi_lock() { std::mutex mtx1, mtx2, mtx3; std::scoped_lock lock(mtx1, mtx2, mtx3); // 一次性锁住多个 // 避免死锁(标准库内部处理)}RAII 文件句柄
Section titled “RAII 文件句柄”#include <fstream>#include <iostream>#include <string>
class FileRAII {public: enum class OpenMode { Read, Write, Append };
FileRAII(const std::string& filename, OpenMode mode) { std::ios::openmode flags = std::ios::in; if (mode == OpenMode::Write) flags = std::ios::out | std::ios::trunc; if (mode == OpenMode::Append) flags = std::ios::out | std::ios::app;
file_.open(filename, flags); if (!file_.is_open()) { throw std::runtime_error("Cannot open file: " + filename); } filename_ = filename; std::cout << "Opened: " << filename_ << "\n"; }
~FileRAII() { if (file_.is_open()) { file_.close(); std::cout << "Closed: " << filename_ << "\n"; } }
// 不可拷贝 FileRAII(const FileRAII&) = delete; FileRAII& operator=(const FileRAII&) = delete;
// 可移动 FileRAII(FileRAII&& other) noexcept : file_(std::move(other.file_)), filename_(std::move(other.filename_)) { other.filename_.clear(); }
void write(const std::string& data) { file_ << data << "\n"; }
std::string read_line() { std::string line; std::getline(file_, line); return line; }
bool is_open() const { return file_.is_open(); }
private: std::fstream file_; std::string filename_;};
int main() { try { FileRAII file("test.txt", FileRAII::OpenMode::Write); file.write("Line 1"); file.write("Line 2"); } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << "\n"; } // 文件自动关闭,无论是否异常}RAII 智能数组包装
Section titled “RAII 智能数组包装”#include <memory>#include <iostream>
template<typename T>class UniqueArray {public: explicit UniqueArray(std::size_t size) : size_(size) { data_ = std::make_unique<T[]>(size); std::cout << "Allocated array of " << size_ << " elements\n"; }
~UniqueArray() { std::cout << "Freed array of " << size_ << " elements\n"; }
// 禁用拷贝 UniqueArray(const UniqueArray&) = delete; UniqueArray& operator=(const UniqueArray&) = delete;
// 访问 T& operator[](std::size_t idx) { return data_[idx]; } const T& operator[](std::size_t idx) const { return data_[idx]; }
std::size_t size() const { return size_; } T* data() { return data_.get(); } const T* data() const { return data_.get(); }
private: std::unique_ptr<T[]> data_; std::size_t size_;};
int main() { { UniqueArray<int> arr(10); for (int i = 0; i < 10; ++i) { arr[i] = i * i; } // 数组在离开作用域时自动释放 }
{ auto arr = std::make_unique<int[]>(5); arr[0] = 1; arr[4] = 5; // unique_ptr 自动释放 }
return 0;}25.4 常见内存问题详解
Section titled “25.4 常见内存问题详解”#include <iostream>#include <memory>
// ❌ 危险:返回局部变量地址int* dangling_pointer() { int local = 42; return &local; // 悬垂指针!local 在函数结束后销毁}
// ✅ 正确:返回静态变量int* static_local() { static int local = 42; // 静态存储期 return &local; // 安全,静态变量生命周期是整个程序}
// ✅ 正确:返回动态分配(需注意所有权)int* dynamicAllocation() { int* p = new int(42); return p; // 调用者负责释放!}
// ✅ 正确:返回智能指针auto smart_pointer() -> std::unique_ptr<int> { return std::make_unique<int>(42);}
// ✅ 正确:使用引用(如果对象有效)int& valid_reference() { static int local = 42; // 静态变量 return local;}
int main() { // 悬垂指针的使用是未定义行为 int* dp = dangling_pointer(); // std::cout << *dp << "\n"; // 可能崩溃、打印垃圾值、或"正常工作"
// 智能指针是最安全的方案 auto sp = smart_pointer(); std::cout << *sp << "\n"; // 100% 安全}空指针和 nullptr
Section titled “空指针和 nullptr”#include <iostream>#include <memory>#include <optional>
// ❌ 危险:解引用空指针void dangerous_null() { int* ptr = nullptr; // *ptr = 42; // 未定义行为!程序可能崩溃
// ✅ 检查后使用 if (ptr != nullptr) { *ptr = 42; }
// ✅ 使用 std::optional(C++17) std::optional<int> opt; if (opt.has_value()) { std::cout << opt.value() << "\n"; }
// ✅ 使用 std::unique_ptr auto sp = std::make_unique<int>(42); if (sp) { std::cout << *sp << "\n"; }}#include <iostream>
// ❌ 危险:未初始化指针void wild_pointer() { int* wild; // 未定义!可能是任意地址 // *wild = 42; // 崩溃或数据损坏
// ✅ 初始化为 nullptr int* ptr = nullptr; // 安全状态}
// ✅ 使用智能指针永远不要担心未初始化void smart_is_safe() { auto sp = std::make_unique<int>(42); // 总是有效 auto sp2; // 编译错误!必须初始化}重复释放和双重删除
Section titled “重复释放和双重删除”#include <iostream>#include <memory>
// ❌ 危险:重复释放void double_free() { int* p = new int(42); delete p; // delete p; // 重复释放!未定义行为
p = nullptr; // 避免措施:删除后置空 // delete p; // delete nullptr 是安全的(无操作)}
// ✅ 使用智能指针避免双重释放void no_double_free_smart() { auto sp = std::make_shared<int>(42); // shared_ptr 在引用计数归零时自动释放 // 多次赋值会自动处理 sp = std::make_shared<int>(100); // 原来的自动释放}
// unique_ptr 也一样void unique_no_double_free() { auto up = std::make_unique<int>(42); up.reset(); // 显式释放 up.reset(); // reset nullptr 是安全的}#include <iostream>#include <memory>#include <vector>
// ❌ 危险:忘记删除void memory_leak() { int* p = new int[1000]; // 复杂的逻辑路径可能跳过 delete if (some_condition()) { return; // 泄漏! } delete[] p;}
// ❌ 危险:异常导致跳过 deletevoid leak_on_exception() { int* p = new int[1000]; // throw std::runtime_error("Oops"); // 泄漏! delete[] p;}
// ✅ 智能指针自动管理void no_leak_smart_ptr() { auto sp = std::make_shared<int>(1000); throw std::runtime_error("Caught!"); // sp 自动销毁}
// ✅ 容器管理动态数组void no_leak_container() { std::vector<int> v(1000); // 栈上对象 // 自动管理内存}#include <iostream>#include <vector>#include <array>#include <span>
// ❌ 危险:C 风格数组无边界检查void buffer_overflow() { int arr[5] = {1, 2, 3, 4, 5}; for (int i = 0; i <= 5; ++i) { // 错误:多迭代一次 std::cout << arr[i] << "\n"; // arr[5] 是未定义行为 }}
// ✅ 使用 std::array(带边界检查)void safe_array() { std::array<int, 5> arr = {1, 2, 3, 4, 5}; // arr.at(5) 会抛出 std::out_of_range try { std::cout << arr.at(5) << "\n"; } catch (const std::out_of_range& e) { std::cout << "Out of range: " << e.what() << "\n"; }}
// ✅ 使用 std::span(C++20)传递数组void process_data(std::span<int> data) { for (int x : data) { std::cout << x << "\n"; } // span 知道大小,不会溢出}
void use_span() { std::vector<int> vec = {1, 2, 3, 4, 5}; std::array<int, 5> arr = {1, 2, 3, 4, 5}; int c_arr[] = {1, 2, 3, 4, 5};
process_data(vec); // 5 elements process_data(arr); // 5 elements process_data(c_arr); // 5 elements}25.5 内存泄漏检测工具
Section titled “25.5 内存泄漏检测工具”Valgrind 简单用法
Section titled “Valgrind 简单用法”# 编译时加调试信息g++ -g -O0 -o program program.cpp
# 运行 valgrindvalgrind --leak-check=full ./programAddressSanitizer
Section titled “AddressSanitizer”# 编译时加 ASang++ -fsanitize=address -g -O1 -o program program.cpp
# 运行./program智能指针的循环引用
Section titled “智能指针的循环引用”#include <iostream>#include <memory>
// ❌ 危险:循环引用导致内存泄漏class Node {public: std::shared_ptr<Node> next; std::shared_ptr<Node> prev; int value;
Node(int v) : value(v) { std::cout << "Node(" << value << ") constructed\n"; }
~Node() { std::cout << "Node(" << value << ") destroyed\n"; }};
void circular_reference_leak() { auto n1 = std::make_shared<Node>(1); auto n2 = std::make_shared<Node>(2);
n1->next = n2; // n2 引用计数 = 2 n2->prev = n1; // n1 引用计数 = 2
// 离开作用域时,引用计数都是 1,不会释放!}
// ✅ 解决:使用 weak_ptr 打破循环class NodeSafe {public: std::weak_ptr<NodeSafe> next; // weak_ptr 不增加引用计数 std::weak_ptr<NodeSafe> prev; int value;
NodeSafe(int v) : value(v) { std::cout << "NodeSafe(" << value << ") constructed\n"; }
~NodeSafe() { std::cout << "NodeSafe(" << value << ") destroyed\n"; }};
void no_circular_leak() { auto n1 = std::make_shared<NodeSafe>(1); auto n2 = std::make_shared<NodeSafe>(2);
n1->next = n2; n2->prev = n1;
// 离开作用域时,引用计数正确归零}
int main() { std::cout << "=== Circular Reference ===\n"; circular_reference_leak(); std::cout << "Nodes still alive due to circular ref!\n\n";
std::cout << "=== No Circular Reference ===\n"; no_circular_leak(); std::cout << "Nodes properly destroyed\n";}25.6 动态数组管理
Section titled “25.6 动态数组管理”C 风格 vs C++ 风格
Section titled “C 风格 vs C++ 风格”#include <iostream>#include <vector>#include <memory>
// ❌ C 风格 - 危险void c_style_array() { int* arr = new int[100]; // 必须手动管理 arr[0] = 1; arr[99] = 100;
// 很容易忘记 delete[] // delete arr; // 错误!应该用 delete[] // delete[] arr; // 正确但容易忘记
// 数组和标量 delete 不匹配是未定义行为}
// ✅ C++98:std::vectorvoid vector_style() { std::vector<int> vec(100); // 自动管理 vec[0] = 1; vec[99] = 100; // 自动释放,无需手动操作}
// ✅ C++11:std::unique_ptr 数组void unique_ptr_array() { auto arr = std::make_unique<int[]>(100); // 动态数组 arr[0] = 1; arr[99] = 100; // 自动 delete[]}
// ✅ C++14:std::make_unique 数组重载void make_unique_array() { auto arr = std::make_unique<int[]>(100); // 等价于 new int[100]}数组视图:std::span
Section titled “数组视图:std::span”#include <iostream>#include <vector>#include <array>#include <span>
void process_array(std::span<int> data) { std::cout << "Processing " << data.size() << " elements\n"; for (int x : data) { std::cout << x << " "; } std::cout << "\n";}
int main() { // C 风格数组 int c_arr[] = {1, 2, 3, 4, 5}; process_array(c_arr);
// std::array std::array<int, 5> arr = {1, 2, 3, 4, 5}; process_array(arr);
// std::vector std::vector<int> vec = {1, 2, 3, 4, 5}; process_array(vec);
// 子视图 std::span<int> sub_view(vec.data() + 1, 3); // 2, 3, 4 process_array(sub_view);}25.7 最佳实践总结
Section titled “25.7 最佳实践总结”内存管理原则
Section titled “内存管理原则”| 原则 | 说明 |
|---|---|
| RAII 优先 | 用析构函数管理资源释放 |
| 智能指针优先 | 避免手动 new/delete |
| 栈优先 | 栈上对象无需管理 |
| 容器优先 | vector/string 替代手动数组 |
| unique_ptr 独占 | 单个所有者的资源用 unique_ptr |
| shared_ptr 共享 | 多个所有者的资源用 shared_ptr |
| weak_ptr 打破循环 | 观察但不拥有的引用用 weak_ptr |
代码检查清单
Section titled “代码检查清单”// 检查:是否用了智能指针而不是 new/delete?auto sp = std::make_shared<T>(); // ✅T* t = new T(); /* ... */ delete t; // ❌
// 检查:unique_ptr 用于独占所有权?auto up = std::make_unique<T>(); // ✅
// 检查:是否避免了数组手动管理?std::vector<int> vec(100); // ✅int* arr = new int[100]; /* ... */ delete[] arr; // ❌
// 检查:析构函数是否声明为 noexcept?class Resource { ~Resource() noexcept { /* 释放资源 */ }};
// 检查:是否在构造函数中完成所有资源获取?class RAIIClass { RAIIClass() : resource1_(), resource2_() { // 所有资源在构造时获取 }};noexcept 析构函数
Section titled “noexcept 析构函数”#include <exception>#include <iostream>
class MayThrow {public: ~MayThrow() { /* 可能抛出异常 */ }};
class NoThrow {public: ~NoThrow() noexcept { /* 绝不抛出 */ }};
// C++11 起,所有容器和智能指针要求析构函数 noexcept// 如果析构函数可能抛出,容器会在异常时调用 std::terminate25.8 章节总结
Section titled “25.8 章节总结”核心概念:
| 概念 | Python 对比 | C++ 实现 |
|---|---|---|
| 资源获取 | with 语句 | 构造函数 |
| 资源释放 | GC 不确定 | 析构函数确定 |
| 对象生命周期 | 引用计数+GC | 作用域/智能指针 |
| 内存泄漏 | GC 处理 | RAII 预防 |
RAII 四法则:
- 构造函数获取资源
- 析构函数释放资源
- 资源管理对象不可拷贝
- 析构函数绝不抛出异常
常见陷阱:
- 悬垂指针 → 用智能指针替代
- 空指针解引用 → 检查或用 optional
- 双重释放 → 用智能指针
- 内存泄漏 → RAII + 智能指针
- 缓冲区溢出 → 用容器和 span
最佳实践:
- 栈上能解决的问题不用堆
- 用
std::make_unique/std::make_shared - 用
std::vector替代手动数组 - 用
std::lock_guard/std::unique_lock管理锁
下章预告:ch26 学习 C++20 Concepts 模板约束,实现编译期类型检查。