前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++抛出异常与传递参数的区别

C++抛出异常与传递参数的区别

作者头像
恋喵大鲤鱼
发布2018-08-03 16:42:33
1.8K0
发布2018-08-03 16:42:33
举报
文章被收录于专栏:C/C++基础C/C++基础

代码便已运行环境:VS2012+Debug+Win32


1.C++异常处理基本格式

C++的异常处理机制有3部分组成:try(检查),throw(抛出),catch(捕获)。把需要检查的语句放在try模块中,检查语句发生错误,throw抛出异常,发出错误信息,由catch来捕获异常信息,并加以处理。一般throw抛出的异常要和catch所捕获的异常类型所匹配。异常处理的一般格式为:

代码语言:javascript
复制
  try
  {
    被检查语句
    throw 异常
  }
  catch(异常类型1)
  {
    进行异常处理的语句1
  }
  catch(异常类型2)
  {
    进行异常处理的语句2
}
catch(...)    // 三个点则表示捕获所有类型的异常  
{  
进行默认异常处理的语句
}

2. 抛出异常与传递参数的区别

从语法上看,C++的异常处理机制中,在catch子句中申明参数与在函数里声明参数几乎没有什么差别。例如,定义了一个名为stuff的类,那么可以有如下的函数申明。

代码语言:javascript
复制
void f1(stuff w);
void f2(stuff& w);
void f3(const stuff& w);
void f4(stuff* p);
void f5(const stuff* p);

同样地,在特定的上下文环境中,可以利用如下的catch语句来捕获异常对象:

代码语言:javascript
复制
catch(stuff w);
catch (stuff& w);
catch(const stuff& w);
catch (stuff* p);
catch (const stuff* p);  

因此,初学者很容易认为用throw抛出一个异常到catch字句中与通过函数调用传递一个参数两者基本相同。它们有相同点,但存在着巨大的差异。造成二者的差异是因为调用函数时,程序的控制权最终还会返回到函数的调用处,但是当抛出一个异常时,控制权永远不会回到抛出异常的地方。相同点就是传递参数和传递异常都可以是传值、传引用或传指针。

下面考察二者的不同点。

(1)区别一:C++标准要求被作为异常抛出的对象必须被拷贝复制。

考察如下程序。

代码语言:javascript
复制
#include <iostream>
using namespace std;

class Stuff{
    int n;
    char c;
public:
    void addr(){
        cout<<this<<endl;
    }
    friend istream& operator>>(istream&, Stuff&);
};

istream& operator>>(istream& s, Stuff& w){
    w.addr();
    cin>>w.n;
    cin>>w.c;
    cin.get();//清空输入缓冲区残留的换行符
    return s;
}

void passAndThrow(){
    Stuff localStuff;
    localStuff.addr();
    cin>>localStuff;   //传递localStuff到operator>>
    throw localStuff;  //抛出localStuff异常
}

int main(){
    try{
        passAndThrow();
    }
    catch(Stuff& w){
        w.addr();
    }
}

程序的执行结果是: 0025FA20 0025FA20 5 c 0025F950

在执行输入操作是,实参localStuff是以传引用的方式进入函数operator>>,形参变量w接收的是localStuff的地址,任何对w的操作但实际上都施加到localStuff上。在随后的抛出异常的操作中,尽管catch子句捕捉的是异常对象的引用,但是捕捉到的异常对象已经不是localStuff,而是它的一个拷贝。原因是throw语句一旦执行,函数passAndThrow()的执行也将结束,localStuff对象将被析构从而结束其生命周期。因此需要抛出localStuff的拷贝。从程序的输出结果也可以看出在catch子句中捕捉到的异常对象的地址与localStuff不同。

即使被抛出的对象不会被释放,即被抛出的异常对象是静态局部变量,甚至是全局性变量,而且还可以是堆中动态分配的异常变量,当被抛出时也会进行拷贝操作。例如,如果将passAndThrow()函数声明为静态变量static,即:

代码语言:javascript
复制
void passAndThrow(){
    static Stuff localStuff;
    localStuff.addr();
    cin>>localStuff;   //传递localStuff到operator>>
    throw localStuff;  //抛出localStuff异常
}

当抛出异常时仍将复制出localStuff的一个拷贝。这表示尽管通过引用来捕捉异常,也不能在catch块中修改localStuff,仅仅能修改localStuff的拷贝。C++规定对被抛出的任何类型的异常对象都要进行强制复制拷贝, 为什么这么做,我目前还不明白。

(2)区别二:因为异常对象被抛出时需要拷贝,所以抛出异常运行速度一般会比参数传递要慢。

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应的类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。

考察如下程序。

代码语言:javascript
复制
#include <iostream>
using namespace std;

class Stuff{
    int n;
    char c;
public:
    Stuff(){
        n=c=0;
    }
    Stuff(Stuff&){
        cout<<"Stuff's copy constructor invoked"<<endl;
        cout<<this<<endl;
    }

    void addr(){
        cout<<this<<endl;
    }
};


class SpecialStuff:public Stuff{
    double d;
public:
    SpecialStuff(){
        d=0.0;
    }

    SpecialStuff(SpecialStuff&){
        cout<<"SpecialStuff's copy constructor invoked"<<endl;
        addr();
    }
};

void passAndThrow(){
    SpecialStuff localStuff;
    localStuff.addr();
    Stuff& sf=localStuff;   
    cout<<&sf<<endl;
    throw sf;               //抛出Stuff类型的异常
}

