洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目13] 使用对象来管理资源

在下面的示例中,我们的工作将围绕一个为投资(或者是股票、证券等等)建模的库展开,在这个库中,各种各样的投资类型都继承自同一个基类——Investment

class Investment { ... };               // 投资类型层次结构的基类

继续上面的示例,我们考虑通过工厂函数(参见条目7)来提供具体的Investment对象:

Investment* createInvestment();      // 返回一个指针,指向Investment

                                                   // 层次结构中动态分配的对象,

                                                   // 调用者必须要自行将其删除

                                                   // (省略参数表以简化代码)

从上面代码中的注释中可以看出,当调用者完成对于createInvestment函数返回对象的操作后,有义务删除这一对象。请看下边代码中一个履行这一义务的函数——f

void f()

{

  Investment *pInv = createInvestment();     // 调用工厂函数

  ...                                        // 使用pInv

  delete pInv;                               // 释放该对象

}

这看上去可以正常运行,但是在一些情况下f可能无法成功删除它从createInvestment获得的对象。在上述代码的“...”部分中的某处可能存在不成熟的return语句。一旦这样的return语句得到了执行,那么程序将永远无法到达delete语句。当createInvestmentdelete在一个循环内使用时,也会出现类似的情形,这样的循环有可能在遇到continuegoto语句而提前退出。最后,“...”中的一些语句还有可能抛出异常。如果真的有异常抛出,程序同样也不会达到delete。无论delete是如何被跳过的,包含Investment对象的内存都将泄漏,同时这些对象所包含的资源都有可能得不到释放。

当然,用心编程就有可能防止这类错误发生,但是请想象一下代码会多么的不固定——你需要不停地修改代码。随着软件不断得到维护,为一个函数添加returncontinue语句可能会对其资源管理策略造成怎样的影响呢,一些人可能由于不完全理解这一问题就这样做了。还有更严重的,就是f函数的“...”部分可能调用了一个这样的函数:它原先不会抛出异常,但在其得到“改进”之后,它突然又开始能够抛出异常了。寄希望于f函数总能达到其中的delete语句并不可靠。

为了确保createInvestment所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,并让该对象的析构函数在程序离开f时自动释放资源。实际上,这就是本条目蕴含理念的一半了:通过将资源放置在对象中,我们可以依赖C++对默认析构函数的自动调用来确保资源及时得到释放。(另一半理念稍后讲解。)

许多资源是在堆上动态分配的,并且仅仅在单一的程序块或函数中使用,这类资源应该在程序离开这一程序块或函数之前得到释放。标准库中的auto_ptr就是为这类情况量身定做的。auto_ptr是一个类似于指针的对象(智能指针),其析构函数可以自动对其所指内容执行delete。以下代码描述了如何使用auto_ptr来防止f潜在的资源泄漏。

void f()

{

  std::auto_ptr<Investment> pInv(createInvestment());

                                   // 调用工厂函数

 

  ...                              // 像前文示例一样使用pInv

 

}                                  // 通过auto_ptr的析构函数

                                   // 自动删除pInv

这一简单的示例向我们展示了使用对象管理资源的两大关键问题:

获取资源后,资源将立即转交给资源管理对象。上边的示例中,createInvestment返回的资源将初始化一个auto_ptr,从而实现对这类资源的管理。事实上,使用对象来管理资源的理念通常称为“资源获取即初始化”(Resource Acquisition Is Initialization,简称RAII),这是因为,在同一个语句中获取一个资源并且初始化一个资源管理对象是很平常的。某些时候获取的资源就是会赋值给一个资源管理对象,而不是初始化。但是无论是哪种途径,在获取到一个资源时,每个资源都都会立即转向一个资源管理对象。

资源管理对象使用其析构函数来确保资源得到释放。由于析构函数是在对象销毁时自动调用的(比如,当对象在其作用域外时),所以不管程序是如何离开一个块的,资源都会被正确地释放。如果释放资源会带来异常,那么事情就会变得错综复杂。但那是条目8中介绍的内容,我们这里不关心这些。

由于当一个auto_ptr被销毁时,它将自动删除其所指向的内容,所以永远不存在多个auto_ptr指向同一个对象的情况,这一点很重要。如果存在的话,这个对象就会被多次删除,这样你的程序就会立即陷入未定义行为。为了防止此类问题发生,auto_ptr有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为null,然后资源的所有权将由复制出的指针独占!

std::auto_ptr<Investment> pInv1(createInvestment());

                                   // pInv1指向createInvestment

                                   // 所返回的对象

 

std::auto_ptr<Investment> pInv2(pInv1);

                                   // 现在pInv2指向这一对象,

                                   // pInv1被重设为null

 

