抛出异常处理函数参数传递在写法上非常相似,都能进行值传递引用传递指针传递

//函数参数
void fun(Object obj){...} //值传递
void fun(Object& obj){...} //非const引用传递
void fun(const Object& obj){...} //const引用传递
void fun(Object* obj){...} //指针传递
void fun(const Object* obj){...} //指针传递
//catch一个异常
catch(Object obj){...} //值传递
catch(Object& obj){...} //非const引用传递
catch(const Object& obj){...} //const引用传递
catch(Object* obj){...} //指针传递
catch(const Object* obj){...} //指针传递

两者的处理机制存在显著差异,主要体现在以下几个方面:

控制权转移方面,函数调用结束后控制权自动返回调用方,而异常处理后控制权不会回到异常抛出点。

参数传递机制上,函数调用仅在值传递时复制参数,异常处理则无论何种传递方式都会复制异常对象。

引用传递规则中,函数参数的非const引用无法绑定临时对象,异常处理的引用类型却能捕获临时对象。

类型转换限制方面,异常处理仅允许两种隐式转换:类继承体系内的类型转换,以及有类型指针到无类型指针的转换。

匹配机制差异表现为,函数调用采用最佳匹配原则寻找最符合参数类型的函数,异常处理则遵循最先匹配原则,按catch块声明顺序执行首个符合条件的异常捕获。

下面就来讲解这些差别的详细内容:


一、控制权转移方面

调用函数时,若未触发异常,函数执行完毕后控制权将自动返回到调用点,程序继续按顺序执行后续代码。

当抛出异常并被捕获处理后,由于异常发生点已中断正常执行流程,控制权无法在异常处理后返回到原抛出位置继续执行后续代码。


二、参数传递机制

由于控制权不会返回抛出端,抛出端的临时对象可能随时被销毁,因此异常处理无论采用何种传递方式都会先创建副本对象。这一机制带来以下关键问题:

异常处理的性能开销 异常处理由于需要创建副本对象,其执行速度必然慢于普通函数调用。即使采用值传递方式,这一性能差异依然存在,具体原因如下文所述。

副本对象的创建数量差异 采用值传递方式时,系统会创建两个副本对象:第一个在抛出异常时生成,第二个在将对象复制到形参时产生。采用引用传递方式时,仅需创建一个副本对象,即在抛出异常时生成。

以静态类型进行复制     异常处理复制副本时以静态类型进行,例如:

class Parent{...};
class Child : public Parent{...};

Child child;
//此时parent静态类型为Parent
//动态类型为child
Parent& parent = child;
throw parent;  //抛出一个类型为Parent的异常

这个情况下,我们在catch之间传递异常会变得麻烦。所以推荐使用无参throw进行catch间的传递

class Parent{...};
class Child : public Parent{...};

catch(Child& child){
    ...
    //只能在catch中使用
    //直接抛出此异常,无需复制副本
    //以动态类型抛出异常
    throw;
    //创建副本抛出
    //以静态类型抛出异常
    //throw child;
}

从这里也能看出,为什么抛出端总是要复制一个副本。因为其不在catch中,不能使用无参throw。

针传递的风险   为了避免对象复制的性能损耗,指针传递看似是理想的解决方案——仅需复制指针本身。然而这种方式隐藏着潜在风险:若传递临时对象的指针,当该临时对象生命周期结束时,指针将指向无效内存区域,任何解引用操作都会导致未定义行为。

指针传递虽能消除数据拷贝,但必须警惕悬挂指针问题。临时对象销毁后,其指针便成为危险源,后续访问必然引发程序错误。


三、引用传递不同

异常处理对于下面这两种引用传递是一样的,都可以捕捉临时对象:

catch(Object& obj){...} //非const引用传递
catch(const Object& obj){...} //const引用传递

四、隐式转换不同

异常处理支持的隐式转换要比函数少,只有「继承架构中的类转换」:一个针对基类写的catch,可以捕获其派生类。并且支持三种传递形式:

class Parent{...};
class Child : public Parent{...};
//以下均可以捕获Child对象/指针
catch(Parent);
catch(Parent&);
catch(const Parent&);
catch(Parent*);
catch(const Parent*);

所有指针类型异常均可通过const void*类型的catch块捕获,这是另一种支持的隐式转换形式。特别值得注意的是catch(...)语法,它能够捕获任何类型的异常。


五、匹配次序不同

函数匹配采用最优策略,即从所有可用的同名函数中选择参数类型最接近的版本。

异常捕获的匹配机制类似于switch语句,遵循最先匹配原则。

抛出的异常会从上到下一个一个的与catch进行匹配,如果匹配上就直接进入这个catch,不管下面有无更适合的catch:

try{
    ...
}
catch(const void*) //所有抛出的指针型异常都进入这个
{ ... } 
catch(const Child*) //将不会捕捉到任何异常
{ ... }
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