《Effective C++》——资源管理(item 13 ~ item 17)

82 阅读6分钟

Item 13:以对象管理资源

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

    • RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种编程习惯,尤其在 C++ 中广泛应用。它的核心理念是将资源的生命周期与对象的生命周期绑定在一起,从而简化资源管理并减少内存泄漏或资源泄露的风险。

    RAII 的主要特点:

    1. 资源管理

      资源(如内存、文件句柄、网络连接等)在对象的构造函数中获取,而在对象的析构函数中释放。这意味着只要对象存在,资源就会保持有效;一旦对象被销毁,资源也会自动释放。

    2. 异常安全

      RAII 确保即使在发生异常的情况下,资源也能得到正确释放。由于析构函数在对象生命周期结束时总会被调用,使用 RAII 可以避免遗漏资源释放的问题。

    3. 简化代码

      RAII 减少了手动管理资源的复杂性,开发者不再需要显式调用释放资源的函数,降低了出错的可能性。

  • “两个常被使用的 RAII classes 分别是 tr1::shared_ptrauto _ptr。前者通常是较佳选择,因为其 copy 行为比较直观。若选择 auto_ptr,复制动作会使它(被复制物)指向 null”

    • tr1::shared_ptr 是一个智能指针,用于管理动态分配的对象的生命周期。它属于 TR1(Technical Report 1),是 C++ 标准库的一部分,后来被包含在 C++11 中的 std::shared_ptr 中。
    • auto_ptr 是 C++98 引入的一种智能指针,用于管理动态分配的对象。它提供了一种简化内存管理的方法,但在 C++11 中被弃用,建议使用 std::unique_ptr 替代。

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

  • “复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为。”

    当我们说“复制 RAII 对象”,我们指的是当一个 RAII 对象被复制时,应该如何处理它所管理的资源。具体来说,有以下几个方面:

    1. 资源的管理

      • RAII 对象负责管理某种资源(如动态分配的内存、文件句柄等)。如果一个 RAII 对象被复制,必须确保新的对象也能管理同样的资源。
    2. 复制行为的决定

      • 资源的复制行为会直接影响 RAII 对象的复制行为。例如,如果资源是独占的(如指针),那么复制对象可能需要独立的资源副本;而如果资源是共享的(如智能指针),则可以共享同一资源。
    3. 避免资源泄漏和双重释放

      • 如果不正确地处理复制操作,可能会导致资源泄漏(原始对象被销毁后资源没有释放)或双重释放(两个对象都尝试释放同一资源)。因此,RAII 对象的复制构造函数和赋值运算符必须明确如何处理这些情况。
  • “普遍而常见的 RAII class copying 行为是:抑制 copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。”

    • 在设计 RAII 类时,通常会采取一些常见的策略来处理复制行为,以确保资源管理的安全和有效性。我们来逐步解析这句话的关键点。

      抑制复制(Suppressing Copying)

      在许多 RAII 类中,复制构造函数和复制赋值运算符被删除或标记为 delete,以阻止对象被复制。这是因为:

      • 资源独占性:许多 RAII 对象管理独占资源(例如文件句柄或动态内存),复制这些对象可能会导致双重释放(两个对象试图释放同一资源)。
      • 简化管理:通过禁止复制,可以简化资源管理的逻辑,避免因复制而引入复杂的资源管理问题。

      施行引用计数法(Reference Counting)

      另一种常见的策略是使用引用计数来管理资源的共享。引用计数允许多个 RAII 对象共享同一资源,而不会导致资源的重复释放。这种方法通常与智能指针相关,如 std::shared_ptr。其优点包括:

      • 资源共享:多个对象可以安全地共享同一资源,只有在最后一个引用计数为零时,资源才会被释放。
      • 安全性:避免了资源的重复释放,确保资源在所有引用被释放后才真正释放。

      其他可能的行为

      除了抑制复制和引用计数,还有其他多种行为可以实现。例如:

      • 深复制:某些 RAII 类可能允许深复制,创建资源的独立副本。这在管理可变状态的资源时可能是合适的。
      • 移动语义:随着 C++11 引入的移动语义,RAII 类可以实现移动构造函数和移动赋值运算符,以高效地转移资源所有权,而不涉及资源的复制。

Item 15:在资源管理类中提供对原始资源的访问

  • “APIs 往往要求访问原始资源(raw resources),所以每一个 RAII class 应该提供一个 ‘取得其所管理之资源’ 的办法。

  • “对原始资源的访问可能经由显式转换或隐式转换,一般而言显式转换比较安全,但隐式转换对客户比较方便。”

Item 16:成对使用 new 和 delete 时要采用相同形式

  • *“如果你在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 []。如果你在 new 表达式中不使用 [],一定不要在相应的 delete 表达式中使用 []。” *
std::string* stringPtr1 = new std::string;
std::string* stringPtr2=  new std::string[100];
...
delete stringPtr1;
delete [] stringPtr2;

数组所用的内存通常还包括 “数组大小” 的记录,以便 delete 知道需要调用多少次析构函数。

Item 17:以独立语句将 newed 对象置入智能指针

  • “以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。”
processWidget(std::shared_ptr<Widget>(new Widget), func());

上述调用可能会泄漏资源!!!

上述语句在调用 processWidget 之前,编译器必须创建代码,做以下三件事:

① 调用 func()

② 执行 new Widget

③ 调用 shared_ptr 构造函数。

然而 C++ 编译器完成这些事情的顺序不确定,能确定的只是 new Widget 一定执行于 shared_ptr 构造函数被调用之前。func() 以第二顺位执行时,若 func() 调用导致异常,new Widget 返回的指针将会遗失,即没有任何机制来释放可能的资源,导致内存泄漏。

避免这类问题的办法很简单:使用分离语句:

std::shared_ptr<Widget> widgetPtr(new Widget()); 
processWidget(widgetPtr, func());