int main(){
    try{
        passAndThrow();
    }
    catch(Stuff& w){
        cout<<"catched"<<endl;
        cout<<&w<<endl;
    }
}

程序输出结果: 0022F814 0022F814 Stuff’s copy constructor invoked 0022F738 catched 0022F738

程序输出结果表明,sf和localStuff的地址是一样的,这体现了引用的作用。把一个SpecialStuff类型的对象当做Stuff类型的对象使用。当localStuff被抛出时,抛出的类型是Stuff类型,因此需要调用Stuff的拷贝构造函数产生对象。在catch中捕获的是异常对象的引用,所以拷贝构造函数构造的Stuff对象与在catch块中使用的对象w是同一个对象,因为他们具有相同的地址0x0022F738。

在上面的程序中,将catch字句做一个小的修改,变成:

代码语言:javascript
复制
catch(Stuff w){…}

程序的输出结果就变成: 0026FBA0 0026FBA0 Stuff’s copy constructor invoked 0026FAC0 Stuff’s copy constructor invoked 0026FC98 catched 0026FC98

可见,类Stuff的拷贝构造函数被调用了2次。这是因为localStuff通过拷贝构造函数传递给异常对象,而异常对象又通过拷贝构造函数传递给catch字句中的对象w。实际上,抛出异常时生成的异常对象是一个临时对象,它以一种程序猿不可见的方式在发挥作用。

(3)区别三:参数传递和异常传递在类型匹配的过程不同,catch字句在类型匹配时比函数调用时类型匹配的要求要更加严格。 考察如下程序。

代码语言:javascript
复制
#include <math.h>
#include <iostream>
using namespace std;

void throwint(){
    int i=5;
    throw i;
}

double _sqrt(double d){
    return sqrt(d);
}

int main(){
    int i=5;
    cout<<"sqrt(5)="<<_sqrt(i)<<endl;
    try{
        throwint();
    }
    catch(double){
        cout<<"catched"<<endl;
    }
    catch(...){
        cout<<"not catched"<<endl;
    }
}

程序输出: sqrt(5)=2.23607 not catched

C++允许从int到double的隐式类型转换,所以函数调用_sqrt(i)中,i被悄悄地转变为double类型,并且其返回值也是double。一般来说,catch字句匹配异常类型时不会进行这样的转换。可见catch字句在类型匹配时比函数调用时类型匹配的要求要更加严格。

不过,在catch字句中进行异常匹配时可以进行两种类型转换。第一种是继承类与基类见的抓换。即一个用来捕获基类的catch字句可以处理派生类类型的异常。这种派生类与基类间的异常类型转换可以作用于数值、引用以及指针。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void*指针的catch字句能捕获任何类型的指针类型异常。

(4)区别四:catch字句匹配顺序总是取决于它们在程序中出现的顺序。函数匹配过程则需要按照更为复杂的匹配规则来顺序来完成。

因此,一个派生类异常可能被处理其基类异常的catch字句捕获,即使同时存在有能处理该派生类异常的catch字句与相同的try块相对应。考察如下程序。

代码语言:javascript
复制
#include <iostream>
using namespace std;

class Stuff{
    int n;
    char c;
public:
    Stuff(){
        n=c=0;
    }
};

class SpecialStuff:public Stuff{
    double d;
public:
    SpecialStuff(){
        d=0.0;
    }
};

int main(){
    SpecialStuff localStuff;
    try{
        throw localStuff;  //抛出SpecialStuff类型的异常
    }
    catch(Stuff&){
        cout<<"Stuff catched"<<endl;
    }
    catch(SpecialStuff&){
        cout<<"SpecialStuff catched"<<endl;
    }
}

程序输出: Stuff catched

程序中被抛出的对象是SpecialStuff类型的,本应由catch(SpecialStuff&)字句捕获,但由于前面有一个catch(Stuff&),而在类型匹配时是允许在派生类和基类之间进行类型转换的,所以最终是由前面的catch子句将异常捕获。不过,这个程序在逻辑上多少存在一些问题,因为处在前面的catch字句实际上阻止了后面的catch子句捕获异常。所以,当有多个catch字句对应同一个try块时,应该把捕获派生类对象的catch字句放在前面,而把捕获基类对象的catch子句放在后面。否则,代码在逻辑上是错误的,编译器也会发出警告。

与上面这种行为相反,当调用一个虚拟函数时,被调用的函数是由发出函数调用的对象的动态类型(dynamic type)决定的。所以说,虚拟函数采用最优适合法,而异常处理采用的是最先适合法。

3.总结

综上所述,把一个对象传递给函数(或一个对象调用虚拟函数)与把一个对象作为异常抛出,这之间有三个主要区别。 第一,把一个对象作为异常抛出时,总会建立该对象的副本。并且调用的拷贝构造函数是属于被抛出对象的静态类型。当通过传值方式捕获时,对象被拷贝了两次。对象作为引用参数传递给函数时,不需要进行额外的拷贝; 第二,对象作为异常被抛出与作为参数传递给函数相比,前者允许的类型转换比后者要少(前者只有两种类型转换形式); 第三,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。


参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008[P355-P364] [2]http://blog.csdn.net/hanchaoman/article/details/5914204 [3]http://dev.yesky.com/171/2602671.shtml

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015年11月29日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.C++异常处理基本格式
  • 2. 抛出异常与传递参数的区别
  • 3.总结
  • 参考文献
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档