C与C++中的异常处理7

原创 2002年03月01日 08:52:00

1.     部分构造及placement delete<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />

讨论在一般情况下的部分构造、动态生成对象时的部分构造,以及用 placement delete来解决部分构造问题。

 

    C++标准要求标准运行库头文件<new>提供几个operator delete的重载形式。在这些重载形式中,Visual C++ 6缺少:

l         void operator delete(void *, void *)

Visual C++ 5缺少:

l         void operator delete(void *, void *)

l         void operator delete(void *, std::nothrow_t const &)

    这些重载形式支持placement delete表达式,并解决了一个特殊问题:释放部分构造的对象。在这次和接下来一次,我将给出一般情况下的部分构造、动态生成对象时的部分构造,以及用 placement delete来解决部分构造问题的例子。

 

1.1     部分构造

    看这个例子:

// Example 1

 

#include <iostream>

 

class A

   {

public:

   A()

      {

      throw 0;

      }

   };

 

int main()

   {

   try

      {

      A a;

      }

   catch(...)

      {

      std::cout <<"caught exception" << std::endl;

      }

   return 0;

   }

    因为A的构造函数抛出了一个异常,a对象没有完全构造。在这个例子中,没有构造函数有可见作用:因为A没有子对象,构造函数实际上没有任何操作。但,考虑这样的变化:

// Example 2

 

#include <iostream>

 

class B

  {

public:

  B()

     {

     throw 0;

     }

  };

 

class A

   {

private:

  B const b;

   };

 

// ... main same as before ...

    现在,A的构造函数不是无行为的,因为它构造了一个B成员对象,而它里面会抛异常。程序对这个异常作出什么反应?

    C++标准中摘下了四条(稍作了简化)原则:

l         一个对象被完全构造,当且仅当它的构造函数已经完全执行,而它的析构函数还没开始执行。

l         如果一个对象包含子对象,包容对象的构造函数只有在所有子对象被完全构造后才开始执行。

l         一个对象被析构,当且仅当它被完全构造。

l         对象按它们被构造的反序进行析构。

    因为抛出了一个异常,B::B没有被完全执行。因此,B的对象A::b既没有被完全构造也没有被析构。

    要证明这点,跟踪相应的类成员:

// Example 3

 

#include <iostream>

 

class B

   {

public:

   B()

      {

     std::cout << "B::B enter" << std::endl;

      throw 0;

     std::cout << "B::B exit" << std::endl;

      }

  ~B()

     {

     std::cout << "B::~B" << std::endl;

     }

   };

 

class A

   {

public:

  A()

     {

     std::cout << "A::A" << std::endl;

     }

  ~A()

     {

      std::cout << "A::~A"<< std::endl;

     }

private:

   B const b;

   };

 

// ...  main same as before ...

    当运行时,程序将只输出

B::B enter

caught exception

从而显示出对象ab既没有完全构造也没有析构。

 

1.2     多对象

    使例子变得更有趣和更有说明力,把它改得允许部分(不是全部)对象被完全构造:

// Example 4

 

#include <iostream>

 

class B

   {

public:

   B(int const ID) : ID_(ID)

      {

      std::cout << ID_ << " B::B enter" <<std::endl;

     if (ID_ > 2)

         throw 0;

      std::cout << ID_ << " B::B exit" <<std::endl;

      }

   ~B()

      {

      std::cout << ID_ << " B::~B" <<std::endl;

      }

private:

  int const ID_;

   };

 

class A

   {

public:

   A() : b1(1), b2(2), b3(3)

      {

      std::cout <<"A::A" << std::endl;

      }

   ~A()

      {

      std::cout <<"A::~A" << std::endl;

      }

private:

   B const b1;

   B const b2;

   B const b3;

   };

 

// ...  main same asbefore ...

    注意B的构造函数现在接受一个对象ID值的参数。用它作B的对象的唯一标记并决定对象是否完全构造。大部分跟踪信息以这些ID开头,显示为:

1 B::B enter

1 B::B exit

2 B::B enter

2 B::B exit

3 B::B enter

2 B::~B

1 B::~B

caught exception

    b1b2完全构造而b3没有。所以,b1b2被析构而b3没有。此外,b1b2的析构按其构造的反序进行。最后,因为一个子对象(b3)没有完全构造,包容对象a也没有完全构造和析构。

 

