return0;
}
doubletriangle(doublea,doubleb,doublec)
{
doublearea;
doubles=(a+b+c)/2;
if(a+b<=c||b+c<=a||c+a<=b)throwa;//当不符合三角形条件抛出异常信息
area=sqrt(s*(s-a)*(s-b)*(s-c));
returnarea;
}
现在结合程序分析怎样进行异常处理。
(1)把可能出现异常的、需要检查的语句或程序放在try后面的花括号中。
由于triangle函数是可以出现异常的部分,所以把while循环连同triangle函数放在try块中。
这些语句是正常流程的一部分,虽然被放在try块中,并不影响它们按照原来的顺序执行。
(2)程序开始运行后,按正常的顺序执行到try块,开始执行try块中的花括号内的语句。
如果在执行try块内的语句过程中没有发生异常,则catch子句不起作用,流程转到catch子句后面的语句继续执行。
(3)如果在执行try块内的语句(包括其所调用的函数)过程中发生异常,则throw运算符抛出一个异常信息。
请看程序中的triangle函数部分,当不满足三角形条件时,throw抛出double类型的异常信息a。
throw抛出异常信息后,流程立即离开本函数,转到其上一级的函数(main函数)。
因此不会执行triangle函数中if语句之后的return语句。
Throw抛出什么样的数据由程序设计者自定,可以是任何类型的数据(包括自定义类型的数据,如类对象)。
(4)这个异常信息提供给try-catch结构,系统会寻找与之匹配的catch子句。
现在a是double型,而catch子句的括号内指定的类型也是double型,二者匹配,即catch捕获了该异常信息,这时就执行catch子句中的语句。
(5)在进行异常处理后,程序并不会自动终止,继续catch子句后面的语句。
本程序输出”end”。
注意,并不是从出现异常点继续执行while循环。
如果在try块的花括号内有10个语句,在执行第3个语句时出现异常,则在处理完该异常后,其余7句不再执行,而转移catch子句后面的语句去继续执行。
下面讲述异常处理的语法。
throw语句:
throw表达式;
try-catch语句:
try
{被检查的语句}
Catch(异常信息类型[变量名])
{进行异常处理的语句}
说明:
(1)被检测的函数必须放在try块中,否则不起作用。
(2)try块和catch块作为一个整体出现,catch块是try结构中的一部分,必须紧跟在try块之后,不能单独使用,在二者之间也不能插入其他语句。
但是在一个try-catch结构中,可以只有try块而无catch块。
即在本函数中只检查而不处理,把catch处理块放在其他函数中。
(3)try和catch块中必须有用花括号括起来的复合语句,即使花括号中只有一个语句,也不能省略。
(4)一个try-catch结构中只能有一个try块,但却可以有多个catch语句,以便与不同的异常信息匹配。
如:
try
{……}
catch(double)
{……}
catch(int)
{……}
catch(char)
{……}
(5)catch后面的圆括号中,一般只写异常信息的类型名,如catch(double)
(6)如果在catch子句中没有指定异常信息的类型,而用了删节号”…”,则表示它可以捕捉任何类型的异常信息。
(7)try-catch结构可以与throw出现在同一个函数中,也可以不在同一函数中,当throw抛出异常信息后,首先在本函数中寻找与之匹配的catch,如果在本函数无try-catch结构或找不到与之匹配的catch,就转到其上一层去处理,如果其上一层也无try-catch结构左找不到与之匹配的catch,则再转到更上一层的try-catch去处理,也就是说转到离开出现异常最近的try-catch结构去处理。
(8)在某些情况下,在throw语句中可以不包括表达式,如throw;,表示不处理这个异常,请上级处理。
此时它将把当前在处理异常信息再次抛出,给某上一层的catch块处理。
(9)如果throw抛出的异常信息找不到与之匹配的catch块,那么系统就会调用一个系统函数terminate,使程序终止运行。
下面举一个函数嵌套情况下检测异常处理。
//例8.2
#include
usingnamespacestd;
voidf1();
voidf2();
voidf3();
intmain()
{
try
{f1();}
catch(double)
{cout<<"OK0!
"<cout<<"end0"<return0;
}
voidf1()
{
try
{f2();}
catch(char)
{cout<<"OK1!
"<cout<<"end1"<}
voidf2()
{
try
{f3();}
catch(char)
{cout<<"OK2!
"<cout<<"end2"<}
voidf3()
{
doublea=0;
try
{throwa;}
catch(float)
{cout<<"OK3!
"<cout<<"end3"<}
OK0!
end0
如f3中改为catch(double)
OK3!
end3
end2
end1
end0
如果将f3中catch改为:
{cout<<"OK3!
"<OK3!
OK0!
end0
8.1.3在函数声明中进行异常情况指定
(1)C++允许在声明函数时列出可能抛出的异常类型,如:
doubletriangle(double,double,double)throw(double);
doubletriangle(double,double,double)throw(int,double,float,char);
(2)如果声明函数时不列出抛出的异常类型,则说明可以抛出任何类型的异常,如:
doubletriangle(double,double,double);
(3)如果声明一个不能抛出异常的函数,写成如下形式:
doubletriangle(double,double,double)throw();
8.1.4在异常处理中处理析构函数
如果在try中定义了类对象,且在执行try的过程中又发生了异常,此时会回到调用try语句的上一层函数(逐层向上转到catch处理块的那一层)。
这样会有已构造而末析构的局部对象。
C++的异常处理机制会在throw抛出异常信息被catch捕获时,对有关的局部对象进行析构函数的调用。
下面我们来看一个实例。
#include
usingnamespacestd;
classA
{
inti;
public:
A(intx)
{
i=x;cout<<"A的构造函数"<
}
~A()
{
cout<<"A的析构函数"<
}
voidmeb()
{
if(i==0)
throwi;
else
cout<
}
};
voidfun()
{
Aa1
(1);
a1.meb();
Aa2(0);
a2.meb();
}
intmain()
{
try
{
fun();
}
catch(intn)
{
cout<<"error"<}
return0;
}
8.2命名空间
在学习本书前面各章时,已经多次看到在程序中用了以下语句:
usingnamespacestd;
这就是使用了命名空间std。
在本节中将对它作较详细的介绍。
8.2.1为什么需要命名空间
命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
在C语言中定义了3个层次的作用域,即文件(编译单元)、函数和复合语句。
C++又引入了类作用域。
在不同的作用域中可以定义相同名字的变量,互不干扰,系统能够区别它们。
下面先简单分析一下作用域的作用,然后讨论命名空间的作用。
如果在文件中定义了两个类,在这两个类中可以有同名的函数。
在引用时,为了区别,应该加上类名作为限定,如:
#include
usingnamespacestd;
classA//声明A类
{
public:
voidfun1();//声明A类中的fun1函数
private:
inti;
};
voidA:
:
fun1(){cout<<"A:
:
fun1"<classB//声明B类
{
public:
voidfun1();//B类中也有fun1函数
voidfun2(){}
};
voidB:
:
fun1(){cout<<"B:
:
fun1"<intmain()
{return0;}
因为我们加上的类名作为限定,所以这样不会发生混淆。
再来看一个问题,如果在文件A中定义了一个全局变量intx=3;在文件B中也再定义一个全局变量intx=5;,在分别对文件A和文件B进行编译时不会有问题。
但是,如果一个程序包括文件A和文件B,那么在进行连接时,会报告出错。
可以通过extern声明同一程序中的两个文件中的同名变量是同一个变量。
//a.cpp
#include
usingnamespacestd;
intx=3;
classA
{
public:
voidf(){cout<};
//b.cpp
#include
usingnamespacestd;
externintx;//如果这里定义xintx=5;就会出现重复定义的错误
classB
{
public:
voidf(){cout<};
//cpp1.cpp
#include"a.cpp"
#include"b.cpp"
#include
usingnamespacestd;
intmain()
{
Aa1;
Bb1;
a1.f();
b1.f();
}
在简单的程序设计中,只要人们小心注意,可以争取不发生错误。
但是,一个大型的应用软件,往往不是由一个人独立完成的,假如不同的人分别定义了类,放在不同的头文件中,在主文件(包含主函数的文件)需要用这些类时,就用#include命令行将这些头文件包含进来。
由于各头文件是由不同的人设计的,有可能在不同的头文件中用了相同的名字来命名所定义的类或函数。
这样在程序中就会出现名字冲突。
请注意以下两个头文件:
//one.h
classString{...};
//somelib.h
classString{...};
如果按照上述方式定义,那么这两个头文件不可能包含在同一个程序中,因为String类会发生冲突。
所谓命名空间,是一种将程序库名称封装起来的方法,它就像在各个程序库中立起一道道围墙。
比如:
//one.h
namespaceone
{ classString{...};}
//somelib.h
namespaceSomeLib
{ classString{...};}
现在就算在同一个程序中使用String类也不会发生冲突了,因为他们分别变成了:
one:
:
String()以及Somelib:
:
String(),这样,就可以通过声明命名空间来区分不同的类或函数等了。
下面来看书中的实例
例8.4名字冲突。
//-----------------程序员甲在头文件header1.h中定义了类Student和函数fun。
//header1.h(头文件1,设其文件名为cc8-4-h1.h)
#include
#include
usingnamespacestd;
classStudent//声明Student类
{public:
Student(intn,stringnam,chars)
{num=n;name=nam;sex=s;}
voidget_data();
private:
intnum;
stringname;
charsex;
};
voidStudent:
:
get_data()//成员函数定义
{cout<doublefun(doublea,doubleb)//定义全局函数(即外部函数)
{returnsqrt(a+b);}
//-----------------------------在main函数所在的文件中包含头文件header1.h:
#include
#include"cc8-4-h1.h"//注意要用双引号,因为文件一般是放在用户目录中的
usingnamespacestd;
intmain()
{Studentstud1(101,"Wang",18);//定义类对象stud1
stud1.get_data();
cout<return0;
}
程序能正常运行,输出为
101Wang18
2.82843
//如果程序员乙写了头文件header2.h,在其中除了定义其他类以外,还定义了类Student和//函数fun,但其内容与头文件header1.h中的Student和函数fun有所不同。
//header2.h(头文件2,设其文件名为cc8-4-h2.h)
#include
#include
usingnamespacestd;
classStudent//声明Student类
{public:
Student(intn,stringnam,chars)//参数与header1中的student不同
{num=n;name=nam;sex=s;}
voidget_data();
private:
intnum;
stringname;
charsex;//此项与header1不同
};
voidStudent:
:
get_data()//成员函数定义
{cout<}
doublefun(doublea,doubleb)//定义全局函数
{returnsqrt(a-b);}//返回值与header1中的fun函数不同
//头文件2中可能还有其他内容
//假如主程序员在其程序中要用到header1.h中的Student和函数fun,因而在程序中包含了
//头文件header1.h,同时要用到头文件header2.h中的一些内容,因而在程序中又包含了头
//文件header2.h。
如果主文件(包含主函数的文件)如下:
//mainfile
#include
#include"cc8-4-h1.h"//包含头文件1
#include"cc8-4-h2.h"//包含头文件2
usingnamespacestd;
intmain()
{Studentstud1(101,″Wang″,18);
stud1.get_data();
cout<return0;
}
这时程序编译就会出错。
因为在预编译后,头文件中的内容取代了对应的#include命令行,这样就在同一个程序文件中出现了两个Student类和两个fun函数,显然是重复定义,这就是名字冲突,即在同一个作用域中有两个或多个同名的实体。
不仅如此,在程序中还往往需要引用一些库,为此需要包含有关的头文件。
如果在这些库中包含有与程序的全局实体同名的实体,或者不同的库中有相同的实体名,则在编译时就会出现名字冲突。
为了避免这类问题的出现,ANSIC++增加了命名空间。
8.2.2什么是命名空间
为了解决上面这个问题,ANSIC++增加了命名空间(namespace)。
所谓命名空间,实际上就是一个由程序设计者命名的内存区域。
程序设计者可以根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。
如:
namespacens1//指定命名空间ns1
{inta;
doubleb;
}
现在命名空间成员包括变量a和b,注意a和b仍然是全局变量,仅仅是把它们隐藏在指定的命名空间中而已。
如果在程序中要使用变量a和b,必须加上命名空间名和作用域分辨符“:
:
”,如ns1:
:
a,ns1:
:
b。
这种用法称为命名空间限定(qualified),这些名字(如ns1:
:
a)称为被限定名(qualifiedname)。
命名空间的作用是建立一些互相分隔的作用域,把一些全局实体分隔开来,以免产生名字冲突。
在声明一个命名空间时,花括号内不仅可以包括变量,而且还可以包括以下类型:
变量(可以带有初始化)、常量、函数(可以是定义或声明)、结构体、类、模板、命名空间(在一个命名空间中又定义一个命名空间,即嵌套的命名空间)。
例如:
namespacens1
{constintRATE=0.08;//常量
doublepay;//变量
doubletax()//函数
{returna*RATE;}
na