420 likes | 617 Views
Object-Oriented Programming in C++ 第八章 C++ 工具. 中国科大学继续教育学院 李艺 leeyi@ustc.edu.cn. 第一章 C++ 的初步知识 第二章 类和对象 第三章 再论类和对象 第四章 运算符重载 第五章 继承与派生 第六章 多态性与虚函数 第七章 输入输出流 第八章 C++ 工具 第九章 模板. 8.1 异常处理 8.2 命名空间 8.3 使用早期的函数库. 8.1 异常处理.
E N D
Object-OrientedProgramming in C++第八章 C++工具 中国科大学继续教育学院 李艺 leeyi@ustc.edu.cn
第一章 C++的初步知识 第二章 类和对象 第三章 再论类和对象 第四章 运算符重载 第五章 继承与派生 第六章 多态性与虚函数 第七章 输入输出流 第八章 C++工具 第九章 模板
8.1 异常处理 8.2 命名空间 8.3 使用早期的函数库
8.1 异常处理 • 概述:程序的错误有两种:一种是编译错误,即语法错误。另一种是在运行时发生的异常(exception) 。 异常处理机制只处理运行时的差错和其它例外情况,不包括编译错误。 • 处理异常的方法有: • 非结构化异常处理:用 exit (n) 或 return (n)的运行期错误处理机制,具有“一个入口,多个出口”的特点。exit()会清空流和关闭打开的文件。abort()却不会清空流,也不关闭打开的文件。 • 结构化的异常处理:按“警告—忽略”、“对话—补救”或安全退出等模式,使程序可以在对运行条件做出适当安排或改善后继续运行下去。
8.1 异常处理 • 基本思想: • C++的异常处理的基本思想是将异常的检测与处理分离。 • 在一个函数体中检测到异常条件满足,但无法确定相应的处理方法时,就引发一个异常,然后由函数的直接或间接调用者处理此异常。 • C++的异常处理建立在三个关键字基础之上: try 、catch 和 throw。
8.1 异常处理 • C++异常处理语句的一般形式 try { //try 块内监视异常 if (条件)throw exception; //由throw 抛出异常 … ; //其它语句 } catch( 类型1 参数1 ) { //catch块内处理代码 } catch( 类型2 参数2 ) { … ; } …… catch( 类型n 参数n ) { … ; } 注意:C++通过try夺取运行期的环境控制权,即异常的引发是由程序员控制的,而不是由程序运行环境或计算机硬件控制的。任何要检测异常的语句或函数调用都必须在try语句块中执行。异常由紧跟在try块后的catch语句来捕获并处理。
8.1 异常处理 #include<iostream.h> #include<math.h> float Div(float x,float y); void main( ) { try { float a=1.0,b; while (a>0.0) { cout << "请输入被除数:"; cin >> a; cout << "请输入除数:"; cin >> b; cout<<"a/b="<<Div(a,b)<<endl; } } catch (float) { cout<<"deviding is zero.\n\n"; } cout<<"that is ok.\n"; } float Div(float x,float y) { if (fabs(y)<0.0001) throw y; return x/y; } 方框中的代码是没有 异常处理机制的代码
8.1 异常处理 • 说明 • 如果预料某段程序代码有可能发生异常,就将它放在try子句的化括号中。如果这段代码运行时真的遇到异常情况,其中的throw表达式就会抛出这个异常。 出现异常时,try语句块提示编译器到哪里查找catch块,没有紧跟try块的catch块是没有作用的。 当没有发生异常的时候,几乎没有和try块相关的运行时成本。查找匹配捕获处理异常的过程只在发生异常的情况下才会进行。
8.1 异常处理 • catch子句后的复合语句是异常处理程序。它捕获由throw表达式抛出的异常。 异常类型说明部分指明该子句处理的异常的类型,它与函数的形参是相似的。可以是某个类型的值,也可以是引用。 如果某个catch语句的参数类型与引发异常的信息数据类型相匹配,则执行该catch语句的异常处理(捕获异常),此时,由throw语句抛出的异常信息(值)传递给catch语句中的参数。
8.1 异常处理 • try语句块必须出现在前,catch紧跟在后。catch之后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。在try{…} 和catch(…){ …}语句之间不得插入任何其它C++语句。 • 如果程序内有多个异常处理模块,则当异常发生时,系统自动查找与该异常类型相匹配的catch模块,查找次序为catch出现的次序。需要注意的是catch处理程序的出现顺序很重要,因为在一个try块中,异常处理程序是按照它出现的顺序被检查的。
8.1 异常处理 • 引发异常的throw语句必须在try语句块内,或是由try语句块中直接或间接调用的函数体执行。throw语句的一般形式为: throw exception; exception表示一个异常值,它可以是任意类型的变量、对象或常量。
8.1 异常处理 void main() { invoke(0); invoke(1); invoke(2); invoke(3); } #include<iostream.h> const double PI=3.1416; void invoke(int x) { try{ if(x==0) throw x+5; //抛出int型的异常 if(x==1) throw 'A'; //抛出cahr型的异常 if(x==2) throw "An apple"; //抛出字符串型的异常 if(x==3) throw PI; //抛出double型的异常 } catch(int i) { cout<<"catch a integer "<<i<<endl; } catch(char c) { cout<<"catch a char "<<c<<endl; } catch(char str[10]) { cout<<"catch a string "<<str<<endl; } catch(double d) { cout<<"catch a double "<<d<<endl; } } 程序运行结果: catch a integer 5 catch a char A catch a string An apple catch a double 3.1416
8.1 异常处理 • 异常的类型匹配规则 C++规定,当一个异常对象和catch子句参数类型符合下列条件时,匹配成功: • 如果catch子句参数类型就是异常对象的类型或其引用 • 如果catch子句参数类型就是异常对象的public基类 • 如果catch子句参数类型为基类指针或引用,而异常对象为派生类指针或引用。 • catch子句参数类型为void*,异常对象为任何类型指针。 • catch子句为catch-all,即catch{…}。 catch处理程序按照其在try块后面的顺序依次为检测,一旦匹配,则后面的就不再检测。
8.1 异常处理 • 异常的匹配规则比函数重载的匹配规则更为严格 try { throw int(); } catch (unsigned int) { …… } 抛出异常的类型是int型,然而handler却期待一个unsigned int。异常处理机制不认为二者是能够匹配的类型;结果,抛出的异常没有被捕获。
8.1 异常处理 • 异常重抛( re-throw ): 当catch捕获异常后,可能不能完全处理异常,在完成某些操作后,可以重新抛出该异常,把异常传递给上层函数的另一个catch子句,由它进一步处理。重新抛出异常的表达式仍然为:throw;被重新抛出的异常就是原来的异常对象。如: void f( ) { try {throw 1;} catch (int e) {//do something; throw; } } int main( ) { try { f(); } catch ( int e) {cout << “exception in main is ” << e;} return 0; }
8.1 异常处理 void main( ) { try{ myFunc( ); } catch (string& e) { cout<<e<<endl; } } #include<iostream> #include <string> using namespace std; void myFunc( ) {try { int a; cout<<"正在执行myFunction函数\n"; string e="输入的数据超限"; cout <<"请输入一个小于5的自然数:"; while (cin>>a) { if ((a<1)||(a>4)) throw e; else { cout<<"输入的数据 "<<a<<" 合格\n\n"; if (a==1) exit (0); else cout <<“请输入一个小于5的自然数:"; } } } catch (string& e) { cout<<"在myFunc( )中捕到一个异常"<<endl; throw e; } }
8.1 异常处理 重抛异常的catch子句应该把自己做过的工作告诉下一个处理异常的catch子句,往往要对异常对象做一定修改,以表达某些信息。 因此catch子句中的异常声明必须被声明为引用,这样修改才能真正做在异常对象自身中,而不是拷贝中。
8.1 异常处理 • catch_all子句 通常异常发生后按栈展开(stack unwinding)退出,动态分配的非类对象资源是不会自动释放的,应该在对应的catch子句中释放。因为我们不知道可能被抛出的全部异常,所以不是为每种可能的异常写一个catch子句来释放资源,而是使用通用形式的catch子句catch_all,格式为: catch(...) { 代码 }
8.1 异常处理 • catch_all子句示例: void fun1( ) {int *res; new res[100];//定义一个资源对象 try{//代码包括使用资源res和某些可能引起异常抛出的操作} } catch(...) {delete res[];//正常退出前释放资源对象res; throw;//重新抛出异常 } catch_all子句可以单独使用,也可以与其它catch子句联合使用。但必须放在相关catch子句表的最后。
8.1 异常处理 • 异常的限制 在异常处理中,由于捕获和抛出一个异常会影响到一个函数与其它函数的相互关系。因此,C++异常机制允许在函数定义后面增加一个抛出类型说明,以限制该函数可以抛出的异常类型。 异常限制的一般格式如下: 返回值类型 函数名(<参数表>) throw (<异常类型列表>) { …. } 使用时应特别注意,经这样定义的函数,抛出的异常数据只能是异常类型列表中的一种。
8.1 异常处理 如果一个函数抛出的异常没有在抛出类型列表中说明或者从所列数据类型之一派生,默认的异常处理过程就是调用名为unexpected()的函数,该函数又调用terminate()函数,后者依次再调用abort()函数终止程序运行。其中,对unexpected()和terminate()用户可以提供自己的函数定义。
8.1 异常处理 例12. 3 异常限制演示 (VC++6.0无此功能) #include <iostream.h> void invoke(int test) throw(int, char) { if (test==0) throw test; if (test==1) throw 'a'; if (test==2) throw 123.23; } void f(int t) { try { invoke(t); } catch (int j) { cout<<"Caught an integer "<<j<<endl; } catch (char c) { cout<<"Caught a char: "<<c<<endl; } catch (double d) { cout<<"Caught a double "<<d<<endl; } } void main( ) { f(0); f(1); f(2); cout<<"End"<<endl; } 执行结果如下: Caught an integer 0 Caught a char: a Abnormal program terminate
8.1 异常处理 • 异常成组 将多个有某种关联的异常放在一组。这些异常通常属于某一个方面的异常。比如文件异常组。 在C++异常处理中,有两种方式把多个异常成组。它们是:异常枚举成组和异常派生类成组。
8.1 异常处理(异常枚举成组) #include <iostream.h> enum errs{err0,err1,err2}; void f(int test) { try { if(test==0) throw err0; if(test==1) throw err1; if(test==2) throw err2; } catch(errs er) { switch(er) { case err0: cout<<"Caught err0 exception\n"; break; case err1: cout<<"Caught err1 exception\n"; break; case err2: cout<<"Caught err2 exception\n"; break; } } } void main( ) { f (2); f (1); f (0); }
8.1 异常处理 • 异常和派生类成组 在C++程序中,表示异常的类通常被组成为一个组(group)或者一个层次结构。对由栈类成员函数抛出的异常: class popOnEmpty{...}; class pushOnFull{...}; 可以定义一个称为Excp的基类,再从该基类派生出这两个异常类。 class Excp{...}; class popOnEmpty:public Excp{...}; class pushOnFull:public Excp{...};
8.1 异常处理 由基类Excp来打印错误信息: class Excp {public: void print (string msg) {cerr << msg << endl; } }; 这样的基类也可以作为其他异常类的基类: class Excp{...}; //所有异常类的基类 class stackExcp:public Excp{...}; //栈异常类的基类 class popOnEmpty:public stackExcp{...}; //栈空退栈异常 class pushOnFull:public stackExcp{...}; //栈满压栈异常 class mathExcp:public Excp{...}; //数学库异常的基类 class zeroOp:public mathExcp{...}; //数学库零操作异常 class divideByZero:public mathExcp{...}; //数学库被零除异常
8.1 异常处理 这里被创建的异常类对象是stackExcp类类型,尽管pse指向一个实际类型为pushOnFull的对象,但那是一个临时对象,拷贝到异常对象的存贮区中时创建的却是stackExcp类的异常对象。所以该异常不能被pushOnFull类型的catch子句处理。 在处理类类型异常时,catch子句的排列顺序是非常重要的。当异常被组织成类层次结构时,类类型的异常可以被该类类型的公有基类的catch子句捕获到。如pushOnFull类类型的异常可以由stackExcp或Excp类类型异常所对应的catch子句处理。为了保证pushOnFull异常的处理由最合适的catch子句来处理,应有如下顺序: catch (pushOnFull){...} //处理pushOnFull异常 catch (stackExcp){...} //处理栈的其他异常 catch (Excp){...} //处理一般异常 派生类类型的catch子句必须先出现,以确保只有在没有其他catch子句适用时,才会进入基类类型的catch子句。
8.1 异常处理 • 异常派生类成组示例:对学生信息(姓名、年龄、成绩)进行输入,要求将输入不合理的数据抛出不用。 • 思路:先定义一个抽象类check;由它派生出三个派生类:nameCheck、ageCheck 和 scoreCheck,来分别检查name、ade、score录入是否合理,如果不合理,则抛出不用。 • 长处:用派生类成组进行异常处理,其catch字句可以非常精练、易读。 #include<iostream> #include<string> using namespace std; class check {public: virtual void set()=0; virtual void showErr()=0; virtual void display()=0; protected: string name; int age; int score; }; // 检查抽象类结束
8.1 异常处理 class nameCheck:public check {public: void set() { cout<<"请输入姓名:"; cin>>name; if (isNum(name)) throw this; } void showErr() { cout << "姓名输入错误!" << endl; } void display() { cout << "\n\n学生姓名:"<<name<<endl; } private: bool isNum(string &s) { char num[] = "1234567890"; for (int i=0;i<10;i++) if (s.find(num[i])!=-1) return true; return false; } }; // 姓名输入检查类结束 class ageCheck:public check {public: void set() { cout<<"请输入年龄:"; cin>>age; if (age<0 || age>120) throw this; } void showErr() { cout << "年龄输入错误!" << endl; } void display() { cout << "学生年龄:"<<age<<endl; } }; // 年龄检查类结束 class scoreCheck:public check {public: void set() { cout<<"请输入成绩:"; cin>>score; if (score<0 || score>100) throw this; } void showErr() { cout << "成绩输入错误!" << endl; } void display() { cout << "学生成绩:"<<score<<endl; } }; // 成绩检查类结束 int main() { try { check *pt1,*pt2,*pt3; pt1 = new nameCheck; pt1->set(); pt2 = new ageCheck; pt2 -> set(); pt3 = new scoreCheck; pt3 -> set(); pt1->display(); pt2->display(); pt3->display(); } catch(check *chk) { chk->showErr(); } return 0; }
8.1 异常处理 类层次结构的异常同样可以重新抛出(rethrow),把一个异常传递给函数调用列表中,更上层的另一个catch子句。形式仍为 throw; 虚函数是类层次结构中多态性的基本手段,异常类层次结构中也可以定义虚拟函数。
8.1 异常处理 例:异常层次结构中的虚函数 #include <iostream.h> class Excp { public: virtual void print( ) {cerr<<”发生异常” <<endl;} }; class stackExp:public Excp { public: virtual void print( ) {cerr<<”栈发生异常”<<endl;} }; class pushOnFull:public stackExcp { public: virtual void print( ) {cerr<<”栈满,不能压栈”<<endl;} }; int main() { try { //抛出一个pushOnFulll异常} catch(Excp&eObj) { eObj.print(); //调用虚函数pushOnFull::print() } }
8.2 命名空间 本课程的各章节的程序中,都用到了这样的语句: using namespac std; 这就是命名空间std。 • 为什么需要命名空间?C语言定义了3个作用域,即文件域,函数域和复合语句域。C++又引入了类作用域。 不同的作用域中可以用相同的变量名,互不干扰。但是,如果是在不同的库文件( *.h )中,有相同的变量名和类名,而不巧又在被一个程序包含、主文件中又调用了该变量,定义了该类对象,于是引起矛盾冲突。
8.2 命名空间 • 什么是命名空间?为了解决这个问题,ANSI C++增加了命名空间的概念。简单地说,就是ANSI C++引入的,可以由用户命名的内存区域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。比如: namespace nsl { int a; double b; } 其中: • namespace 是定义命名空间的关键字; • nsl 是用户指定的空间名。 • 花括号内包含的a,b,是命名空间成员。
8.2 命名空间 注意a 和b 仍然是全局变量,仅仅把它们隐藏在命名空间中,而程序中如果要使用变量a 和b,必须加上空间名和域分辨符。如:nsl::a,nsl::b 等。这些名字称为被限定名。 C++中的命名空间和被限定名的关系,类似与操作系统中文件夹和其中文件的关系。 • 命名空间的作用:是建立一些互相分隔的作用域,把一些全局实体分割开来,以免产生名字冲突。命名空间中的被限定名可以是: • 常量和变量( 可以带有初始化 ); • 函数( 可以是定义或声明 ); • 结构体或类; • 模板或另一个命名空间( 意味着,命名空间可以嵌套 )
8.2 命名空间 • 使用命名空间解决名字冲突:我们举例说明如何解决冲突。 // header1.h #include <string> #include <cmath> using namespace std; namespace ns1 // 声明命名空间ns1 { class student // 在ns1中声明学生类 {public: student (int n, string nam, int a) { num=n; name=nam; age=a; } void get_data( ); private: int num; string name; int age; }; void student::get_data() {cout<<num<<““<<name <<““<<age<<endl; double fun (double a, double b) { // 在ns1中定义fun函数 return sqrt(a+b); } } // header2.h #include <string> #include <cmath> using namespace std; namespace ns2 // 声明命名空间ns2 { class student // 在ns1中声明学生类 {public: student (int n, string nam, char s ) { num=n; name=nam; sex=s; } void get_data( ); private: int num; char name[20]; int age; }; void student::get_data() {cout<<num<<““<<name <<““<<sex<<endl; double fun (double a, double b) { // 在ns1中定义fun函数 return sqrt(a-b); } }
8.2 命名空间 • 两个头文件中有相同名字的类和函数,分别放在不同的命名空间中,避免冲突。 • 定义对象时,要使用: 空间明::类名 对象名(参数表) 的形式来定义对象。 • 定义好的对象不在命名空间中,而在main 函数的范围里,所以使用对象时,不能加空间的名字! // main file #include <iostream> #include “header1.h” #include “header2.h” using namespace std; int main( ) { nsl::student stud1(101,”Wang”,18); stud1.get_data( ); // 不要写成ns1::stud1.get_data( ) cout<<ns1::fun(5,3)<<endl; ns2::student stud2(102,”Li”,’f’); stud2.get_data( ); cout<<ns2::fun(5,3)<<endl; return 0; }
8.2 命名空间 • 使用命名空间的简化形式:如果在一个命名空间中定义了至少10个实体,就需要至少使用10次 using 命名空间名。能否简化呢?可以。 C++提供 using namespace 语句来实现简化。其一般形式为: using namespace 命名空间名; 例如, using namespace ns1; 声明了在本作用域中要用到命名空间ns1 中的成员,在使用该命名空间的任何成员时,都不必使用 ns1 空间名来限定。好象全局对象一样。如果紧接上面的声明,有如下语句: student stud1(101, “Wang”,18); 则是指定义一个ns1空间中的 student 类对象。 注意,这种简化方法只能是使用少量的命名空间,并且不至于产生冲突的情况下,才能简化。如上一个示例中,ns1两个ns1命名空间就不能简化,因为会引起冲突。
8.2 命名空间 • 无名的命名空间:C++允许使用没有名字的命名空间,如: namespace { void fun( ) { cout<<“OK!”<<endl; } } 这种无名的名字空间在其他文件中无法引用,只能在本文件的作用域内有效。无名空间中的成员,如上面提到的函数 fun 的作用域是从定义位置开始到文件结束的范围。
8.2 命名空间 • 标准命名空间 std :为了解决C++标准库中的标识符与程序中的全局标识符之间,以及不同库之间的同名冲突,应该将不同库的标识符在不同的命名空间中定义。 标准C++库的所有标识符都是在一个名为 std 命名空间中定义的。或者说,标准头文件( 如 stream)中的函数、类、对象和类模板是在命名空间 std 中定义的。 这样,在程序中用到C++标准库的被限定成员时,如: std::cout<<“OK.”<<endl; 在程序头一般要加上 using namespace std; 每当用到std 命名空间中的被限定成员时,就可省略std:: 限定语。如:cout<<“OK.”<<endl;
8.3 使用早期的函数库 在C语言的发展过程中,产生了丰富的函数库。C++可以使用这些函数库。要使用它们,就必须在程序中包含相关的头文件。 C++使用这些头文件有两种方法: • 用C语言的传统方法。头文件名包括后缀.h,如 stdio.h、 math.h 等,由于C语言没有命名空间,头文件并不存放在命名空间中,因此在C++程序文件中如果用到带.h 后缀的头文件时,不必使用命名空间,只需要包含要用到的头文件名就行。如: #include <math.h>
8.3 使用早期的函数库 • 用C++的新方法:C++标准要求系统提供的头文件不包括后缀.h 。例如,iostream, string 。为了区别于C语言的头文件,C++头文件相应的文件名之前都加一字母 c 。例如,C语言的 stdio.h 文件,C++中改名为 cstdio。C语言中的string.h 头文件,C++中改名为 cstring 。并且后者增加了很多内容,不是同一个文件。 此外,由于这些函数都是在命名空间 std 中声明的,因此在程序中要对命名空间 std 作声明。比如: #include <cstdio> #include <cmath> using namespace std;
习题: • 本章习题请上网查阅教学网页: http://staff.ustc.edu.cn/~leeyi