5 . 1 简单语句
- C++语言中的大多数语句都以分号结束,一个表达式,比如ival + 5 , 末尾加上分号就变成了表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃掉
- 求值结果:ival + 5; // 一条没什么实际用处的表达式语句
- cout << ival; // 一条有用的表达式语句
- 第一条语句没什么用处,因为虽然执行了加法,但是相加的结果没被使用。比较普遍的情况是,表达式语句中的表达式在求值时附带有其他效果,比如给变量赋了新值或者输出了结果。
空语句
- 最简单的语句是空语句(nullstatement),空语句中只含有一个单独的分号: ; //空语句
- 如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。例如,我们想读取输入流的内容直到遇到一个特定的值为止,除此之外什么事情也不做:
- while循环的条件部分首先从标准输入读取一个值并且隐式地检查cin,判断读取是否成功。假定读取成功,条件的后半部分检查读进来的值是否等于sought的值。如果发现了想要的值,循环终止;否则,从cin中继续读取另一个值,再一次判断循环的条件。
别漏写分号,也别多写分号
- 因为空语句是一条语句,所以可用在任何允许使用语句的地方。由于这个原因,某些看起来非法的分号往往只不过是一条空语句而已,从语法上说得过去。下面的片段包含两条语句:表达式语句和空语句。
- ival = vl + v2;; / / 正确:第二个分号表示一条多余的空语句
- 多余的空语句一般来说是无害的,但是如果在if或者while的条件后面跟了一个额外的分号就可能完全改变程序员的初衷。例如,下面的代码将无休止地循环下去:
- 无终止条件 分号使条件改变失效
- 虽然从形式上来看执行递增运算的语句前面有缩进,但它并不是循环的一部分。循环条件后面跟着的分号构成了一条空语句,它才是真正的循环体。
复合语句(块)
- 复合语句(compoundstatement)是指用花括号括起来的(司能为空的)语句和声明的序列,复合语句也被称作块(block),一个块就是一个作用域(参见2.2.4节,第43页),在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。
- 如果在程序的某个地方,语法上需要一条语句,但是逻辑上需要多条语句,则应该使用复合语句。例如,while或者for的循环体必须是一条语句,但是我们常常需要在循环体内做很多事情,此时就需要将多条语句用花括号括起来,从而把语句序列转变成块。
- 举个例子,回忆1.4.1节(第10页)的while循环:
- 程序从逻辑上来说要执行两条语句,但是while循环只能容纳一条。此时,把要执行的语句用花括号括起来,就将其转换成了一条(复合)语句。
- 块不可以使用分号进行结尾
5.2语句作用域
- 可以在if、switch、while和for语句的控制结构内定义变量。定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了:
5.3条件语句
- C++语言提供了两种按条件执行的语句。一种是if语句,它根据条件决定控制流;另外一种是switch语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条。
5.3.1 if语句
- if语句(ifstatement)的作用是:判断一个指定的条件是否为真,根据判断结果决定是否执行另外一条语句。if语句包括两种形式:一种含有else分支,另外一种没有。简单if语句的语法形式是
- 在这两个版本的if语句中,condition都必须用圆括号包围起来。condition可以是一个表达式,也可以是一个初始化了的变量声明(参见5.2节,第155页)。不管是表达式还是变量,其类型都必须能转换成(参见4.11节,第141页)布尔类型。通常情况下,statement和statement2是块语句。
- 如果condition为真,执行statemento当statement执行完成后,程序继续执行if语句后面的其他语句。
- 如果condition为假,跳过statemento对于简单if语句来说,程序继续执行if语句后面的其他语句;对于ifelse语句来说,执行statement2
使用 if else语句
- 我们举个例子来说明if语句的功能,程序的目的是把数字形式表示的成绩转换成字母形式。假设数字成绩的范围是从0到100(包括100在内),其中100分对应的字母形式是"A++”,低于60分的成绩对应的字母形式是“F”。其他成绩每10个划分成一组;60到69(包括69在内)对应字母"D”、70到79对应字母"C”,以此类推。使用vector对象存放字母成绩所有可能的取值:
- 判断grade的值是否小于60,根据结果选择执行if分支还是else分支。在else分支中,由成绩计算得到一个下标,具体过程是:首先从grade中减去50,然后执行整数除法(参见4.2节,在125页),去掉余数后所得的商就是数组scores对应的下标。
悬垂else
- 当一个if语句嵌套在另一个if语句内部时,很可能if分支会多于else分支。事实上,之前那个成绩转换的程序就有4个if分支,而只有2个else分支。这时候问题出现了:我们怎么知道某个给定的else是和哪个if匹配呢?
- 这个问题通常称作悬垂else(danglingelse),在那些既有if语句又有ifelse语句的编程语言中是个普遍存在的问题。不同语言解决该问题的思路也不同,就C而言,它规定else与离它最近的尚未匹配的if匹配,从而消除了程序的二义性。当代码中if分支多于else分支时,程序员有时会感觉比较麻烦。举个例子来说明,对于添加加号减号的那个最内层的ifelse语句,我们用另外一组条件改写它:
5.3.2switch语句
- switch语句(switchstatement)提供了一条便利的途径使得我们能够在若干固定选项中做出选择。举个例子,假如我们想统计五个元音字母在文本中出现的次数,程序逻辑应该如下所示:
- 从输入的内容中读取所有字符。令每一个字符都与元音字母的集合比较。如果字符与某个元音字母匹配,将该字母的数量加1。显示结果。
- 要想实现这项功能,直接使用switch语句即可:
- switch语句首先对括号里的表达式求值,该表达式紧跟在关键字switch的后面,可以是一个初始化的变量声明(参见5.2节,第155页)。表达式的值转换成整数类型,然后与每个case标签的值比较。如果表达式和某个case标签的值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达了switch的结尾或者是遇到一条break语句为止。我们将在5.5.1节(第170页)详细介绍break语句,简言之,break语句的作用是中断当前的控制流。此例中,break语句将控制权转移到switch语句外面。因为switch是while循环体内唯一的语句,所以从switch语句中断出来以后,程序的控制权将移到while语句的右花括号处。此时while语句内部没有其他语句要执行,所以while会返回去再一次判断条件是否满足。
- 如果switch语句的表达式和所有case都没有匹配上,将直接跳转到switch结构之后的第一条语句。刚刚说过,在上面的例子中,退出switch后控制权回到while语句的条件部分。case关键字和它对应的值一起被称为case标签(caselabel)。case标签必须是整型常量表达式(参见2.4.4节,第58页):
switch内部的控制流
- 理解程序在case标签之间的执行流程非常重要。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。要想避免执行后续case分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个case标签之前应该有一条break语句。
- 然而,也有一些时候默认的switch行为才是程序真正需要的。每个case标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break语句,使得程序能够连续执行若干个case标签。例如,也许我们想统计的是所有元音字母出现的总次数:
- 在上面的代码中,几个case标签连写在一起,中间没有break语句。因此只要ch是元 音字母,不管到底是五个中的哪一个都执行相同的代码。
- C++程序的形式比较自由,所以case标签之后不一定非得换行。把几个case标签写在一行里,强调这些case代表的是某个范围内的值:
default标签
- 如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签(defaultlabel)后面的语句。例如,可以增加一个计数值来统计非元音字母的数量,只要在default分支内不断递增名为otherCnt的变量就可以了:
switch内部的变量定义
- 如前所述,switch的执行流程有可能会跨过某些case标签。如果程序跳转到了某个特定的case,则switch结构中该case标签之前的部分会被忽略掉。这种忽略掉一部分代码的行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办?
- 答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。
- 假设上述代码合法,则一旦控制流直接跳到false分支,也就同时略过了变量filename和ival的初始化过程。此时这两个变量位于作用域之内,跟在false之后的代码试图在尚未初始化的情况下使用它们,这显然是行不通的。因此C++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
- 如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。
5 . 4 迭代语句
- 迭代语句通常称为循环,它重复执行操作直到满足某个条件才停下来。while和 for 语句在执行循环体之前检查条件,do while语句先执行循环体,然后再检查条件。
- 定义在while条件部分或者while循环体内的变量每次迭代都经历从创建到销毁的过程。
- while循环结束之后 循环控制变量仍然可以使用
- 第一个循环从标准输入中读取数据,我们一开始不清楚循环要执行多少次,当cin读取到无效数据、遇到其他一些输入错误或是到达文件末尾时循环条件失效。第二个循环重复执行直到遇到一个负值为止,循环终止后,beg或者等于v.end(),或者指向v中一个小于0的元素。可以在while循环外继续使用beg的状态以进行其他处理。
传统for循环的执行流程
- 我们以3.2.3节 (第 85页 )的 for循环为例:
- 求值的顺序如下所示:
- 1循环开始时,首先执行一次init-statement此例中,定义index并初始化为0。
- 2.接下来判断condition.如果index不等于s.size()而且在s[index]位置的字符不是空白,则执行for循环体的内容。否则,循环终止。如果第一次迭代时条件就为假,for循环体一次也不会执行。
- 3.如果条件为真,执行循环体。此例中,for循环体将s[index]位置的字符改写成大写形式。
- 4.最后执行express。此例中,将index的值加1。这4步说明了for循环第一次迭代的过程。其中第1步只在循环开始时执行一次,第2、3、4步重复执行直到条件为假时终止,也就是在s中遇到一个空白字符或者index大于s.size()时终止。
- 牢记for语句头中定义的对象只在for循环体内可见。因此在上面的例子中,for循环结束后index就不可用了。
省略for语句头的某些部分
- for语句头能省略掉init-statement condition和expression中的任何一个(或者全部)。如果无须初始化,则我们可以使用一条空语句作为init-statement.例如,对于在vector对象中寻找第一个负数的程序,完全能用for循环改写:
- 注意,分号必须保留以表明我们省略掉了init-statementc说得更准确一点,分号表示的是一个空的init-statement。在这个循环中,因为所有要做的工作都在for语句头的条件和表达式部分完成了,所以for循环体也是空的。其中,条件部分决定何时停止查找,表达式部分递增迭代器。
- 省略condition的效果等价于在条件部分写了一个true。因为条件的值永远是true,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去:
- 我们也能省略掉for语句头中的expresssion,但是在这样的循环中就要求条件部分或者循环体必须改变迭代变量的值。举个例子,之前有一个将整数读入vector的while循环,我们使用for语句改写它:
- 因为条件部分能改变i 的值,所以这个循环无须表达式部分。其中,条件部分不断检查输入流的内容,只要读取完所有的输入或者遇到一个输入错误就终止循环。
- 因为对于do-while来说先执行语句或者块,后判断条件,所以不允许在条件部分定义变量:
5.5跳转语句
- 跳转语句中断当前的执行过程。C++语言提供了4种跳转语句:break、continue,goto和returno本章介绍前三种跳转语句,return语句将在6.3节(第199页)进行介绍。
- 标记为#1的break语句负责终止连字符case标签后面的for循环。它不但不会终止switch语句,甚至连当前的case分支也终止不了。接下来,程序继续执行for循环之后的第一条语句,这条语句可能接着处理连字符的情况,也可能是另一条用于终止当前分支的break语句。标记为#2的break语句负责终止switch语句,但是不能终止while循环。执行完这个break后,程序继续执行while的条件部分
5.6 try语句块和异常处理
- 异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。
- 当程序的某部分检测到一个它无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障将在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。
- 如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。例如,如果程序的问题是输入无效,则异常处理部分可能会要求用户重新输入正确的数据;如果丢失了数据库连接,会发出报警信息。
- 异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
5.6.1throw表达式
- 程序的异常检测部分使用throw表达式引发一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句。
- 举个简单的例子,回忆1.5.2节(第20页)把两个Sales_item对象相加的程序。这个程序检查它读入的记录是否是关于同一种书籍的,如果不是,输出一条信息然后退出。
- try语句块的一开始是关键字try,随后紧跟着一个块,这个块就像大多数时候那样是花括号括起来的语句序列。
- 跟在try块之后的是一个或多个catch子句。catch子句包括三部分:关键字catch、括号内一个(可能未命名的)对象的声明(称作异常声明,exceptiondeclaration)以及一个块。当选中了某个catch子句处理异常之后,执行与之对应的块。catch-旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。try语句块中的program-statements组成程序的正常逻辑,像其他任何块一样,program-statements可以有包括声明在内的任意C++语句。一如往常,try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
- 程序本来要执行的任务出现在try语句块中,这是因为这段代码可能会抛出一个runtime_error类型的异常。try语句块对应catch子句,该子句负责处理类型为runtime_error的异常。
- 如果try语句块的代码抛出了runtime_error异常,接下来执行catch块内的语句。在我们书写的catch子句中,输出一段提示信息要求用户指定程序是否继续。如果用户输入,n,,执行break语句并退出while循环;否则,直接执行while循环的右侧花括号,意味着程序控制权跳回到while条件部分准备下一次迭代。给用户的提示信息中输出了err.what()的返回值。我们知道err的类型是runtime_error,因此能推断what是runtime_error类的一个成员函数(参见1.5.2节,第20页)。每个标准库异常类都定义了名为what的成员函数,这些函数没有参数,返回值是C风格字符串(即const char*),其中,runtime_error的what成员返回的是初始化一个具体对象时所用的string对象的副本。如果上一节编写的代码抛出异常,则本节的catch子句输出
- throw 抛出错误信息和错误对象绑定;try捕捉错误信息;catch处理,输出之前throw中 错误信息和错误对象绑定的结果
5.6.3标准异常
- C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
- exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
- stdexcept头文件定义了几种常用的异常类,详细信息在表5.1中列出。
- new头文件定义了bad_alloc异常类型,这种类型将在12.1.2节(第407页)详细介绍。
- type_info头文件定义了bad_cast异常类型,这种类型将在19.2节(第731页)详细介绍
- what函数返回的C风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what返回的内容由编译器决定。
小结
- C++语言仅提供了有限的语句类型,它们中的大多数会影响程序的控制流程:
- while、for和dowhile语句,执行迭代操作。
- if和switch语句,提供条件分支结构。
- continue语句,终止循环的当前一次迭代。
- break语句,退出循环或者switch语句。
- goto语句,将控制权转移到一条带标签的语句。
- try和catch,将一段可能抛出异常的语句序列括在花括号里构成try语句块。catch子句负责处理代码抛出的异常。
- throw表达式语句,存在于代码块中,将控制权转移到相关的catch子句。
- return语句,终止函数的执行。我们将在第6登介绍return语句。除此之外还有表达式语句和声明语句。表达式语句用于求解表达式,关于变量的声明和定义在第2章已经介绍过了。
词汇
- 复合语句(compound statement)和块是同义词。
- 异常处理代码(exception handler)程序某处引发异常后,用于处理该异常的另一处代码。和 catch子句是同义词。
- 异常安全(exception safe) 是一-个术语,表示的含义是当抛出异常后,程序能执行正确的行为。
- 表达式语句(expression statement)即一条表达式后面跟上一个分号,令表达式执行求值过程。
- 控制流(flow of control) 程序的执行路径
- 带标签语句(labeled statement)前面带有标签的语句。所谓标签是指一个标识符以及紧跟着的一个冒号。对于同一个标识符来说,用作标签的同时还能用于其他目的,互不干扰。
- 空 语 句 (null statement)只含有一个分号的语句
- 引 发 (raise)含义类似于throw。在 C++语言中既可以说抛出异常,也可以说引发异常。
- 范围 for 语 句 (range for statement)在一个序列中进行迭代的语句。
- terminate是一个标准库函数,当异常没有 被捕捉到时调用。terminate终止当前程序的执行。