首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

配平与异常

  支持异常的语言可能会使解除资源的分配很棘手。如果有异常被抛出,你怎样保证在发生异常之前分配的所有资源都得到清理?答案在一定程度上取决于语言。

C++异常机制下配平资源

  C++支持try…catch异常机制。遗憾的是,这意味着在退出某个捕捉异常、并随即将其重新抛出的例程时,总是至少有两条可能的路径:

    void doSomething(void) {

 

      Node *n = new Node;

 

      try {

        // do something

      }

      catch (...) {

        delete n;

        throw;

      }

      delete n;

    }


  注意我们创建的节点是在两个地方释放的—一次是在例程正常的退出路径上,一次是在异常处理器中。这显然违反了DRY原则,可能会发生维护问题。

  但是,我们可以对C++的语义加以利用。局部对象在从包含它们的块中退出时会被自动销毁。这给了我们一些选择。如果情况允许,我们可以把“n”从指针改变为栈上实际的Node对象:

 

    void doSomething1(void) {

      Node n;

      try {

        // do something

      }

      catch (...) {

        throw;

      }

    }

  在这里,不管是否抛出异常,我们都依靠C++自动处理Node对象的析构。

  如果不可能不使用指针,可以通过在另一个类中包装资源(在这个例子中,资源是一个Node指针)获得同样的效果。

 

    // Wrapper class for Node resources

    class NodeResource {

      Node *n;

     public:

      NodeResource() { n = new Node; }

      ~NodeResource() { delete n; }

      Node *operator->() { return n; }

    };

    void doSomething2(void) {

      NodeResource n;

      try {

        // do something

      }

      catch (...) {

        throw;

      }

    }

  现在包装类NodeResource确保了在其对象被销毁时,相应的节点也会被销毁。为了方便起见,包装提供了解除引用操作符->,这样它的使用者可以直接访问所包含的Node对象中的字段。

  因为这一技术是如此有用,标准C++库提供了模板类auto_ptr,能自动包装动态分配的对象。

    void doSomething3(void) {

      auto_ptr<Node> p (new Node);

      // Access the Node as p->...

      // Node automatically deleted at end

    }

Java中配平资源

  与C++不同,Java实现的是自动对象析构的一种“懒惰”形式。未被引用的对象被认为是垃圾收集的候选者,如果垃圾收集器回收它们,它们的finalize方法就会被调用。尽管这为开发者提供了便利,他们不再须要为大多数内存泄漏承受指责,但同时也使得实现C++方式的资源清理变得很困难。幸运的是,Java语言的设计者考虑周详地增加了一种语言特性进行补偿:finally子句。当try块含有finally子句时,如果try块中有任何语句被执行,该子句中的代码就保证会被执行。是否有异常抛出没有影响(即或try块中的代码执行了return语句)——finally子句中的代码都将会运行。这意味着我们可以通过这样的代码配平我们的资源使用:

    public void doSomething() throws IOException {

 

      File tmpFile = new File(tmpFileName);

      FileWriter tmp = new FileWriter(tmpFile);

 

      try {

        // do some work

      }

      finally {

        tmpFile.delete();

      }

    }

  该例程使用了一个临时文件,不管例程怎样退出,我们都要删除该文件。finally块使得我们能够简洁地表达这一意图。

当你无法配平资源时

  有时基本的资源分配模式并不合适。这通常会出现在使用动态数据结构的程序中。一个例程将分配一块内存区,并把它链接进某个更大的数据结构中,这块内存可能会在那里呆上一段时间。

  这里的诀窍是为内存分配设立一个语义不变项。你须要决定谁为某个聚集数据结构(aggregate data structure)中的数据负责。当你解除顶层结构的分配时会发生什么?你有三个主要选择:

1.      顶层结构还负责释放它包含的任何子结构。这些结构随即递归地删除它们包含的数据,等等。

2.      只是解除顶层结构的分配。它指向的(没有在别处引用的)任何结构都会被遗弃。

3.      如果顶层结构含有任何子结构,它就拒绝解除自身的分配。

  这里的选择取决于每个数据结构自身的情形。但是,对于每个结构,你都须明确做出选择,并始终如一地实现你的选择。在像C这样的过程语言中实现其中的任何选择都可能会成问题:数据结构自身不是主动的。在这样的情形下,我们的偏好是为每个重要结构编写一个模块,为该结构提供分配和解除分配设施(这个模块也可以提供像调试打印、序列化、解序列化和遍历挂钩这样的设施)。

  最后,如果追踪资源很棘手,你可以通过在动态分配的对象上实现一种引用计数方案,编写自己有限的自动垃圾回收机制。More Effective C++[Mey96]一书专设了一节讨论这一话题。

检查配平

  因为注重实效的程序员谁也不信任,包括我们自己,所以我们觉得,构建代码、对资源确实得到了适当释放进行实际检查,这总是一个好主意。对于大多数应用,这通常意味着为每种资源类型编写包装,并使用这些包装追踪所有的分配和解除分配。在你的代码中的特定地方,程序逻辑将要求资源处在特定的状态中:使用包装对此进行检查。

  例如,一个长期运行的、对请求进行服务的程序,很可能会在其主处理循环的顶部的某个地方等待下一个请求到达。这是确定自从上次循环执行以来,资源使用未曾增长的好地方。

  在一个更低、但用处并非更少的层面上,你可以投资购买能检查运行中的程序的内存泄漏情况(及其他情况)的工具。Purifywww.rational.com)和Insure++www.parasoft.com)是两种流行的选择。

相关内容:

l       按合约设计109

l       断言式编程122

l       解耦与得墨忒耳法则138

挑战

l       尽管没有什么途径能够确保你总是释放资源,某些设计技术,如果能够始终如一地加以应用,将能对你有所帮助。在上文中我们讨论了为重要数据结构设立语义不变项可以怎样引导内存解除分配决策。考虑一下,“按合约设计”(109页)可以怎样帮助你提炼这个想法。

练习

22.  有些CC++开发者故意在解除了某个指针引用的内存的分配之后,把该指针设为NULL。这为什么是个好主意?  (解答在292

23.  有些Java开发者故意在使用完某个对象之后,把该对象变量设为NULL,这为什么是个好主意?  (解答在292

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论