对象生命周期与 RAII
RAII(Resource Acquisition Is Initialization)是 C++ 最重要的编程惯用法,也是现代 C++ 资源管理的核心哲学。
什么是 RAII
RAII 的核心思想:资源的获取与对象的初始化绑定,资源的释放与对象的销毁绑定。
对象构造 → 获取资源(内存、文件、锁、网络连接...)
对象析构 → 释放资源(自动,无论正常退出还是异常)cpp
// 不用 RAII 的写法(危险)
void bad_example() {
FILE* f = fopen("data.txt", "r");
// ... 如果这里抛出异常,f 永远不会被关闭!
process(f);
fclose(f); // 可能永远执行不到
}
// RAII 写法(安全)
#include <fstream>
void good_example() {
std::ifstream f("data.txt"); // 构造时打开文件
process(f);
// 函数结束,f 析构,文件自动关闭
// 即使 process() 抛出异常也没问题
}对象生命周期
构造顺序
cpp
#include <iostream>
struct Base {
Base() { std::cout << "Base 构造\n"; }
~Base() { std::cout << "Base 析构\n"; }
};
struct Member {
Member(int v) : val(v) { std::cout << "Member(" << v << ") 构造\n"; }
~Member() { std::cout << "Member 析构\n"; }
int val;
};
struct Derived : Base {
Member m1{1};
Member m2{2};
Derived() { std::cout << "Derived 构造\n"; }
~Derived() { std::cout << "Derived 析构\n"; }
};
// 输出顺序:
// Base 构造 ← 1. 基类先构造
// Member(1) 构造 ← 2. 成员按声明顺序构造
// Member(2) 构造
// Derived 构造 ← 3. 派生类构造体
// Derived 析构 ← 析构顺序完全相反
// Member 析构
// Member 析构
// Base 析构初始化列表
cpp
class Connection {
std::string host_;
int port_;
bool connected_;
public:
// 正确:使用初始化列表,按成员声明顺序初始化
Connection(std::string host, int port)
: host_(std::move(host)) // 移动,避免拷贝
, port_(port)
, connected_(false)
{}
// 错误:在构造体内赋值(先默认构造,再赋值,多一次操作)
Connection(std::string host, int port) {
host_ = host; // 先默认构造 string,再拷贝赋值
port_ = port;
connected_ = false;
}
};自定义 RAII 包装器
文件句柄
cpp
#include <cstdio>
#include <stdexcept>
class FileHandle {
FILE* fp_ = nullptr;
public:
explicit FileHandle(const char* path, const char* mode) {
fp_ = fopen(path, mode);
if (!fp_) throw std::runtime_error("无法打开文件");
}
~FileHandle() {
if (fp_) fclose(fp_);
}
// 禁止拷贝(资源不能共享)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 允许移动
FileHandle(FileHandle&& other) noexcept : fp_(other.fp_) {
other.fp_ = nullptr; // 转移所有权
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (fp_) fclose(fp_);
fp_ = other.fp_;
other.fp_ = nullptr;
}
return *this;
}
FILE* get() const { return fp_; }
operator bool() const { return fp_ != nullptr; }
};
void use_file() {
FileHandle f("data.txt", "r");
// 使用 f.get() 操作文件
// 函数结束自动关闭
}互斥锁 RAII
cpp
#include <mutex>
std::mutex mtx;
void safe_operation() {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁
// ... 临界区操作
// 函数结束,lock 析构,自动解锁
}
// unique_lock:更灵活,支持延迟加锁、手动解锁
void flexible_lock() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 做一些不需要锁的工作
lock.lock(); // 手动加锁
// 临界区
lock.unlock(); // 手动解锁(可选)
// 析构时若仍持有锁,自动解锁
}通用 ScopeGuard
cpp
#include <functional>
#include <utility>
// 通用作用域守卫:离开作用域时执行任意清理操作
class ScopeGuard {
std::function<void()> cleanup_;
bool active_ = true;
public:
explicit ScopeGuard(std::function<void()> f) : cleanup_(std::move(f)) {}
~ScopeGuard() {
if (active_) cleanup_();
}
void dismiss() { active_ = false; } // 取消清理
ScopeGuard(const ScopeGuard&) = delete;
ScopeGuard& operator=(const ScopeGuard&) = delete;
};
// 使用宏简化
#define SCOPE_EXIT(code) ScopeGuard _guard_##__LINE__([&]{ code; })
void example() {
auto* resource = acquire_resource();
SCOPE_EXIT(release_resource(resource)); // 无论如何都会执行
if (error_condition()) return; // 安全退出
// ... 正常逻辑
}五法则(Rule of Five)
如果你需要自定义以下任意一个,通常需要全部定义:
cpp
class Resource {
int* data_;
size_t size_;
public:
// 1. 析构函数
~Resource() { delete[] data_; }
// 2. 拷贝构造(深拷贝)
Resource(const Resource& other)
: data_(new int[other.size_])
, size_(other.size_)
{
std::copy(other.data_, other.data_ + size_, data_);
}
// 3. 拷贝赋值(copy-and-swap 惯用法)
Resource& operator=(Resource other) { // 注意:值传递触发拷贝构造
swap(*this, other);
return *this;
}
// 4. 移动构造(转移所有权,noexcept 很重要!)
Resource(Resource&& other) noexcept
: data_(other.data_)
, size_(other.size_)
{
other.data_ = nullptr;
other.size_ = 0;
}
// 5. 移动赋值
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
friend void swap(Resource& a, Resource& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
};零法则(Rule of Zero)
更好的做法:让编译器生成所有特殊成员函数,通过组合 RAII 类型来管理资源。
cpp
// 不需要手写任何特殊成员函数!
class Connection {
std::unique_ptr<Socket> socket_; // 自动管理内存
std::string host_; // 自动管理字符串
std::vector<uint8_t> buffer_; // 自动管理数组
public:
Connection(std::string host, int port)
: socket_(std::make_unique<Socket>(host, port))
, host_(std::move(host))
, buffer_(4096)
{}
// 析构、拷贝、移动全部由成员自动处理
};关键认知
RAII 不只是"自动释放内存",它是 C++ 异常安全的基础。任何资源(锁、文件、连接、内存)都应该用 RAII 包装。遵循零法则,让编译器帮你管理资源。