一、简单语句
一个表达式,比如ival+5,末尾加上分号就变成了表达式语句。表达式语句的作用是执行表达式并丢弃掉求值结果。
空语句:只有一个单独的分号
如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句:
while(cin>>s && s!=sought)
;//空语句
使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。
别漏写分号,也别多写分号:
ival=v1+v2;; //正确:第二个分号表示一条多余的空语句
//错误:额外的分号,循环体是那条空语句
while(iter!=svec.end()) ; //while循环体是那条空语句
++iter; //递增运算并不属于循环的一部分
多余的空语句并非总是无害的
复合语句(块)
复合语句是指用花括号括起来的(可能为空)语句和声明的序列,复合语句也被称作块。
一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。
块不以分号为结束。
所谓空块,是指内部没有任何语句的一对话括号。空块的作用等价于空语句:
while(cin>>s && s!=sought)
{} //空块
二、语句作用域
定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了:
while(int i=get_num())
cout<<i<<endl;
i=0; //错误:在循环外部无法访问i
三、条件语句
条件语句有两种,一种是if语句,它根据条件决定控制流;另外一种是switch语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条。
1.if语句
第一种:
if(condition)
statement
第二种:
if(condition)
statement
else
statement2
condition可以是一个表达式,也可以是一个初始化了的变量声明。不管是表达式还是变量,其类型都必须能转换成布尔类型。
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(grade!=100)
if(grade % 10>7)
lettergrade+='+'; //末尾是8或者9的成绩添加一个加号
else if(grade % 10<3)
lettergrade+='-'; //末尾是0、1或者2的成绩添加一个减号
}
悬垂else:
else与if的匹配问题称作悬垂else,就C++而言,它规定else与离它最近的尚未匹配的if匹配,从而消除了程序的二义性。
2、switch语句
例子——统计五个元音字母在文本中出现的次数:
//为每个元音字母初始化其计数值
unsigned aCnt=0,eCnt=0,iCnt=0,oCnt=0,uCnt=0;
char ch;
while(cin>>ch){
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语句的作用是中断当前的控制流。
case关键字和它对应的值一起被称为case标签。case标签必须是整型常量表达式:
char ch=getVal();
int ival=42;
switch(ch){
case 3.14: //错误:case标签不是一个整数
case ival: //错误:case标签不是一个常量
//...
}
任何两个case标签的值不能相同,否则就会引发错误。另外,default也是一种特殊的case标签。
switch内部的控制流:
每个case标签只能对应一个值,但是有时候我们希望两个或者更多个值共享一组操作。此时,我们就故意省略掉break语句,使得程序能够连续执行若干个case标签:
//统计所有元音字母出现的总次数
unsigned vowelCnt=0;
//...
switch (ch)
{
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++vowelCnt;
break;
}
漏写break容易引发缺陷:
如果缺少了break,当某个case标签匹配成功后,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。
尽管switch语句并不是非得在最后一个标签后面写上break,但是为了安全起见,最好这么做。因为这样的话,即使以后再增加新的case分支,也不用再在前面补充break语句了。
default标签:
如果没有任何一个case标签能配得上switch表达式的值,程序将执行紧跟在default标签后面的语句:
switch(ch){
case'a':case'e':case'i':case'o':case'u':
++vowelCnt;
break;
default:
++otherCnt;
break;
}
即使不准备在default标签下做任何工作,定义一个default标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。
标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case标签。如果switch结构以一个空的default标签作为结束,则该default标签后面必须跟上一条空语句或一个空块。
switch内部的变量定义:
如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。C++不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置:
case true:
//因为程序的执行流程可能绕开下面的初始化语句,所以该switch语句不合法
string file_name; // 错误:控制流绕过一个隐式初始化的变量
int ival=0; //错误:控制流绕过一个显式初始化的变量
int jval; //正确:因为jval没有初始化
break;
case false:
jval=next_num();
if(file_name.empty())
//...
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都能在变量的作用域之外:
case true:
{
//正确:声明语句位于语句块内部
string file_name=get_file_name;
//...
}
break;
case false:
if(file_name.empty())//错误:file_name不在作用域内
四、迭代语句
1.while语句
语法形式:
while(condition)
statement
while的条件部分可以是一个表达式或者是一个带初始化的变量声明。
定义在while条件部分或者while循环体内的变量每次迭代都经历从创建到销毁的过程。
使用while循环:
当不确定到底要迭代多少次时,使用while循环比较合适。
2.传统的for语句
语法形式:
for(init-statement;condition;expression)
statement
init-statement必须是以下三种形式中的一种:声明语句、表达式语句或者空语句。
牢记for语句头中定义的对象只在for循环体内可见。
for语句头中的多重定义:
和其他声明一样,init-statement也可以定义多个对象。但是init-statement只能有一条声明语句,因此,所有变量的基础类型必须相同:
for(decltype(v.size())i=0,sz=v.size();i!=sz;++i)
v.push_back(v[i]);
省略for语句头的某些部分:
for语句头能省略掉init-statement、condition和expression中的任何一个(或者全部)。但是,分号必须保留
3.范围for语句
语法形式:
for(declaration:expression)
statement
expression表示的必须是一个序列,比如花括号括起来的初始值列表、数组、vector、string,这些类型的共同特点是拥有能够返回迭代器的begin和end成员。
确保类型相容最简单的办法是使用auto类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
不能通过范围for语句增加vector对象(或者其他容器)的元素:在范围for语句中,预存了end()的值。一旦在序列中添加(删除)元素,end函数的值就可能变得无效了。
4.do while语句
do while语句和while语句的区别:do while语句先执行循环体后再检查条件。不管条件的值如何,至少执行一次循环:
do
statement
while(condition);
do while语句应该在括号包围起来的条件后面用一个分号表示语句结束。
condition使用的变量必须定义在循环体之外。如果允许在条件部分定义变量,则变量的使用出现在定义之前,这是非常不合理的:
do{
//...
mumble(foo);
}while(int foo=get_foo()); //错误,将变量声明放在了do的条件部分
五、跳转语句
1、break语句
break语句负责终止离它最近的while、do while、for或switch语句,并从这些语句之后的第一条语句开始继续执行。
2、continue语句
continue语句终止最近的循环中的当前迭代并立即开始下一次迭代。continue语句只能出现在for、while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部。
3、goto语句
goto语句的作用是从goto语句无条件跳转到同一函数内的另一条语句。
语法形式:
goto label;
其中,label是用于标识一条语句的标识符。带标签语句是一种特殊的语句,在它之前有一个标识符以及一个冒号:
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;
}
六、try语句块和异常处理
1、throw表达式
程序的异常检测使用throw表达式引发一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是跑出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句:
if(item1.isbn()!=item2.isbn())
throw runtime_error("Data must refer to same ISBN");
//如果程序执行到了这里,表示两个ISBN是相同的
cout<<item1 + item2<<endl;
2.try语句块
try语句块的通用语法形式是:
try{
program-statements
}catch (exception-declaration) {
handler-statements
}catch (exception-declaration) {
handler-statements
}//...
当选中了某个catch子句处理异常之后,执行与之对应的块。
一如既往,try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。
while (cin>>item1>>item2){
try{
throw runtime_error("Data must refer to same ISBN");
}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对象的副本。
函数在寻找处理代码的过程中退出:
在复杂的系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个try语句块。寻找处理代码的过程与函数调用链刚好相反,沿着程序的执行路径逐层回退,直到找到合适类型的catch子句为止。
如果最终还是没能找到任何匹配的catch子句,程序转到名为terminate的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
3.标准异常
异常类分别定义在4个头文件中:
- exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息;
- stdexcept头文件定义了几种常用的异常类;
- new头文件定义了bad_alloc异常类型;
- type_info头文件定义了bad_cast异常类型;
exception | 最常见的问题 |
runtime_error | 只有运行时才能检测出的问题 |
range_error | 运行时出错:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时出错:计算上溢 |
underflow_error | 运行时出错:计算下溢 |
logic_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的值不存在 |
invalid_argument | 逻辑错误:无效参数 |
length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |