一、运行时类型信息
1、typeid运算符
头文件:#include<typeinfo>
C++的标准头文件,都对应相应的类
//sizeof(类型/变量/表达式),返回内存大小
typeid(类型/变量/表达式),返回typeinfo类型的对象,其中包含name()成员函数,返回字符串,描述类型信息
虽然与函数调用形式相同,但是typeid是操作符。
int x; cout<<typeid(int).name()<<endl;//不同编译器的字符串描述可能不同 cout<<typeid(x).name()<<endl;
cout<<typeid(int* [5]).name()<<endl;
cout<<typeid(int(*) [5]).name()<<endl;//数组指针
class X{ protected: virtual void foo(void){} }; class Y{ protected: void foo(void){} } class Z{ void foo(void){} }; void func(X& x){ /* if(!strcmp(typeid(x).name(),"1Y")){ cout<<"Y"<<endl; } else if(!strcmp(typeid(x).name(),"1Z")){ cout<<"Z"<<endl; } else(!strcmp(typeid(x).name(),"1X")){ cout<<"X"<<endl; } */ //typeinfo类中已经重载的operator==函数 if(!strcmp(typeid(x).==typeid(Y))){ cout<<"Y"<<endl; } else if(!strcmp(typeid(x)==typeid(Z))){ cout<<"Z"<<endl; } else(!strcmp(typeid(x)==typeid(X)){ cout<<"X"<<endl; } }
注意:
typeid的使用是基于基类中存在虚函数,并且子类继承并覆盖了虚函数。否则typeid无法获取类型信息。
2、动态类型转换运算符(day3)
目标类型变量=dynamic_cast<目标类型>目标源类型变量
使用场景:适用于具有多态继承关系的父子类指针或者引用之间的显式转换。
class A{virtual void foo(void){}}; class B:publicA{void foo(void)}; class C:publicA{void foo(void)}; class D{}; B b; A* pa=&b; //B* pb=pa;//向下转换,编译报错 //B* pb=static_cast<B*>(pa);//ok B* pb=dynamic_cast<B*>(pa);//ok //C* pc=static_cast<C*>(pa);//因为pa已经指向了B*的类型,虽然可以通过,但是不安全 C* pc=dynamic_cast<C*>(pa);//程序执行阶段进行转换,而静态转换是程序编译阶段进行转换,虽然不报错,但是pc的地址将为空。执行时动态类型转换会做类型检查,如果是无关的类,无法转换。 D* pc=dynamic_cast<D*>(pa);//也为空 //可以打印出pa,pb,pc的地址,可见pc为空地址 //使用引用的不合理类型动态转换时,执行时会进程会被终止
dynamic_cast在转换的过程中,会根据多态的特性,检查父子类的指针或者引用目标类型是否一致,如果一致则转换成功,否则转换失败,如果是指针转换,则返回NULL,如果是引用转换,则抛出异常“bad_cast”
二、异常(Exception)
1、常见错误
1)语法错误
2)逻辑错误
3)功能错误
4)设计缺陷
5)需求不符
6)环境异常
7)操作不当
2、C的错误处理机制
1)通过返回值表示错误
class A{ public: A(void){cout<<"A::A()"<<endl;} ~A(void){cout<<"~A::A()"<<endl;} }; //通过返回值表示错误 int func3(void){ A a; FILE* fp=fopen("none.text","r"); if(fp==NULL){ cout<<"file open error!"<<endl; return -1; } fclose(fp) return 0; } int func2(void){ A a; if(func3()==-1){ return -1; } return 0; } int func1(void){ A a; if(func2()==-1){ return -1; } //... retuern 0; } int main(void){ if(func1()==-1){ return -1; } //... return 0; } //通过返回值表示错误
栈区对象在出现异常之后,能够正常释放
2)通过远程跳转来处理错误
jmp_buf g_env;//包含头文件#include<setjmp.h>
class A{ public: A(void){cout<<"A::A()"<<endl;} ~A(void){cout<<"~A::A()"<<endl;} }; //通过返回值表示错误 int func3(void){ A a; FILE* fp=fopen("none.text","r"); if(fp==NULL){ longjmp(g_env,-1); } //... fclose(fp) return 0; } int func2(void){ A a; func3(); return 0; } int func1(void){ A a; func2(); //... retuern 0; } int main(void){ if(setjmp(g_nev)==-1){//先设置g_env,如果有错误,会再次直接跳转到此处
cout<<"file open error!"<<endl;
}
func1(); //... return 0; }
使用远程跳转栈区对象无法得到释放
通过返回值表示错误
优点:函数调用路径中所有的局部对象都能够得到正常的析构,不会内存泄漏。
缺点:错误处理流程比较复杂,逐层判断,代码臃肿
通过盐城跳转机制处理错误
优点:不需要逐层判断,实现一步到位的错误处理,代码精简
缺点:函数调用的路径中布局对象失去被析构的机会,形成内存析构
3、C++的异常处理机制
结合C中两种错误处理的优点,同时避免他们的缺点,在形式上实现一步到位的错误处理,无需逐层判断返回值,所有的局部对象得到正常的析构。
class A{ public: A(void){cout<<"A::A()"<<endl;} ~A(void){cout<<"~A::A()"<<endl;} }; //通过返回值表示错误 int func3(void){ A a; FILE* fp=fopen("none.text","r"); if(fp==NULL){ throw -1;//抛出异常 } //... fclose(fp) return 0; } int func2(void){ A a; func3(); return 0; } int func1(void){ A a; func2(); //... retuern 0; } int main(void){ try{ func1(); //...出现异常,此处将不会得到执行,直接跳转到catch } catch(int ex/*抛出的异常数据类型*/){ cout<<"file open error!"<<endl; return -1; } //... return 0; }
如果执行到throw语句,会逐层返回执行},并且内存会得到释放
4、C++异常语法
(1)异常抛出
throw 异常对象;//抛出的异常会被放到安全区,无法手动访问
如:
throw -1;
throw "file error";
throw 对象;
(2)异常捕获
try{
//可能引发异常的语句
}
catch(异常类型1){
//异常类型1的处理
}
catch(异常类型2){
//异常类型2的处理
}
...
catch(.../*可以匹配任意类型*/){
//针对其他类型的处理
}
class FileError{ public: FileError(){} FileError(const string& file,int line):m_file(file),m_line(line){ cout<<"抛出位置"<<m_file<<","<<m_line; } private: string m_file; int m_line; }; class A{ public: A(void){cout<<"A::A()"<<endl;} ~A(void){cout<<"~A::A()"<<endl;} }; //通过返回值表示错误 int func3(void){ A a; FILE* fp=fopen("none.text","r"); if(fp==NULL){ throw FileError(__FILE__,__LINE__); /* 注意,是双下划线,而不是单下划线 __FILE__ 包含当前程序文件名的字符串 __LINE__ 表示当前行号的整数 __DATE__ 包含当前日期的字符串 __STDC__ 如果编译器遵循ANSI C标准,它就是个非零值 __TIME__ 包含当前时间的字符串 */ //FileError ex; //throw ex; throw "file error"; throw -1;//抛出异常 } //... fclose(fp) return 0; } int func2(void){ A a; func3(); return 0; } int func1(void){ A a; func2(); //... retuern 0; } int main(void){ try{ func1(); //...出现异常,此处将不会得到执行,直接跳转到catch } catch(int ex/*抛出的异常数据类型*/){ cout<<"file open error!"<<endl; return -1; } catch(const char* ex){ cout<<"file error!"<<endl; return -1; } catch(FileError& ex){//如果抛出的是对象,最好是用引用 cout<<"FileError"<<endl; return -1; } //... return 0; }
注意:
(1)如果没有类型可以匹配,那么抛出的异常将被系统锁捕获,进程将被回收。内存被释放
(2)如果有两个连续的throw语句,只会被执行一个,因为在throw时候,直接跳转到}执行。
(3)如果抛出的是类的对象,最好使用引用。
(4)在面向对象的编程中,一般都是抛出对象,而不是整数或者字符串等基本类型,因为类比基本类型可以存储更多信息。比如日志等
5、异常-扩展
class A{}; class B:public A{}; void func(void){ //... throw(B); //throw(A); } int main(void){ try{ func(); } catch(A& ex){//向上造型可以匹配B类异常 cout<<"捕获到A类异常"<<endl; return -1; } catch(B& ex){ cout<<"捕获到B类的异常"<<endl; return -1; } return 0; } //上述代码中B异常将无法被捕获,无论是抛A,还是抛B,正确的处理方式应该把子类的异常捕获放在基类之前,防止发生向上造型。
注意:
catch的匹配是自上而下进行匹配,而不是选择最优匹配,所以应该把子类的异常捕获放在基类之前,防止发生向上造型。
6、异常说明
1)可以在函数原型中增加异常说明,说明该函数可能抛出的异常类型。提前通知编译器,函数会抛出的异常类型
返回类型 函数名(形参表)[cosnt]throw(异常类型表){...}
不加异常说明列表,异常也能够被正常捕获,和不加的区别在于,如果函数抛出了与说明列表不符的类型,这个异常将不会被捕获。自然也会被系统所捕获。
2)函数的异常说明只是一种承诺,表示该函数不会抛出说明列表意外的类型。意外的异常将会被系统所捕获。
3)如果不写异常说明,表示可以抛出任何异常
4)空异常说明,throw(),表示不会抛出任何异常。
5)如果函数的声明和定义分开,在声明和定义部分都要加上异常说明。并且说明列表必须相同,但是顺序可以改变。
7、异常说明与多态
class FileError{}; class MemError{}; class Base{ public: virtual void func(void)throw(FileError,MemError){} }; class Derived:public Base{ public: void func(void){}//虚函数覆盖会失败,因为子类的虚函数覆盖函数没有异常说明,这里异常说明范围可以缩小,但是不能扩大 };
如果基类中的虚函数带有异常说明,它的子类中,该函数的覆盖版本不能比基类版本抛出更多异常,否则编译器报出“放松throw限定”错误