目录

三 Resource Management 资源管理

13. Use objects to manage resources 以对象管理资源(RAII 核心思想)

核心解释

示例:手动管理 vsRAII 管理(避免内存泄漏)

关键要点

14. 在资源管理类中小心 copying 行为

核心解释

4 种典型拷贝行为及示例

1. 禁止拷贝(最直接)

2. 对底层资源使用引用计数(最常用)

3. 复制底层资源(深拷贝)

4. 转移底层资源的所有权

15. Provide access to raw resources in resource-managing classes 在资源管理类中提供对原始资源的访问

核心解释

两种原始资源访问方式及利弊

1. 显式转换(安全优先)

2. 隐式转换(便捷优先)

示例:显式转换 vs 隐式转换(以字体资源管理为例)

关键误区澄清

条款 16:Use the same form in corresponding uses of new and delete 成对使用 new 和 delete 时要采取相同形式

核心解释

示例 1:基础错误示例

示例 2:typedef 的隐藏坑(极易忽略)

条款 17:Store newed objects in smart pointers in standalone statements 以独立语句将 newed 对象置入智能指针

核心解释

核心问题:编译器的执行顺序重排

解决办法:独立语句拆分

示例:合写语句(有风险)vs 独立语句(无风险)

额外说明


三 Resource Management 资源管理

C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从不归还它,会导致内存泄漏),但内存只是你要管理的资源之一,其他常见的还有mutex,网络sockets以及数据库连接等,不再使用它们时,必须将它还给系统

13. Use objects to manage resources 以对象管理资源(RAII 核心思想)

核心解释

手动管理资源的最大问题:控制流异常跳转、提前返回时,资源释放代码(如delete)可能无法执行,导致泄漏。解决思想:RAII(Resource Acquisition Is Initialization,资源取得时机便是初始化时机) —— 获得资源后立刻存入管理对象,利用 C++ 对象离开作用域自动调用析构函数的特性,确保资源无论如何都会被释放(析构函数异常处理见条款 8)。

  • RAII 对象构造函数:获取资源(内存、锁、连接等);
  • RAII 对象析构函数:释放资源(无需手动调用,编译器自动执行);
  • 典型 RAII 对象:智能指针(std::shared_ptr/std::unique_ptr)、std::lock_guard(互斥锁管理)。

示例:手动管理 vsRAII 管理(避免内存泄漏)

#include <iostream>
#include <memory> // 智能指针头文件
using namespace std;

class Widget {};

// 反例:手动管理资源,异常导致泄漏
void func1() {
    Widget* pw = new Widget(); // 获取资源
    // 若此处抛出异常(如调用其他函数抛异常),delete永远不会执行
    delete pw; // 释放资源(可能无法执行)
}

// 正例:RAII智能指针管理资源,无泄漏风险
void func2() {
    shared_ptr<Widget> pw(new Widget()); // 获取资源即初始化RAII对象
    // 即使此处抛异常,pw离开作用域时析构函数自动调用delete
}

int main() {
    func2(); // 函数结束,pw析构,资源释放
    return 0;
}

关键要点

  • RAII 的核心是将资源生命周期与 RAII 对象生命周期绑定,对象生则资源在,对象死则资源释;
  • 无需关注控制流如何离开作用域,编译器保证析构函数执行,从根本上解决资源泄漏。

请记住:

  • 为防止资源泄露,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源

14. 在资源管理类中小心 copying 行为

核心解释

RAII 对象的核心是管理底层资源,因此RAII 对象的拷贝行为由底层资源的拷贝语义决定 —— 复制 RAII 对象必须同步处理其管理的资源,不存在统一的拷贝规则,需根据资源特性选择合适的拷贝行为,共 4 种典型方案。

4 种典型拷贝行为及示例

1. 禁止拷贝(最直接)

适用场景:底层资源不支持拷贝(如互斥锁、单例资源、数据库唯一连接),允许 RAII 对象拷贝无任何意义。实现方式:参考条款 6,要么将拷贝构造 / 赋值声明为private不实现,要么继承Uncopyable基类。

#include <mutex>
using namespace std;

// 模拟互斥锁管理类(互斥锁不支持拷贝,禁止RAII对象拷贝)
class MutexLock : private Uncopyable { // 继承Uncopyable禁用拷贝
public:
    MutexLock(mutex& m) : mtx(m) { mtx.lock(); } // 构造:获取资源(加锁)
    ~MutexLock() { mtx.unlock(); } // 析构:释放资源(解锁)
private:
    mutex& mtx;
};

