Skip to content

对象生命周期与 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 包装。遵循零法则,让编译器帮你管理资源。

系统学习 C++ 生态,深入底层架构