C语言常见问题.docx

上传人:b****6 文档编号:6004176 上传时间:2023-01-02 格式:DOCX 页数:16 大小:29.80KB
下载 相关 举报
C语言常见问题.docx_第1页
第1页 / 共16页
C语言常见问题.docx_第2页
第2页 / 共16页
C语言常见问题.docx_第3页
第3页 / 共16页
C语言常见问题.docx_第4页
第4页 / 共16页
C语言常见问题.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

C语言常见问题.docx

《C语言常见问题.docx》由会员分享,可在线阅读,更多相关《C语言常见问题.docx(16页珍藏版)》请在冰豆网上搜索。

C语言常见问题.docx

C语言常见问题

C++异常处理

2011/06/1811:

45

C++异常处理机制是一个用来有效地处理运行错误的非常强大且灵活的工具,它提供了更多的弹性、安全性和稳固性,克服了传统方法所带来的问题。

事实上,C++中的异常处理机制是一种把控制权从异常发生的地点转移到一个匹配的处理函数或功能块的机制。

其中,异常可以是内建数据类型变量,也可以是对象。

一般来说,异常处理机制包括4个部分。

    try语句块:

即一个定义异常的语句块。

    catch语句块:

即一个或多个和try语句块相关的处理,它们放在catch语句块中。

    throw表达式:

即抛出异常语句。

一般来说,try语句块包含可能抛出异常的代码。

例如,下列语句可能引发内存空间溢出的异常,其就包含在try语句中。

try

{

    int*p=newint[1000000];

}

一个try语句块后面将跟有一个或多个catch语句,其中,每一个catch语句可以处理不同类型的异常。

例如:

try

{

    int*p=newint[1000000];

    //...

}

catch(std:

:

bad_alloc&)                            //内存空间不够,分配内存失败

{

}

catch(std:

:

bad_cast&)                            //转型失败,分配内存失败

{

}

catch语句块仅仅被在try语句块中的throw表达式及函数所调用。

其中,throw表达式包括一个关键字throw及相关参数。

例如:

try

{

    throw5;

}

catch(intn)

{

}

读者需要注意的是,throw表达式和返回语句很相似。

此外,throw语句可以没有操作数,其格式如下所示:

throw;

注意:

一般来说,如果目前没有异常被处理,那么执行一个没有操作数的throw语句后,编译系统将会调用terminate()函数结束程序。

当一个异常被抛出后,C++运行机制首先在当前的作用域寻找合适的处理catch语句块。

如果不存在这样一个处理,那么将会离开当前的作用域,进入更外围的一层继续寻找。

这个过程不断地进行下去直到合适的处理被找到为止。

此时堆栈已经被解开,并且所有的局部对象被销毁。

如果始终都没有找到合适的处理,那么程序将会终止。

C++ 异常处理 

函数传递和异常传递区别

2011/05/3115:

28

参考moreeffectivec++条款9~条款15,帖出条款九供参考

5.4ItemM12:

理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异  

从语法上看,在函数里声明参数与在catch子句中声明参数几乎没有什么差别:

classWidget{...};//一个类,具体是什么类

 //在这里并不重要

voidf1(Widgetw);//一些函数,其参数分别为

voidf2(Widget&w);//Widget,Widget&,或

voidf3(constWidget&w);//Widget*类型

voidf4(Widget*pw);  

voidf5(constWidget*pw);

catch(Widgetw)...//一些catch子句,用来

catch(Widget&w)...//捕获异常,异常的类型为

catch(constWidget&w)...//Widget,Widget&,或

catch(Widget*pw)...//Widget*

catch(constWidget*pw)...  

你因此可能会认为用throw抛出一个异常到catch子句中与通过函数调用传递一个参数两者基本相同。

这里面确有一些相同点,但是他们也存在着巨大的差异。

让我们先从相同点谈起。

你传递函数参数与异常的途径可以是传值、传递引用或传递指针,这是相同的。

但是当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。

产生这个差异的原因是:

你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

有这样一个函数,参数类型是Widget,并抛出一个Widget类型的异常:

//一个函数,从流中读值到Widget中

istreamoperator>>(istream&s,Widget&w);

voidpassAndThrowWidget()

{

 WidgetlocalWidget;

 cin>>localWidget;//传递localWidget到operator>>

 throwlocalWidget;//抛出localWidget异常

}

当传递localWidget到函数operator>>里,不用进行拷贝操作,而是把operator>>内的引用类型变量w指向localWidget,任何对w的操作实际上都施加到localWidget上。

这与抛出localWidget异常有很大不同。

