登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> 程序员学前班[不再更新,只读] >> 主题: 异常处理的编程方法 前言 [转]     [回主站]     [分站链接]
标题
异常处理的编程方法 前言 [转]
clq
2007-9-10 15:04:13 发表 编辑


[图片]


总结

   (1) 与函数的参数的传递类似, C++的异常对象的传递也分指针、传值和引用三种方式;
  (2) 与函数的参数的传递不同的是,异常对象的传递是向上逆序的,而且是跳跃式的。

  下一篇文章详细介绍C++的异常对象按传值的方式被复制和传递。朋友们,不要错过,请继续吧!

clq
2007-9-10 15:04:27 发表 编辑




  上一篇文章中对C++的异常对象如何被传递做了一个概要性的介绍,其中得知C++的异常对象的传递方式有指针方式、传值方式和引用方式三种。现在开始讨论最简单的一种传递的方式:按值传递。

异常对象在什么时候构造?

  1、按传值的方式传递异常对象时,被抛出的异常都是局部变量,而且是临时的局部变量。什么是临时的局部变量,这大家可能都知道,例如发生函数调用时,按值传递的参数就会被临时复制一份,这就是临时局部变量,一般临时局部变量转瞬即逝。

  主人公阿愚对这开始有点不太相信。不会吧,谁说异常对象都是临时的局部变量,应该是普通的局部变量,甚至是全局性变量,而且还可以是堆中动态分配的异常变量。是的,这上面说的好象没错,但是实际真实发生的情况是,每当在throw语句抛出一个异常时,不管你原来构造的对象是什么性质的变量,此时它都会复制一份临时局部变量,还是具体看看例程吧!如下:

class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<}

MyException (const MyException& old_e)
{
m_name = old_e.m_name;

cout << "拷贝一个MyException异常对象,名称为:"<}

operator= (const MyException& old_e)
{
m_name = old_e.m_name;

cout << "赋值拷贝一个MyException异常对象,名称为:"<}

virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <}

string GetName() {return m_name;}

protected:
string m_name;
};