1.3     动态分配对象

    将类A改为其成员变量是动态生成的:

// Example 5

 

#include <iostream>

 

// ... class B same as before ...

 

class A

   {

public:

   A() : b1(new B(1)), b2(new B(2)), b3(new B(3))

      {

      std::cout <<"A::A" << std::endl;

      }

   ~A()

      {

     delete b1;

     delete b2;

     delete b3;

      std::cout <<"A::~A" << std::endl;

      }

private:

   B * const b1;

   B * const b2;

   B * const b3;

   };

 

// ... main same as before ...

    这个形式符合C++习惯用法:在包容对象的构造函数里分配成员变量,并对其填充数据,然后在包容对象的析构函数里释放它们。

    编译并运行例5。输出是:

1 B::B enter

1 B::B exit

2 B::B enter

2 B::B exit

3 B::B enter

caught exception

    其结果与例4相似,但有一个巨大的不同:因为~A没有被执行,其中的delete语句也就没有执行,被成功分配的*b1*b2的析构函数也没有调用。例四中的不妙状况(三个对象析构了两个)现在更差了(三个对象一个都没有析构)。

    实际上,没有比这更坏的了。记住,delete b1语句有两个作用:

l         调用*b1的析构函数~b

l         调用operator delete释放*b1所占有的内存。

    所以我们不光是遇到~B没有被调用所导致的问题,还有每个B对象造成的内存泄漏问题。这不是件好事。

    B对象是A私有的,它们是实现细节,对程序的其它部分是不可见的。用动态生成B的子对象来代替自动生成B的子对象不该改变程序的外在行为,这表明了我们的例子在设计上的缺陷。

 

1.4     析构动态生成的对象

    为了最接近例4的行为,我们需要在任何情况强迫delete语句的执行。将它们放入~A明显不起作用。我们需要找个能起作用的地方,我们知道它能被执行的地方。跳入脑海的解决方法中,最优雅的方法来自于C++标准运行库:

// Example 6

 

#include <iostream>

#include <memory>

 

// ... class B same as before ...

 

class A

   {

public:

   A() : b1(new B(1)), b2(new B(2)), b3(new B(3))

      {

      std::cout << "A::A" << std::endl;

      }

   ~A()

      {

      std::cout << "A::~A" << std::endl;

      }

private:

  std::auto_ptr<B> const b1;

  std::auto_ptr<B> const b2;

  std::auto_ptr<B> const b3;

   };

 

// ... main same as before ...

    auot_ptr读作“auto-pointer”。如名所示,auoto-pointer表现为通常的指针和自动对象的混合体。

    std::auto_ptr是在<memory>中申明的类模板。一个std::auto_ptr<B>类型的对象的表现非常象一个通常的B*类型对象,关键的不同是:auto_ptr是一个实实在在的类对象,它有析构函数,而这个析构函数将在B*所指对象上调用delete。最终结果是:动态生成的B对象如同是个自动B对象一样被析构。

    可以把一个auto_ptr<B>对象当作对动态生成的B对象的简单包装。在包装消失(析构)时,它也将被包装对象带走了。要实际看这个魔术戏法,编译并运行例6。结果是:

1 B::B enter

1 B::B exit

2 B::B enter

2 B::B exit

3 B::B enter

2 B::~B

1 B::~B

caught exception

    Bingo!输出和例4相同。

    你可能会奇怪为什么没有为b3调用~B。这表明了auto_ptr包装上的失败?根本不是。我们所读过的规则还在起作用。对b3进行的构造函数的调用接受了new B(3)传过来的参数。于是发生了一个异常终止了b3的构造。因为b3没有完全构造,它同样不会析构。

    藏在atuo-pointer后面的想法没有新的地方;string对象实际上就是char数组的auto-pointer型包装。虽然如此,我仍然期望有一天我能更详细的讨论auto_ptr及其家族,目前只要把auto_ptr当作一个保证发生异常时能析构动态生成的对象的简单方法。

 

1.5     预告

    既然b3的析构函数没有被调用,也就没有为其内存调用delete。如前面所见,被包装的B对象受到两个影响:

l         析构函数~B没有被调用。这是意料中的甚至是期望中的,因为B对象在先前没有完全构造。

