空语句
; //空语句
如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。
//重复读入数据直至到达文件末尾或某次输入的值等于sought
while (cin >> s && s != sought)
; //空语句
C++语言提供了两种按条件执行的语句。
有些编码风格要求在if或else之后 必须写上花括号(对 while 和 for 语句的循环体两端也有同样的要求) 。
这么做的好处是可以避免代码混乱不清,以后修改代码时如果想添加别的语句,也可以很容易地找到正确位置。
if(condition){
statement
}else if(condition){
statement
}else{
statement
}
举个例子,假如我们想统计五个元音字母在文本中出现的次数,程序逻辑应该如下所示:
要想实现这项功能,直接使用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标签的值比较。
break语句的作用是中断当前的控制流。此例中, break语句将控制权转移到switch语句外面。因为switch是 while循环体内唯一的语句,所以从switch语句中断出来以后,程序的控制权将移到while语句的右花括号处。此时 while语句内部没有其他语句要执行,所以 while会返回去再一次判断条件是否满足。
case关键字和它对应的值一起被称为case标签(case label)。case标签必须是整型常量表达式:
char ch = getval();
int ival = 42;
switch(ch){
case 3.14: //错误:case标签不是一个整数
case ival: //错误:case标签不是一个常量
// ...
任何两个 case标签的值不能相同,否则就会引发错误。另外,default也是一种特殊的case标签。
switch内部的控制流
理解程序在case标签之间的执行流程非常重要。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch 的结尾处才会停下来。
例如,也许我们想统计的是所有元音字母出现的总次数:
unsigned vowelCnt = 0;
//...
switch (ch){
//出现了a、e、 i、o或u中的任意一个都会将vowelCnt的值加1
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;
}
default标签
例如,可以增加一个计数值来统计非元音字母的数量,只要在 default分支内不断递增名为otherCnt 的变量就可以了:
//如果ch是一个元音字母,将相应的计数值加1
switch (ch){
case 'a': case 'e': case 'i': case 'o': case 'u':
++vowe1Cnt;
break;
default:
++otherCnt;
break ;
}
在这个版本的程序中,如果 ch 不是元音字母,就从 default标签开始执行并把otherCnt加1。
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在作用域内,但是没有被初始化
//...
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。
case true:
{
//正确:声明语句位于语句块内部
string file_name = get_file_name() ;
// ...
}
break ;
case false:
if (file_name.empty()) //错误:file_name 不在作用域之内
迭代语句通常称为循环,它重复执行操作直到满足某个条件才停下来。
定义在while条件部分或者while循环体内的变量每次迭代都经历从创建到销毁的过程。
while (condtion)
statement
牢记for语句头中定义的对象只在for循环体内可见。
for (init-statemen; condition; expression)
statement
for语句头中的多重定义
举个例子,我们用下面的循环把vector的元素拷贝一份添加到原来的元素后面:
//记录下v的大小,当到达原来的最后一个元素后结束循环
for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
v.push_back(v[i]);
在这个循环中,我们在 init-statement里同时定义了索引i和循环控制变量sz。
省略for语句头的某些部分
如果无须初始化,则我们可以使用一条空语句作为 init-statement。例如,对于在vector对象中寻找第一个负数的程序,完全能用for循环改写:
auto beg = v.begin();
for ( /*空语句 */ ; beg != v.end() && *beg >= 0; ++beg)
; //什么也不做
注意,分号必须保留以表明我们省略掉了init-statement。说得更准确一点,分号表示的是-一个空的init-statement。
省略condition 的效果等价于在条件部分写了一个true。因为条件的值永远是true,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去:
for (int i = 0; /*条件为空*/; ++i){
//对i进行处理,循环内部的代码必须负责终止迭代过程!
}
我们也能省略掉for语句头中的expression,但是在这样的循环中就要求条件部分或者循环体必须改变迭代变量的值。
举个例子,之前有一个将整数读入vector的 while循环,我们使用for语句改写它:
vector v;
for (int i; cin >> i; /*表达式为空*/)
v.push_back (i);
因为条件部分能改变i的值,所以这个循环无须表达式部分。其中,条件部分不断检查输入流的内容,只要读取完所有的输入或者遇到一个输入错误就终止循环。
范围for语句(range for statement)的语法形式是:
for (declaration : expression)
statement
vector v = {0,1,2,3,4,5,6,7,8,9};
//范围变量必须是引用类型,这样才能对元素执行写操作
for (auto &r : v) //对于v中的每一个元素
r *= 2; //将v中每个元素的值翻倍,改变了r所绑定的元素的值。
//范围for语句的定义来源于与之等价的传统for语句:
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg){
auto &r = *beg; //r必须是引用类型,这样才能对元素执行写操作
r*= 2; //将v中每个元素的值翻倍
}
学习了范围for语句的原理之后,我们也就不难理解为什么强调不能通过范围for语句增加vector对象的元素了。
在范围for语句中,预存了end()的值。一旦在序列中添加(删除)元素,end函数的值就可能变得无效了。
do while语句的语法形式如下所示:
do
statement
while (condition) ;
do while语句应该在括号包围起来的条件后面用一个 分号 表示语句结束。
我们可以使用do while循环(不断地)执行加法运算:
//不断提示用户输入一对数,然后求其和
string rsp; //作为循环的条件,不能定义在do的内部
do {
cout << "please enter two values: ";
int val1 = 0, val2 = 0 ;
cin >> val1 >> val2 ;
cout << "The sum of " << val1 << " and " << val2
<< " = " << vall + val2 << " \n\n"
<< "More? Enter yes or no: ";
cin >> rsp;
} while ( !rsp.empty() && rsp[0] != 'n' ) ;
break 语句只能出现在迭代语句或者 switch语句内部(包括嵌套在此类循环里的语句或块的内部)。break 语句的作用范围仅限于最近的循环或者switch:
string buf ;
while (cin >> buf && !buf.empty()){
switch (buf[o]){
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
例如,下面的程序每次从标准输入中读取一个单词。循环只对那些以下画线开头的单词感兴趣,其他情况下,我们直接终止当前的迭代并获取下一个单词:
string buf;
while (cin >> buf && !buf.empty()){
if (buf[0] != '_' )
continue; //接着读取下一个输入
//程序执行过程到了这里?说明当前的输入是以下画线开始的;接着处理buf……
}
注: 不要在程序中使用 goto语句,因为它使得程序既难理解又难修改。
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。
典型的异常包括 失去数据库连接 以及 遇到意外输入 等。处理反常行为可能是设计所有系统最难的一部分。
异常处理机制为程序 异常检测 和 异常处理 这两部分的协作提供支持。在 C++语言中,异常处理包括:
//首先检查两条数据是否是关于同一种书籍的
if (item1.isbn() != item2.isbn())
throw runtime_error ( "Data must refer to same ISBN");
//如果程序执行到了这里,表示两个ISBN是相同的
cout << item1 + item2 << endl;
在这段代码中,如果 ISBN不一样就抛出一个异常,该异常是类型runtime_error的对象。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。
类型runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。我们必须初始化 runtime_error的对象,方式是给它提供一个string对象或者一个C风格的字符串,这个字符串中有一些关于异常的辅助信息。
try语句块的通用语法形式是
try {
program-statements
} catch (exception-declaration){
handler-statements
} catch (exception-declaration){
handler-statements
} // ...
编写处理代码
在之前的例子里,我们使用了一个throw表达式以避免把两个代表不同书籍的Sales_item相加。
我们假设执行sales_item对象加法的代码是与用户交互的代码分离开来的。其中与用户交互的代码负责处理发生的异常,它的形式可能如下所示:
while (cin >> item1 >> item2) {
try {
//执行添加两个sales_item对象的代码
//如果添加失败,代码抛出一个runtime_error异常
} catch (runtime_error err){
//提醒用户两个ISBN必须一致,询问是否重新输入
cout << err.what()
<< "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if ( !cin ll c == 'n' )
break; //跳出while循环
}
}
程序本来要执行的任务出现在 try语句块中,这是因为这段代码可能会抛出一个runtime_error类型的异常。
try语句块对应一个catch子句,该子句负责处理类型为runtime_error的异常。如果try语句块的代码抛出了runtime error异常,接下来执行catch 块内的语句。
给用户的提示信息中输出了err.what()的返回值。我们知道err的类型是runtime_error,因此能推断what是runtime_error类的一个成员函数。每个标准库异常类都定义了名为what的成员函数,这些函数没有参数,返回值是C风格字符串(即 const char*)。其中,runtime_error的 what成员返回的是初始化一个具体对象时所用的string对象的副本。如果上一节编写的代码抛出异常,则本节的catch子句输出
Data must refer to same ISBN
Try Again? Enter y or n
函数在寻找处理代码的过程中退出
C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。
stdexcept头文件定义了几种常用的异常类,详细信息在下表中列出。
new头文件定义了bad_alloc异常类型,这种类型将在12.1.2节(第407页)详细介绍。
type_info头文件定义了bad_cast异常类型,这种类型将在19.2节(第731页))详细介绍。

标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。
我们只能以默认初始化的方式初始化 exception、bad_alloc和 bad _cast对象,不允许为这些对象提供初始值。
其他异常类型的行为则恰好相反:应该使用string对象或者C风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。
异常类型只定义了一个名为what的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char*。该字符串的目的是提供关于异常的一些文本信息。
what函数返回的C风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what返回的内容由编译器决定。