void main()
{
try
{
{
// 构造一个异常对象,这是局部变量
MyException ex_obj1("ex_obj1");

// 这里抛出异常对象
// 注意这时VC编译器会复制一份新的异常对象,临时变量
throw ex_obj1;
}

}
catch(...)
{
cout<<"catch unknow exception"<}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  拷贝一个MyException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  catch unknow exception
  销毁一个MyException异常对象,名称为:ex_obj1

  瞧见了吧,异常对象确实是被复制了一份,如果还不相信那份异常对象是在throw ex_obj1这条语句执行时被复制的,你可以在VC环境中调试这个程序,再把这条语句反汇编出来,你会发现这里确实插入了一段调用拷贝构造函数的代码。

  2、而且其它几种抛出异常的方式也会有同样的结果,都会构造一份临时局部变量。执着的阿愚可是每种情况都测试了一下,代码如下:

// 这是全局变量的异常对象
// MyException ex_global_obj("ex_global_obj");
void main()
{
try
{
{
// 构造一个异常对象,这是局部变量
MyException ex_obj1("ex_obj1");

throw ex_obj1;

// 这种也是临时变量
// 这种方式是最常见抛出异常的方式
//throw MyException("ex_obj2");

// 这种异常对象原来是在堆中构造的
// 但这里也会复制一份新的异常对象
// 注意:这里有资源泄漏呦!
//throw *(new MyException("ex_obj2"));

// 全局变量
// 同样这里也会复制一份新的异常对象
//throw ex_global_obj;
}

}
catch(...)
{
cout<<"catch unknow exception"<}

  大家也可以对每种情况都试一试,注意是不是确实无论哪种情况都会复制一份本地的临时变量了呢!

  另外请朋友们特别注意的是,这是VC编译器这样做的,其它的C++编译器是不是也这样的呢?也许不一定,不过很大可能都是采取这样一种方式(阿愚没有在其它每一种C++编译器都做过测试,所以不敢妄下结论)。

  为什么要再复制一份临时变量呢?是不是觉得有点多此一举,不!朋友们,请仔细再想想,因为假如不这样做,不把异常对象复制一份临时的局部变量出来,那么是不是会导致一些问题,或产生一些矛盾呢?的确如此!试想在抛出异常后,如果异常对象是局部变量,那么C++标准规定了无论在何种情况下,只要局部变量离开其生存作用域,局部变量就必须要被销毁,可现在如果作为局部变量的异常对象在控制进入catch block之前,它就已经被析构销毁了,那么问题不就严重了吗?因此它这里就复制了一份临时变量,它可以在catch block内的异常处理完毕以后再销毁这个临时的变量。

  主人公阿愚现在好像是逐渐得明白了,原来如此,但仔细一想,不对呀!上面描述的不准确呀!难道不可以在离开抛出异常的那个函数的作用域时,先把异常对象拷贝复制到上层的catch block中,然后再析构局部变量,最后才进入到catch block里面执行吗!分析的非常的棒!阿愚终于有些系统分析员的头脑了。是的,现在的VC编译器就是按这种顺序工作的。

  可那到底为什么要复制临时变量呢?呵呵!要请教阿愚一个问题,如果catch后面的是采用引用传递异常对象的方式,也即没有拷贝复制这一过程,那么怎办?那个引用指向谁呀,指向一个已经析构了的异常对象!(总不至于说,等执行完catch block之后,再来析构原来属于局部变量的异常对象,这也太荒唐了)。所以吗?才如此。

  可阿愚还是觉得不对劲呀!现在谈论的是异常对象按传值的方式被复制和传递的情况,你又怎么牵扯讨论到引用的方式了呢!OK!OK!OK!即便是引用传递异常对象的方式下,需要一份临时的异常对象(能保证不被析构,而局部变量则…),那么也可以在引用传递异常方式下采用这样的一种复制一份临时异常对象的做法;而在按值传递的方式就没有必要这样做(毕竟对象复制需要时间,会降低效率)。呵呵!想得倒是挺好,挺美!可不要忘记的是,程序员在抛出异常的时候怎么知道上面的catch block是采用哪种方式(是引用还是传值)?万一哪位大仙写出的程序,在上层的catch block有的是引用传递方式,而有的是按值传递方式,那怎么办!所以没辙了吧!采用复制一个临时的变量的方式是最安全、最可靠的方式,虽然说这样做会影响效率。

异常对象按传值复制

  现在开始涉及到关键的地方,当catch block捕获到一个异常后,控制流准备转移到catch block之前,异常对象必须要通过一定的方式传递过来,假如是按传值传递(根据catch关键字后面定义的异常对象的数据类型),那么此时就会发生一次异常对象的拷贝构造过程。示例如下:

void main()
{
try
{
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyException ex_obj1("ex_obj1");

throw ex_obj1;

}

}
// 由于这里定义的是“按值传递”,所以这里会发生一次拷贝构造过程
catch(MyException e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  拷贝一个MyException异常对象,名称为:ex_obj1
  拷贝一个MyException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  捕获到一个MyException类型的异常,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1

  通过结果可以看出确实又多发生了一次异常对象的拷贝复制过程,因此在catch block中进行错误处理时,我们可以放心存储异常对象,因为不管C++异常处理模型到底是采用什么方法,总之当前这个异常对象已经被复制到了当前catch block的作用域中。

异常对象什么时候被销毁

  通过上面的那个程序运行结果还可以获知,每个被拷贝复制出来的异常对象都会得到被销毁的机会。而且销毁都是在catch block执行之后,包括那个被抛出的属于临时局部变量的异常对象也是在执行完catch block之后,这很神奇吧!不过暂时先不管它。先搞清catch block中的那个按值拷贝传入的异常对象到底确切的是在什么时候被析构。示例如下:

void main()
{
try
{
{
// 构造一个对象,当obj对象离开这个作用域时析构将会被执行
MyException ex_obj1("ex_obj1");

throw ex_obj1;

}

}
// 由于这里定义的是“按值传递”,所以这里会发生一次拷贝构造过程
catch(MyException e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<}

// 加入一条语句,判断e什么时候销毁
cout<<"在这之前还是之后呢?"<}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  拷贝一个MyException异常对象,名称为:ex_obj1
  拷贝一个MyException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  捕获到一个MyException类型的异常,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1
  在这之前还是之后呢?

  看到了吗!发生那条语句之前,因此基本可以判断那个异常对象是在离开catch block时发生的析构,这样也算是情理之中,毕竟catch block中的异常处理模块对异常对象的存取使用已经完毕,过河拆桥有何不对!。为了进一步验证一下。从VC中copy出相关的反汇编代码。如下:

368: catch(MyException e)
00401CDA mov byte ptr [ebp-4],3
369: {
370: cout<<"捕获到一个MyException类型的异常,名称为:"<00401CDE lea eax,[ebp-64h]
00401CE1 push eax
00401CE2 lea ecx,[e]
00401CE5 call @ILT+60(MyException::GetName) (00401041)
00401CEA mov dword ptr [ebp-70h],eax
00401CED mov ecx,dword ptr [ebp-70h]
00401CF0 mov dword ptr [ebp-74h],ecx
00401CF3 mov byte ptr [ebp-4],4
00401CF7 mov esi,esp
00401CF9 mov edx,dword ptr [__imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z
00401CFF push edx
00401D00 mov eax,dword ptr [ebp-74h]
00401D03 push eax
00401D04 mov edi,esp
00401D06 push offset string "\xb2\xb6\xbb\xf1\xb5\xbd\xd2\xbb\xb8\xf6MyException\xc0\xe0\xd0\xcd\xb5\x
00401D0B mov ecx,dword ptr [__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0041614c)
00401D11 push ecx
00401D12 call dword ptr [__imp_??6std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z (004
00401D18 add esp,8
00401D1B cmp edi,esp
00401D1D call _chkesp (00401982)
00401D22 push eax
00401D23 call std::operator<< (0040194e)
00401D28 add esp,8
00401D2B mov ecx,eax
00401D2D call dword ptr [__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01
00401D33 cmp esi,esp
00401D35 call _chkesp (00401982)
00401D3A mov byte ptr [ebp-4],3
00401D3E mov esi,esp
00401D40 lea ecx,[ebp-64h]
00401D43 call dword ptr [__imp_??1?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@XZ
00401D49 cmp esi,esp
00401D4B call _chkesp (00401982)
371: } // 瞧瞧下面,catch block后不是调用析构函数了吗?
00401D50 mov byte ptr [ebp-4],2
00401D54 lea ecx,[e]
00401D57 call @ILT+45(MyException::~MyException) (00401032)
00401D5C mov eax,offset __tryend$_main$1 (00401d62)
00401D61 ret
372: cout<<"在这之前还是之后呢?"<00401D62 mov dword ptr [ebp-4],0FFFFFFFFh

异常对象标示符的有效作用域

  到目前为止大家已经可以知道,按值传递的异常对象的作用域是在catch block内,它在进入catch block块之前,完成一次异常对象的拷贝构造复制过程(从那个属于临时局部变量的异常对象进行复制),当离开catch block时再析构销毁异常对象。据此可以推理出,异常对象标示符(也就是变量的名字)也应该是在catch block内有效的,实际catch block有点像函数内部的子函数,而catch后面跟的异常对象就类似于函数的参数,它的标示符的有效域也和函数参数的很类似。看下面的示例程序,它是可以编译通过的。

void main()
{
// 这里定义一个局部变量,变量名为e;
MyException e;
try
{

}
// 这里有一个catch block,其中变量名也是e;
// 实际可以理解函数内部的子函数
catch(MyException e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<}
// 这里又一个catch block,其中变量名还是e;而且数据类型也不同了。
catch(std::exception e)
{
e.what();
}
}

小心异常对象发生对象切片

  C++程序员知道,当函数的参数按值传递时,可能会发生对象的切片现象。同样,如果异常对象按传值方式复制异常对象时,也可能会发生异常对象的切片。示例如下:

class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<}

MyException (const MyException& old_e)
{
m_name = old_e.m_name;

cout << "拷贝一个MyException异常对象,名称为:"<}

operator= (const MyException& old_e)
{
m_name = old_e.m_name;

cout << "赋值拷贝一个MyException异常对象,名称为:"<}

virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <}

string GetName() {return m_name;}

virtual string Test_Virtual_Func() { return "这是MyException类型的异常对象";}

protected:
string m_name;
};

class MyMemoryException : public MyException
{
public:
MyMemoryException (string name="none") : MyException(name)
{
cout << "构造一个MyMemoryException异常对象,名称为:"<}

MyMemoryException (const MyMemoryException& old_e)
{
m_name = old_e.m_name;

cout << "拷贝一个MyMemoryException异常对象,名称为:"<}

virtual string Test_Virtual_Func() { return "这是MyMemoryException类型的异常对象";}

virtual ~ MyMemoryException ()
{
cout << "销毁一个MyMemoryException异常对象,名称为:" <}
};

void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");

cout <throw ex_obj1;

}

}
// 注意这里发生了对象切片,异常对象e已经不是原原本本的那个被throw出
// 的那个对象了
catch(MyException e)
{
// 调用虚函数,验证一下这个异常对象是否真的发生了对象切片
cout<}
}

  程序运行的结果是:

总结

   (1) 被抛出的异常对象都是临时的局部变量;
  (2) 异常对象至少要被构造三次;
  (3) catch 后面带的异常对象的作用域仅限于catch bock中;
  (4) 按值传递方式很容易发生异常对象的切片。

  下一篇文章讨论C++的异常对象按引用的方式被复制和传递。继续吧!

clq
2007-9-10 15:05:13 发表 编辑




  上一篇文章详细讨论了C++的异常对象按值传递的方式,本文继续讨论另外的一种的方式:引用传递。

异常对象在什么时候构造?

  其实在上一篇文章中就已经讨论到了,假如异常对象按引用方式被传递,异常对象更应该被构造出一个临时的变量。因此这里不再重复讨论了。

异常对象按引用方式传递

  引用是C++语言中引入的一种数据类型形式。它本质上是一个指针,通过这个特殊的隐性指针来引用其它地方的一个变量。因此引用与指针有很多相似之处,但是引用用起来较指针更为安全,更为直观和方便,所以C++语言建议C++程序员在编写代码中尽可能地多使用引用的方式来代替原来在C语言中使用指针的地方。这些地方主要是函数参数的定义上,另外还有就是catch到的异常对象的定义。

  所以异常对象按引用方式传递,是不会发生对象的拷贝复制过程。这就导致引用方式要比传值方式效率高,此时从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生两次对象的构造过程(一次是异常对象的初始化构造过程,另一次就是当执行throw语句时所发生的临时异常对象的拷贝复制的构造过程)。而按值传递的方式总共是发生三次。看看示例程序吧!如下:

void main()
{
try
{
{
throw MyException();

}

}
// 注意:这里是定义了引用的方式
catch(MyException& e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:none
  拷贝一个MyException异常对象,名称为:none
  销毁一个MyException异常对象,名称为:none
  捕获到一个MyException类型的异常,名称为:none
  销毁一个MyException异常对象,名称为:none

  程序的运行结果是不是显示出:异常对象确实是只发生两次构造过程。并且在执行catch block之前,局部变量的异常对象已经被析构销毁了,而属于临时变量的异常对象则是在catch block执行错误处理完毕后才销毁的。

那个被引用的临时异常对象究竟身在何处?

  呵呵!这还用问吗,临时异常对象当然是在栈中。是的没错,就像发生函数调用时,与引用类型的参数传递一样,它也是引用栈中的某块区域的一个变量。但请大家提高警惕的是,这两处有着非常大的不同,其实在一开始讨论异常对象如何传递时就提到过,函数调用的过程是有序的的压栈过程,请回顾一下《第9集 C++的异常对象如何传送》中函数的调用过程与“栈”那一节的内容。栈是从高往低的不断延伸扩展,每发生一次函数调用时,栈中便添加了一块格式非常整齐的函数帧区域(包含参数、返回地址和局部变量),当前的函数通过ebp寄存器来寻址函数传入的参数和函数内部的局部变量。因此这样对栈中的数据存储是非常安全的,依照函数的调用次序(call stack),在栈中都有唯一的一个对应的函数帧一层层地从上往下整齐排列,当一个函数执行完毕,那么最低层的函数帧清除(该函数作用域内的局部变量都析构销毁了),返回到上一层,如此不断有序地进行函数的调用与返回。

  但发生异常时的情况呢?它的异常对象传递却并没有这么简单,它需要在栈中把异常对象往上传送,而且可能还要跳跃多个函数帧块完成传送,所以这就复杂了很多,当然即便如此,只要我们找到了源对象数据块和目标对象数据块,也能很方便地完成异常对象的数据的复制。但现在最棘手的问题是,如果采用引用传递的方式将会有很大的麻烦,为什么?试想!前面多次提到的临时异常对象是在那里构造的?对象数据又保存在什么地方?毫无疑问,对象数据肯定是在当前(throw异常的函数)的那个函数帧区域,这是处于栈的最低部,现在假使匹配到的catch block是在上层(或更上层)的函数中,那么将会导致出现一种现象:就是在catch block的那个函数(执行异常处理的模块代码中)会引用下面抛出异常的那个函数帧中的临时异常对象。主人公阿愚现在终于恍然大悟了(不知阅读到此处的C++程序员朋友们现在领会了作者所说的意思没有!如果还没有,自己动手画画栈图看看),是啊!确是如此,这太不安全了,按理说当执行到catch block中的代码时,它下面的所有的函数帧(包括抛出异常的哪个函数帧)都将会无效,但此时却引用到了下面的已经失效了的函数帧中的临时异常对象,虽说这个异常对象还没有被析构,但完全有可能会发生覆盖呀(栈是往下扩展的)!

  怎么办!难道真的有可能会发生覆盖吗?那就太危险了。朋友们!放心吧!实际情况是绝对不会发生覆盖的。为什么?哈哈!编译器真是很聪明,它这里采用了一点点技巧,巧妙的避免的这个问题。下面用一个跨越了多个函数的异常的例子程序来详细阐述之,如下:

void test2()
{
throw MyException();
}

void test()
{
test2();
}

void main()
{
try
{
test();
}
catch(MyException& e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<}

cout<<"那个临时的异常对象应该是在这之前析构销毁"<}

  怎样来分析呢?当然最简单的方法是调试一下,跟踪它的ebp和esp的变化。首先在函数调用的地方和抛出异常的地方设置好断点,F5开始调试,截图如下:

clq
2007-9-10 15:05:49 发表 编辑

[图片]



纪录一下ebp和esp的值(ebp 0012FF70;esp 0012FEF8),通过ebp和esp可以确定main函数的函数帧在栈中位置,F5继续,截图如下:

clq
2007-9-10 15:06:04 发表 编辑

[图片]



同样也纪录一下ebp和esp的值(ebp 0012FE9C;esp 0012FE08),通过ebp和esp可以看出栈是往下扩展,此时的ebp和esp指向抛出异常的test2函数的函数帧在栈中位置,F5继续,此时抛出异常,控制进入main函数中的catch(MyException& e)中,截图如下:

clq
2007-9-10 15:06:26 发表 编辑

[图片]


 请注意了,现在ebp恢复了main函数在先前时的函数帧在栈中位置,但esp却并没有,它甚至比刚刚抛出异常的那个test2函数中的esp还要往下,这就是编译器编译程序时耍的小技巧,当前ebp和esp指向的函数帧实际上并不是真正的main函数原来的哪个函数帧,它实际上包含了多个函数的函数帧,因此catch block执行程序时当然不会发生覆盖。我们还是看看异常对象所引用指向的临时的变量究竟身在何处。截图如下:

clq
2007-9-10 15:06:38 发表 编辑

[图片]


哈哈!e指向了0x0012fe7c内存区域,再看看上面的抛出异常的test2函数的函数帧的ebp和esp的值。结果0x0012fe7c恰好是ebp 0012FE9C和esp 0012FE08之间。
不过阿愚又开始有点疑惑了,哦!这样做岂不是破坏了函数的帧栈吗,结果还不导致程序崩溃呀!呵呵!不用担心,F5继续,截图如下:

clq
2007-9-10 15:07:01 发表 编辑

[图片]

当离开了catch block作用域之后,再看看ebp和esp的值,是不是和最开始的那个main函数进入时的ebp和esp一模一样,哈哈!恢复了,厉害吧!先暂时不管它是如何恢复的,总之ebp和esp都是得以恢复了,而且同时catch block执行时也不会发生异常对象的覆盖。这就解决了异常对象按引用传递时可能存在的不安全隐患。

  引用方式下,异常对象会发生对象切片吗?

  当然不会,要不测试一下,把上一篇文章中的对应的那个例子改为按引用的方式接受异常对象。示例如下:

void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");

cout <throw ex_obj1;

}

}
// 注意这里引用的方式了
// 还会发生了对象切片吗?
catch(MyException& e)
{
// 调用虚函数,验证一下这个异常对象是否发生了对象切片
cout<}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1

  抛出一个MyMemoryException类型的异常

  构造一个MyException异常对象,名称为:none
  拷贝一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1

  这是MyMemoryException类型的异常对象

  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1

总结

  (1) 被抛出的异常对象都是临时的局部变量;
  (2) 异常对象至少要被构造二次;
  (3) catch 后面带的异常对象的作用域仅限于catch bock中;
  (4) 按引用方式传递不会发生异常对象的切片。

  下一篇文章讨论C++的异常对象被按指针的方式传递。继续吧!

clq
2007-9-10 15:08:34 发表 编辑





  上两篇文章分别详细讨论了C++的异常对象按值传递和按引用传递的两种方式,本文继续讨论最后的一种的方式:按指针传递。

异常对象在什么时候构造?

  1、与按值和按引用传递异常的方式相比,在按指针传递异常的方式下,异常对象的构造方式有很大的不同。它必须是在堆中动态构造的异常对象,或者是全局性static的变量。示例程序如下:

void main()
{
try
{
// 动态在堆中构造的异常对象
throw new MyMemoryException("ex_obj1");
}
catch(MyException* e)
{
cout<GetName()<
delete e;
}
}

  2、注意,通过指针方式传递的异常对象不能是局部变量,否则后果很严重,示例如下:

void main()
{
try
{
// 局部变量,异常对象
MyMemoryException ex_obj1("ex_obj1");

// 抛出一个指针类型的异常
// 注意:这样做很危险,因为ex_obj1这个对象离开了这个作用域即
// 析构销毁
throw &ex_obj1;
}
catch(MyException* e)
{
// 下面语句虽然不会导致程序崩溃,但是e->GetName()取得的结果
// 也是不对的。
cout<GetName()<
// 这条语句会导致程序崩溃
// delete e;
}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1

  捕获到一个MyException*类型的异常,名称为:
  异常对象按指针方式被传递
  指针是历史悠久的一种数据类型形式,这可以追溯到C语言和PASCAL语言中。异常对象按指针方式传递,当然更是不会发生对象的拷贝复制过程。所以这种方式传递异常对象是效率最高的,它从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生唯一的一次对象的构造过程,那就是异常对象最初在堆中的动态创建时的初始化的构造过程。也看看示例程序吧!如下:

void main()
{
try
{
// 动态在堆中构造的异常对象
throw new MyMemoryException("ex_obj1");
}
// 注意:这里是定义了按指针方式传递异常对象
catch(MyException* e)
{
cout<GetName()<
delete e;
}
}

  程序运行的结果是:
  构造一个MyException异常对象,名称为:ex_obj1
  构造一个MyMemoryException异常对象,名称为:ex_obj1

  捕获到一个MyException*类型的异常,名称为:ex_obj1
  销毁一个MyMemoryException异常对象,名称为:ex_obj1
  销毁一个MyException异常对象,名称为:ex_obj1

  呵呵!程序的运行结果是不是显示出异常对象只有一次的构造过程。挺好挺好!

异常对象什么时候被销毁

  异常对象动态地在堆上被创建,同时它也要动态的被销毁,否则就必然会发生内存泄漏。那么异常对象应该在什么时候被销毁比较合适呢?当然应该是在catch block块中处理完毕后再销毁它才比较合理。示例如下:

void main()
{
try
{
// 动态在堆中构造的异常对象
throw new MyMemoryException("ex_obj1");
}
catch(MyException* e)
{
cout<GetName()<
// 这里需要显示的删除异常对象
delete e;
}
}

  指针方式下,异常对象会发生对象切片吗?
  当然不会,试都不用试,阿愚非常有把握确信这一点。

总结

  (1) 被抛出的异常对象不能是局部变量或临时变量,必须是在堆中动态构造的异常对象,或者是全局性static的变量;
  (2) 异常对象只会被构造一次;
  (3) catch 后面带的异常对象的作用域仅限于catch bock中;
  (4) 异常对象动态地在堆上被创建,同时它也要动态的被销毁。

  至此为止,C++的异常对象的传递的三种方式(指针、传值和引用)都已经讨论过了,主人公阿愚到此算是松了一大口气,终于把这块比较难肯一点的骨头给拿下了(呵呵!^_^)。为了更进一步巩固这些知识。下一篇文章准备对这三种方式来一个综合性的大比较!去看看吧!

clq
2007-9-10 15:10:40 发表 编辑

[图片]



  上几篇文章已经分别对C++的异常对象的几种不同的传递方式进行了详细地讨论。它们可以被分为按值传递,按引用传递,以及按指针传递等三种方式,现在该是对它们进行全面盘点总结的时候了。希望这种对比、总结及分析对朋友们理解这三种方式的各种区别有所帮助。 按值传递 引用传递 指针传递
语法 catch(std::exception e) catch(std::exception& e) catch(std::exception* e)
如何抛出异常? ①throw exception()
②exception ex;throw ex;
③throw ex_global; ①throw exception()
②exception ex;throw ex;
③throw ex_global;
①throw new exception();
异常对象的构造次数 三次 二次 一次
效率 低 中 高
异常对象什么时候被销毁 ①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁
③catch后面的那个类似参数的异常对象也是在catch block执行完毕后销毁 ①局部变量离开作用域时销毁
②临时变量在catch block执行完毕后销毁 异常对象动态地在堆上被创建,同时它也要动态的被销毁,销毁的时机是在catch block块中处理完毕后进行
发生对象切片 可能会 不会 不会
安全性 较低,可能会发生对象切片 很好 低,依赖于程序员的能力,可能会发生内存泄漏;或导致程序崩溃
综合性能 差 好 一般
易使用性 好 好 一般


  至此,对C++中的异常处理机制与模型已经进行了非常全面的阐述和分析,包括C++异常的语法,C++异常的使用技巧,C++异常与面向对象的相互关系,以及异常对象的构造、传递和最后析构销毁的过程。

  主人公阿愚现在已经开始有点小有成就感了,他知道自己对她(C++中的异常处理机制)已有了相当深入的了解,并且把她完全当成了一个知己,在自己的编程生涯中再也开始离不开她了。而且他与她的配合已经变得十分的默契,得心应手。但是有时好像还是有点迷糊,例如,对于在C++异常重新被抛出时(rethrow),异常对象的构造、传递和析构销毁的过程又将如何?有哪些不同之处?,要想了解更多的细节,程序员朋友们!请跟主人公阿愚进入到下一篇文章中,GO!


总数:61 页次:2/7 首页 << 上一页 下一页  >>  尾页  
总数:61 页次:2/7 首页 << 上一页 下一页  >>  尾页  


所在合集/目录



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1