语句概述
这里是 C++ 语句的形式化定义:
语句:
声明
表达式可选;
{ 语句列表可选 }
try { 语句列表可选 } 处理模块列表
case 常量表达式 : 语句
default : 语句
break;
continue;
return 表达式可选;
goto 标识符;
标识符 : 语句
选择语句
循环语句
选择语句:
if( 条件 ) 语句
if( 条件 ) 语句 else 语句
switch( 条件 ) 语句
循环语句:
while( 条件 ) 语句
do 语句 while ( 条件 );
for( for 初始化语句可选; 条件可选; 表达式可选) 语句
for( for 初始化声明 : 表达式) 语句
语句列表:
语句 语句列表可选
条件:
表达式
类型修饰符 声明符 = 表达式
类型修饰符 声明符{表达式}
处理模块列表:
处理模块 处理模块列表可选
处理模块:
catch( 表达式声明 ){ 语句列表可选 }
分号本身也是一条语句,即空语句(empty statement)。
“花括号”({ }
)括起来的一个可能为空的语句序列称为块(block)或者复合语句(compound statement)。块中声明的名字的作用域到块的末尾就结束了。
声明(declaration)是一条语句,没有赋值语句或过程调用的语句;赋值和函数调用不是语句,它们是表达式。
for 初始化语句(for-init-statement)要么是声明,要么是一条表达式语句(expression-statement),它们都以分号结束。
for 初始化声明(for-init-declaration)必须是一个未初始化变量的声明。
try 语句块(try-block)的作用是处理异常。
声明作为语句
一个声明就是一条语句。除非变量被声明成 static
,否则在控制线程传递给当前声明语句的同时执行初始化器。允许把声明当成一条语句使用(当然还能用在其他一些场合)的目的是尽量减少由未初始化变量造成的程序错误,并且让代码的局部性更好。在绝大多数情况下,如果没有为变量找到一个合适的值,暂时不要声明它。
选择语句
if 语句和 switch 语句都需要首先检测一个值:
if( 条件 ) 语句
if( 条件 ) 语句 else 语句
switch( 条件 ) 语句
条件(condition)可能是一个表达式,也可能是一个声明。
if 语句
在 if
语句中,如果条件为真,则执行第一条(或者唯一的一条)语句;否则,执行第二条语句(如果有的话)。
if (condition) {
// 第一条语句
} else {
// 第二条语句
}
即使条件的求值结果不是布尔值,也能尽量隐式地转换成 bool
类型。因此,算术类型及指针类型的表达式都能作为条件。
“普通的”enum
可以先隐式地转换成整数,然后再转换成 bool
类型,但是 enum class
不能。例如:
enum Color { Red, Green, Blue };
Color c = Red;
if (c) { /* ... */ }
enum class Shape { Circle, Square, Triangle };
Shape s = Shape::Circle;
if (s) { /* ... */ } // 错误:枚举类不能隐式转换为bool
if (s == Shape::Circle) { /* ... */ } // 正确:必须显式比较
逻辑运算符 &&
||
!
经常在条件中出现。对于运算符 &&
和 ||
来说,除非必需,否则运算符右侧的运算对象不会被求值(短路)。
一个名字只能在声明它的作用域中使用。在 if
语句中,一个分支声明的名字不能在另一个分支中直接使用。例如:
if (condition) {
int x = 10;
// x 在这里是有效的
} else {
// x 在这里是无效的
}
switch 语句
switch
语句在一组候选项(case
标签)中进行选择。case
标签中出现的表达式必须是整型或枚举类型的常量表达式。在同一个 switch
语句中,一个值最多被 case
标签使用一次。
switch (expression) {
case constant1:
// 语句
break;
case constant2:
// 语句
break;
default:
// 语句
}
谨记 switch
语句的每一个分支都应该有一条结束语句(break
语句),否则程序将会继续执行下一个分支的内容。
switch
语句中 default
分支的使用:
-
default
分支可以用于处理最常出现的情况。 -
default
分支也可以用于处理取值错误的情况,此时所有有效的取值都包含在case
分支中。
不使用 default
分支的情况:
- 当
switch
语句的每个分支对应枚举类型中的一个枚举值时,最好不使用default
分支。 - 这样做可以让编译器负责发现并报告
case
分支与枚举值未能完全匹配的问题。
enum class A { A1, A2, A3 };
void foo1(A a) {
switch (a) {
case A::A1: break;
case A::A2: break;
}
}
void foo2(A a) {
switch (a) {
case A::A1: break;
case A::A2: break;
default: break;
}
}
我在 Visual Studio 的 MSVC 上,使用默认的 Debug 和 Release 模式都没有观察到,但实际上,应该是存在的,详看在线编译器
<source>: In function 'void foo1(A)': <source>:4:12: warning: enumeration value 'A3' not handled in switch [-Wswitch] 4 | switch (a) { | ^ Compiler returned: 0
case 分支中的声明
C++ 允许在 switch 语句的块内声明变量,但是不能不初始化。
#include <string>
void f(int i) {
switch (i)
{
case 0:
int x; // 未初始化
int y = 3; // 错误:程序有可能跳过该声明(显式初始化)
std::string s; // 错误:程序有可能跳过该声明(隐式初始化)
case 1:
++x; // 错误:试图使用未初始化的对象
++y;
s = "nasty!";
}
}
如果我们确实需要在 switch 语句中使用变量,最好把该变量的声明和使用限定在一个块中。
条件中的声明
if (double d = prim(true)) {
left /= d;
}
首先声明 d
并给它赋了初值,然后把初始化后的 d
的值作为条件的值进行检查。d
的作用域从声明处开始,到条件控制的语句结束为止。假设还有一个 else
分支与上面的 if
分支对应,则 d
在两个分支中都有效。
条件中的声明语句只能声明并初始化一个变量或 const
。
循环语句
范围 for 语句
for( for 初始化声明 : 表达式) 语句
命名元素的变量的作用域是整个 for
语句。
冒号之后的表达式必须是一个序列(一个范围),换句话说,如果我们对它调用
v.begin()
和 v.end()
或者 begin(v)
和 end(v)
,得到的应该是迭代器:
- 编译器首先尝试寻找并使用成员
begin
和end
。如果找到了begin
和end
,但是
它们不能表示一个范围(比如,begin
有可能是变量而非函数),则当前的范围for
是错误的。 - 如果没有找到,则编译器继续在外层作用域寻找
begin
/end
成员。如果找不到
或者找到的不能用(比如begin
不接受当前序列类型的实参),则范围for
是错
误的。
如果想在范围 for 循环中修改元素的值,则应该使用元素的引用。
for 语句
for (int i = 0; i != max; ++i) {
v[i] = i * i;
}
等价于
int i = 0; // 引入循环变量
while (i != max) { // 检验终止条件
v[i] = i * i; // 执行循环体
++i; // 递增循环变量
}
while 语句
与 for
语句相比,while
语句更适合处理以下两种情况:一是没有一个明显的循环变量,二是程序员觉得把负责更新循环变量的语句置于循环体内更自然。for
语句很容易改写成等价的 while
语句,反之亦然。
for (初始化; 条件; 更新) {
语句;
}
可以改写为:
初始化;
while (条件) {
语句;
更新;
}
反之,while
语句:
while (条件) {
语句;
}
也可以改写为:
for (; 条件;) {
语句;
}
do 语句
do-while
循环是一种后测试循环结构,这意味着它会先执行循环体内的代码,然后再判断条件是否满足。如果条件为真,则继续执行循环体;如果条件为假,则退出循环。
do-while
循环至少会执行一次循环体。
do {
// 循环体
} while (条件表达式);
退出循环
常见的退出循环的方法:
-
break
语句:break
语句用于立即终止最内层的switch
语句或while
、do-while
、for
循环,并跳出该结构,继续执行循环后面的代码。 return
语句:return
语句用于从当前函数返回,并可以带一个返回值。如果return
在循环内部,它将终止循环并结束包含该循环的函数。continue
语句:continue
语句用于跳过当前循环的剩余部分,并立即开始下一次迭代。它不是用来跳出循环的,而是用来跳过当前迭代。- 抛出异常(
throw
):可以在循环内部抛出一个异常,然后在函数的外部捕获它,从而退出循环。这是一种不常见的做法,因为它涉及到异常处理的开销,并且可能会使代码的流程控制变得复杂。
goto 语句
goto 标识符;
标识符 : 语句
标签的作用域是标签所处的函数。这意味着你能用 goto
从块的范围跳进跳出,唯一的限制是不能跳过初始化器或者跳入到异常处理程序。
注释
关于注释的建议:
- 在针对每个源文件的注释中指明:该文件中的声明有何共同点、对应的参考手册条目、程序员的名字以及维护该文件所需的其他信息。
- 为每个类、模板和名字空间分别编写注释。
- 为每个非平凡的函数分别编写注释并指明:函数的目的、用到的算法(如果很明显的话可以不用提),以及该函数对其应用环境所做的某些设定。
- 为全局和名字空间内的每个变量及常量分别编写注释。
- 为某些不太明显或不可移植的代码编写注释。
- 其他情况,则几乎不需要注释了。