if
语句
if else
语句if
语句else
(dangling else)switch
语句
switch
内部的控制流break
容易引发缺陷default
标签switch
内部的变量定义while
语句
while
循环for
语句
for
语句头中的多重定义for
语句头的某些部分
init-statement
condition
expression
范围for
语句 (range for statement)do while
语句break
语句continue
语句goto
语句try
语句块和异常处理
throw
表达式try
语句块
末尾加上分号就变成了表达式语句(expression statement)
如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。
例如,我们想读取输入流的内容直到遇到一个特定的值为止,除此之外什么事情也不做:
// 重复读入数据直至到达文件末尾或某次输入的值等于 sought
while (cin >> s && s != sought)
; // 空语句
复合语句(compound statement)是指用花括号括起来的语句和声明的序列,复合语句也被称作块(block)。一个块就是一个作用域。在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。
所谓空块,是指内部没有任何语句的一对花括号。空块的作用等价于空语句:
while (cin >> s && s != sought)
{
} // 空块
可以在if
、switch
、while
、和for
语句的控制结构内定义变量。定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了。
练习5.4:说明下列例子的含义,如果存在问题,试着修改它。
(a) while (string::iterator iter != s.end()) {
/* ...*/ }
(a)是非法的,它的原意是希望在while
语句的控制结构当中定义一个string::iterator
类型的变量iter
,然后判断iter
是否达到了s
的末尾,只要还没有到达末尾就执行循环体的内容。
但是该式把变量的定义和关系判断混合在了一起,如果要使用iter
与其他值比较,必须首先为iter
赋初值。
// 修改后的程序应该是:
string::iterator iter = s.begin();
whilie (iter != s.end())
{
++iter;
/*...*/
}
(b) while (bool status = find(word)) {
/* ...*/ }
if (!status) {
/* ...*/ }
(b)时非法的,变量status
定义在while
循环控制结构的内部,其作用域仅限于while
循环。
if
语句已经位于while
循环的作用域之外,status
在if
语句内是一个未命名的无效变量。
要想在if
语句中继续使用status
,需要把它定义在while
循环之前。
// 修改后的程序应该是:
bool status;
while (status = find(word)) {
/* ...*/}
if (!status) {
/* ...*/}
if
语句if else
语句例子:假设数字成绩的范围是从0
到100
(包括100在内),其中100
分对应的字母形式是“A++”,低于60
分的成绩对应的字母形式是“F”。
其他成绩每 10 个划分成一组:60
到69
(包括69在内)对应字母“D”、70
到79
对应字母“C”,以此类推。
const vector<string> scores = {
"F", "D", "C", "B", "A", "A++"};
string lettergrade;
if (grade < 60)
lettergrade = scores[0];
else
lettergrade = scores[(grade-50)/10];
if
语句例子:试着给那些合格的成绩后面添加一个加号或减号。
如果成绩的末位是8
或者9
,添加一个加号;如果末位是0
、1
或2
,添加一个减号:
if (grade % 10 > 7)
lettergrade += '+'; // 末尾是8或者9的成绩添加一个加号
else if (grade % 10 < 3)
lettergrade += '-'; // 末尾是0、1或者2的成绩添加一个减号
else
(dangling else)C++规定else
与离它最近的尚未匹配的if
匹配,从而消除程序的二义性。
// 错误:实际的执行过程并非像缩进格式显示的那样;else分支匹配的是内层if语句
if (grade % 10 >= 3)
if (grade % 10 > 7)
lettergrade += '+'; // 末尾是8或者9的成绩添加一个加号
else
lettergrade += '--'; // 末尾是3、4、5、6或者7的成绩添加一个减号!
/*虽然我们希望当grade的末位小于3时执行else分支,然而不管我们是什么意图也不管程序如何锁紧,这里的else分支其实是内层if语句的一部分*/
执行过程实际上等价于如下形式:
// 缩进格式与执行过程相符,但不是程序员的意图
if (grade % 10 >= 3)
if (grade % 10 > 7)
lettergrade += '+'; // 末尾是8或者9的成绩添加一个加号
else
lettergrade += '--'; // 末尾是3、4、5、6或者7的成绩添加一个减号!
要想使else
分支和外层的if
语句匹配起来,可以加个花括号:
// 末尾是8或者9的成绩添加一个加号,末尾是0、1或者2的成绩添加一个减号
if (grade % 10 >= 3) {
if (grade % 10 > 7)
lettergrade += '+'; // 末尾是8或者9的成绩添加一个加号
} else
lettergrade += '-'; // 末尾是0、1或者2的成绩添加一个减号
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;
}
}
此例中,break
语句将控制权转移到switch
语句外面。因为switch
是while
循环体内唯一的语句,所以从switch
语句中断出来以后,程序的控制权将移到while
语句的右花括号处。此时while
语句内部没有其他语句要执行,所以while
会返回去再一次判断条件是否满足。
case
关键字和它对应的值一起被称为case标签(case label)。
switch
内部的控制流每个case
标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break
语句,使得程序能够连续执行若干个case
标签。
// 统计所有元音字母出现的总次数
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 是元音字母,不管到底是五个钟的哪一个都执行相同的代码。
也可以把几个case
标签写在一行里,强调这些case
代表的是某个范围内的值:
switch (ch)
{
// 另一种合法的书写形式
case 'a'; case 'e'; case 'i'; case 'o'; case 'u';
++vowelCnt;
break;
}
练习5.12:修改统计元音字母的程序,使其能统计以下含有两个字符的字符序列的数量:ff、fl 和 fi。
/* 练习5.12:修改统计元音字母的程序,使其能统计以下含有两个字符的字符序列的数量:ff、fl 和 fi。
【解答】
我们的设定是一个字符只会被统计一次。
如果用户输入的序列是 xxxxxfflxxx,则统计结果是ff: 1次、fl: 0次、fi: 0次。
如果用户输入的序列是 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 << "Please enter a text: " << 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 << "The number of ff is: " << ffCnt << endl;
cout << "The number of fl is: " << flCnt << endl;
cout << "The number of fi is: " << fiCnt << endl;
return 0;
}
break
容易引发缺陷break
语句的作用是中断当前的控制流。
注意:尽管switch
语句不是非得在最后一个标签后面写上break
,但是为了安全起见,最好这么做。因为这样的话,即便以后再增加新的case
分支,也不用再在前面补充break
语句了。
default
标签例子:增加一个计数值来统计非元音字母的数量,只要在default
分支内不断递增名为otherCnt
的变量就可以了:
// 如果 ch 是一个元音字母,将相应的计数值加 1
switch (ch) {
case 'a'; case 'e'; case 'i'; case 'o'; case 'u';
++vowelCnt;
break;
default:
++otherCnt;
break;
}
建议:即使不准备在default
标签下做任何工作,定义一个default
标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。
如果switch
结果以一个空的default
标签作为结束,则该default
标签后面必须跟上一条空语句或一个空块。
switch
内部的变量定义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();
// ...
}
break;
case false:
if (file_name.empty()) // 错误:file_name 不在作用域之内
练习5.13:下面显示的每个程序都含有一个常见的编程错误,指出错误在哪里,然后修改它们(P164)。
【出题思路】
switch
语句有几个语法要点:
break;
语句,case
标签职能有一个值且不能是变量。(a) 的错误是在每个 case
分支中都缺少了 break
; 语句,造成的后果是一旦执行了前面的 case
分支,必定还会继续执行接下来的其他 case
分支。
// 修改后的程序如下所示:
unsigned aCnt = 0, eCnt = 0, iouCnt = 0;
char ch = next_text();
switch (ch) {
case 'a':
aCnt++;
break;
case 'e':
eCnt++;
break;
default:
iouCnt++;
break;
}
(b) 的错误是在 case
分支中定义并初始化了变量 ix
,同时在default
分支中使用了该变量,此使如果控制流跳过case
分支而直接到达default
分支,则会试图使用未经初始化的变量,因而该程序无法通过编译。
解决办法是,把ix
的帝国一放置在switch
语句之前。
// 修改后的程序如下所示:
unsigned index = some_value();
int ix;
switch (index) {
case 1:
ix = get_value();
ivec[ ix ] = index;
break;
default:
ix = ivec.size() - 1;
ivec[ ix ] = index;
}
© 的错误是在同一个case
标签中放置了多个值,而C++规定一个case
标签只能对应一个值。
// 修改后的程序如下所示:
unsigned evenCnt = 0, oddCnt = 0;
int digit = get_num() % 10;
switch (digit) {
case 1:
case 3:
case 5:
case 7:
case 9:
oddCnt++;
break;
case 2:
case 4:
case 6:
case 8:
case 10:
evenCnt++;
break;
}
(d) 的错误是使用变量作为case
标签的内容,C++规定,case
标签的内容只能是整型常量表达式。
// 修改后的程序如下所示:
const unsigned ival = 512, 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;
}
while
语句while
循环while
循环比较合适,比如读取输入的内容就是如此。while
循环,这就是我们想在循环结束后访问循环控制变量。/* 练习5.14:编写段程序,从标准输入中读取若干 string 对象并查找连续重复出现的单词。
所谓连续重复出现的意思是:一个单词后面紧跟着这个单词本身。要求记录连续重复出现的最大次数以及对应的单词。
如果这样的单词存在,输出重复出现的最大次数;
如果不存在,输出一条信息说明任何单词都没有连续出现过。
例如,如果输入是
how now now now brown cow cow
那么输出应该表明单词 now 连续出现了 3 次。*/
#include
#include
using namespace std;
int main()
{
string currString, preString = "", maxString;
int currCnt = 1, maxCnt = 0;
while (cin >>currString)
{
if (currString == preString)
{
++currCnt;
if (currCnt > maxCnt)
{
maxCnt = currCnt;
maxString = currString;
}
}
else // 如果当前字符串与前一个字符串不一致,重置 currCnt
{
currCnt = 1;
} // 更新 preString 以便于下一次循环使用
preString = currString;
}
if (maxCnt > 1)
cout << "The most frequently occured string is: " << maxString
<< ", and the number of occurence is: " << maxCnt << endl;
else
cout << "Every string occurrs only once." << endl;
return 0;
}
for
语句for 语句的语法形式是:
for (init-statement; condition; expression)
statement
Note: 牢记for
语句头中定义的对象只在for
循环体内可见。因此在上面的例子中,for
循环结束后index
就不可用了。
for
语句头中的多重定义// 用下面的循环把vector的元素拷贝一份添加到原来的元素后面
// 记录下 v 的大小,当到达原来的最后一个元素后结束循环
for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
v.push_back(v[i]);
// 这个循环中,我们同时定义了索引 i 和循环控制变量 sz
for
语句头的某些部分for
语句头能省略掉init-statement
、condition
和expression
中的任何一个(或者全部)。
如果无须初始化,则我们可以使用一条空语句作为 init-statement
。
init-statement
例如,对于vector
对象中寻找第一个负数的程序,完全能用for
循环改写:
auto beg = v.begin();
for (/* 空语句 */; beg != v.end() && *beg >= 0; ++beg)
; // 什么也不做
注意,分号必须保留以表明我们省略掉了init-statement
。说得更准确一点,分号表示的是一个空的init-statement
。
要做的工作都在for
语句头的条件和表达式部分完成了,所以for
循环体也是空的。其中,条件部分决定何时停止查找,表达式部分递增迭代器。
condition
省略condition
的效果等价于在条件部分写了一个true
。因为条件的值永远是true
,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去:
for (int i = 0; /* 条件为空 */; ++i) {
// 对 i 进行处理,循环内部的代码必须负责终止迭代过程!
}
expression
举例:之前有一个将整数读入vector
的while
循环,我们使用for
语句改写它:
vector<int> v;
for (int i; cin >> i; /* 表达式为空 */)
v.push_back(i);
因为条件部分能改变i
的值,所以这个循环无须表达式部分。
其中,条件部分不断检查输入流的内容,只要读取完所有的输入或者遇到一个输入错误就终止循环。
范围for
语句 (range for statement)for (declaration : expression)
statement
expression
必须是一个序列,比如用花括号括起来的初始值列表、数组、vector、string,这些类型的共同特点是拥有能返回迭代器的begin
和end
成员。
declaration
中确保类型相容最简单的办法是使用auto
类型说明符。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
举例:把 vector 对象中的每个元素都翻倍
vector<int> v = {
0,1,2,3,4,5,6,7,8,9};
// 范围变量必须是引用类型,这样才能对元素执行写操作
for (auto &r : v)
r *= 2; // 将 v 中每个元素的值翻倍
do while
语句do
statement
while (condition);
// 使用 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
<< " = " << val1 + val2 << "\n\n"
<< "More? Enter yes or no: ";
cin >> rsp;
} while (!rsp.empty() && rsp[0] != 'n');
break
语句break
语句负责终止离它最近的 while
、do while
、for
或switch
语句,并从这些语句之后的第一条语句开始继续执行。
break
语句只能出现在迭代语句或者switch
语句内部(包括嵌套在此类循环里的语句或块的内部)。break
语句的作用范围仅限于最近的循环或者switch
:
/* 练习5.20:编写一段程序从标准输入中读取 string 对象的序列
直到连续出现两个相同的单词或者所有单词都读完为止。
使用 while 循环次读取一个单词,当一个单词连续出现两次时使用 break 语句终止循环。
输出连续重复出现的单词,或者输出一个消息说明没有任何单词是连续重复出现的。*/
#include
#include
using namespace std;
int main()
{
string currString, preString;
bool bl = true;
cout << "Please enter a group of strings: " << endl;
while (cin >> currString)
{
if (currString == preString)
{
bl = false;
cout << "连续出现的字符串是: " << currString << endl;
break;
}
preString = currString;
}
if (bl)
cout << "没有连续出现的字符串" << endl;
return 0;
}
/* 本题用到了一个起标识作用的布尔值 bl,当 bl 为真时表示没有连续出现的字符串,
一旦我们发现了存在连续出现的字符串,就立即把 bl 的值置为 false。 */
continue
语句continue
语句终止最近的循环中的当前迭代并立即开始下一次迭代。
break
语句类似的是,出现在嵌套循环中的 continue
语句也仅作用于离它最近的循环。break
语句不同的是,continue
虽然终止了当前迭代,但是并不终止循环;而 break
语句则直接跳出循环。continue
语句中断当前的迭代,但是仍然继续执行循环。
while
或者 do while
语句来说,继续判断条件的值;for
循环来说,继续执行 for
语句头的 expression;
范围 for
语句来说,则是用序列中的下一个元素初始化循环控制变量。例子:下面的程序每次从标准输入中读取一个单词。循环只对那些以下划线开头的单词感兴趣,其他情况下,我们直接终止当前的迭代并获取下一个单词:
string buf;
while (cin >> buf && !buf.empty()) {
if (buf[0] != '_')
continue; // 接着读取下一个输入
// 程序执行过程到了这里?说明当前的输入是以下划线开始的;接着处理 buf……
}
goto
语句goto语句的作用是从goto
语句无条件跳转到同一函数内的另一条语句。
注意:不要再程序中使用goto
语句,因为它使得程序既难理解又难修改。
try
语句块和异常处理典型的异常包括失去数据库连接以及遇到意外输入等。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
throw表达式(throw expression),异常检测部分使用throw
表达式来表示它遇到了无法处理的问题。我们说throw
引发(raise)了异常。
try语句块(try block),异常处理部分使用try
语句块处理异常。try
语句块以关键字try
开始,并以一个或多个catch子句(catch clause)结束。try
语句块中代码抛出的异常通常会被某个catch
子句处理。因为catch
子句“处理”异常,所以它们也被称作异常处理代码(exception handler)。
一套异常类(exception class),用于在throw
表达式和相关的catch
子句之间传递异常的具体信息。
throw
表达式举例:之前把两个Sales_item
对象相加的程序,检查它读入的记录是否是关于同一种书籍的;如果不是,输出一条信息然后退出。
Sales_item item1, item2;
cin >> item1 >> item2;
// 首先检查 item1 和 item2 是否表示同一种书籍
if (item1.isbn() == item2.isbn()) {
cout << item1 + 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是相同的
cout << item1 + item2 << endl;
在这段代码中,如果ISBN
不一样就抛出一个异常,该异常是类型runtime_error
的对象。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。
类型runtime_error
是标准库异常类型的一种,定义在stdexcept
头文件中。
练习5.24:修改你的程序,使得当第二个数是 0 时抛出异常。
/* 练习5.24:修改你的程序,使得当第二个数是 0 时抛出异常。
先不要设定 catch 子句,运行程序并真的为除数输入 0,看看会发生什么?*/
#include
#include
using namespace std;
int main()
{
cout << "Please enter the dividend and the divisor: " << endl;
int ival1, ival2;
cin >> ival1 >> ival2;
if (ival2 == 0)
{
// cout << "除数不能为0" << endl;
// return -1;
throw runtime_error("The divisor can't be 0.");
}
cout << "The result is: " << ival1 / ival2 << endl;
return 0;
}
/*
在本题中,我们设定当检测到除数为 0 时抛出一个 runtime_error 异常,因为没有 catch 语句,所以系统只报告异常而并不处理它。
3 0
terminate called after throwing an instance of 'std::runtime_error'
what(): The divisor can't be 0.
*/
try
语句块try
语句块的通用语法形式是:
try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
} // ...
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 || c == 'n')
break; // 跳出 while 循环
}
}
runtime_error
的what
成员返回的是初始化一个具体对象时所用的string
对象的副本。
如果上一节编写的代码抛出异常,则本节的catch
子句输出:
Data must refer to same ISBN
Try Again? Enter y or n
练习5.25:修改上一题的程序,使用 try 语句块去捕获异常。使用 try-catch 结构实现完整的异常处理机制。
/* 练习5.25:修改上一题的程序,使用 try 语句块去捕获异常。
catch 子句应该为用户输出一条提示信息,询问其是否输入新数并重新执行 try 语句块的内容。
【出题思路】使用 try-catch 结构实现完整的异常处理机制。
*/
#include
#include
using namespace std;
int main()
{
cout << "Please enter the dividend and the divisor: " << endl;
int ival1, ival2;
while (cin >> ival1 >> ival2)
{
try
{
if (ival2 == 0)
{
throw runtime_error("The divisor can't be 0.");
}
cout << "The result is: " << ival1 / ival2 << endl;
}
catch(runtime_error err)
{
cout << err.what() << endl;
cout << "Do you want to continue (y or n)? ";
char ch;
cin >> ch;
if (ch != 'y' && ch != 'Y')
break;
}
}
return 0;
}
提示:编写异常安全的代码非常困难
在异常发生期间正确执行“清理”工作的程序被称作异常安全(exception safe)的代码。
exception
头文件定义了最通用的异常类exception
。它只报告异常的发生,不提供任何额外信息。stdexcept
头文件定义了几种常用的异常类。new
头文件定义了bad_alloc
异常类型type_info
头文件定义了bad_cast
异常类型表5.1: | 定义的异常类 |
---|---|
exception |
最常见的问题 |
runtime_error |
只有在运行时才能检测出的问题 |
range_error |
运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error |
运行时错误:计算上溢 |
underflow_error |
运行错误:计算下溢 |
logic_error |
程序逻辑错误 |
domain_error |
逻辑错误:参数对应的结果值不存在 |
invalid_argument |
逻辑错误:无效参数 |
length_error |
逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range |
逻辑错误:使用一个超出有效范围的值 |