pInv1 = pInv2;                     // 现在pInv1指向这一对象

                                   // pInv2被重设为null

在这一古怪的复制方法中,由于auto_ptr必须仅仅指向一个资源,因此增加了对于资源管理的潜在需求。这意味着auto_ptr并不适合于所有动态分配的资源。比如说,STL容器要求其内容表现出“正常”的复制行为,所以在容器中放置auto_ptr是不允许的。

引用计数智能指针(reference-counting smart pointer,简称RCSP)是auto_ptr的一个替代品。RCSP是这样的智能指针:它可以跟踪有多少的对象指向了一个特定的资源,同时当没有指针在指向这一资源时,智能指针会自动删除该资源。可以看出,RCSP的行为与垃圾回收器很相似。然而,与垃圾回收器不同的是,RCSP不能够打断循环引用(比如两个不同的、空闲的、互相指向对方的对象)。

TR1的tr1::shared_ptr就是一个RCSP,于是你可以按下面的方式来编写f

void f()

{

  ...

  std::tr1::shared_ptr<Investment>

    pInv(createInvestment());      // 调用工厂函数

 

  ...                              // 像前文示例一样使用pInv

 

}                                  // 通过shared_ptr的析构函数

                                   // 自动删除pInv

上面的代码与使用auto_ptr时几乎完全相同,但是复制shared_ptr的行为更加自然:

void f()

{

  ...

 

  std::tr1::shared_ptr<Investment> pInv1(createInvestment());

                                    // pInv1指向createInvestment

                                    // 所返回的对象

 

  std::tr1::shared_ptr<Investment> pInv2(pInv1);

                                    // 现在pInv1pInv2均指向同一对象

 

  pInv1 = pInv2;                    // 同上因为什么都没有改变

  ...

}                                   // pInv1pInv2被销毁,

                                    // 它们所指向的对象也被自动删除了

由于复制tr1::shared_ptr的工作可以“如期进行,所以在auto_ptr会出现另类的复制行为的地方,比如STL容器以及其它一些上下文中,这类指针能够安全地应用。

但是,请不要迷失方向。本条目并不是专门讲解auto_ptrtr1::shared_ptr的,也不是讲解智能指针的。本条目的核心内容是使用对象管理资源的重要性。auto_ptrtr1::shared_ptr仅仅是这类对象的示例。(关于tr1::shared_ptr的更多信息,请参见条目14、18和54。)

auto_ptrtr1::shared_ptr在析构函数中使用的都是delete语句,而不是delete[]。(条目16中描述了二者的区别。)这就意味着对于动态分配的数组使用auto_ptrtr1::shared_ptr不是一个好主意。但是遗憾的是,这样的代码会通过编译:

std::auto_ptr<std::string> aps(new std::string[10]);

                                   // 坏主意!

                                   // 这里将使用错误的删除格式

 

std::tr1::shared_ptr<int> spi(new int[1024]);

                                   // 同样的问题

你可能会很吃惊,因为在C++中没有类似于auto_ptrtr1::shared_ptr的方案来解决动态分配数组的问题,甚至TR1中也没有。这是因为vectorstring通常都可以代替动态分配的数组。如果你仍然希望存在类似于auto_ptrtr1::shared_ptr的数组类,请参见Boost的相关内容(见条目55)。那儿会满足你需求:Boost提供了boost::scoped_arrayboost::shared_array来处理相关问题。

本条目中指引你使用对象来管理资源。如果你手动释放资源(比如使用delete而不是使用资源管理类),你就在做一些错事。诸如auto_ptrtr1::shared_ptr等封装好的资源管理类通常可以让遵循本条目的建议变成一件很容易的事情,但是某些情况下,你的问题无法使用这些现成的类来解决,此时你便需要创建自己的资源管理类。但这并没有想象中那么难,但是确实需要你考虑一些细节问题。这些细节问题就是条目14和条目15的主题。

最后说一下,我必须指出createInvestment的原始指针返回类型存在着潜在的内存泄漏问题,因为调用者十分容易忘记在返回时调用delete。(甚至在调用者使用auto_ptrtr1::shared_ptr来运行delete时,他们仍然需要在一个智能指针对象中保存createInvestment的返回值。)解决这一问题需要改变createInvestment的接口,这是条目18的主题。

时刻牢记

为了避免资源泄漏,可以使用RAII对象,这类对象使用构造函数获取资源,析构函数释放资源。

auto_ptrtr1::shared_ptr是两个常用并且实用的RAII类。通常情况下tr1::shared_ptr是更好的选择,因为它的复制行为更加直观。复制一个auto_ptr将会使其重设为null

posted on 2007-05-07 18:52 ★ROY★ 阅读(961) 评论(0)  编辑 收藏 引用 所属分类: Effective C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理