Skip to content

Ch 25: RAII 和内存管理

  • 掌握 RAII 模式的核心思想和实现原理
  • 理解栈和堆的区别以及内存布局
  • 学会实现 RAII 资源管理类
  • 掌握常见内存问题的识别和避免方法
  • 理解 C++ 内存模型和生命周期规则
# Python - 依赖垃圾回收器自动管理内存
def process():
data = load_data() # 创建大对象
result = heavy_computation(data)
return result
# data 在函数结束后变成垃圾,但 GC 回收时机不确定
# 可能在函数返回后立即回收,也可能延迟数秒

Python GC 的特点:

  • 引用计数为主 + 标记-清除为辅
  • 对象引用计数归零时立即回收(基本确定)
  • 循环引用需要标记-清除GC处理(延迟)
  • 无法控制回收时机
// 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 的核心优势:

  1. 确定性:资源在析构时立即释放
  2. 异常安全:异常传播时栈上对象仍会析构
  3. 封装性:资源管理逻辑隐藏在类内部
  4. 无泄漏:所有路径都能保证资源释放
# 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 立即析构,文件立即关闭,无需 GC
+------------------+ 高地址
| 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
}
#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 保证即使异常也会解锁
}
#include <mutex>
std::mutex mtx;
// 推荐:使用标准库 lock_guard
void 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); // 一次性锁住多个
// 避免死锁(标准库内部处理)
}
#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";
}
// 文件自动关闭,无论是否异常
}
#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;
}
#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% 安全
}
#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; // 编译错误!必须初始化
}
#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;
}
// ❌ 危险:异常导致跳过 delete
void 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
}
Terminal window
# 编译时加调试信息
g++ -g -O0 -o program program.cpp
# 运行 valgrind
valgrind --leak-check=full ./program
Terminal window
# 编译时加 ASan
g++ -fsanitize=address -g -O1 -o program program.cpp
# 运行
./program
#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";
}
#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::vector
void 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]
}
#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);
}
原则说明
RAII 优先用析构函数管理资源释放
智能指针优先避免手动 new/delete
栈优先栈上对象无需管理
容器优先vector/string 替代手动数组
unique_ptr 独占单个所有者的资源用 unique_ptr
shared_ptr 共享多个所有者的资源用 shared_ptr
weak_ptr 打破循环观察但不拥有的引用用 weak_ptr
// 检查:是否用了智能指针而不是 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_() {
// 所有资源在构造时获取
}
};
#include <exception>
#include <iostream>
class MayThrow {
public:
~MayThrow() { /* 可能抛出异常 */ }
};
class NoThrow {
public:
~NoThrow() noexcept { /* 绝不抛出 */ }
};
// C++11 起,所有容器和智能指针要求析构函数 noexcept
// 如果析构函数可能抛出,容器会在异常时调用 std::terminate

核心概念:

概念Python 对比C++ 实现
资源获取with 语句构造函数
资源释放GC 不确定析构函数确定
对象生命周期引用计数+GC作用域/智能指针
内存泄漏GC 处理RAII 预防

RAII 四法则:

  1. 构造函数获取资源
  2. 析构函数释放资源
  3. 资源管理对象不可拷贝
  4. 析构函数绝不抛出异常

常见陷阱:

  • 悬垂指针 → 用智能指针替代
  • 空指针解引用 → 检查或用 optional
  • 双重释放 → 用智能指针
  • 内存泄漏 → RAII + 智能指针
  • 缓冲区溢出 → 用容器和 span

最佳实践:

  • 栈上能解决的问题不用堆
  • 用 std::make_unique/std::make_shared
  • 用 std::vector 替代手动数组
  • 用 std::lock_guard/std::unique_lock 管理锁

下章预告:ch26 学习 C++20 Concepts 模板约束,实现编译期类型检查。