C++程序中最常使用的资源就是动态分配内存,但内存只是你必须管理的众多资源之一。其他常见资源还包括文件描述符(file descriptors)、互斥锁(mutex locks)、数据库连接以及网络sockets等。
不论是哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。
13.Use objects to mange resources.(以对象管理资源)
(1)有如下Investment类:
1 2 3 4 |
class Investment { ... }; //假设,这个程序通过一个工厂函数供应我们某特定的Investment对象 Investment* createInvestment(); //这里为了简化,刻意不写参数 |
其中,createInvestment的调用端使用了函数返回的对象后,使用完该资源后,有责任删除之。进一步假设有如下函数f履行了这个责任:
1 2 3 4 5 6 |
void f() { Investment* pInv = createInvestment(); //调用工厂函数 ... delete pInv; //释放pInv所指对象 } |
(2)以为这就完了吗?NO!有若干情况下f可能无法删除它得自createInvestment的投资对象:
1)第一种情况,代码”…”中一个过早的return语句被执行起来,控制流将不会执行到delete语句处。
2)第二种情况,代码”…”中的语句抛出异常,那么控制流也不会执行到delete语句处。
因此单纯依赖f来执行其delete语句是行不通的。
(3)对此,为确保createInvestment返回的资源总是被释放,我们需要将资源放进对象内,当控制流离开f,该对象的析构函数会自动释放那些资源。实际上这正是隐身于本条款背后的半边想法:把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。(稍后讨论另半边想法)
(4)许多资源被动态分配于heap内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。auto_ptr是个“类指针对象”,也就是所谓“智能指针”,其析构函数自动对其所指对象调用delete:
1 2 3 4 5 |
void f() { std::auto_ptr<Investment> pInv(createInvestment()); ... } |
这个例子示范“以对象管理资源”的两个关键想法:
1)获得资源后立刻放进管理对象(managing object)内。实际上“以对象管理资源”的观念通常被称为“资源取得时机便是初始化时机”(Resourse Acquisition Is Initialization, RAII)。
2)管理对象(managing object)运行析构函数确保资源被释放。
由于auto_ptr被销毁时会自动删除它所指之物,所以一定要注意别让多个auto_ptr同时指向同一对象。为了预防这个问题,auto_ptr有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权。
(5)auto_ptr的替代方案是“引用计数型智能指针”(reference-counting smart pointer,RCSP)。RCSP提供的行为类似垃圾回收,不同的是RCSP无法打破环状引用,例如两个其实已经没被使用的对象彼此互指,因为好像还处在“被使用”状态。
1)TR1的tr1:shared_ptr就是个RCSP,所以可以这么写f:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void f() { ... std::tr1::shared_ptr<Investment> pInv(createInvestment()); ... } //这段代码看起来和使用auto_ptr几乎相同, //但shared_ptr的复制行为正常多了: void f() { ... std::tr1::shared_ptr<Investment> pInv1(createInvestment()); std::tr1::shared_ptr<Investment> pInv2(pInv1);//两个指针指向同一个对象 pInv1 = pInv2;//同上,无任何改变 ... } //pInv1和pInv2被销毁,它们所指的对象也就被自动销毁 |
2)auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。那意味在动态分配而得的array身上使用这两个指针是个坏主意。
注:C++11新标准已经引入了shared_ptr、unique_ptr以及weak_ptr智能指针了。
请注意:
(1)为防止资源泄露,请使用RAII(资源获得时初始化)对象,它们在构造函数中获得资源并在析构函数中释放资源。
(2)两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使他指向null。
14.Think carefully about copying behavior in resource-manging classes.(在资源管理类中小心copying行为)
…
…
请记住:
(1)赋值RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
(2)普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法(reference counting)。
15.Provide access to raw resources in resource-managing classes.(在资源管理类中提供对原始资源的访问)
(1)有的时候,我们需要绕开资源管理对象直接访问原始资源,比如条款13中,我们导入了一个观念:使用智能指针保存工厂函数createInvestment的调用结果:
std::tr1::shared_ptr<Invetsment> pInv(createInvestment());
假设我们希望以某个函数处理Investment对象,像这样:
int daysHeld(const, Investment* pi);
//如果我们这么调用它,编译不通过
int days = daysHeld(pInv);
因为daysHeld需要的是Investment*指针,我们传给它的却是个类型为tr1::shared_ptr<Investment>的对象。这个时候需要一个函数可将RAII class对象转换为其所内含之原始资源。
(2)有两个做法可以达到目标:显式转换和隐式转换
1)tr1::shared_ptr和auto_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针(的复件):
int days = daysHeld(pInv.get()); //将pInv内的原始指针传给daysHeld
2)tr1::shared_ptr和auto_ptr也重载了指针取值操作符(operator->和operator*),它们允许转换至底部原始指针:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Investment { public: bool isTaxFree() const; ... }; Investment* createInvestment(); //factory 函数 //令tr1::shared_ptr管理一笔资源 std::tr1::shared_ptr<Investment> pil(createInvestment()); bool taxable1 = !(pil->isTaxFree()); //经由operator->访问资源 ... //令auto_ptr管理一笔资源 std::auto_ptr<Investment> pi2(createInvestment()); bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源 ... |
请记住:
(1)APIs往往要求访问原始资源raw resources,所以每一个RAII class应该提供一个“取得其所管理的资源”的办法。
(2)对原始资源的访问可能经由显示转换或隐式转换,一般而言显示转换比较安全,但隐式转换对客户比较方便。