和大多数语言一样,C++提供了条件执行语句、重复执行相同代码的循环语句和用于中断当前控制流的跳转语句。本章将详细介绍C++语言所支持的这些语句。
通常情况下,语句是顺序执行的。但除非是最简单的程序,否则仅有顺序执行远远不够。因此C++语言提供了一组控制流(flow-of-control)语句以支持更复杂的执行路径。
C++语言的大多数语句都以分号结束,一个表达式,末尾加上分号就变成了表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃掉求值结果:
ival+5;//一条没什么实际用处的表达式语句
cout<<ival;//一条有用的表达式语句
第一条语句没什么用,因为虽然执行力加法,但是相加的结果没有被使用。比较普遍的情况是:表达式语句中的表达式在求值时附带了其它效果,比如说给变量赋了新值或者输出了结果。
空语句
最简单的语句时空语句(null statement),空语句中只含有一个单独的分号。如果在程序的某个地方,语法上需要一个语句但是逻辑上不需要,此时应该用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。例如,我们想读取输入流的 内容直到遇到一个特定的值为止,除此之外什么事情也不做:
//重复读入数据直至到达文件某位或者某处输入 的值等于sought
while(cin>>s && s!=sought)
;//空语句
使用空语句时,应该加上注释,从而令读这段代码的人知道该语句是有意省略的。
别漏写分号,也别多写分号
因为空语句是一条语句,所以可用在任何允许使用语句的地方。所以某些看起来非法的分号往往只不过是一条空语句而已,从语法上说得过去。多余的空语句一般来说是无害的,但是如果在if或者while的条件后面跟了一个额外的分号就可能完全改变程序员的初衷。例如,下面的代码将无限循环下去:
//出现了额外的分号,循环体是那条空语句
while(iter!=svec.end()) ;//while循环体是那条空语句
++iter;//递增运算不属于循环的一部分
复合语句(块)
复合语句(compoud statement)是指用花括号括起来的(可能为空)的语句和声明序列,复合语句也被称为块(block)。一个块就是一个作用域,在块中引入的名只能在块内部以及嵌套在块中的子块里访问。 通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。
如果在程序的某个地方,语法上需要一条语句,但是逻辑上需要多条语句,则应该给使用复合语句。例如,while循环和for循环,if-else语句。此时我们就应该将多条语句用花括号括起来,从而把语句序列转换为块。
所谓空块,是指内部没有任何语句的一对花括号。空块的作用等价于空语句。
可以在if、switch、while和for语句的控制结构内定义变量。定义在控制结构当作的变量只在相应的语句的内部可见,一旦语句结束,变量也就超出其作用范围了:
while(int i=get_num())//每次迭代时创建并初始化i
cout<<i<<endl;
i=0;//错误,在循环外部访问i
如果其他代码也需要访问控制变量,则变量必须定义在语句的外部:
//寻找第一个负值元素
auto beg = v.begin();
while( beg!=v.end() && *beg>=0)
++beg;
if(beg==v.end())
//此时我们知道v中所有元素都大于等于0
因为控制结构定义的对象的值马上就要由结构本身使用,所以这些变量必须初始化。
C++语言提供了两种按条件执行的语句。一种是if,它根据条件决定控制流;另外一种是switch语句,他计算一个整型表达式的值加粗样式,然后根据这个值从几条执行路径中选择一条。我们这里着重介绍switc。
使用if else语句
我们举个例子来说明if语句的功能,程序的目的是把数字形式表示的成绩转换成字母形式。假设数字成绩的范围是0-100,其中100分对应的字母形式是A++,低于60分的成绩对应的字母形式是F。其他成绩每10个划分为一组;[60,69]对应字母D,依次类推。使用vect对象存放字母成绩所有可能的取值:
const vector<string> scores = {"F","D","C","B","A","A++"};
我们使用if-else语句解决该问题,根据成绩是否合格执行不同的操作:
string lettergrade;
if(grade<60)
lettergrade = scores[0];
else
lettergrade = scores[(grade-50)/10];
判断grade的值是否小于60,根据结果选择执行if分支还是else分支。在else分支中,由成绩计算得到一个下标,具体过程是:首先从grade中减去50,然后执行整数除法,去掉余数后所得的商就是数组scores对应的下标。
嵌套if语句
接下来让我们的程序更有趣一点。试着给那些合格的成绩后面添加一个加号或减号。如果成绩的末位是8或9,添加一个加号;如果是0、1或2,添加一个减号:
if(grade % 10 > 7)
lettergrade +='+';//末尾是8或9
else if(grade % 10 <3)
lettergrade +='-';//末尾是0,1或2
接下来将这段程序整合到之前的代码中:
string lettergrade;
if(grade<60)
lettergrade = scores[0];
else{
lettergrade = scores[(grade-50)/10];
if(grade!=100){//只要不是A++,就考虑添加加号或减号
if(grade % 10 > 7)
lettergrade +='+';//末尾是8或9
else if(grade % 10 <3)
lettergrade +='-';//末尾是0,1或2
}
}
注意使用花括号
有一种常见的错误:本来程序中有几条语句应该作为一个块来执行,但是我们忘了用花括号将这些语句包围。在下面的例子中,添加加号减号的代码将被无条件执行,这显然违背了我们的初衷:
string lettergrade;
if(grade<60)
lettergrade = scores[0];
else
lettergrade = scores[(grade-50)/10];
//虽然下面的语句中有缩进,但是因为没有花括号,所以无论什么情况下都会执行接下来的代码
//不及格的成绩也会加上加号减号,错误
if(grade!=100){//只要不是A++,就考虑添加加号或减号
if(grade % 10 > 7)
lettergrade +='+';//末尾是8或9
else if(grade % 10 <3)
lettergrade +='-';//末尾是0,1或2
}
要想发现这类错误,通常非常困难,毕竟这段代码从逻辑上看起来是正确的。
为了避免此类问题,有些编码风格要求在if和else之后必须写上花括号(对for和while同样)。这么做的好处是可以避免代码混乱不清,以后想修改代码添加别的语句是,也可以很容易找到正确的位置。
许多编辑器和开发环境都提供一种辅助工具,它可以自动地缩进代码以匹配其语法结构。善用此类工具益处多多。
悬垂else
当一个if语句嵌套在另一个 if语句内部时,很可能if分支会多余else分支。事实上,之前那个成绩转换程序就有4个if分支,而只有2个else分支。这时候问题出现了:如何知道某个给定的else是和哪个if匹配呢?
这个问题通常被称为悬垂else(dangling else),在那些既有if语句又有if-else语句的编程语言中是个普遍存在的问题。不同语言解决该问题的思路也不同,就C++而言,它规定else与离他最近的尚未匹配的if匹配,从而消除了程序的二义性。
当代码中的if语句多于else分支时,程序员有时会感到麻烦。举个例子来说,对于添加加号减号的那个最内层的if else语句,我们用另外一组条件改写它:
if(grade % 10 >=3)
if(grade % 10 >7)
lettergrade +='+';
else
lettergrade += '-';
从代码的格式上来看,程序的初衷是想else和外层if匹配。然而,实际上是与内层if匹配的。
使用花括号控制执行路径
要想使else分支和外层的if语句匹配起来,可以在if语句的两端加上花括号,使其成为一个块。
switch语句提供了一条便利的途径使得我们可以在若干固定选项中做出选择。举个例子,假如我们想统计五个元音字母在文本中出现的次数,程序逻辑应该如下:
从输入的内容中读取所有字符。
令每一个字符都与元音字母的集合比较。
如果字符与某个元音字母匹配,该字母的数量加1.
要想实现这个功能,直接使用switch语句即可:
//为每个元音字母初始化其计数值
unsigned acnt=0,ecnt=0,icnt=0,ocnt=0,ucnt=0;
char ch;
while(cin>>ch){
//如果ch是元音字母,将其对应的计数值加1
switch (ch){
case 'a':
++acnt;
break;
case 'e':
++ecnt;
break;
case 'i':
++icnt;
break;
case 'o':
++ocnt;
break;
case 'u':
++ucnt;
break;
}
}
//输出结果
cout<<"number of vowel a:\t"<<acnt<<'\n'
<<"number of vowel e:\t"<<ecnt<<'\n'
<<"number of vowel i:\t"<<icnt<<'\n'
<<"number of vowel o:\t"<<ocnt<<'\n'
<<"number of vowel u:\t"<<ucnt<<endl;
switch语句首先对括号里的表达式求值,该表达式紧跟在关键字switch后面,可以是个初始化的变量声明。表达式的值转换成整数类型,然后与每个case标签的值比较。
如果表达式和某个case标签值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达了switch的结尾或者是遇到一条break语句为止。
我们将在5.5.1节中详细介绍break语句,简言之,break语句的作用是中断当前的控制流。此例中,break语句将控制权转移到switch之外。因为switch语句是while循环中唯一的语句,所以从switch语句中断出来以后,程序的控制权将移到while语句的右花括号处。此时while语句内部没有其他语句要执行,所以while会返回再一次判断条件是否满足。
如果和所有的case都没有匹配上,将直接跳转到switch结构之后的第一条语句。刚刚说过,就回到了while语句的条件部分。
case关键字和它对应的值一起被称作case标签(case label)。case标签必须是整型常量表达式:
char ch =getval();
int ival=42;
switch(ch){
case 3.14://错误,不是整数
case ival://错误,不是常量
另外,任何两个case标签的值不能相同,否则就会引发错误。default也是一种特殊的标签,关于它的知识我们在162页介绍。
switch内部的控制流
除非程序显式的中断了switch的顺序执行,否则直到switch结尾才会停下来。要想避免执行后续的case分支,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个case标签 之前应该有一条break语句。
然而,也有一些时候默认的switch行为才是程序真正需要的。每个case标签只能对应一个值,但有时我们希望两个或多个值共享同一组操作。此时我们就故意省略掉break语句,使得程序可以连续执行若干个case标签。
例如,我们想统计所有元音字母出现的总次数:
unsigned vowelCnt=0;
//...
switch(ch){
case 'a';
case 'e';
case 'i';
case 'o';
case 'u';
++vowelCnt;
break;
}
上面的代码,几个case标签连写在一起,中间没有break语句。因此只要ch是元音字母,不管到底是哪哥都执行相同的代码。
C++程序的形式比较自由,所以case标签之后不一定换行。把几个case标签写在一行里,强调这些case代表的是某一个范围内的值:
switch(ch){
case 'a';case 'e'; case 'i';case 'o';case 'u';
++vowelCnt;
break;
}
我们建议,一般不要省略case分支最后的break语句,如果没写,最好加上一段注释说清楚程序的逻辑。因为这样的话,即使以后再增加新的case分支,也不用再在前面补充break语句了。
default标签
如果没有任何一个case标签能匹配山switch表达式的值,程序将执行紧跟在default标签后面的语句。例如可以增加一个计数值来统计非元音字母的数量,只要在default分支内不断递增名为othercnt的变量就可以了。
switch(ch){
case 'a';case 'e'; case 'i';case 'o';case 'u';
++vowelCnt;
break;
default:
++otherCnt;
break;
}
即使不准备default标签下做任何工作,定义一个default标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。
标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case标签。如果switch结构以一个空的default标签作为结束,则该default标签后面必须跟上一条空语句或一个空块。
switch内部的变量定义
如前所述,switch的执行流程有可能会跨过某些case标签。如果程序跳转到了某个特定的case,则switch结构中该case标签之前的部分会被忽略掉。这种忽略掉一部分的代码行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办?
答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。
case true:
//因为程序的执行流程可能绕开下面的初始化语句,所以该switch语句不合法
string file_name;//错误,控制流绕过一个隐式初始化的变量
int ival = 0;//错误,控制流绕过一个显式初始化的变量
int jval;//正确,因为jval没有初始化
break;
case false:
//正确,虽然jval在作用域内,但它没有初始化
jval = next_num();//正确,给jval赋一个值
if(file_name.empty())//file_name在作用域内,但是没有初始化
//...
假设上述代码合法,则一旦控制流直接跳到false分支,也就同时略过了变量file_name和ival的初始化过程。此时这两个变量位于作用域之内,跟在false之后的代码试图在尚未初始化的情况下使用它们,这显然是不对的。 因为C++规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。
case true:
{
//正确,声明语句位于语句块内部
string file_name = get_file_name();
//...
}
case fale:
if(file_name.empty())//错误:file_name不在作用域之内
迭代语句通常为循环,它重复执行操作直到某种条件满足时才停下来。while和for在执行循环之前检查条件,do while语句先执行循环体,然后再检查条件。
while的条件部分可以是个表达式或者是一个带初始化的变量声明。通常来说,应该由条件本身或者是循环体设法改变表达式的值,否则循环可能无法终止。定义在while条件部分或循环体内的变量每次迭代都经历从创建到销毁的过程。
使用while循环
如果我们无法确定要迭代多少次,使用while循环比较合适,比如我们经常用到的读取输入内容。还有一种情况也应该使用for循环,这就是我们想在循环结束后访问循环控制变量。例如:
vector<int> v;
int i;
//重复读入数据,直至到达文件末尾或者遇到其他问题
while(cin>>i)
v.push_back(i);
//寻找第一个负值元素
auto beg=v.begin();
while(beg!=v.end()&&*beg>=0)
++beg;
if(beg==v.end())
//此时我们知道v中的元素都大于等于0
传统for循环的执行流程
我们以3.2.3节的for循环为例:
//重复处理s中的字符直至我们处理完全部字符或者遇到了一个表示空白的字符
for(decltype(s.size()) index=0;index!=s.size()&&!isspace(s[index]);++index)
s[index]=toupper(s[index]);//将当前字符改写成大写字符
求值顺序如下所示:
1.循环开始,首先执行一次init-statement。此例中,定义index并初始化为0.
2.接下来判断condition。如果index不等于s.size()而且s[index]位置的字符不是空白,则执行for循环体的内容。否则循环终止。如果第一次迭代时条件就为假,for循环体一次也不会执行。
3.如果条件为真,执行循环体,此例中,for循环体将s[index]位置的字符改写成大写形式。
4.最后执行expression。此例中,将index加1。
这4步说明了for循环第一次迭代的过程。其中第一步只在循环开始时执行一次,剩下的步重复执行知道条件为假时终止。
**牢记for语句头中定义的对象只在for循环体可见。因此在上面的例子中,for循环结束后index就不可用了。
for语句头中的多重定义
和其他声明一样,init-statement也可以定义多个对象。但是init-statement只能有一条声明语句,因此,所有变量的基础类型必须相同。举个例子,我们用下面的循环把vector的元素拷贝一份到原来的元素后面:
//记录下v的大小,当到达原来的最后一个元素后结束循环
for(decltype(v.size()) i =0,sz=v.size();i!=sz;++i)
v.push_back(v[i]);
同时在init-statement里定义了索引i和循环控制变量sz。
省略for语句头的某些部分
for语句能省略掉括号中三个语句的任何一个或者全部。
如果无须初始化,则可以使用空语句作为init-statement。例如,对于在vector对象中寻找第一个负数的程序,完全能用for循环改写:
auto beg=v.begin();
for(/*空语句*/;beg!=v.end()&& *beg>=0;++beg)
;//什么也不做
注意,分号必须保留以表明我们省略掉了init-statement。说得更准确一点,分号表示的是一个空的init-statement。在这个循环中,因为所有要做的工作都在for语句头的条件和表达式部分完成了,所以for循环体也是空的。其中,条件部分决定何时停止查找,表达式部分递增迭代器。
省略condition的效果等于在条件部分写了一个true。因为条件的值永远是true,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去:
for(int i=0;/*条件为空*/;++i){
//对i进行处理,循环内部的代码必须负责终止迭代过程。
}
我们也能省略掉for语句头中的expression,但是在这样的循环中就要求条件部分或者循环体必须改变迭代变量的值。 举个例子,之前有一个将整数读入vector的while循环,我们使用for语句改写它:
vector<int> v;
for(int i;cin>>i;/*表达式*/)
v.push_back(i);
因为条件部分能改变i的值,所以这个循环无须表达式部分。其中,条件部分不断检查输入流的内容,只要完整读取完所有输入或者遇到一个输入错误就终止循环。
C++11新标准引入了一种更简单的for语句,这种语句可以遍历容器或其他序列的所有元素。范围for语句(range for statement)的语法形式是:
for(declaration:expression)
statement
expression表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者vector和string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin和end成员。
declaration定义的是一个变量,序列中的每个元素都能转换成该变量的类型。**确保类型相容的最简单办法是使用auto类型说明符,**这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
每次迭代都会重新定义循环控制变量,并将其初始化成序列的下一个值,之后才会执行statement。像往常一样,statement可以是一条件单独的语句也可以是一个块。所有元素都处理完毕后终止循环。
之前我们已经接触过这样的循环,下面的例子将把vector对象中的每一个元素都翻倍,它包含了范围for语句的几乎所有语法特征:
vector<int> v={0,1,2,3,4,5,6,7,8,9};
for(auto &r: v)//范围变量必须是引用类型,这样才能对元素执行写操作
r*=2;
范围for语句的定义来源于与之等价的传统for语句:
for(auto beg=v.begin(),end=v.end();beg!=end;++beg){
auto &r=*beg;
r*=2;
}
通过 学习范围for语句的原理后,我们也就不难理解为什么我们在3.3.2节强调不能通过范围for语句增加vector对象或者其他容器的元素了。在范围for语句中预存了end()的值。一旦在序列中添加或者删除元素,end函数的值就可能变得无效了。关于这一点,将在9.3.6节做更详细的介绍。
do while 语句和while 语句非常相像,唯一的区别是,do while语句先执行循环体后检查条件。不管条件的值如何,都至少执行一次循环。语法形式如下:
do
statament;
while(condition);//分号表示结束
在do语句中,求condition的值之前首先执行一次statement,condition不能为空。如果condition的值为假,循环终止,否则重复循环过程。condition使用的变量必须定义在循环体之外。
我们可以使用do while循环不断地执行加法运算:
//不断地提醒用户输入一对数,然后求和
string rsp;//作为循环的条件,不能定义在do的内部
do{
cout<<"please enter two values:";
int val1=0,val2=0;
cin>>val1>>val2;
cout<<"the num of ="<<val1+val2<<"\n\n"
<<"more? enter yes or no:"
cin>>rsp;
}while(!rsp.empty()&& rsp[0] !='n');
循环首先提示用户输入两个数字,然后输出他们的和并询问用户是否继续。条件部分检查用户做出的回答。如果没有回答或者回答是n都将停止循环。否则继续执行。
因为对于dowhile 来说先执行语句或者块,后判断条件,所以不允许在条件部分定义变量。
跳转语句中断当前的执行过程。C++语言提供了4种跳转 语句:break,continue、goto、return。return语句将在6.3节介绍。
break语句(break statement)负责终止离它最近的while、do while 、for或switch语句,并从这些语句之后的第一条开始继续执行。
break语句只能出现在迭代语句或者switch语句内部(包括嵌套在此类循环里的语句或块的内部)。break语句的作用范围仅限于最近的循环或者switch:
string buf;
while(cin>>buf&&!buf.empty()){
switch(buf[0]){
case '-':
for(auto it=buf.begin()+1;it!=buf.end();++it){//处理到第一个空白为止
if(*it== ' ')
break;//#1,离开for循环
}
//break #1将控制权转移到这里
//剩余的'-'处理:
break;//#2 离开switch语句
case '+':
//...
}//结束switch
//结束switch:break #2将控制权转移到这里
}//结束while循环
第一个break负责终止连字符case标签后的for循环。但它不会终止switch语句,甚至连当前的case分支也终止不了。接下来程序继续执行for循环后的第一个语句,这条语句有可能继续处理连字符的情况,也可能是另一条用于终止当前分支的break语句。
第二个break负责终止switch语句,但是不能终止while循环。执行完这个break后,程序继续执行while的条件部分。
continue语句终止最近的循环中的当前迭代并立即开始下一次迭代。它只出现在for、while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部。和break语句类似的是,出现在嵌套循环中的continue语句也仅作用于离他最近的循环。和break不同的是,只有当switch语句嵌套在迭代语句内部时,才能在switch里使用continue。
对于while 和do while语句来说,继续判断条件的值;对于传统的for循环来说,继续执行for语句头的expression;对于范围for循环来说,则是用序列中的下一个初始化循环控制变量。
例如下面的程序每次从标准输入中读取一个单词。循环只对那些以下画线开头的单词感兴趣,其他情况,我们直接终止当前的迭代并获取下一个单词:
string buf;
while(cin>>buf && !buf.empty()){
if(buf[0] !='_')
continue;
//程序执行过程到了这里说明当前的输入是以下划线为开头的;接着处理buf
}
goto语句的作用是从goto语句无条件跳转到同一函数内的另一条语句。这里我们建议,不要在程序中使用goto语句,因为它使得程序既难理解又难修改。
goto语句的语法形式是:
goto label;
其中,label是用于标识一条语句的标识符。带标签语句(labeled statement)是一种特殊的语句,在它之前有一个标示符以及一个冒号:
end:return;//带标签语句,可以作为goto的目标
标签标示符独立于变量或其他标识符的名字,因此,标签标示符可以和程序中其他实体的标示符使用同一个名字而不会相互干扰。goto语句和控制权转向的那条带标签的语句必须位于同一个函数内。
和switch语句类似,goto语句也不能将程序的控制权从变量的作用域之外转移到作用域之内:
//...
goto end;
int ix=10;//错误,goto语句绕过了带初始化的变量定义
end:
//错误,此处的代码需要使用ix,但是goto语句绕过了它的声明
ix=42;
向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它:
//向后跳过一个带初始化的变量定义是合法的
begin:
int sz=get_size();
if(sz<=0){
goto begin;
}
在上面的代码中,goto语句执行后将销毁sz。因为跳回到begin的动作跨过了sz的定义语句,所以sz将重新定义并初始化。
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输出等。处理反常行为可能是设计所有系统最难的一部分。
当程序的某个部分检测到一个它为无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障将在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。
如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。例如,如果程序的问题是输入无效,则异常处理可能会要求用户重新输入正确的数据;如果丢失了数据库连接,会发出报警信息。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
throw表达式(throw expression),异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw引发了(raise)异常。
try语句块(try block),异常处理的部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句(catch clause)结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句处理异常,所以它们也被称作异常处理代码(expression handler)。
一套异常类(expression class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。
程序的异常检测部分使用throw表达式引发一个异常。throw表达式包含关键字throw和紧跟其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面紧跟一个分号,从而构成一条表达式语句。
举个简单的例子,回忆1.5.2节把两个Sales_item对象相加的程序。这个程序检查它读入的记录是否是关于同一种书籍的,如果不是,输出一条信息然后退出。
Sales_item item1,item2;
cin>>item1>>item2;
//首先检查item和item2是否表示同一种书籍
if(item1.isbn()==item2.isbn()){
cout<<item+item2<<endl;
return 0;//表示成功
}
else{
cerr<<"Data must refer to same ISBN"<<endl;
return -1;
}
在真实的程序中,应该把对象相加的代码和用户交互的代码分离开来。此例中我们改写程序使得检查完成后不再直接输出一条信息,而是抛出一个异常:
//首先检查两条数据是否是关于同一书籍
if(item1.isbn()!=item2.isbn())
throw runtime_error("data must refer to same ISBN");
//如果程序执行到了这里,表示两个isbn是相同的。
在这段代码中,如果isbn不一样就抛出一个异常,该异常类型是runtime_error的对象。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。
类型runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。关于标准库异常类型更多的知识将在5.6.3 节介绍。我们必须初始化runtime_error的对象,方式是给它提供一个string对象或者一个C风格的字符串,这个字符串中有些关于异常的辅助信息。
try语句块的通用语法形式是:
try{
program-statements
}catch(exception-declaration){
hander-statements
}catch(exception-declaration){
hander-statements
}//...
try语句块的开始是关键字try,随后紧跟一个块,这个块就像大多数时候那样是花括号括起来的语句序列。
跟在try块之后的是一个或多个catch子句。catch子句包括三个部分:关键字catch、括号内一个(可能未命名的)对象的声明(称作异常声明,exception declaration)以及一个块。当选中了某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。
try语句块中的program-statements组成程序的正常逻辑,向其他任何块一样,program-statements可以有包括声明在内的任意C++语句。一如往常,try语句块内声明的变量在块外部无法访问,特别是catch子句内也无法访问。
编写处理代码
在之前的例子里,我们使用了一个throw表达式以避免把两个代表不同书籍的Sales_item相加。我们假设执行Saels_item对象加法的代码是与用户交互的代码分离开来的。其中与用户交互的代码负责处理发生的异常,它的形式可能如下所示:
while(cin>> item1>>item2){
try{
//执行添加两个Sales_item对象的代码
//如果添加失败,代码抛出一个runtime_error 异常
}catch(runtime_error){
//提醒用户两个isbn必须一致,询问是否重新输入
cout<<err.what()
<<"\nTry Again? Enter y or n"<<endl;
char c;
cin>>c;
if(!cin||c=='n')
break;//跳出while循环
}
}
程序本来要执行的任务出现在try语句中,这是因为这段代码可能会抛出一个runtime_error类型的错误。
给用户的提示信息中输出了err.what()返回值。我们知道err的类型是runtime_error,因此能推断出what是runtime_error类的一个成员函数。每个标准库异常类都定义了名为what的成员函数,这些成员函数没有参数,返回类型是C风格字符串(即const char*)。其中,runtime_errord的what成员返回的是初始化一个具体对象时所用的string对象的副本。 如果上一节编写的代码抛出异常,则本节的catch子句输出
data must refer to same isbn
try again?enter y or n
函数在寻找处理代码的过程中退出
在复杂系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个try语句块。例如,一个try语句块可能调用了包含另一个try语句块的函数,新的try语句块可能调用了包含又一个try语句块的新函数,依次类推。
寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没有 找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch子句,这个新的函数也被终止,继续搜搜调用它的函数。依次类推,沿着函数执行路径逐层回退,直到找到适当类型的catch子句为止。
如果最终还是没有找到任何匹配的catch子句,程序转到名为terminate的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
对于那些没有任何try语句块定义的异常,也按照类似的方式处理:毕竟,没有try语句块也就意味着没有匹配的catch子句。如果一段程序没有try语句块且发生了异常,系统会调用terminate函数并终止当前程序的执行。
提示:编写异常安全代码非常困难
要好好理解这句话:异常中断了程序的正常流程。异常发生时,调用者请求的一部分计算可能已经完成了,另一部分则尚未完成。通常情况下,略过部分程序意味着某些对象处理到一半就嘎然而止了,从而导致对象处于无效或未完成的状态,或者资源没有正常释放,等等。那些在异常发生期间正确执行了清理工作的程序被称作异常安全代码(exception safe)代码。然而经验表明,编写异常安全的代码非常困难,这部分知识也超出了本书的范围。
对于一些程序来说,当异常发生时只是简单地终止程序。此时,我们不怎么需要担心异常安全的问题。
但是对于那些确实需要异常安全并继续执行的代码来说,就要加倍注意了。我们必须时刻清楚异常何时发生,发生后程序应该如何确保对象有效、资源无泄漏、程序处于合理状态等。
我们在本书中介绍一些毕竟常规的提升异常安全性的技术。但是读者需要注意,如果你的程序要求非常鲁棒的异常处理,那么仅有我们的技术还远远不够。
C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
stdexcept头文件定义了几种常用的异常类,详细信息在197页表5.1中列出。
new头文件定义了bad_alloc异常类型,这种类型将在12.1.2节详细介绍。
type_info头文件定义了bad_cast异常类型,这种类型将在19.2节详细介绍。
标准库类型只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。
我们只能以默认初始化的方式初始化exception、bad_alloc和bad_cast对象,不允许为这些对象提供初值。
其他异常类型的行为则恰好相反:应该使用string对象或者C风格字符串初始化这些类型的对象,但不允许使用默认初始化的方式。 当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。
异常类型只定义一个名为what的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char*。该字符串的目的是提供关于异常的一些文本信息。
what函数返回的C风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what返回的内容由编译器决定。
C++仅提供有限的语句类型,它们中的大多数会影响程序的控制流程:
while、for、do while语句,执行迭代操作。
if和switch语句,提供分支结构
continue 语句,终止循环的当前依次迭代。
break,退出循环或者switch语句。
goto语句,将控制权转移到一条带标签的语句。
try 和catch,将一段可能抛出异常的语句序列在花括号里构成try语句块。catch子句负责处理代码抛出的异常。
throw表达式语句,存在于代码中,将控制权转移到相关的catch子句。
return语句,终止函数的执行。我们将在第6章介绍return语句。
练习5.1 什么是空语句?什么时候会用到空语句?
解答:
空语句是最简单的语句,空语句由一个单独的分号构成。如果在程序的某个地方,语法上需要一条语句,但逻辑上不需要,此时就应该使用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。使用空语句时最好加上注释,从而令代码的阅读者知道这条语句时有意省略内容的。
练习5.2 什么是块?什么时候会用到块?
解答:
块,是指用花括号括起来的语句和声明的序列,也称为复合语句。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。如果在程序当某个地方,需要一条语句,而逻辑上需要多条语句,此时应该使用块,块不需要以分号结束。
例如,循环体是一条语句,但是我们通常需要在循环体内做很多事情,此时就应该把多条语句用花括号括起来,从而把语句序列转变成块。
练习5.3 使用逗号运算符重写1.4.1节的while循环,使它不再需要块,观察改写之后的代码的可读性提高了还是降低了。
解答:
利用逗号运算符改写之后的形式如下所示:
while(val<=10)
sum+=val,++val;
很明显,改写之后的代表不够清晰,可读性降低了。
练习5.4 说明下列例子的含义,如果存在问题,试着修改它。
while(string :: iterator iter != s.end()){/*...*/}
while(bool status =find(word) {/*...*/}
if(!status) {/*...*/}
解答:
第一行是非法的,它的原意是希望在while语句的控制结构中定义一个string::iterator类型的变量iter,然后判断iter是否到达了s末尾,只要还没到达末尾就执行循环体的内容。但是该式把变量的定义和关系判断混合在了一起,如果要使用iter与其他值比较,必须首先为iter赋初值。修改后的程序应该是:
string :: iterator iter =s.begin();
while(iter!=s.end()){
++iter;
/*...*/
}
第二行是非法的, 变量status定义在while循环控制结构的内部,其作用域仅限于while循环。if语句已经位于while循环的作用域之外,status在if语句内是一个未定义的无效变量。 要想在if语句中继续使用status,需要把它定义在while循环之前。修改后的程序应该是:
bool status;
while(status=find(word)){/*...*/}
if(!status) {/*...*/}
练习5.5 写一段自己的程序,使用ifelse语句实现把数字成绩转换成字母成绩的要求。
解答:
#include
using namespace std;
int main(){
int grade;
cout<<"请输入您的成绩:";
cin>>grade;
if(grade<0||grade>100){
cout<<"改成绩不合法"<<endl;
return -1;
}
if(grade==100){
cout<<"等级成绩是:"<<"A++"<<endl;
return -1;
}
if(grade<60){
cout<<"等级成绩是:"<<"F"<<endl;
return -1;
}
int iu=grade/10;//成绩的十位数
int it=grade%10;//成绩的个位数
string score,level,lettergrade;
//根据十位数确定score
if(iu==9)
score="A";
else if(iu==8)
score="B";
else if(iu==7)
score="C";
else
score="D";
//根据成绩的十位数确定level
if(it<3)
level="-";
else if(it>7)
level="+";
else
level="";
//累加求得等级成绩
lettergrade=score+level;
cout<<"等级成绩是:"<<lettergrade<<endl;
return 0;
}
练习5.6 改写上一题的程序,使用条件运算符代替ifelse语句。
解答:
#include
using namespace std;
int main(){
int grade;
cout<<"请输入您的成绩:";
cin>>grade;
if(grade<0||grade>100){
cout<<"改成绩不合法"<<endl;
return -1;
}
if(grade==100){
cout<<"等级成绩是:"<<"A++"<<endl;
return -1;
}
if(grade<60){
cout<<"等级成绩是:"<<"F"<<endl;
return -1;
}
int iu=grade/10;//成绩的十位数
int it=grade%10;//成绩的个位数
string score,level,lettergrade;
//根据十位数确定score
score=(iu==9) ? "A":(iu==8) ? "B":(iu==7) ? "C":"D";
//根据成绩的十位数确定level
level =(it<3) ? "-":(it>7) ? "+":"";
//累加求得等级成绩
lettergrade=score+level;
cout<<"等级成绩是:"<<lettergrade<<endl;
return 0;
}
练习5.7 改正下列代码段中的错误。
if(ival1!=ival2)
ival1=ival2
else ival=ival2=0;
if(ival < minval)
minval=ival;
occurs=1;
if(int ival=get_value())
cout<<"ival="<<ival<<endl;
if(!ival)
cout<<"ival=0\n"
if(ival=0)
ival=get.value();
解答:
if(ival1!=ival2)
ival1=ival2;
else ival=ival2=0;
if(ival < minval){
minval=ival;
occurs=1;
}
int ival;
if(ival=get_value())
cout<<"ival="<<ival<<endl;
if(!ival)
cout<<"ival=0\n"
if(ival==0)
ival=get.value();
练习5.8 什么是垂悬else?C++语言是如何处理else子句的?
解答:
见本书讲解。
练习5.9 编写一段程序,使用一系列if语句统计从cin读入的文本中有多少元音字母。
解答:
#include
using namespace std;
int main(){
unsigned int vowelcnt=0;
char ch;
cout<<"请输入一段文本:"<<endl;
while(cin>>ch){
if(ch=='a')
++vowelcnt;
if(ch=='e')
++vowelcnt;
if(ch=='i')
++vowelcnt;
if(ch=='o')
++vowelcnt;
if(ch=='u')
++vowelcnt;
}
cout<<"你输入的文本有"<<vowelcnt<<"个元音字母"<<endl;
return 0;
}
练习5.10 我们之前实现的统计元音字母的程序存在一些问题:如果元音字母以大写形式出现,就不会被统计。编写一段程序,既统计小写形式也统计大写形式。
解答:
#include
using namespace std;
int main(){
unsigned int acnt=0,ecnt=0,icnt=0,ocnt=0,ucnt=0;
char ch;
cout<<"请输入一段文本:"<<endl;
while(cin>>ch){
switch(ch){
case 'a':
case 'A':
++acnt;
break;
case 'e':
case 'E':
++ecnt;
break;
case 'i':
case 'I':
++icnt;
break;
case 'o':
case 'O':
++ocnt;
break;
case 'u':
case 'U':
++ucnt;
break;
}
}
cout<<"元音字母的a的数量是:"<<acnt<<endl;
cout<<"元音字母的e的数量是:"<<ecnt<<endl;
cout<<"元音字母的i的数量是:"<<icnt<<endl;
cout<<"元音字母的o的数量是:"<<ocnt<<endl;
cout<<"元音字母的u的数量是:"<<ucnt<<endl;
return 0;
}
练习5.11 修改统计元音字母的程序,使其也能统计空格、制表符和换行符的数量。
解答:
#include
using namespace std;
int main(){
unsigned int acnt=0,ecnt=0,icnt=0,ocnt=0,ucnt=0;
unsigned int spacecnt=0,tabcnt=0,newlinecnt=0;
char ch;
cout<<"请输入一段文本:"<<endl;
while(cin.get(ch)){
switch(ch){
case 'a':
case 'A':
++acnt;
break;
case 'e':
case 'E':
++ecnt;
break;
case 'i':
case 'I':
++icnt;
break;
case 'o':
case 'O':
++ocnt;
break;
case 'u':
case 'U':
++ucnt;
break;
case ' ':
++spacecnt;
break;
case '\t':
++tabcnt;
break;
case '\n':
++newlinecnt;
break;
}
}
cout<<"元音字母的a的数量是:"<<acnt<<endl;
cout<<"元音字母的e的数量是:"<<ecnt<<endl;
cout<<"元音字母的i的数量是:"<<icnt<<endl;
cout<<"元音字母的o的数量是:"<<ocnt<<endl;
cout<<"元音字母的u的数量是:"<<ucnt<<endl;
cout<<"空格的数量是:"<<spacecnt<<endl;
cout<<"制表符的数量是:"<<tabcnt<<endl;
cout<<"换行符的数量是:"<<newlinecnt<<endl;
return 0;
}
练习5.12 修改统计元音字母的程序,使其能统计以下含有两个字符的字符序列的数量:ff、fl和fi。
解答:
统计字符串的问题,通常比较复杂,我们的设定是一个字符只会被统计一次。假设我们输入的序列是xxxxxfiffffflxxx,则统计结果是ff:2次、fl:1次、fi:1次。
#include
using namespace std;
int main(){
unsigned int ffcnt=0,flcnt = 0,ficnt=0;
char ch,prech='\0';
cout<<"请输入一段文本:"<<endl;
while(cin >> ch){
bool bl=true;
if(prech == 'f'){
switch(ch){
case 'f':
++ffcnt;
bl=false;
break;
case 'l':
++flcnt;
break;
case 'i':
++ficnt;
break;
}
}
if(!bl)
prech='\0';
else
prech=ch;
}
cout<<"ff的数量是:"<<ffcnt<<endl;
cout<<"fl的数量是:"<<flcnt<<endl;
cout<<"fi的数量是:"<<ficnt<<endl;
return 0;
}
练习5.13 下面显示的的每个程序都有一个常见的编程错误,指出错误在哪里,然后修改它们。
unsigned acnt = 0,ecnt = 0,ioucnt = 0;
char ch = next_text();
switch (ch){
case 'a' :acnt++;
case 'e' :ecnt++:
default : ioucnt++;
}
这段程序的错误是在于每个case分支都缺少break语句。
unsigned index = some_value();
switch (index) {
case 1:
int ix=get_value();
ivec[ix] =index;
break;
default:
ix=ivec.size()-1;
ivec[ix]= index;
}
这段程序的错误是在case分支中定义并初始化了ix,同时在default分支中使用了该变量,此时如果控制流跳过case分支而直接到达default分支,则会试图使用未经初始化的变量,因而该程序无法通过编译。解决办法是把ix的定义放在switch语句之前:int ix;
unsigned evencnt = 0,oddcnt=0;
int digit = get_num() % 10;
switch(digit){
case 1,3,5,7,9:
oddcnt++;
break;
case 2,4,6,8,10;
evencnt++;
break;
}
这段程序的错误是在同一个case标签中放置了多个值,而C++规定一个case标签只能对应一个值。
unsigned ival=522,jval = 1024,kval=4096;
unsigned bufsize;
unsigned swt=get_bufcnt();
switch(swt){
case ival:
bufsize = ival * sizeof(int);
break;
case jval:
bufsize = jval * sizeof(int);
break;
case kval:
bufsize = kval * sizeof(int);
break;
}
这段程序的错误在于使用变量作为case标签的内容,C++规定,case标签的内容只能是整型常量表达式。要将第一行改为const。
练习5.14 编写一段程序,从标准输入中读取若干string对象并查找连续重复出现的单词。所谓连续重复出现的意思是:一个单词后面紧跟着这个单词本身。要求记录连续出现的最大次数和对应的单词。如果这样的单词存在,输出重复出现的最大次数;如果不存在,输出一条信息说明 任何单词都没有连续出现过。例如,如果输入是 how now now now brown cow cow,那么输出应该表明单词now连续出现了三次。
解答:
#include
#include
using namespace std;
int main(){
//定义3个字符串变量分别表示:
//当前操作的字符串、前一个字符串、当前出现次数最多的字符串
string currstring,prestring ="",maxstring;
//定义2个整型变量,分别表示:
//当前连续出现的字符串变量、当前出现次数最多的字符串数量
int currcnt=1,maxcnt=0;
while(cin>>currstring){//如果当前字符串和前一个字符串一致,更新状态
if(currstring == prestring){
++currcnt;
if(currcnt>maxcnt){
maxcnt=currcnt;//更新最大次数
maxstring=currstring;
}
}
else{//如果当前字符串与前一个字符串不一致,重置currcnt
currcnt=1;
}
prestring= currstring; //更新prestring以便下次循环使用
}
if(maxcnt>1)
cout<<"出现最多的字符串是:"<<maxstring<<",次数是:"<<maxcnt<<endl;
else
cout<<"每个字符串都只出现了一次"<<endl;
return 0;
}
练习5.15 说明下列循环的含义并改正其中的错误。
for(int ix=0;ix!=sz;++ix){/*...*/}
if(ix!=sz)
//...
在for语句里定义了ix,然后试图在for语句之外继续使用ix。
int ix;
for(ix!=sz;++ix){/*...*/}
有两个错误,一是变量ix未经初始化就直接使用,二是for语句的控制结果缺少第一句话,在语法上是错误的。
for(int ix=0;ix!=sz;++ix,++sz){/*...*/}
因为ix会和sz同步增长,所以这是死循环。
练习5.16 while循环特别适用那些条件保持不变、反复执行操作的情况。例如,当未到达文件末尾时,不断读取下一个值。for循环则更像是在按步骤迭代,它的索引值在某个范围内依次变化。根据每种循环的习惯用法各自编写一段成,然后分别用另一种循环改写。如果只能使用一种循环,你倾向于使用哪种?为什么?
解答:
如果只能 使用一种循环,作者倾向于使用for循环。for循环的有点是结构严谨,便于控制程序的逻辑。
练习5.17 假设有两个包含整数的vector对象,编写一段程序,检查其中一个vector对象是否是另一个的前缀。为了实现这一目标,对于两个不等长的vector对象,只需挑出长度较短的那个,把它所有元素和另一个vector对象比较即可。例如,如果两个vector对象的元素分别是0、1、1、2和0、1、1、2、3、5、8,则程序的返回结果应该是真。
解答:
#include
#include
using namespace std;
int main(){
vector<int> v1={0,1,1,2};
vector<int> v2={0,1,1,2,3,5,8};
vector<int> v3={3,5,8};
vector<int> v4={3,5,0,2,7};
auto it1=v1.begin();
auto it2=v2.begin();
auto it3=v3.begin();
auto it4=v4.begin();
while(it1!=v1.cend()&&it2!=v2.cend()){
if(*it1!=*it2){
cout<<"v1和v2不存在前缀关系"<<endl;
break;
}
++it1;
++it2;
}
if(it1==v1.cend()){
cout<<"v1是v2的前缀"<<endl;
}
if(it2==v2.cend()){
cout<<"v2是v1的前缀"<<endl;
}
return 0;
}
练习5.18 说明下列循环的含义并改正错误。
do
int v1,v2;
cout<<"please enter two numbers to sum:";
if(cin>>v1>>v2)
cout<<"sum is :" <<v1+v2<<endl;
while(cin);
这段代码的含义是每次循环读入两个整数并输出它们的和。但是因为do-while语句的循环体必须是一条语句或者一条语句块。所以应该将循环体用花括号括起来。
do{
//...
}while(int ival=get_response());
这段代码的含义是当get_response的返回值不为0时执行循环体。但是因为dowhile语句不允许在循环条件中定义变量,所以应该把定义放在do语句前面。
do{
int ival =get_response();
}while(ival);
这段代码的含义时当ival返回值不为0时执行循环体。但是因为出现在dowhile语句条件部分的变量必须定义在循环体之外,所以该程序是错误 的,应该将int ival放在do语句前面。
练习5.19 编写一段程序,使用do-while语句循环重复地执行下述任务:首先提示用户输入两个string对象,然后挑出较短的那个输出。
#include
#include
using namespace std;
int main(){
do{
cout<<"请输入两个字符串:"<<endl;
string str1,str2;
cin>>str1>>str2;
if(str1.size()<str2.size())
cout<<"长度较小的字符串是:"<<str1<<endl;
else if(str2.size()<str1.size())
cout<<"长度较小的字符串是:"<<str2<<endl;
else
cout<<"两字符串等长"<<endl;;
}while(cin);
return 0;
}
练习5.20 编写一段程序,从标准输入中读取string对象的序列直到继续出现两个相同的单词或所有单词都读完为止。使用for循环一次读取一个单词,当一个单词连续出现两次时使用break语句终止循环。输出连续重复的的单词,或者输出一个消息说明没有任何单词是连续重复出现的。
#include
#include
using namespace std;
int main(){
string currstring, prestring;
bool b1=true;
cout<<"请输入一组字符串:"<<endl;
while(cin>>currstring){
if(currstring==prestring){
b1=false;
cout<<"连续重复出现的字符串是:"<<currstring<<endl;
break;
}
prestring=currstring;
}
if(b1)
cout<<"没有连续出现的字符串"<<endl;
return 0;
}
练习5.21 修改5.5.1节练习题的程序,使其找到重复单词必须是以大写字母开头。
解答:
#include
#include
using namespace std;
int main(){
string currstring, prestring;
bool b1=true;
cout<<"请输入一组字符串:"<<endl;
while(cin>>currstring){
if(!isupper(currstring[0]))//如果开头字母不是大写则结束本次循环进入下次循环
continue;
if(currstring==prestring){
b1=false;
cout<<"连续重复出现的字符串是:"<<currstring<<endl;
break;
}
prestring=currstring;
}
if(b1)
cout<<"没有连续出现的字符串"<<endl;
return 0;
}
练习5.22 本节的最后一个例子是跳回到begin,其实使用循环能更好地完成该任务。重写这段代码,注意不再使用goto语句。
解答:
int sz;
do{
sz=get_size()
}while(sz<=0);
练习5.23 编写一段程序,从标准输入中读取两个整数,输出第一个数除以第二个数的结果。
解答:
#include
using namespace std;
int main(){
cout<<"请依次输入两个整数(除数和被除数):"<<endl;
int ival1,ival2;
cin>>ival1>>ival2;
if(ival2==0){
cout<<"除数不能为0"<<endl;
return -1;
}
cout<<"两数相除的结果是:"<<ival1/ival2<<endl;
return 0;
}
练习5.24 修改你的程序,使得当第二个数是0时抛出异常。先不要设定catch子句,运行程序并真的为除数输入0,看看会发生什么?
解答:
#include
#include
using namespace std;
int main(){
cout<<"请依次输入两个整数(除数和被除数):"<<endl;
int ival1,ival2;
cin>>ival1>>ival2;
if(ival2==0){
throw runtime_error("除数不能为0");
}
cout<<"两数相除的结果是:"<<ival1/ival2<<endl;
return 0;
}
在本题中,我们设定当检测出除数为0时抛出一个runtime_error异常,因为没有catch语句,所以系统只报告异常而不处理异常。在我的编译环境中,系统给出的报错信息是:
请依次输入两个整数(除数和被除数):
3 0
terminate called after throwing an instance of 'std::runtime_error'
what(): 除数不能为0
练习5.25 修改上一题的程序,使用try语句块去捕获异常。catch子句应该为用户输出一条提示信息,询问其是否输入新数并重新执行try语句块的内容。
#include
#include
using namespace std;
int main(){
cout<<"请依次输入两个整数(除数和被除数):"<<endl;
int ival1,ival2;
while(cin>>ival1>>ival2){
try{
if(ival2==0){
throw runtime_error("除数不能为0");
}
cout<<"两数相除的结果是:"<<ival1/ival2<<endl;
}catch(runtime_error)
{
cout<<err.what()<<endl;
cout<<"需要继续吗?y or n";
char ch;
cin>>ch;
if(ch!='y'&&ch!='Y')
break;
}
}
return 0;
}
最后编译运行报错:err was not declared in this scope。目前还没找到原因。