l         内存没有被通过operator delete释放。不管是不是意料中的,它绝不是期望中的,因为B对象所占用的内存被分配了,即使B对象没有在此内存中完全构造。

    我需要operator delete被调用,即使~B没有被调用。要实现这点,编译器必须在脱离delete语句的情况下调用operator delete。因为我知道b3是我的例子中的讨厌对象,我可以显式地为b3的内存调用operator delete;但要知道这只是教学程序,通常情况下我们不能预知哪个构造函数将失败。

    不,我们所需要的是编译器检测到动态生成对象时的构造函数失败时隐含调用operator delete来释放对象占用的内存。这有些效仿编译器在自动对象构造失败时的行为:对象的内存如同程序体中的无用单元一样,是可回收的。

幸好,它有个大喜结局。要看这个结局,需到下回。在下回结束时,我将揭示C++语言如何提供了这个完美特性,为什么标准运行库申明了placement operator delete,以及为什么你可能想在自己的库或类中做同样的事。

C/C++异常处理的对比

本文主要介绍C异常处理与C++异常处理的区别。 包括errno、signal、nonlocal goto、异常的捕获、异常规格说明(exception specification)、标准异常对象等。...
  • yeming81
  • yeming81
  • 2010年06月16日 00:19
  • 4699

JAVA异常处理 与C++的不同

*Java异常处理模型   对于一个非常熟悉 C++ 异常处理模型的程序员来说,它几乎可以不经任何其它培训和学习,就可以完全接受和能够轻松地使用 Java 语言中的异常处理编程方法。这是因为 Java...
  • ljlove2008
  • ljlove2008
  • 2008年10月14日 23:32
  • 2472

Visual C++异常处理机制原理与应用(三)——C/C++结构化异常处理之try-except异常处理的使用(上)

在微软的VC++中,C/C++结构化异常处理机制一共包含两部分内容:终止处理程序和异常处理程序,本文主要介绍异常处理程序的相关内容。...
  • LPWSTR
  • LPWSTR
  • 2017年12月03日 21:45
  • 109

MATLAB与c/c++之矩阵操作差别

1)MATLAB默认数组(矩阵)访问下标是从1开始的,而c/c++默认是从0开始; 2)MATLAB的二位数组(矩阵)的数据存放顺序默认为列优先(从第一列自上向下存放和访问,再第二列。。。。),而...
  • wonengguwozai
  • wonengguwozai
  • 2016年10月10日 21:23
  • 627

C++异常处理机制总结

参考文档:《C++编程思想》《C++Primer》《More effective C++》 一、             传统的错误处理机制: 1.         返回值或全局错误状态标志。缺点:需...
  • MulinB
  • MulinB
  • 2007年08月29日 10:07
  • 2268

Unix/Linux C++应用开发-异常以及错误处理

计算机应用程序中离不开错误处理,尤其是生产型大型软件系统。应用软件系统运行属于循环处理事务,出错后需要保证不能让软件程序直接退出。这就需要使用一定的程序容错处理来应对。一般情况下,大型软件开发中的软件...
  • wangfengwf
  • wangfengwf
  • 2013年09月11日 21:15
  • 30142

C++的和Java的异常机制

    程序总会出现异常的,需要我们去处理。C++和JAVA都有自己异常机制,我们应该遵循着去处理异常。那它们的异常机制有何异同呢?    要注意一点:异常机制处理异常是要付出代价的,即异常处理的代码...
  • Windy83
  • Windy83
  • 2006年11月17日 01:37
  • 1929

c++primer之try语句块和异常处理

try语句块和异常处理。。异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。 。。当我们的...
  • u014365862
  • u014365862
  • 2015年08月16日 22:03
  • 904

C++异常处理实例

/************************************************************************************************ *...
  • JarvisChu
  • JarvisChu
  • 2011年07月22日 22:41
  • 4027

Jni C/C++运行时遇到异常怎么办?捕获与抛出

有个头疼的问题,Jni C/C++遇到问题闪退怎么办?有办法,我们可以在异常发生后通过判断清除异常解决,保持程序及时反应处理。 比如: package crash; import java.se...
  • zhangbuzhangbu
  • zhangbuzhangbu
  • 2016年10月26日 23:09
  • 2147
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C与C++中的异常处理7
举报原因:
原因补充:

(最多只允许输入30个字)