Skip to content

Ch 12: 内存管理

  • 理解 Python GC vs C++ RAII 的设计哲学差异
  • 掌握智能指针的用法和适用场景
  • 识别并避免常见内存泄漏模式
  • 理解移动语义如何优化性能
  • 了解内存分配器的概念

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_b
list_b.append(list_a) # list_b 引用 list_a
# 即使删除引用,循环引用也被 GC 处理
# 手动触发 GC(一般不需要)
import gc
gc.collect()

Python 的垃圾回收器主要基于引用计数,但引用计数无法处理循环引用,所以还使用了分代垃圾回收:

  1. 引用计数:每个对象有个计数器,引用增加+1,减少-1,为0时立即回收
  2. 分代回收:将对象按存活时间分代(0/1/2代),新对象更频繁检查
  3. 循环垃圾回收:检测并回收循环引用的对象组
import sys
# 引用计数示例
a = [1, 2, 3] # refcount = 1
print(sys.getrefcount(a)) # 2(调用时多一个临时引用)
b = a # refcount = 2
c = a # refcount = 3
del b # refcount = 2
del c # refcount = 1
# 删完才回收

优势:

  • 程序员不需要关心内存释放
  • 避免大多数悬垂指针问题
  • 开发效率高

局限:

  • 引用计数有额外开销(每次操作都要修改计数)
  • 循环引用可能导致内存延迟回收
  • GC 暂停可能造成卡顿
  • 无法控制对象生命周期

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++ 的内存布局有助于理解内存管理:

#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自动程序结束
代码区程序代码自动程序结束

智能指针是 C++11 引入的 RAII 内存管理工具,解决了手动管理内存的问题:

独占所有权模式——同一时间只能有一个 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_deleter

共享所有权模式——多个 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_shared
auto 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)++;
}

打破循环引用的弱引用指针:

#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 指向它,就能正确销毁
}
场景推荐指针原因
单一所有权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 控制块也有内存开销
  • 可能导致对象生命周期过长(内存占用高)

RAII(Resource Acquisition Is Initialization)是 C++ 的核心编程范式:

#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");
} // 自动关闭文件,即使异常发生
#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_;
}
};
#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;
}
};

移动语义是 C++11 最重要的特性之一,避免不必要的拷贝:

int x = 42; // x 是左值(可以取地址)
int y = x; // y 是左值
// 42 是右值(字面量,不能取地址)
// x + 1 是右值(临时对象)
// 左值引用
int& ref = x; // 左值引用
// 右值引用(C++11)
int&& rref = 42; // 右值引用
int&& rref2 = x + 1; // 临时对象的右值引用
#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_; }
};
#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 起强制优化)
}
}

移动语义对容器性能有巨大影响:

#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 内存中构造
}
#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; // 悬垂指针问题
}
#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
}
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 销毁时才释放
}

高级话题:使用自定义分配器控制内存分配策略:

#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 元编程与模板。