Effective C++学习笔记三(资源管理)

条款13:以对象管理资源

许多资源被动态分配于heap内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形式而设计的特制产品。auto_ptr是个"类指针(pointer-like)对象",也就是所谓"智能指针",其析构函数自动对其所指对象调用delete。

以对象管理资源的两个关键想法:

  • 获得资源后立刻放进管理对象内。我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。
  • 管理对象运用析构函数确保资源被释放。不论控制流如何离开区块,一旦对象被销毁(例如当对象离开作用域)其析构函数自然会被自动调用,于是资源被释放。如果资源释放动作可能导致抛出异常,事情变得有点棘手,但条款8已经能够解决这个问题。

1. 为防止资源泄露,请使用RAII对象(RAII:资源取得时机便是初始化时机),它们在构造函数中获得资源并在析构函数中释放资源。()

2. 两个常被使用的RAII classes分别是shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作(通过copy构造函数或copy assignment操作符)会使它(被复制物)指向null。(auto_ptr和shared_ptr两者都在其析构函数内做delete而不是delete[]动作。那意味在动态分配而得的array身上使用auto_ptr或shared_ptr是个馊主意。尽管如此那么做仍能通过编译。注意并没有特别针对"C++"动态分配数组“而设计的类似auto_ptr或shared_ptr那样的东西。那是因为vector和string总是可以取代动态分配而得的数组。)

如果你打算手工释放资源(例如使用delete而非使用一个资源管理类;resource-managing class),容易发生某些错误。罐装式的资源管理类如auto-ptr和shared_ptr往往比较能够轻松遵循本条款忠告。

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

由于并非所有资源都是heap-based,对这种资源而言,像auto_ptr和shared_ptr这样的智能指针往往不适合作为资源掌管者。既然如此,有可能偶而你会发现需要建立自己的资源管理类.但是当一个RAII对象被复制(也即是当资源管理类被复制时),会发生什么事?

  • 禁止复制。许多时候允许RAII对象被复制并不合理。此时应该禁止之。条款6告诉你怎么做:将copyying操作声明为private且不去定义它们,或继承一个copyying操作声明为private的父类,或定义为删除的(C++11新标准)。
  • 对底层资源祭出"引用计数法"(reference-count)。有时候我们希望保有资源,直到它的最后一个使用者(某对象)被销毁。这种情况下复制RAII对象时,应该将资源的"被引用数"递增。shared_ptr便是如此。通常只要内含一个shared_ptr成员变量。RAII class便可实现出reference-counting行为。然而很不幸shared_ptr的缺省行为是"当引用次数为0时删除其所指物",那不是我们要的行为,幸运的是shared_ptr允许指定所谓的"删除器"(deleter),那是一个函数或函数对象(function object),当引用次数为0时便被调用(此机能并不存在于auto_ptr-------它总是将其指针删除)。
  • 复制底部资源。若需要"资源管理类"的唯一理由是,当你不再需要某个复件时确保它被释放。此情况下复制资源管理对象,应该同时也复制其所包覆的资源。也就是说复制资源管理对象时,进行的是"深度拷贝"
  • 转移底部资源的拥有权。此时资源的拥有权会从被复制物转移到目标物。如条款13所述,这是auto_ptr奉行的复制意义。

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

2. 普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。(Copying函数(包括copy构造函数和copy assignment操作符)有可能被编译器自动创建出来,因此除非编译器所生版本做了你想要做的事,否则你得自己编写它们。)

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

资源管理类(resource-managing classes)很棒。在一个完美世界中你将依赖这样的classes来处理和资源之间的所有互动,而不是玷污双手直接处理原始资源(raw resources)。但这个世界并不完美。许多APIs直接置涉资源,所以不得不绕过资源管理对象(resource-managing objects)直接访问原始资源(raw resources).
是否该提供一个显式转换(例如get成员函数)将RAII class转换为其底部资源,或是应该提供隐式转换,答案主要取决于RAII class被设计执行的特定工作,以及它被使用的情况。最佳设计很可能是坚持条款18的忠告:"让接口容易被正确使用,不易被误用"。通常显示转换函数如get是比较受欢迎的路子,因为它将"非故意之类型转换"的可能性最小化了。然而有时候,隐式类型转换所带来的"自然用法"也会引发天秤倾斜。

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

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

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

当你使用new(也就是通过new动态生成一个对象),有两件事发生。第一,内存被分配出来(通过名为operator new的函数,见条款49和条款51)。第二,针对此内存会有一个(或更多)构造函数被调用。当你使用delete,也有两件事发生:针对此内存会有一个(或更多)析构函数被调用,然后内存才被释放(通过名为operator delete的函数,见条款51)。delete的最大问题在于:即将被删除的内存之内究竟有多少对象?这个问题的答案决定了有多少个析构函数必须被调用起来。

当你对着一个指针使用delete,唯一能够让delete知道内存中是否存在一个"数组大小记录"的办法就是:由你来告诉它。如果你使用delete时加上中括号(方括号),delete便认定指针指向一个数组,否则它便认定指针指向单一对象。

最好尽量不要对数组形式做typedefs动作,因为当程序员以new创建该种typedef类型对象时,不一定总记得是以delete还是delete [ ]  来调用。

1. 如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

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

像类似processWidget(std::shared_ptr<Widget>(new Widget),priority())的函数中:调用priority,执行"new Widget",调用shared_ptr构造函数三件事以什么样的次序完成弹性很大。这和其他语言如Java和C#不同,那两种语言总是以特定次序完成该函数参数的核算。如果编译器选择先new Widget,调用priority,调用shared_ptr构造函数这样的次序,但是万一对priority的调用导致异常,会发生什么事,此种情况下"new Widget"返回的指针将会遗失,因为它尚未被置入shared_ptr内,后者是我们期盼用来防卫资源泄漏的武器。这样在对processWidget的调用过程中可能引发资源泄漏,因为在"资源被创建(经由'new Widget')"和"资源被转换为资源管理对象"两个时间点之间有可能发生异常干扰。避免这类问题的办法很简单:使用分离语句,分别写出创建Widget将它置入一个智能指针,然后再把那个智能指针传给processWidget;

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

你可能感兴趣的:(C++,C++,effective)