Item 13:以对象管理资源
-
“为防止资源泄漏,请使用 RAII 对象,它们在构造函数中获得资源并在析构函数中释放资源。”
- RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种编程习惯,尤其在 C++ 中广泛应用。它的核心理念是将资源的生命周期与对象的生命周期绑定在一起,从而简化资源管理并减少内存泄漏或资源泄露的风险。
RAII 的主要特点:
-
资源管理:
资源(如内存、文件句柄、网络连接等)在对象的构造函数中获取,而在对象的析构函数中释放。这意味着只要对象存在,资源就会保持有效;一旦对象被销毁,资源也会自动释放。
-
异常安全:
RAII 确保即使在发生异常的情况下,资源也能得到正确释放。由于析构函数在对象生命周期结束时总会被调用,使用 RAII 可以避免遗漏资源释放的问题。
-
简化代码:
RAII 减少了手动管理资源的复杂性,开发者不再需要显式调用释放资源的函数,降低了出错的可能性。
-
“两个常被使用的 RAII classes 分别是
tr1::shared_ptr和auto _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 对象被复制时,应该如何处理它所管理的资源。具体来说,有以下几个方面:
-
资源的管理:
- RAII 对象负责管理某种资源(如动态分配的内存、文件句柄等)。如果一个 RAII 对象被复制,必须确保新的对象也能管理同样的资源。
-
复制行为的决定:
- 资源的复制行为会直接影响 RAII 对象的复制行为。例如,如果资源是独占的(如指针),那么复制对象可能需要独立的资源副本;而如果资源是共享的(如智能指针),则可以共享同一资源。
-
避免资源泄漏和双重释放:
- 如果不正确地处理复制操作,可能会导致资源泄漏(原始对象被销毁后资源没有释放)或双重释放(两个对象都尝试释放同一资源)。因此,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());