不论通过传值捕获异常还是通过引用捕获(不能通过指针捕获这个异常,因为类型不匹配)都将进行lcalWidget的拷贝操作,也就说传递到catch子句中的是localWidget的拷贝。

必须这么做,因为当localWidget离开了生存空间后,其析构函数将被调用。

如果把localWidget本身(而不是它的拷贝)传递给catch子句,这个子句接收到的只是一个被析构了的Widget,一个Widget的“尸体”。

这是无法使用的。

因此C++规范要求被做为异常抛出的对象必须被复制。

即使被抛出的对象不会被释放,也会进行拷贝操作。

例如如果passAndThrowWidget函数声明localWidget为静态变量(static),

voidpassAndThrowWidget()

{

 staticWidgetlocalWidget;//现在是静态变量(static);

 //一直存在至程序结束

 cin>>localWidget;//象以前那样运行

 throwlocalWidget;//仍将对localWidget

}//进行拷贝操作

当抛出异常时仍将复制出localWidget的一个拷贝。

这表示即使通过引用来捕获异常,也不能在catch块中修改localWidget;仅仅能修改localWidget的拷贝。

对异常对象进行强制复制拷贝,这个限制有助于我们理解参数传递与抛出异常的第二个差异:

抛出异常运行速度比参数传递要慢。

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。

该拷贝构造函数是对象的静态类型(statictype)所对应类的拷贝构造函数,而不是对象的动态类型(dynamictype)对应类的拷贝构造函数。

比如以下这经过少许修改的passAndThrowWidget:

classWidget{...};

classSpecialWidget:

publicWidget{...};

voidpassAndThrowWidget()

{

 SpecialWidgetlocalSpecialWidget;

 ...

 Widget&rw=localSpecialWidget;//rw引用SpecialWidget

 throwrw;//它抛出一个类型为Widget

 //的异常

}

这里抛出的异常对象是Widget,即使rw引用的是一个SpecialWidget。

因为rw的静态类型(static  

type)是Widget,而不是SpecialWidget。

你的编译器根本没有注意到rw引用的是一个SpecialWidget。

编译器所注意的是rw的静态类型(statictype)。

这种行为可能与你所期待的不一样,但是这与在其他情况下C++中拷贝构造函数的行为是一致的。

(不过有一种技术可以让你根据对象的动态类型dynamictype进行拷贝,参见条款M25)

异常是其它对象的拷贝,这个事实影响到你如何在catch块中再抛出一个异常。

比如下面这两个catch块,乍一看好像一样:

catch(Widget&w)//捕获Widget异常

{

 ...//处理异常

 throw;//重新抛出异常,让它

}//继续传递

catch(Widget&w)//捕获Widget异常

{

 ...//处理异常

 throww;//传递被捕获异常的

}//拷贝

这两个catch块的差别在于第一个catch块中重新抛出的是当前捕获的异常,而第二个catch块中重新抛出的是当前捕获异常的一个新的拷贝。

如果忽略生成额外拷贝的系统开销,这两种方法还有差异么?

当然有。

第一个块中重新抛出的是当前异常(currentexception),无论它是什么类型。

特别是如果这个异常开始就是做为SpecialWidget类型抛出的,那么第一个块中传递出去的还是SpecialWidget异常,即使w的静态类型(statictype)是Widget。

这是因为重新抛出异常时没有进行拷贝操作。

第二个catch块重新抛出的是新异常,类型总是Widget,因为w的静态类型(statictype)是Widget。

一般来说,你应该用throw来重新抛出当前的异常,因为这样不会改变被传递出去的异常类型,而且更有效率,因为不用生成一个新拷贝。

(顺便说一句,异常生成的拷贝是一个临时对象。

正如条款19解释的,临时对象能让编译器优化它的生存期(optimizeitoutofexistence),不过我想你的编译器很难这么做,因为程序中很少发生异常,所以编译器厂商不会在这方面花大量的精力。

让我们测试一下下面这三种用来捕获Widget异常的catch子句,异常是做为passAndThrowWidgetp抛出的:

catch(Widgetw)...//通过传值捕获异常

catch(Widget&w)...//通过传递引用捕获

 //异常

catch(constWidget&w)...//通过传递指向const的引用

 //捕获异常

我们立刻注意到了传递参数与传递异常的另一个差异。

一个被异常抛出的对象(刚才解释过,总是一个临时对象)可以通过普通的引用捕获;它不需要通过指向const对象的引用(reference-to-const)捕获。

在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里(参见条款M19),但是在异常中却被允许。

让我们先不管这个差异,回到异常对象拷贝的测试上来。

我们知道当用传值的方式传递函数的参数,我们制造了被传递对象的一个拷贝(参见EffectiveC++条款22),并把这个拷贝存储到函数的参数里。

同样我们通过传值的方式传递一个异常时,也是这么做的。

当我们这样声明一个catch子句时:

catch(Widgetw)...//通过传值捕获

会建立两个被抛出对象的拷贝,一个是所有异常都必须建立的临时对象,第二个是把临时对象拷贝进w中(WQ加注,重要:

是两个!

)。

同样,当我们通过引用捕获异常时,

catch(Widget&w)...//通过引用捕获

catch(constWidget&w)...//也通过引用捕获

这仍旧会建立一个被抛出对象的拷贝:

拷贝同样是一个临时对象。

相反当我们通过引用传递函数参数时,没有进行对象拷贝。

当抛出一个异常时,系统构造的(以后会析构掉)被抛出对象的拷贝数比以相同对象做为参数传递给函数时构造的拷贝数要多一个。

我们还没有讨论通过指针抛出异常的情况。

不过,通过指针抛出异常与通过指针传递参数是相同的。

不论哪种方法都是一个指针的拷贝被传递。

但,你不能认为抛出的指针是一个指向局部对象的指针,因为当异常离开局部变量的生存空间时,该局部变量已经被释放。

Catch子句将获得一个指向已经不存在的对象的指针。

这种行为在设计时应该予以避免。

(WQ加注,也就是说:

必须是全局的或堆中的。

对象从函数的调用处传递到函数参数里与从异常抛出点传递到catch子句里所采用的方法不同,这只是参数传递与异常传递的区别的一个方面;第二个差异是在函数调用者或抛出异常者与被调用者或异常捕获者之间的类型匹配的过程不同。

比如在标准数学库(thestandardmathlibrary)中sqrt函数:

doublesqrt(double);//fromor

我们能这样计算一个整数的平方根,如下所示:

inti;  

doublesqrtOfi=sqrt(i);

毫无疑问,C++允许进行从int到double的隐式类型转换,所以在sqrt的调用中,i被悄悄地转变为double类型,并且其返回值也是double。

(有关隐式类型转换的详细讨论参见条款M5)一般来说,catch子句匹配异常类型时不会进行这样的转换。

见下面的代码:

voidf(intvalue)

{

 try{

 if(someFunction()){//如果someFunction()返回

 throwvalue;//真,抛出一个整形值

 ...

 }

 }

 catch(doubled){//只处理double类型的异常

 ...  

 }

 ...

}

在try块中抛出的int异常不会被处理double异常的catch子句捕获。

该子句只能捕获类型真真正正为double的异常,不进行类型转换。

因此如果要想捕获int异常,必须使用带有int或int&参数的catch子句。

不过在catch子句中进行异常匹配时可以进行两种类型转换。

第一种是继承类与基类间的转换。

一个用来捕获基类的catch子句也可以处理派生类类型的异常。

例如在标准C++库(STL)定义的异常类层次中的诊断部分(diagnosticsportion)(参见EffectiveC++条款49)。

捕获runtime_errors异常的Catch子句可以捕获range_error类型和overflow_error类型的异常;可以接收根类exception异常的catch子句能捕获其任意派生类异常。

这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上:

catch(runtime_error)...//cancatcherrorsoftype

catch(runtime_error&)...//runtime_error,

catch(construntime_error&)...//range_error,or

 //overflow_error

catch(runtime_error*)...//cancatcherrorsoftype

catch(construntime_error*)...//runtime_error*,

 //range_error*,or

 //overflow_error*

第二种是允许从一个类型化指针(typedpointer)转变成无类型指针(untypedpointer),所以带有constvoid*指针的catch子句能捕获任何类型的指针类型异常:

catch(constvoid*)...//捕获任何指针类型异常  

传递参数和传递异常间最后一点差别是catch子句匹配顺序总是取决于它们在程序中出现的顺序。

因此一个派生类异常可能被处理其基类异常的catch子句捕获,即使同时存在有能直接处理该派生类异常的catch子句,与相同的try块相对应。

例如:

try{

 ...

}

catch(logic_error&ex){//这个catch块将捕获

 ...//所有的logic_error

}//异常,包括它的派生类

 

catch(invalid_argument&ex){//这个块永远不会被执行

 ...//因为所有的

}//invalid_argument

 //异常都被上面的

 //catch子句捕获。

与上面这种行为相反,当你调用一个虚拟函数时,被调用的函数位于与发出函数调用的对象的动态类型(dynamictype)最相近的类里。

你可以这样说虚拟函数采用最优适合法,而异常处理采用的是最先适合法。

如果一个处理派生类异常的catch子句位于处理基类异常的catch子句后面,编译器会发出警告。

(因为这样的代码在C++里通常是不合法的。

)不过你最好做好预先防范:

不要把处理基类异常的catch子句放在处理派生类异常的catch子句的前面。

象上面那个例子,应该这样去写:

try{

 ...

}

catch(invalid_argument&ex){//处理invalid_argument

 ...//异常

}

catch(logic_error&ex){//处理所有其它的

 ...//logic_errors异常

}

综上所述,把一个对象传递给函数或一个对象调用虚拟函数与把一个对象做为异常抛出,这之间有三个主要区别。

第一、异常对象在传递时总被进行拷贝;当通过传值方式捕获时,异常对象被拷贝了两次。

对象做为参数传递给函数时不一定需要被拷贝。

第二、对象做为异常被抛出与做为参数传递给函数相比,前者类型转换比后者要少(前者只有两种转换形式)。

最后一点,catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。

当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里,即使该类不是在源代码的最前头。

inline函数的使用

2010/09/2911:

01

(一)inline函数(摘自C++Primer的第三版)

      在函数声明或定义中函数返回类型前加上关键字inline即把min()指定为内联。

     inlineintmin(intfirst,intsecend){/****/};

       inline函数对编译器而言必须是可见的,以便它能够在调用点内展开该函数。

与非inline函数不同的是,inline函数必须在调用该函数的每个文本文件中定义。

当然,对于同一程序的不同文件,如果inline函数出现的话,其定义必须相同。

对于由两个文件compute.C和draw.C构成的程序来说,程序员不能定义这样的min()函数,它在compute.C中指一件事情,而在draw.C中指另外一件事情。

如果两个定义不相同,程序将会有未定义的行为.

       为保证不会发生这样的事情,建议把inline函数的定义放到头文件中。

在每个调用该inline函数的文件中包含该头文件。

这种方法保证对每个inline函数只有一个定义,且程序员无需复制代码,并且不可能在程序的生命期中引起无意的不匹配的事情。

(二)内联函数的编程风格(摘自高质量C++/C编程指南)

关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。

如下风格的函数Foo不能成为内联函数:

inlinevoidFoo(intx,inty);//inline仅与函数声明放在一起

voidFoo(intx,inty)

{

}

而如下风格的函数Foo则成为内联函数:

voidFoo(intx,inty);

inlinevoidFoo(intx,inty)//inline与函数定义体放在一起

{

}

所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。

一般地,用户可以阅读函数的声明,但是看不到函数的定义。

尽管在大多数教科书中内联函数的声明、定义体前面都加了inline关键字,但我认为inline不应该出现在函数的声明中。

这个细节虽然不会影响函数的功能,但是体现了高质量C++/C程序设计风格的一个基本原则:

声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。

定义在类声明之中的成员函数将自动地成为内联函数,例如

classA

{

public:

voidFoo(intx,inty){}//自动地成为内联函数

}

将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程

风格,上例应该改成:

//头文件

classA

{

public:

voidFoo(intx,inty);

}

//定义文件

inlinevoidA:

:

Foo(intx,inty)

{

}

慎用内联

内联能提高函数的执行效率,为什么不把所有的函数都定义成内联函数?

如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?

内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的

执行效率。

如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收

获会很少。

另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,

消耗更多的内存空间。

以下情况不宜使用内联:

(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。

(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

类的构造函数和析构函数容易让人误解成使用内联更有效。

要当心构造函数和析构

函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。

所以不要随便地将构造函数和析构函数的定义体放在类声明中。

一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明

了inline不应该出现在函数的声明中)。

C++语言支持函数内联,其目的是为了提高函数的执行效率(速度)。

在C程序中,可以用宏代码提高执行效率。

宏代码本身不是函数,但使用起来象函数。

预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、

返回参数、执行return等过程,从而提高了速度。

使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。

对于C++而言,使用宏代码还有另一种缺点:

无法操作类的私有数据成员。

让我们看看C++的"函数内联"是如何工作的。

对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。

如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。

在调用一个内联函数时,编译器首先检查调用是否正确

(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。

如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。

这个过程与预处理有显著的不同,因为预处理器不能进行类型安全检查,或者进行自动类型转换。

假如内联函数是成员函数,对象的地址(this)会被放在合适的地方,这也是预处理器办不到的。

C++语言的

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 经管营销 > 公共行政管理

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1