C++异常(4)
时间:2010-05-05 来源:landuochong
第9集 C++的异常对象如何被传递
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月16日
在相遇篇的第4集文章中,曾经讲到过在C++的异常处理模型中,是用“对象”来描述程序中出现的异常,并且在那篇文章中详细讨论了这样做所带来的诸多好处,其中之一呢就是:对象一般都很好地实现了对象的构造、对象的销毁、对象的转存复制,这为异常处理模型中异常对象的转存复制和对象销毁提供了很好的支持。是的没错,但是所谓的异常对象到底是如何被复制和传递呢?从本篇文章开始,和接下来的几篇文章中,主人公阿愚将和大家一同比较深入地探讨这个问题,并力求弄清每一个重要的细节。
概述
呵呵!sorry,居然忘了阐述一下定义。那就是“C++的异常对象被传递”指的是什么?想当然大家也都知道,这指的就是异常出现时throw出的异常对象如何被传递到catch block块中,catch block中的异常处理模块再根据异常对象提供的异常信息做出相应的处理。程序员朋友们也许认为这很简单,其实说简单也好像不太简单,因为这种对象的传递或复制可能发生在同一个函数的不同程序块作用域间,也有可能是从当前的函数传递到上一个函数中,更有可能是把异常对象传递复制到上上(甚至更多层)的函数中。
异常对象的传递有点类似于函数调用过程中的参数传递的过程。瞧!catch关键字的语法不就跟函数的定义有点类似吗?作为入参的异常对象也是用括号被括起来的,只不过catch只能是拥有一个参数。另外连catch(…)语法也是抄袭函数定义的方式,表示接受任意类型的数据对象。
C++程序中函数的调用是通过“栈”来实现的,其中参数的传递也是保存到栈中,以实现两个函数间的数据共享。那么异常对象的传递呢?当然也是通过栈,其实这是很明显的一件事情,因为异常对象本身肯定是局部变量,因此它也肯定是被保存在栈中的。不过异常对象的传递毕竟还是与函数参数的传递有很大的不同,函数参数的传递是严谨的、一级一级的对象数据的压栈过程和出栈过程;但异常对象的传递却远比这要复杂些,因为它这是逆序的,属于局部变量的异常对象可能要往上层(或更上层)函数传递,它的过程是一个跳跃式的或比较混乱的过程。关于异常对象的传递具体是如何实现的,在爱的秘密篇中分析C++异常处理模型的实现时会再做详细阐述。而目前需要搞清楚的是,这个过程中所需要遵从的一些规律或标准。
函数的参数的传递一般有指针、传值和引用三种方式,同样,异常对象的传递也同样有这三种方式的区别。现在开始,主人公阿愚分别讲述每一种方式下异常对象是如何被传递的,不过在正式开始之前,还是先简要总结函数调用的过程,以及这过程栈的变化。因为这对随后的具体分析和理解也许大有帮助。
函数的调用过程与“栈”
C++程序员对这个过程肯定非常熟悉,因此这里不做细致的讲述,只做一个概要性的总结。
(1) 函数的调用过程实质上利用栈来实现的指令(eip)执行远程转移和返回的过程;它在CPU指令级别上就得到了支持(CALL和RET指令);
(2) 每个线程都有一个自己的栈,因此每个线程的函数调用执行是相互不受影响的;
(3) C和C++中的函数参数的入栈顺序一般是从右到左进行;
(4) C++中的函数的参数的传递一般有指针、传值和引用三种方式;
(5) C和C++中函数的返回值一般都是保存到EAX寄存器中返回的;
(6) C和C++中函数中寻址参数和局部变量,一般都是通过EBP寄存器加上偏移来进行的,如参数一般是:[EBP+XX],而局部变量则一般是:[EBP-XX];
(7) 在程序运行时,EBP中的值一般是指向当前的函数调用帧,而ESP一般指向栈顶。
如果对上面论述有不太清楚或不太熟悉的朋友,建议先看看专门讲述C++设计和编程方面的书籍。下面给出一个线程运行期间,它栈中所保存的数据的布局(部分片段),如下图:
总结
(1) 与函数的参数的传递类似, C++的异常对象的传递也分指针、传值和引用三种方式;
(2) 与函数的参数的传递不同的是,异常对象的传递是向上逆序的,而且是跳跃式的。
下一篇文章详细介绍C++的异常对象按传值的方式被复制和传递。朋友们,不要错过,请继续吧! 第10集 C++的异常对象按传值的方式被复制和传递
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月16日
上一篇文章中对C++的异常对象如何被传递做了一个概要性的介绍,其中得知C++的异常对象的传递方式有指针方式、传值方式和引用方式三种。现在开始讨论最简单的一种传递的方式:按值传递。
异常对象在什么时候构造?
1、按传值的方式传递异常对象时,被抛出的异常都是局部变量,而且是临时的局部变量。什么是临时的局部变量,这大家可能都知道,例如发生函数调用时,按值传递的参数就会被临时复制一份,这就是临时局部变量,一般临时局部变量转瞬即逝。
主人公阿愚对这开始有点不太相信。不会吧,谁说异常对象都是临时的局部变量,应该是普通的局部变量,甚至是全局性变量,而且还可以是堆中动态分配的异常变量。是的,这上面说的好象没错,但是实际真实发生的情况是,每当在throw语句抛出一个异常时,不管你原来构造的对象是什么性质的变量,此时它都会复制一份临时局部变量,还是具体看看例程吧!如下:
class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<<m_name<< endl;
}
MyException (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
operator= (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "赋值拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <<m_name<< endl;
}
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"<<endl;
}
}
程序运行的结果是:
构造一个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"<<endl;
}
大家也可以对每种情况都试一试,注意是不是确实无论哪种情况都会复制一份本地的临时变量了呢!
另外请朋友们特别注意的是,这是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类型的异常,名称为:"<<e.GetName()<<endl;
}
}
程序运行的结果是:
构造一个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.GetName()<<endl;
}
// 加入一条语句,判断e什么时候销毁
cout<<"在这之前还是之后呢?"<<endl;
}
程序运行的结果是:
构造一个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类型的异常,名称为:"<<e.GetName()<<endl;
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<<"在这之前还是之后呢?"<<endl;
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类型的异常,名称为:"<<e.GetName()<<endl;
}
// 这里又一个catch block,其中变量名还是e;而且数据类型也不同了。
catch(std::exception e)
{
e.what();
}
}
小心异常对象发生对象切片
C++程序员知道,当函数的参数按值传递时,可能会发生对象的切片现象。同样,如果异常对象按传值方式复制异常对象时,也可能会发生异常对象的切片。示例如下:
class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<<m_name<< endl;
}
MyException (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
operator= (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "赋值拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <<m_name<< endl;
}
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异常对象,名称为:"<<m_name<< endl;
}
MyMemoryException (const MyMemoryException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyMemoryException异常对象,名称为:"<<m_name<< endl;
}
virtual string Test_Virtual_Func() { return "这是MyMemoryException类型的异常对象";}
virtual ~ MyMemoryException ()
{
cout << "销毁一个MyMemoryException异常对象,名称为:" <<m_name<< endl;
}
};
void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");
cout <<endl<< "抛出一个MyMemoryException类型的异常" <<endl<<endl;
throw ex_obj1;
}
}
// 注意这里发生了对象切片,异常对象e已经不是原原本本的那个被throw出
// 的那个对象了
catch(MyException e)
{
// 调用虚函数,验证一下这个异常对象是否真的发生了对象切片
cout<<endl<<e.Test_Virtual_Func()<<endl<<endl;
}
}
程序运行的结果是:
总结
(1) 被抛出的异常对象都是临时的局部变量;
(2) 异常对象至少要被构造三次;
(3) catch 后面带的异常对象的作用域仅限于catch bock中;
(4) 按值传递方式很容易发生异常对象的切片。
下一篇文章讨论C++的异常对象按引用的方式被复制和传递。继续吧!
第11集 C++的异常对象按引用方式被传递
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月18日
上一篇文章详细讨论了C++的异常对象按值传递的方式,本文继续讨论另外的一种的方式:引用传递。
异常对象在什么时候构造?
其实在上一篇文章中就已经讨论到了,假如异常对象按引用方式被传递,异常对象更应该被构造出一个临时的变量。因此这里不再重复讨论了。
异常对象按引用方式传递
引用是C++语言中引入的一种数据类型形式。它本质上是一个指针,通过这个特殊的隐性指针来引用其它地方的一个变量。因此引用与指针有很多相似之处,但是引用用起来较指针更为安全,更为直观和方便,所以C++语言建议C++程序员在编写代码中尽可能地多使用引用的方式来代替原来在C语言中使用指针的地方。这些地方主要是函数参数的定义上,另外还有就是catch到的异常对象的定义。
所以异常对象按引用方式传递,是不会发生对象的拷贝复制过程。这就导致引用方式要比传值方式效率高,此时从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生两次对象的构造过程(一次是异常对象的初始化构造过程,另一次就是当执行throw语句时所发生的临时异常对象的拷贝复制的构造过程)。而按值传递的方式总共是发生三次。看看示例程序吧!如下:
void main()
{
try
{
{
throw MyException();
}
}
// 注意:这里是定义了引用的方式
catch(MyException& e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<<e.GetName()<<endl;
}
}
程序运行的结果是:
构造一个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类型的异常,名称为:"<<e.GetName()<<endl;
}
cout<<"那个临时的异常对象应该是在这之前析构销毁"<<endl;
}
怎样来分析呢?当然最简单的方法是调试一下,跟踪它的ebp和esp的变化。首先在函数调用的地方和抛出异常的地方设置好断点,F5开始调试,截图如下:
纪录一下ebp和esp的值(ebp 0012FF70;esp 0012FEF8),通过ebp和esp可以确定main函数的函数帧在栈中位置,F5继续,截图如下:
同样也纪录一下ebp和esp的值(ebp 0012FE9C;esp 0012FE08),通过ebp和esp可以看出栈是往下扩展,此时的ebp和esp指向抛出异常的test2函数的函数帧在栈中位置,F5继续,此时抛出异常,控制进入main函数中的catch(MyException& e)中,截图如下
请注意了,现在ebp恢复了main函数在先前时的函数帧在栈中位置,但esp却并没有,它甚至比刚刚抛出异常的那个test2函数中的esp还要往下,这就是编译器编译程序时耍的小技巧,当前ebp和esp指向的函数帧实际上并不是真正的main函数原来的哪个函数帧,它实际上包含了多个函数的函数帧,因此catch block执行程序时当然不会发生覆盖。我们还是看看异常对象所引用指向的临时的变量究竟身在何处。截图如下:
哈哈!e指向了0x0012fe7c内存区域,再看看上面的抛出异常的test2函数的函数帧的ebp和esp的值。结果0x0012fe7c恰好是ebp 0012FE9C和esp 0012FE08之间。
不过阿愚又开始有点疑惑了,哦!这样做岂不是破坏了函数的帧栈吗,结果还不导致程序崩溃呀!呵呵!不用担心,F5继续,截图如下:
当离开了catch block作用域之后,再看看ebp和esp的值,是不是和最开始的那个main函数进入时的ebp和esp一模一样,哈哈!恢复了,厉害吧!先暂时不管它是如何恢复的,总之ebp和esp都是得以恢复了,而且同时catch block执行时也不会发生异常对象的覆盖。这就解决了异常对象按引用传递时可能存在的不安全隐患。
引用方式下,异常对象会发生对象切片吗?
当然不会,要不测试一下,把上一篇文章中的对应的那个例子改为按引用的方式接受异常对象。示例如下:
void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");
cout <<endl<< "抛出一个MyMemoryException类型的异常" <<endl<<endl;
throw ex_obj1;
}
}
// 注意这里引用的方式了
// 还会发生了对象切片吗?
catch(MyException& e)
{
// 调用虚函数,验证一下这个异常对象是否发生了对象切片
cout<<endl<<e.Test_Virtual_Func()<<endl<<endl;
}
}
程序运行的结果是:
构造一个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++的异常对象被按指针的方式传递。继续吧!
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月16日
在相遇篇的第4集文章中,曾经讲到过在C++的异常处理模型中,是用“对象”来描述程序中出现的异常,并且在那篇文章中详细讨论了这样做所带来的诸多好处,其中之一呢就是:对象一般都很好地实现了对象的构造、对象的销毁、对象的转存复制,这为异常处理模型中异常对象的转存复制和对象销毁提供了很好的支持。是的没错,但是所谓的异常对象到底是如何被复制和传递呢?从本篇文章开始,和接下来的几篇文章中,主人公阿愚将和大家一同比较深入地探讨这个问题,并力求弄清每一个重要的细节。
概述
呵呵!sorry,居然忘了阐述一下定义。那就是“C++的异常对象被传递”指的是什么?想当然大家也都知道,这指的就是异常出现时throw出的异常对象如何被传递到catch block块中,catch block中的异常处理模块再根据异常对象提供的异常信息做出相应的处理。程序员朋友们也许认为这很简单,其实说简单也好像不太简单,因为这种对象的传递或复制可能发生在同一个函数的不同程序块作用域间,也有可能是从当前的函数传递到上一个函数中,更有可能是把异常对象传递复制到上上(甚至更多层)的函数中。
异常对象的传递有点类似于函数调用过程中的参数传递的过程。瞧!catch关键字的语法不就跟函数的定义有点类似吗?作为入参的异常对象也是用括号被括起来的,只不过catch只能是拥有一个参数。另外连catch(…)语法也是抄袭函数定义的方式,表示接受任意类型的数据对象。
C++程序中函数的调用是通过“栈”来实现的,其中参数的传递也是保存到栈中,以实现两个函数间的数据共享。那么异常对象的传递呢?当然也是通过栈,其实这是很明显的一件事情,因为异常对象本身肯定是局部变量,因此它也肯定是被保存在栈中的。不过异常对象的传递毕竟还是与函数参数的传递有很大的不同,函数参数的传递是严谨的、一级一级的对象数据的压栈过程和出栈过程;但异常对象的传递却远比这要复杂些,因为它这是逆序的,属于局部变量的异常对象可能要往上层(或更上层)函数传递,它的过程是一个跳跃式的或比较混乱的过程。关于异常对象的传递具体是如何实现的,在爱的秘密篇中分析C++异常处理模型的实现时会再做详细阐述。而目前需要搞清楚的是,这个过程中所需要遵从的一些规律或标准。
函数的参数的传递一般有指针、传值和引用三种方式,同样,异常对象的传递也同样有这三种方式的区别。现在开始,主人公阿愚分别讲述每一种方式下异常对象是如何被传递的,不过在正式开始之前,还是先简要总结函数调用的过程,以及这过程栈的变化。因为这对随后的具体分析和理解也许大有帮助。
函数的调用过程与“栈”
C++程序员对这个过程肯定非常熟悉,因此这里不做细致的讲述,只做一个概要性的总结。
(1) 函数的调用过程实质上利用栈来实现的指令(eip)执行远程转移和返回的过程;它在CPU指令级别上就得到了支持(CALL和RET指令);
(2) 每个线程都有一个自己的栈,因此每个线程的函数调用执行是相互不受影响的;
(3) C和C++中的函数参数的入栈顺序一般是从右到左进行;
(4) C++中的函数的参数的传递一般有指针、传值和引用三种方式;
(5) C和C++中函数的返回值一般都是保存到EAX寄存器中返回的;
(6) C和C++中函数中寻址参数和局部变量,一般都是通过EBP寄存器加上偏移来进行的,如参数一般是:[EBP+XX],而局部变量则一般是:[EBP-XX];
(7) 在程序运行时,EBP中的值一般是指向当前的函数调用帧,而ESP一般指向栈顶。
如果对上面论述有不太清楚或不太熟悉的朋友,建议先看看专门讲述C++设计和编程方面的书籍。下面给出一个线程运行期间,它栈中所保存的数据的布局(部分片段),如下图:
总结
(1) 与函数的参数的传递类似, C++的异常对象的传递也分指针、传值和引用三种方式;
(2) 与函数的参数的传递不同的是,异常对象的传递是向上逆序的,而且是跳跃式的。
下一篇文章详细介绍C++的异常对象按传值的方式被复制和传递。朋友们,不要错过,请继续吧! 第10集 C++的异常对象按传值的方式被复制和传递
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月16日
上一篇文章中对C++的异常对象如何被传递做了一个概要性的介绍,其中得知C++的异常对象的传递方式有指针方式、传值方式和引用方式三种。现在开始讨论最简单的一种传递的方式:按值传递。
异常对象在什么时候构造?
1、按传值的方式传递异常对象时,被抛出的异常都是局部变量,而且是临时的局部变量。什么是临时的局部变量,这大家可能都知道,例如发生函数调用时,按值传递的参数就会被临时复制一份,这就是临时局部变量,一般临时局部变量转瞬即逝。
主人公阿愚对这开始有点不太相信。不会吧,谁说异常对象都是临时的局部变量,应该是普通的局部变量,甚至是全局性变量,而且还可以是堆中动态分配的异常变量。是的,这上面说的好象没错,但是实际真实发生的情况是,每当在throw语句抛出一个异常时,不管你原来构造的对象是什么性质的变量,此时它都会复制一份临时局部变量,还是具体看看例程吧!如下:
class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<<m_name<< endl;
}
MyException (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
operator= (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "赋值拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <<m_name<< endl;
}
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"<<endl;
}
}
程序运行的结果是:
构造一个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"<<endl;
}
大家也可以对每种情况都试一试,注意是不是确实无论哪种情况都会复制一份本地的临时变量了呢!
另外请朋友们特别注意的是,这是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类型的异常,名称为:"<<e.GetName()<<endl;
}
}
程序运行的结果是:
构造一个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.GetName()<<endl;
}
// 加入一条语句,判断e什么时候销毁
cout<<"在这之前还是之后呢?"<<endl;
}
程序运行的结果是:
构造一个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类型的异常,名称为:"<<e.GetName()<<endl;
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<<"在这之前还是之后呢?"<<endl;
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类型的异常,名称为:"<<e.GetName()<<endl;
}
// 这里又一个catch block,其中变量名还是e;而且数据类型也不同了。
catch(std::exception e)
{
e.what();
}
}
小心异常对象发生对象切片
C++程序员知道,当函数的参数按值传递时,可能会发生对象的切片现象。同样,如果异常对象按传值方式复制异常对象时,也可能会发生异常对象的切片。示例如下:
class MyException
{
public:
MyException (string name="none") : m_name(name)
{
cout << "构造一个MyException异常对象,名称为:"<<m_name<< endl;
}
MyException (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
operator= (const MyException& old_e)
{
m_name = old_e.m_name;
cout << "赋值拷贝一个MyException异常对象,名称为:"<<m_name<< endl;
}
virtual ~ MyException ()
{
cout << "销毁一个MyException异常对象,名称为:" <<m_name<< endl;
}
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异常对象,名称为:"<<m_name<< endl;
}
MyMemoryException (const MyMemoryException& old_e)
{
m_name = old_e.m_name;
cout << "拷贝一个MyMemoryException异常对象,名称为:"<<m_name<< endl;
}
virtual string Test_Virtual_Func() { return "这是MyMemoryException类型的异常对象";}
virtual ~ MyMemoryException ()
{
cout << "销毁一个MyMemoryException异常对象,名称为:" <<m_name<< endl;
}
};
void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");
cout <<endl<< "抛出一个MyMemoryException类型的异常" <<endl<<endl;
throw ex_obj1;
}
}
// 注意这里发生了对象切片,异常对象e已经不是原原本本的那个被throw出
// 的那个对象了
catch(MyException e)
{
// 调用虚函数,验证一下这个异常对象是否真的发生了对象切片
cout<<endl<<e.Test_Virtual_Func()<<endl<<endl;
}
}
程序运行的结果是:
总结
(1) 被抛出的异常对象都是临时的局部变量;
(2) 异常对象至少要被构造三次;
(3) catch 后面带的异常对象的作用域仅限于catch bock中;
(4) 按值传递方式很容易发生异常对象的切片。
下一篇文章讨论C++的异常对象按引用的方式被复制和传递。继续吧!
第11集 C++的异常对象按引用方式被传递
--------------------------------------------------------------------------------
CSAI.CN原创 王胜祥 2005年05月18日
上一篇文章详细讨论了C++的异常对象按值传递的方式,本文继续讨论另外的一种的方式:引用传递。
异常对象在什么时候构造?
其实在上一篇文章中就已经讨论到了,假如异常对象按引用方式被传递,异常对象更应该被构造出一个临时的变量。因此这里不再重复讨论了。
异常对象按引用方式传递
引用是C++语言中引入的一种数据类型形式。它本质上是一个指针,通过这个特殊的隐性指针来引用其它地方的一个变量。因此引用与指针有很多相似之处,但是引用用起来较指针更为安全,更为直观和方便,所以C++语言建议C++程序员在编写代码中尽可能地多使用引用的方式来代替原来在C语言中使用指针的地方。这些地方主要是函数参数的定义上,另外还有就是catch到的异常对象的定义。
所以异常对象按引用方式传递,是不会发生对象的拷贝复制过程。这就导致引用方式要比传值方式效率高,此时从抛出异常、捕获异常再到异常错误处理结束过程中,总共只会发生两次对象的构造过程(一次是异常对象的初始化构造过程,另一次就是当执行throw语句时所发生的临时异常对象的拷贝复制的构造过程)。而按值传递的方式总共是发生三次。看看示例程序吧!如下:
void main()
{
try
{
{
throw MyException();
}
}
// 注意:这里是定义了引用的方式
catch(MyException& e)
{
cout<<"捕获到一个MyException类型的异常,名称为:"<<e.GetName()<<endl;
}
}
程序运行的结果是:
构造一个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类型的异常,名称为:"<<e.GetName()<<endl;
}
cout<<"那个临时的异常对象应该是在这之前析构销毁"<<endl;
}
怎样来分析呢?当然最简单的方法是调试一下,跟踪它的ebp和esp的变化。首先在函数调用的地方和抛出异常的地方设置好断点,F5开始调试,截图如下:
纪录一下ebp和esp的值(ebp 0012FF70;esp 0012FEF8),通过ebp和esp可以确定main函数的函数帧在栈中位置,F5继续,截图如下:
同样也纪录一下ebp和esp的值(ebp 0012FE9C;esp 0012FE08),通过ebp和esp可以看出栈是往下扩展,此时的ebp和esp指向抛出异常的test2函数的函数帧在栈中位置,F5继续,此时抛出异常,控制进入main函数中的catch(MyException& e)中,截图如下
请注意了,现在ebp恢复了main函数在先前时的函数帧在栈中位置,但esp却并没有,它甚至比刚刚抛出异常的那个test2函数中的esp还要往下,这就是编译器编译程序时耍的小技巧,当前ebp和esp指向的函数帧实际上并不是真正的main函数原来的哪个函数帧,它实际上包含了多个函数的函数帧,因此catch block执行程序时当然不会发生覆盖。我们还是看看异常对象所引用指向的临时的变量究竟身在何处。截图如下:
哈哈!e指向了0x0012fe7c内存区域,再看看上面的抛出异常的test2函数的函数帧的ebp和esp的值。结果0x0012fe7c恰好是ebp 0012FE9C和esp 0012FE08之间。
不过阿愚又开始有点疑惑了,哦!这样做岂不是破坏了函数的帧栈吗,结果还不导致程序崩溃呀!呵呵!不用担心,F5继续,截图如下:
当离开了catch block作用域之后,再看看ebp和esp的值,是不是和最开始的那个main函数进入时的ebp和esp一模一样,哈哈!恢复了,厉害吧!先暂时不管它是如何恢复的,总之ebp和esp都是得以恢复了,而且同时catch block执行时也不会发生异常对象的覆盖。这就解决了异常对象按引用传递时可能存在的不安全隐患。
引用方式下,异常对象会发生对象切片吗?
当然不会,要不测试一下,把上一篇文章中的对应的那个例子改为按引用的方式接受异常对象。示例如下:
void main()
{
try
{
{
MyMemoryException ex_obj1("ex_obj1");
cout <<endl<< "抛出一个MyMemoryException类型的异常" <<endl<<endl;
throw ex_obj1;
}
}
// 注意这里引用的方式了
// 还会发生了对象切片吗?
catch(MyException& e)
{
// 调用虚函数,验证一下这个异常对象是否发生了对象切片
cout<<endl<<e.Test_Virtual_Func()<<endl<<endl;
}
}
程序运行的结果是:
构造一个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++的异常对象被按指针的方式传递。继续吧!
相关阅读 更多 +