mutex m;
void func() {
    MutexLock lock(m);
    // MutexLock lock2 = lock; // 编译错误:拷贝被禁用
}
2. 对底层资源使用引用计数(最常用)

适用场景:希望多个 RAII 对象共享一份底层资源,直到最后一个 RAII 对象被销毁时,才释放资源。实现方式:使用std::shared_ptr(原生支持引用计数),支持自定义删除器(不仅限于释放内存,可适配任意资源的释放逻辑)。

#include <memory>
#include <iostream>
using namespace std;

// 自定义删除器:适配非内存资源的释放(示例为内存,可替换为关闭连接、释放句柄等)
void custom_deleter(Widget* ptr) {
    cout << "自定义删除器释放资源:" << ptr << endl;
    delete ptr;
}

int main() {
    // shared_ptr构造:传入资源指针+自定义删除器
    shared_ptr<Widget> p1(new Widget(), custom_deleter);
    shared_ptr<Widget> p2 = p1; // 拷贝:引用计数+1(此时计数=2)
    
    p1.reset(); // p1释放管理权,计数=1
    p2.reset(); // p2释放管理权,计数=0 → 调用自定义删除器释放资源
    return 0;
}

关键特性shared_ptr的引用计数是线程安全的,自定义删除器不影响 RAII 对象的拷贝语义。

3. 复制底层资源(深拷贝)

适用场景:希望每个 RAII 对象拥有独立的底层资源副本,拷贝 RAII 对象时,需完整复制其管理的资源(避免多个对象共享同一份资源导致的冲突)。实现方式:为 RAII 类实现深拷贝版的拷贝构造和拷贝赋值运算符,复制资源而非仅复制资源指针。

#include <cstring>
using namespace std;

// 模拟字符串资源管理类(深拷贝)
class MyStringRAII {
public:
    MyStringRAII(const char* s) { // 构造:获取资源(分配内存)
        len = strlen(s);
        data = new char[len+1];
        strcpy(data, s);
    }

    // 拷贝构造:深拷贝底层资源(每个对象有独立内存)
    MyStringRAII(const MyStringRAII& rhs) {
        len = rhs.len;
        data = new char[len+1];
        strcpy(data, rhs.data);
    }

    // 拷贝赋值:深拷贝底层资源
    MyStringRAII& operator=(const MyStringRAII& rhs) {
        if (this == &rhs) return *this;
        // 释放当前资源
        delete[] data;
        // 复制新资源
        len = rhs.len;
        data = new char[len+1];
        strcpy(data, rhs.data);
        return *this;
    }

    ~MyStringRAII() { delete[] data; } // 析构:释放资源
private:
    char* data;
    size_t len;
};

int main() {
    MyStringRAII s1("hello");
    MyStringRAII s2 = s1; // 拷贝:s2拥有独立的"hello"内存
    return 0;
}
4. 转移底层资源的所有权

适用场景:希望仅有一个 RAII 对象管理底层资源,拷贝时将资源所有权从源对象转移给目标对象,源对象变为 “空状态”(不再管理任何资源)。实现方式:使用std::unique_ptr(原生支持独占所有权,C++11 后支持移动语义),或为自定义 RAII 类实现移动构造 / 移动赋值。

#include <memory>
using namespace std;

int main() {
    unique_ptr<Widget> p1(new Widget()); // p1拥有资源所有权
    // unique_ptr<Widget> p2 = p1; // 编译错误:禁止普通拷贝
    unique_ptr<Widget> p2 = move(p1); // 移动拷贝:所有权从p1转移到p2
    // p1现在为nullptr,不再管理任何资源;p2拥有原资源
    return 0;
}

当一个RAII对象被复制,会发生什么事?

  1. 禁止复制:许多时候允许RAII对象被复制并不合理.如果复制行为对RAII class不合理,应该禁止(条款6)

  2. 对底层资源使用引用计数法:有时候我们希望保有资源,直到它的最后一个使用者被销毁(std::shared_ptr就是这个方法)

    // 补充:自己可以选择"销毁"方式,不一定是释放内存
    void custom_deleter(int* ptr){
      std::cout << "custom_deleter" << ptr << std::endl;
      delete ptr;
    }
    std::shared_ptr<int> ptr(new int(5), custom_deleter);
  3. 复制底层资源:有时候,只要你喜欢,可以针对一份资源拥有其任何数量的副本.而你需要资源管理类的唯一理由是:当你不再需要某个资源时确保它被释放.在此情况下复制资源管理对象,应该复制其所包覆的所有资源(深拷贝)

  4. 转移底层资源的拥有权

