正文

第10集 C++的异常对象按传值的方式被复制和传递2006-01-20 12:42:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/mroske/9756.html

分享到:

  上一篇文章中对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++的异常对象按引用的方式被复制和传递。继续吧!

阅读(2992) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册