Ch 12: 内存管理
- 理解 Python GC vs C++ RAII 的设计哲学差异
- 掌握智能指针的用法和适用场景
- 识别并避免常见内存泄漏模式
- 理解移动语义如何优化性能
- 了解内存分配器的概念
12.1 Python 垃圾回收回顾
Section titled “12.1 Python 垃圾回收回顾”Python 使用自动垃圾回收机制(GC)来管理内存:
# Python - 自动垃圾回收class Resource: def __init__(self, name): self.name = name self.data = [] print(f"Resource {name} created")
def __del__(self): # 析构函数 - 当对象被销毁时调用 # 但调用时机不确定(GC 决定) print(f"Resource {self.name} destroyed")
# 创建和销毁r = Resource("test")r = None # 删除引用,但 GC 可能延迟回收
# 循环引用 - Python GC 能处理list_a = []list_b = []list_a.append(list_b) # list_a 引用 list_blist_b.append(list_a) # list_b 引用 list_a# 即使删除引用,循环引用也被 GC 处理
# 手动触发 GC(一般不需要)import gcgc.collect()Python GC 的工作原理
Section titled “Python GC 的工作原理”Python 的垃圾回收器主要基于引用计数,但引用计数无法处理循环引用,所以还使用了分代垃圾回收:
- 引用计数:每个对象有个计数器,引用增加+1,减少-1,为0时立即回收
- 分代回收:将对象按存活时间分代(0/1/2代),新对象更频繁检查
- 循环垃圾回收:检测并回收循环引用的对象组
import sys
# 引用计数示例a = [1, 2, 3] # refcount = 1print(sys.getrefcount(a)) # 2(调用时多一个临时引用)
b = a # refcount = 2c = a # refcount = 3del b # refcount = 2del c # refcount = 1# 删完才回收Python GC 的优势与局限
Section titled “Python GC 的优势与局限”优势:
- 程序员不需要关心内存释放
- 避免大多数悬垂指针问题
- 开发效率高
局限:
- 引用计数有额外开销(每次操作都要修改计数)
- 循环引用可能导致内存延迟回收
- GC 暂停可能造成卡顿
- 无法控制对象生命周期
12.2 C++ 内存管理模型
Section titled “12.2 C++ 内存管理模型”C++ 采用手动+RAII 的内存管理方式,给予程序员完全控制权:
#include <memory>#include <iostream>
// 手动管理 - 危险区域void manual_memory() { // ❌ 危险:手动分配内存 int* ptr = new int(42); std::cout << "Value: " << *ptr << std::endl; delete ptr; // 必须手动释放 ptr = nullptr; // 避免悬垂指针
// ❌ 常见错误 int* dangling() { int local = 42; return &local; // 返回局部变量地址! }
// ❌ 内存泄漏 void leak() { int* p = new int[100]; // 忘记 delete[] // 函数退出时内存泄漏 }
// ❌ 重复释放 void double_free() { int* p = new int(42); delete p; delete p; // 未定义行为! }}C++ 内存区域
Section titled “C++ 内存区域”理解 C++ 的内存布局有助于理解内存管理:
#include <iostream>#include <vector>#include <string>
int global_var = 42; // BSS 段(未初始化)或 Data 段(已初始化)
static int static_var = 42; // Data 段
int main() { int local_var = 42; // 栈 const int const_var = 42; // 栈(可能放入只读存储)
std::string s = "hello"; // s 在栈上,内容在堆上 std::vector<int> v(100); // v 在栈上,数据在堆上
// 动态分配 int* heap_var = new int(42); // 堆 int* heap_array = new int[100]; // 堆
delete heap_var; delete[] heap_array;
return 0;}| 区域 | 内容 | 管理方式 | 生命周期 |
|---|---|---|---|
| 栈 (Stack) | 局部变量、函数参数 | 自动 | 函数结束 |
| 堆 (Heap) | new/malloc | 手动(智能指针) | 手动控制 |
| 全局/静态 | 全局变量、静态变量 | 自动 | 程序结束 |
| 常量区 | 字面量、const | 自动 | 程序结束 |
| 代码区 | 程序代码 | 自动 | 程序结束 |
12.3 智能指针详解
Section titled “12.3 智能指针详解”智能指针是 C++11 引入的 RAII 内存管理工具,解决了手动管理内存的问题:
12.3.1 std::unique_ptr
Section titled “12.3.1 std::unique_ptr”独占所有权模式——同一时间只能有一个 unique_ptr 拥有对象:
#include <memory>#include <iostream>
void unique_ptr_demo() { // 创建 unique_ptr auto ptr = std::make_unique<int>(42);
// 解引用访问 std::cout << *ptr << std::endl; // 42 std::cout << ptr.get() << std::endl; // 内存地址
// 独占所有权 - 不能拷贝 // auto ptr2 = ptr; // 编译错误! // auto ptr2(ptr); // 编译错误!
// 可以移动 auto ptr2 = std::move(ptr); // ptr 变为空,ptr2 拥有对象 if (!ptr) { std::cout << "ptr is now empty" << std::endl; }
// 释放前可以检查 if (ptr2) { std::cout << "ptr2 owns an object" << std::endl; }
// 超出作用域时自动 delete} // ptr2 自动释放内存unique_ptr 的数组特化:
// unique_ptr 数组版本auto arr = std::make_unique<int[]>(10);arr[0] = 1;arr[9] = 9;// 不要用 unique_ptr<int> 指向数组!// 正确:unique_ptr<int[]> 或使用 vector
// 错误示例// auto bad = std::make_unique<int>(10); // 单一对象// bad[0] = 1; // 编译错误自定义删除器:
#include <memory>#include <fstream>
// 自定义删除器示例:文件句柄auto file_deleter = [](FILE* f) { if (f) { std::fclose(f); std::cout << "File closed" << std::endl; }};
std::unique_ptr<FILE, decltype(file_deleter)> file( std::fopen("data.txt", "r"), file_deleter);
if (file) { // 使用文件}// 关闭时自动调用 file_deleter12.3.2 std::shared_ptr
Section titled “12.3.2 std::shared_ptr”共享所有权模式——多个 shared_ptr 可以共享同一个对象:
#include <memory>#include <iostream>
void shared_ptr_demo() { // 创建 shared_ptr auto ptr = std::make_shared<int>(42);
// 拷贝构造 - 引用计数增加 auto ptr2 = ptr; // 引用计数从 1 变为 2 auto ptr3(ptr); // 引用计数从 2 变为 3
std::cout << "Ref count: " << ptr.use_count() << std::endl; // 3
// 所有指针都能访问同一对象 *ptr2 = 100; std::cout << *ptr << std::endl; // 100
// 指针超出作用域,引用计数减少 ptr3.reset(); // 显式释放,计数减 1
// 最后一个 shared_ptr 释放时,对象被销毁}shared_ptr 的内部结构:
shared_ptr 包含两个指针:1. 管理对象指针(指向堆内存)2. 控制块指针(指向控制块)
控制块包含:- 引用计数- 弱引用计数(weak_ptr 用)- 删除器(可选)- 分配器(可选)make_shared 的优势:
// 推荐:使用 make_sharedauto sp = std::make_shared<int>(42);
// 不推荐:直接构造std::shared_ptr<int> sp2(new int(42));
// 原因:make_shared 将对象和控制块分配在同一个内存块// 减少内存分配次数,提高缓存命中率shared_ptr 的线程安全:
#include <memory>#include <thread>
// shared_ptr 本身引用计数的操作是线程安全的// 但指向的对象的访问需要自行同步
std::shared_ptr<int> global_ptr = std::make_shared<int>(42);
// 线程安全:引用计数操作void increment() { auto local = global_ptr; // 拷贝,引用计数安全 // 使用 local}
// 注意:对象数据的并发访问仍需加锁std::mutex mtx;int* unsafe_data; // 裸指针
void safe_increment() { std::lock_guard<std::mutex> lock(mtx); (*unsafe_data)++;}12.3.3 std::weak_ptr
Section titled “12.3.3 std::weak_ptr”打破循环引用的弱引用指针:
#include <memory>#include <iostream>
void weak_ptr_demo() { // 创建 shared_ptr auto sp = std::make_shared<int>(42);
// 创建 weak_ptr(不增加引用计数) std::weak_ptr<int> wp = sp;
std::cout << "Ref count: " << sp.use_count() << std::endl; // 1
// 检查对象是否还存在 if (!wp.expired()) { std::cout << "Object still alive" << std::endl; // 临时提升为 shared_ptr 使用 if (auto lock = wp.lock()) { std::cout << "Value: " << *lock << std::endl; } }
// 原始 shared_ptr 释放后,weak_ptr 过期 sp.reset(); if (wp.expired()) { std::cout << "Object destroyed" << std::endl; }}循环引用问题与解决:
#include <memory>#include <iostream>
class Node {public: std::string name; std::weak_ptr<Node> parent; // 用 weak_ptr 打破循环 std::vector<std::shared_ptr<Node>> children;
Node(const std::string& n) : name(n) {} ~Node() { std::cout << "Node " << name << " destroyed" << std::endl; }};
void tree_demo() { auto root = std::make_shared<Node>("root"); auto child = std::make_shared<Node>("child");
root->children.push_back(child); child->parent = root; // weak_ptr 不增加引用计数
// 即使 child 还被 root 引用着, // 只要没有其他 shared_ptr 指向它,就能正确销毁}智能指针选择指南
Section titled “智能指针选择指南”| 场景 | 推荐指针 | 原因 |
|---|---|---|
| 单一所有权 | std::unique_ptr | 最安全、最高效 |
| 多个所有者 | std::shared_ptr | 共享所有权 |
| 打破循环引用 | std::weak_ptr | 观察但不拥有 |
| 原生 API 接口 | std::unique_ptr<T[]> | 自定义删除器 |
| 缓存实现 | std::weak_ptr | 缓存中临时引用 |
不要使用 shared_ptr 的情况:
- 能用
unique_ptr的时候就用unique_ptr shared_ptr有引用计数开销(原子操作,更慢)shared_ptr控制块也有内存开销- 可能导致对象生命周期过长(内存占用高)
12.4 RAII 模式详解
Section titled “12.4 RAII 模式详解”RAII(Resource Acquisition Is Initialization)是 C++ 的核心编程范式:
12.4.1 RAII 原理
Section titled “12.4.1 RAII 原理”#include <fstream>#include <mutex>#include <iostream>
// RAII 核心思想:// 1. 资源在构造函数中获取// 2. 资源在析构函数中释放// 3. 对象生命周期由作用域管理
class FileGuard {private: std::fstream file_; std::string filename_;
public: FileGuard(const std::string& filename) : filename_(filename) { file_.open(filename); if (!file_.is_open()) { throw std::runtime_error("Cannot open file"); } }
~FileGuard() { if (file_.is_open()) { file_.close(); std::cout << "File " << filename_ << " closed" << std::endl; } }
// 禁止拷贝 FileGuard(const FileGuard&) = delete; FileGuard& operator=(const FileGuard&) = delete;
// 允许移动 FileGuard(FileGuard&&) = default; FileGuard& operator=(FileGuard&&) = default;
void write(const std::string& data) { file_ << data << std::endl; }};
// 使用void process() { FileGuard fg("data.txt"); // 打开文件 fg.write("hello");} // 自动关闭文件,即使异常发生12.4.2 std::lock_guard 与 mutex
Section titled “12.4.2 std::lock_guard 与 mutex”#include <mutex>
class Counter {private: int count_ = 0; mutable std::mutex mtx_; // mutable 允许在 const 方法中加锁
public: void increment() { // lock_guard 在构造时加锁,析构时自动解锁 std::lock_guard<std::mutex> lock(mtx_); count_++; }
int value() const { std::lock_guard<std::mutex> lock(mtx_); return count_; }};12.4.3 scoped_lock(C++17)
Section titled “12.4.3 scoped_lock(C++17)”#include <mutex>
class BankAccount {private: int balance_ = 0; mutable std::mutex mtx1_, mtx2_;
public: void transfer_to(BankAccount& other, int amount) { // std::scoped_lock 同时锁定多个 mutex // 避免死锁(按固定顺序加锁) std::scoped_lock lock(mtx1_, other.mtx2_); balance_ -= amount; other.balance_ += amount; }};12.5 移动语义
Section titled “12.5 移动语义”移动语义是 C++11 最重要的特性之一,避免不必要的拷贝:
12.5.1 左值与右值
Section titled “12.5.1 左值与右值”int x = 42; // x 是左值(可以取地址)int y = x; // y 是左值
// 42 是右值(字面量,不能取地址)// x + 1 是右值(临时对象)
// 左值引用int& ref = x; // 左值引用
// 右值引用(C++11)int&& rref = 42; // 右值引用int&& rref2 = x + 1; // 临时对象的右值引用12.5.2 移动构造函数与移动赋值
Section titled “12.5.2 移动构造函数与移动赋值”#include <vector>#include <iostream>
class Buffer {private: int* data_; size_t size_;
public: Buffer(size_t size) : data_(new int[size]), size_(size) { std::cout << "Constructor: allocated " << size << std::endl; }
// 移动构造函数 Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; // 避免析构时重复释放 other.size_ = 0; std::cout << "Move constructor" << std::endl; }
// 移动赋值运算符 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { delete[] data_; // 释放原有资源 data_ = other.data_; size_ = other.size_; other.data_ = nullptr; other.size_ = 0; std::cout << "Move assignment" << std::endl; } return *this; }
~Buffer() { if (data_) { std::cout << "Destructor: freed " << size_ << std::endl; delete[] data_; } else { std::cout << "Destructor: null buffer" << std::endl; } }
size_t size() const { return size_; }};12.5.3 移动语义的实际效果
Section titled “12.5.3 移动语义的实际效果”#include <vector>
void move_demo() { std::vector<int> v1 = {1, 2, 3, 4, 5};
// 拷贝:复制整个 vector std::vector<int> v2 = v1; std::cout << "v1 size: " << v1.size() << std::endl; // 5
// 移动:将 v1 的资源转移给 v3,v1 变为空 std::vector<int> v3 = std::move(v1); std::cout << "v1 size: " << v1.size() << std::endl; // 0 std::cout << "v3 size: " << v3.size() << std::endl; // 5
// 函数返回值优化(RVO / NRVO) std::vector<int> create_vector() { std::vector<int> v{1, 2, 3}; return v; // 移动而非拷贝(C++17 起强制优化) }}移动语义与容器
Section titled “移动语义与容器”移动语义对容器性能有巨大影响:
#include <vector>#include <string>
void container_performance() { std::vector<std::string> v;
// 如果 string 没有移动构造,会触发拷贝 // 现代 C++ string 有短字符串优化(SSO) // 大字符串会使用移动构造
for (int i = 0; i < 1000; i++) { std::string s = "string number " + std::to_string(i); v.push_back(std::move(s)); // 移动而非拷贝 // 注意:移动后 s 状态未定义,不应再使用 }
// emplace_back 直接在容器中构造,避免拷贝/移动 struct Point { int x, y; Point(int x, int y) : x(x), y(y) {} };
std::vector<Point> points; points.emplace_back(1, 2); // 直接在 vector 内存中构造}12.6 常见内存问题与避免
Section titled “12.6 常见内存问题与避免”12.6.1 内存泄漏
Section titled “12.6.1 内存泄漏”#include <memory>
// 内存泄漏场景void leak_examples() { // 1. 忘记 delete int* p = new int(42); // 忘记 delete p
// 2. 早期返回 void bad_function() { int* p = new int(42); if (something) return; // 泄漏! delete p; }
// 3. 异常 void bad_with_exception() { int* p = new int(42); throw std::runtime_error("oops"); // 泄漏! delete p; }}
// 解决方法:使用智能指针void fixed_versions() { // unique_ptr 自动管理 auto p = std::make_unique<int>(42); // 自动释放
// 如果必须使用裸指针,用完后置空 int* raw = new int(42); delete raw; raw = nullptr; // 悬垂指针问题}12.6.2 悬垂指针与引用
Section titled “12.6.2 悬垂指针与引用”#include <iostream>
void dangling_demo() { int* p;
{ int x = 42; p = &x; // p 指向局部变量 } // x 销毁,p 成为悬垂指针
// *p = 100; // 未定义行为!
// 解决方法:使用智能指针 std::unique_ptr<int> sp;
{ auto local = std::make_unique<int>(42); sp = std::move(local); } // sp 仍有效
std::cout << *sp << std::endl; // 42}12.6.3 双重释放
Section titled “12.6.3 双重释放”void double_free_demo() { int* p = new int(42); delete p; // delete p; // 未定义行为!
// shared_ptr 自动处理 auto sp1 = std::make_shared<int>(42); auto sp2 = sp1; // 共享所有权 // 只有最后一个 shared_ptr 销毁时才释放}12.7 自定义分配器
Section titled “12.7 自定义分配器”高级话题:使用自定义分配器控制内存分配策略:
#include <memory>#include <vector>
// 内存池分配器示例template<typename T>class PoolAllocator {public: using value_type = T;
PoolAllocator() = default; template<typename U> PoolAllocator(const PoolAllocator<U>&) {}
T* allocate(size_t n) { std::cout << "Allocating " << n << " elements" << std::endl; return static_cast<T*>(::operator new(n * sizeof(T))); }
void deallocate(T* p, size_t) { std::cout << "Deallocating" << std::endl; ::operator delete(p); }};
// 使用内存池分配器void allocator_demo() { std::vector<int, PoolAllocator<int>> v; v.push_back(1); v.push_back(2);}- Python GC 自动回收内存,但无法精确控制生命周期
- C++ 手动管理内存需要
new/delete配对 - 智能指针提供安全的自动内存管理
unique_ptr独占所有权,适用于大多数场景shared_ptr共享所有权,有额外开销weak_ptr打破循环引用,观察但不拥有- RAII 模式确保资源正确释放
- 移动语义避免不必要的拷贝,提高性能
- 移动构造函数应标记
noexcept - 自定义分配器可以优化特定场景的内存管理
下章预告:ch13 元编程与模板。