请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定了RAII对象的copying行为
  • 普遍而常见的RAII class copying行为是:抑制copying,实行引用计数法.不过其他行为也都可能被实现

15. Provide access to raw resources in resource-managing classes 在资源管理类中提供对原始资源的访问

核心解释

RAII 类封装了底层资源,但实际开发中大量 API 要求直接传入原始资源(raw resource)(如系统调用、第三方库接口仅接受指针 / 句柄 / 连接符),因此 RAII 类必须提供原始资源访问接口,否则无法适配现有 API,失去实用价值。常见实现方式有两种,各有优劣,需根据场景选择。

两种原始资源访问方式及利弊

1. 显式转换(安全优先)

通过专用成员函数(如get())获取原始资源,调用者需显式调用,意图明确,避免意外的资源访问。

  • 典型示例:std::shared_ptr/std::unique_ptrget()方法(返回管理的原始指针);
  • 优势:安全,无隐式转换的意外风险;
  • 劣势:稍显繁琐,需手动调用get()
2. 隐式转换(便捷优先)

通过类型转换运算符实现,调用者可直接将 RAII 对象当作原始资源使用,无需显式调用函数。

  • 优势:便捷,对调用者透明,降低使用成本;
  • 劣势:存在隐式转换风险,可能导致原始资源 “虚吊”(dangle)—— 资源被 RAII 对象释放后,隐式获取的原始资源仍被使用。

示例:显式转换 vs 隐式转换(以字体资源管理为例)

#include <iostream>
using namespace std;

// 模拟原始字体资源句柄
typedef void* FontHandle;
// 模拟系统API:仅接受原始FontHandle
void useFont(FontHandle fh) { cout << "使用字体句柄:" << fh << endl; }

// 字体资源RAII管理类
class Font {
public:
    // 构造:获取字体资源(模拟)
    Font(FontHandle fh) : f(fh) {}
    // 析构:释放字体资源(模拟)
    ~Font() { cout << "释放字体资源:" << f << endl; }

    // 方式1:显式转换 —— 专用get()方法
    FontHandle get() const { return f; }

    // 方式2:隐式转换 —— 类型转换运算符(注释后为仅显式转换)
    operator FontHandle() const { return f; }

private:
    FontHandle f; // 管理的原始字体资源
};

int main() {
    FontHandle rawFh = (FontHandle)0x123456;
    Font f(rawFh);

    // 显式转换调用API:意图明确,安全
    useFont(f.get());

    // 隐式转换调用API:便捷,无需get()
    useFont(f);

    // 隐式转换的风险:意外获取原始资源,导致虚吊
    FontHandle danglingFh = f; // 隐式转换为原始句柄
    // f离开作用域,析构函数释放rawFh → danglingFh成为虚吊句柄
    // 此时使用danglingFh会导致未定义行为
    return 0;
}

// 某些程序员认为到处要求显式转换会使客户倒尽胃口,不再愿意使用这个类,从而增加了泄漏字体的可能性,而Font class的主要设计就是为了防止资源泄露
// 但隐式转换也有一定风险
Font f1;
FontHandle f2 = f1;
// 原意是拷贝一个Font对象,却反而将f1隐式转换为了FontHandle,而后才复制它
// 当你释放f1后,f2成为了"虚吊的"(dangle)
// 这与"封装"并不冲突,RAII classes并不是为了封装某物而存在的,它们的存在是为了确保一个特殊行为-资源释放会发生

关键误区澄清

RAII 类的核心设计目标是确保资源释放,而非封装资源 —— 提供原始资源访问并非破坏封装,而是为了适配实际 API 需求,这是 RAII 类实用化的必要条件。

请记住:

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个"取得其所管理之资源"的办法
  • 对原始资源的访问可能经由显式转换或隐式转换.一般而言显示转换比较安全,但隐式转换对客户比较方便

条款 16:Use the same form in corresponding uses of new and delete 成对使用 new 和 delete 时要采取相同形式

核心解释

C++ 中new/delete的内存布局和处理逻辑与是否使用数组标识[]强绑定,核心规则:new[]delete必须带[]new不带[]delete也必须不带[],违反则导致未定义行为(内存泄漏、堆破坏、程序崩溃)。

  • new T:分配单个 T 对象的内存,调用一次 T 的构造函数,内存布局为 “单个对象信息”;
  • new T[]:分配 T 数组的内存,调用 N 次 T 的构造函数,内存布局包含 “数组长度信息(cookie)”+“多个对象信息”;
  • delete p:释放单个对象内存,调用一次析构函数,仅处理 “单个对象信息”;
  • delete[] p:释放数组内存,先根据 “数组长度信息” 调用 N 次析构函数,再释放整体内存。

示例 1:基础错误示例

#include <iostream>
using namespace std;

class Widget {
public:
    Widget() { cout << "Widget构造" << endl; }
    ~Widget() { cout << "Widget析构" << endl; }
};

int main() {
    // 单个对象:new不带[]
    Widget* p1 = new Widget();
    delete p1; // 正确:delete不带[] → 调用1次析构
    // delete[] p1; // 错误:布局不匹配 → 未定义行为

    // 数组对象:new带[]
    Widget* p2 = new Widget[3];
    delete[] p2; // 正确:delete带[] → 调用3次析构
    // delete p2; // 错误:布局不匹配 → 未定义行为(仅调用1次析构,内存泄漏)
    return 0;
}

示例 2:typedef 的隐藏坑(极易忽略)

typedef定义数组类型时,使用new创建该类型对象,本质是数组分配(new []),释放时必须使用delete[],即使代码中未显式写[],这是高频错误点。

#include <string>
using namespace std;

// typedef定义数组类型:AddressLine是"4个string的数组"
typedef string AddressLine[4];

int main() {
    // 看似是单个对象分配,实际是数组分配(new string[4])
    AddressLine* pal = new AddressLine;
    // 释放时必须带[],否则未定义行为
    delete[] pal; // 正确
    // delete pal; // 错误:未调用4个string的析构,内存泄漏
    return 0;
}

请记住:

  • new和delete的使用中,[]要一一对应

条款 17:Store newed objects in smart pointers in standalone statements 以独立语句将 newed 对象置入智能指针

核心解释

new创建的对象直接作为智能指针构造函数的参数(合写语句),可能因编译器执行顺序优化导致隐蔽的资源泄漏,解决办法是将 “创建对象” 和 “置入智能指针” 拆分为两个独立语句,确保智能指针能及时接管资源。

核心问题:编译器的执行顺序重排

C++ 标准仅规定函数参数的求值顺序未定义,编译器会为了优化性能调整参数的计算顺序,以以下代码为例:

// 函数声明
int priority(); // 可能抛出异常的函数
void processWidget(shared_ptr<Widget>, int);

// 合写语句:new对象直接传入智能指针构造
processWidget(shared_ptr<Widget>(new Widget()), priority());

编译器可能的执行顺序(非唯一):

  1. 执行new Widget() —— 分配内存,创建 Widget 对象(资源已获取);
  2. 执行priority() —— 计算第二个参数的值;
  3. 执行shared_ptr<Widget>(...) —— 智能指针构造,接管资源。

泄漏风险:若步骤 2 的priority()抛出异常,步骤 3 的智能指针构造永远不会执行,而步骤 1 已获取的 Widget 资源无人接管,成为 “孤儿资源”,导致内存泄漏。

解决办法:独立语句拆分

将合写语句拆分为两步,编译器不会重排跨语句的执行顺序,确保智能指针在可能抛异常的函数调用前就接管资源:

  1. 第一步:创建对象并置入智能指针(RAII 对象初始化,资源已被管理);
  2. 第二步:调用目标函数,传入智能指针。

即使后续函数调用抛异常,智能指针已存在,离开作用域时会自动析构释放资源,无泄漏风险。

示例:合写语句(有风险)vs 独立语句(无风险)

#include <memory>
#include <iostream>
using namespace std;

class Widget {};
int priority() { throw runtime_error("优先级计算失败"); } // 必抛异常
void processWidget(shared_ptr<Widget>, int) {}

int main() {
    // 错误:合写语句,存在泄漏风险
    // processWidget(shared_ptr<Widget>(new Widget()), priority());
    // 执行顺序:new Widget() → priority()抛异常 → 智能指针未构造 → 资源泄漏

    // 正确:独立语句,无泄漏风险
    shared_ptr<Widget> pw(new Widget()); // 步骤1:智能指针接管资源
    processWidget(pw, priority());       // 步骤2:调用函数,即使抛异常,pw已管理资源

    // 即使priority()抛异常,pw离开作用域时析构,释放Widget资源
    return 0;
}

额外说明

shared_ptr的构造函数是explicit的,因此processWidget(new Widget(), priority())会直接编译报错(无法隐式转换),但这并不解决合写语句的泄漏问题,仅能避免语法错误,核心解决方案仍是独立语句拆分

请记住:

  • 以独立语句将newed对象存储入智能指针内.如果不这么做,一旦异常被抛出,有可能导致难以察觉的资源泄露
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