第9章  语句(Statments)

目录

9.1  引言

9.2  语句概述

9.3  声明作为语句

9.4  选择语句

9.4.1  if语句

9.4.2  switch语句

9.4.2.1  case中的声明

9.4.3  条件中的声明

9.5  迭代语句(反复循环,不断推进)

9.5.1  范围for语句(Range-for)

9.5.2  for语句

9.5.3  while语句

9.5.4  do语句

9.5.5  退出循环

9.6  goto语句

9.7  注释和缩进

9.8  建议


9.1  引言

    C++ 提供了一组传统且灵活的语句。基本上,所有有趣或复杂的内容都可以在表达式和声明中找到。请注意,声明是语句,而表达式在末尾添加分号后就变成了语句。

    与表达式不同,语句没有值。相反,语句用于指定执行顺序。例如:

a = b+c; // 表达式语句

if (a==7) // if语句

b = 9; // execute 当且仅当 a==9 时执行

从逻辑上讲,a=b+c if 之前执行,正如大家所期望的那样。只要结果与简单执行顺序的结果相同,编译器就可以重新排序代码以提高性能。

9.2  语句概述

    语句:

            声明

            表达式(可选)

               { 语句列表(可选) }

               Try{  语句列表(可选) } handler 列表

            case 常量表达式:语句

            default: 语句

           break;

            continue;

                     return 表达式(可选);

            goto 标识符;

            标识符:语句

            选择语句

            迭代语句

    选择语句:

            if(条件) 语句

            if(条件) 语句 else 语句

            switch(条件语句

    迭代语句:

           while(条件语句

           do语句while(表达式);

                     for(for初始化条件(可选)表达式(可选))  语句

           for(for初始化声明表达式)  语句

表达式列表:

       语句    语句列表(可选)

条件:

       表达式

        类型指定符声明符=表达式

        类型指定符声明符{表达式}

handler列表:

        handler handler列表(可选)

handler:

            catch(异常声明) 表达式列表(可选)}

分号本身就是一个语句,一个空语句。

    “花括号”(即 { } )内的(可能为空的)语句序列称为块或复合语句。在块中声明的名称在其块的末尾作用域结束(§6.3.4)。

    声明是语句,不存在赋值语句或过程调用语句;赋值和函数调用是表达式

    for初始化语句必须是声明或表达式语句。请注意,两者都以分号结尾。

    for初始化声明必须是单个未初始化变量的声明。处理异常的语句 try块 在 §13.5 中描述。

9.3  声明作为语句

    声明是一种语句。除非将变量声明为static,否则每当控制线程通过声明时,都会执行其初始化器(另请参阅 §6.4.2)。允许在可以使用语句的任何地方进行声明(以及其他一些地方;§9.4.3、§9.5.2)的原因是使程序员能够最大限度地减少未初始化变量引起的错误,并允许更好的代码局部性。在变量有值可保存之前,很少有必要引入变量。例如:

void f(vector<string>& v, int i, const char p)
{
   if (p==nullptr) return;
   if (i<0 || v.size()<=i)
   error("bad index");
   string s = v[i];
   if (s == p) {
   //
...
   }
   //
...
}

对于许多常量和单赋值编程风格(其中对象的值在初始化后不会更改),将声明放在可执行代码之后的能力至关重要。对于用户定义类型,将变量的定义推迟到合适的初始化器可用时也可以提高性能。例如:

void use()
{

string s1;

s1 = "The best is the enemy of the good.";
// ...

}

这要求先进行默认初始化(为空字符串),然后进行赋值。这可能比简单地初始化为所需值要慢:

string s2 {"Voltaire"};

声明变量而不使用初始化器的最常见原因是它需要语句来赋予其所需的值。输入变量是少数几个合理的例子之一:

void input()
{

int buf[max];
int count = 0;
for (int i; cin>>i;) {
if (i<0) error("unexpected negative value");
if (count==max) error("buffer overflow");
buf[count++] = i;
}
// ...

}

我假设 error() 不会返回;如果返回,此代码可能会导致缓冲区溢出。通常,push_back()(§3.2.1.3,§13.6,§31.3.6)为此类示例提供了更好的解决方案。

9.4  选择语句

可以通过 if 语句或 switch 语句来测试一个值:

if ( 条件 ) 语句
if ( 条件 ) 语句else 语句
switch (条件) 语句

条件可以是表达式或声明(§9.4.3)。

9.4.1  if语句

    在 if 语句中,如果条件为真,则执行第一个(或唯一)语句,否则执行第二个语句(如果指定)。如果条件计算结果不同于bool,则(如果可能)将其隐式转换为bool这意味着任何算术或指针表达式都可以用作条件。例如,如果 x 是整数,则

if (x) // ...

意味着

if (x != 0) // ...

对于一个指针 p ,

if (p) // ...

是测试“p 是否指向有效对象(假设初始化正确)?”的直接语句,相当于

if (p != nullptr) // ...

请注意,“普通”enum可以隐式转换为整数,然后转换为bool,而枚举类则不能(8.4.1)。例如:

enum E1 { a, b };

enum class E2 { a, b };

void f(E1 x, E2 y)

{

if (x) // OK

// ...

if (y) // : 无到bool值的转换

// ...

if (y==E2::a) // OK

// ...

}

逻辑运算符

       &&   ||    !

最常用于条件。运算符 && || 非必要不会计算其第二个参数。例如,

if (p && 1<p−>count) // ...

仅当 p 不为 nullptr 时,才会测试 1 < p>count

对于在两个会产生值的选项之间进行选择,条件表达式(§11.1.3)比 if 语句更直接地表达了意图。例如:

int max(int a, int b)

{

return (a>b)?a:b; // return a b 的大者

}

名称只能在声明它的作用域内使用。特别是,它不能在 if 语句的另一个分支上使用。例如:

void f2(int i)

{

    if (i) {

           int x = i+2;

           ++x;

           // ...

    }

    else {

           ++x; // : x不在其作用域

    }

           ++x; // : x不在其作用域

}

if 语句的分支不能仅仅是一个声明。如果我们需要在分支中引入名称,则必须将其包含在块中(§9.2)。例如:

void f1(int i)

{

    if (i)

int x = i+2; // : if表达分支之声明

}

9.4.2  switch语句

     switch 语句在一组备选方案(case 标签)中进行选择。case 标签中的表达式必须是整数或枚举类型的常量表达式。switch 语句中的 case 标签的值不得超过一次。例如:

void f(int i)

{

    switch (i) {

    case 2.7: // : 浮点数用作case标签

    // ...

    case 2:

    // ...

    case 4−2: // : case标签中2用了两次

    // ...

};

或者,switch 语句也可以写成一组 if 语句。例如:

switch (val) {

           case 1:

                  f();

                  break;

           case 2:

                  g();

                  break;

           default:

                  h();

                  break;

}

这可以表达为:

if (val == 1)

    f();

else if (val == 2)

    g();

else

    h();

含义相同,但第一个 (switch) 版本更受欢迎,因为操作的性质(针对一组常量测试单个值)是明确的。这使得 switch 语句对于非平凡示例更易于阅读。它通常还会导致生成更好的代码,因为没有理由重复检查单个值。相反,可以使用跳转表。

    请注意,除非您想继续执行下一个case,否则必须以某种方式终止 switch case。考虑:

    使用 val==1 进行调用,输出将让初学者大吃一惊:

case 1

case 2

default: case not found

最好对故意出现串case (fall-through) 的(罕见)情况进行注释,这样未注释的串case就可以被视为错误。例如:

switch (action) { // handle (action,value) pair

    case do_and_print:

           act(value);

           // no break: fall through to print

    case print:

           print(value);

           break;

           // ...

}

break是终止case的最常见方式,但返回case通常也很有帮助(§10.2.1)。

    switch 语句何时应具有default?没有一个答案可以涵盖所有情况。default的一种用途是处理最常见的情况。另一种常见用途则恰恰相反:default:操作只是一种捕获错误的方法;每个有效的替代方案都包含在case中。但是,有一种情况不应使用default:如果 switch 旨在为枚举的每个枚举项提供一个default。如果是这样,则省略default将使编译器有机会警告一组几乎但不完全匹配枚举项集的case。例如,这几乎肯定是一个错误:

enum class Vessel { cup, glass, goblet, chalice };

void problematic(Vessel v)

{

    switch (v) {

    case Vessel::cup: /* ... */ break;

    case Vessel::glass: /* ... */ break;

    case Vessel::goblet: /* ... */ break;

    }

}

在维护过程中增加新的枚举项时很容易发生这种错误。

    对于“不可能”的枚举项的测试最好单独进行。

9.4.2.1  case中的声明

    在 switch 语句块内声明变量是可能的,而且很常见。但是,不可能绕过初始化。例如:

void f(int i)

{

    switch (i) {

    case 0:

    int x; // 未初始化

    int y = 3; // : 可以绕过声明(显式初始化)

    string s; //  : 可以绕过声明(隐式初始化)

    case 1:

    ++x; //  : 使用未初始化变量

    ++y;

    s = "nasty!";

    }

}

这里,如果 i==1,执行线程将绕过 ys 的初始化,并且 f() 将无法编译。不幸的是,因为 int 型数据不初始化也能编译,所以声明 x 不是一个错误。然而,它的使用是一个错误:我们读取了一个未初始化的变量。不幸的是,编译器通常只会对未初始化变量的使用发出警告,而无法可靠地捕获所有此类误用。像往常一样,应避免使用未初始化的变量(§6.3.5.1。

    如果我们需要 switch 语句中的变量,我们可以将其声明和使用括在块中来限制其范围。有关示例,请参阅 §10.2.1 中的 prim()

9.4.3  条件中的声明

    为了避免意外滥用变量,明智的做法是将变量引入尽可能小的范围。特别是,通常最好将局部变量的定义推迟到可以为其赋予初始值为止。这样,在赋初值之前使用该变量就不会惹上麻烦。

    这两个原则最优雅的应用之一是在条件中声明变量。考虑一下:

         if (double d = prim(true)) {

left /= d;

break;

}

这里,d 被声明和初始化,初始化后的 d 的值被测试为条件的值。d 的范围从其声明点延伸到条件控制的语句的末尾。例如,如果 if 语句有一个 else 分支,d 将在两个分支上都处于作用域内。

    显而易见且传统的替代方法是在条件之前声明 d。然而,这打开了d 的使用作用域(从字面量上讲),使其可以在初始化之前或预期使用寿命之后使用:

double d;

// ...

d2 = d; // oops!

// ...

if (d = prim(true)) {

left /= d;

break;

}

// ...

d = 2.0; // d的两个不相关用法

    除了在条件中声明变量的逻辑好处之外,这样做还可以产生最紧凑的源代码。

    条件中的声明必须声明并初始化单个变量或 const 值。

9.5  迭代语句(反复循环,不断推进)

        循环可以表示为 for- while- do-语句:

while ( 条件 ) 语句

do 语句while ( 表达式) ;

for ( for初始语句条件(可选);表达式(可选) ) 语句

for ( for声明 : 表达式) 语句

for初始语句必需为声明或表达式语句。注意,两者兼以分号结束。

    for语句的语句(称为控制语句或循环体)会重复执行直到条件不成立或程序员以某种方式(例如,breakreturnthrow、或goto)中止循环为止。

    更复杂的循环可以表示为一个算法加上一个 lambda 表达式(§11.4.2)。

9.5.1  范围for语句(Range-for)

最简单的循环是范围语句;它只是让程序员访问范围中的每个元素。例如:

int sum(vector<int>& v)

{

int s = 0;

for (int x : v)

s+=x;

return s;

}

for (int x : v) 可以读作“对于范围 v 中的每个元素 x”或仅仅是“对于 v 中的每个 xv 的元素按从第一个到最后一个的顺序被访问。

    命名元素(此处为 x)的变量的范围是 for 语句。

冒号后的表达式必须表示一个序列(一个范围);也就是说,它必须产生一个这样的值——我们可以为此值调用 v.begin() v.end() begin(v) end(v) 来获得一个迭代器(§4.5):

    [1] 编译器首先查找成员 beginend,并尝试使用它们。如果发现 beginend 不能用作范围(例如,因为成员 begin 是变量而不是函数),则 范围for 会出错。

[2] 否则,编译器会在封闭作用域内查找 begin/end 成员对。如果没有找到或如果找到的内容无法使用(例如,因为 begin 没有采用序列类型的参数),则范围for会出错。

    编译器使用 vv+N 作为内置数组 T v[N]begin(v) end(v)<iterator> 标头为内置数组和所有标准库容器提供 begin(c) end(c)。对于我们自己设计的序列,我们可以像定义标准库容器一样定义 begin() end()(§4.4.5)。

    示例中的受控变量 x 指的是当前元素,当使用等效的 for 语句时,它等效于 p

int sum2(vector<int>& v)

{

int s = 0;

for (auto p = begin(v); p!=end(v); ++p)

s+=p;

return s;

}

如果您需要修改范围for 循环中的元素,则元素变量应该是引用。例如,我们可以像这样增加向量的每个元素:

void incr(vector<int>& v)

{

for (int& x : v)

++x;

}

引用也适用于可能很大的元素,因此将它们复制到元素值可能会很昂贵。例如:

template<class T> T accum(vector<T>& v)

{

T sum = 0;

for (const T& x : v)

sum += x;

return sum;

}

请注意,范围 for 循环是一种刻意简单的构造。例如,使用它你不能同时接触两个元素,也不能同时有效地遍历两个范围。为此,我们需要一个通用的 for语句。

9.5.2  for语句

    还有更通用的 for语句,可以更好地控制迭代。循环变量、终止条件和更新循环变量的表达式在一行中明确呈现。例如:

void f(int v[], int max)

{

for (int i = 0; i!=max; ++i)

v[i] = ii;

}

这等价于

void f(int v[], int max)

{

int i = 0; //引入循环变量

while (i!=max) { // 测试终止条件

v[i] = ii; // 执行循环体

++i; // 叠加循环变量

}

}

    变量可以在 for 语句的初始化部分中声明。如果该初始化部分是声明,则它引入的变量(或多个变量)在 for 语句结束之前一直处于作用域内。

    在 for 循环中,控制变量的正确类型并不总是很明显,因此 auto 常常派上用场:

for (auto p = begin(c); c!=end(c); ++p) {

// ... 对容易c中的元素使用迭代器p

}

       如果需要在退出 for 循环后知道索引的最终值,则必须在 for 循环之外声明索引变量(例如,参见 §9.6)。

    如果不需要初始化,则初始化语句可以为空。

    如果省略了应该增加循环变量的表达式,我们必须在其他地方更新某种形式的循环变量,通常是在循环体中。如果循环不是简单的“引入循环变量、测试条件、更新循环变量”类型,通常最好将其表示为 while 语句。但是,请考虑这个优雅的变体:

for (string s; cin>>s;)

v.push_back(s);

这里,读取和测试终止并结合在 cin>> 中,因此我们不需要显式循环变量。在另一方面,使用 for 而不是 while 允许我们将“当前元素”的范围限制在循环本身(for 语句)中。

    for 语句对于表达没有明确终止条件的循环也很有用:

    for (;;) { // ‘‘永远

// ...

}

(译注:上面的循环只有1个跳转指令,即无条件跳转,因此这个循环语句最优,其指令形式如下:

00007FF61C8636DD  jmp         wmain+0Dh (07FF61C8636DDh)  )

然而,许多人认为这个习惯用法晦涩难懂,更喜欢使用:

while(true) { // ‘‘永远

// ...

}

(译注:上面的循环有4条指令,在跳转之前有个比较,即条件跳转,因此这个循环语句性能不如上一个优,其指令形式如下:

00007FF6B78C36DD  xor         eax,eax  ;eax清0,运行这一步后寄存器 eax 值为 0

00007FF6B78C36DF  cmp         eax,1  ; 目的操作数 < 源操作数 (0 < 1) ,标志位 SF OF

00007FF6B78C36E2  je          wmain+16h (07FF6B78C36E6h) ; 当ZF标致为1的时候发生跳转

                            ;显然不成立,这一步不跳转

00007FF6B78C36E4  jmp         wmain+0Dh (07FF6B78C36DDh) ;这一步跳转到 xor eax,eax 

)

9.5.3  while语句

       while 语句会执行其受控语句,直到其条件变为假。例如:

template<class Iter, class Value>

Iter find(Iter first, Iter last, Value val)

{

while (first!=last && first!=val)

++first;

return first;

}

当没有明显的循环变量或循环变量的更新自然发生在循环体中间时,我倾向于使用 while 语句而不是 for 语句

       for 语句(§9.5.2)很容易重写为等效的 while 语句,反之亦然。

9.5.4  do语句

do 语句与 while 语句类似,不同之处在于条件位于语句主体之后。例如:

void print_backwards(char a[], int i) // i must be positive

{

cout << '{';

do {

cout << a[−−i];

} while (i);

cout << '}';

}

这可能被这样调用:print_backwards(s,strlen(s)); 但很容易犯一个可怕的错误。例如,如果 s 是空字符串怎么办?

    根据我的经验,do 语句是错误和混乱的根源。原因是它的主体总是在评估条件之前执行一次。但是,为了使主体正常工作,与条件非常相似的东西必须即使在第一次执行时也成立。我发现,当程序首次编写和测试时,或者在其前面的代码被修改之后,条件没有按预期成立,这种情况比我想象的要多得多。我也更喜欢“在我能看到的地方”预先执行条件。因此,我建议避免使用 do 语句。

9.5.5  退出循环

    如果省略迭代语句(forwhiledo 语句)的条件,则循环不会终止,除非用户通过 breakreturn(§12.1.4)、goto(§9.6)、throw(§13.5)或某些不太明显的方式(例如调用 exit()(§15.4.3))明确退出循环。break 会“跳出”最近的封闭 switch 语句(§9.4.2)或迭代语句。例如:

void f(vector<string>& v, string terminator)

{

char c;

string s;

while (cin>>c) {

// ...

if (c == '\n') break;

// ...

}

}

当我们需要将循环体“留在中间”时,我们会使用 break。除非它会扭曲循环的逻辑(例如,需要引入额外的变量),否则通常最好将完整的退出条件作为 while 语句或 for 语句的条件。

有时,我们不想完全退出循环,我们只想到达循环体的末尾。continue 会跳过迭代语句体的其余部分。例如:

void find_prime(vector<string>& v)

{

for (int i = 0; i!=v.siz e(); ++i) {

if (!prime(v[i]) continue;

return v[i];

}

}

continue之后,将执行循环的增量部分(如果有),然后执行循环条件(如果有)。因此 find_prime() 可以等效地写成:

void find_prime(vector<string>& v)

{

for (int i = 0; i!=v.siz e(); ++i) {

if (!prime(v[i]) {

return v[i];

}

}

}

9.6  goto语句

C++ 拥有臭名昭著的 goto 语句:

goto  标识符 ;

标识符: 语句

goto 在一般的高级编程中用处不大,但当 C++ 代码是由程序生成而不是由人直接编写时,它会非常有用;例如,goto 可用于由解析器生成器根据语法生成的解析器。                  

标签的作用域是它所在的函数(§6.3.4)。这意味着您可以使用 goto 跳入和跳出块。唯一的限制是您不能跳过初始化程序或跳入异常处理程序(§13.5)

在普通代码中,goto 的少数合理用途之一是跳出嵌套循环或 switch 语句(break 只会跳出最内层的循环或 switch 语句)。例如:

void do_something(int i, int j)

// do something to a two-dimensional matrix called mn

{

for (i = 0; i!=n; ++i)

for (j = 0; j!=m; ++j)

if (nm[i][j] == a)

goto found;

// not found

// ...

found:

// nm[i][j] == a

}

请注意, goto 只是向前跳转以退出其循环。它不会引入新循环或进入新范围。这使它成为 goto 最不麻烦且最不令人困惑的用法。

9.7  注释和缩进

明智地使用注释和一致地使用缩进可以使阅读和理解程序的任务更加愉快。有几种不同的一致缩进样式正在使用。我认为没有根本理由偏爱其中一种(尽管像大多数程序员一样,我有自己的偏好,这本书反映了这些偏好)。注释样式也是如此。

注释可能会被误用,严重影响程序的可读性。编译器不理解注释的内容,因此无法确保注释

• 有意义,

• 描述程序,并且

• 是最新的。

大多数程序的注释都难以理解、含糊不清,甚至完全错误。糟糕的注释比没有注释更糟糕。

如果某件事可以用编程语言本身来表述,那么就应该这样做,而不仅仅是在注释中提及。此注释针对的是以下注释:

// variable "v" must be initialized

// variable "v" must be used only by function "f()"

// call function "init()" before calling any other function in this file

// call function "cleanup()" at the end of your program

// don’t use function "weird()"

// function "f(int ...)" takes two or three arguments

通过正确使用 C++,通常可以使得此类注释变得没有必要。

一旦某件事通过编程语言清楚地表达了出来,就不应该在注释中再次提及。例如:

a = b+c; // a becomes b+c

count++; // increment the counter

此类注释不仅多余,而且更糟糕。它们增加了读者需要阅读的文本量,而且经常会模糊程序结构,并且可能存在错误。但请注意,此类注释在编程语言教科书中被广泛用于教学目的。这是教科书中的程序与实际程序的众多不同之处之一。

好的注释说明了一段代码应该做什么(代码的意图),而代码(仅)说明了它做什么(就其如何执行而言)。注释最好以适当高的抽象级别表达,以便人们无需深入研究细节即可轻松理解。

我的偏好是:

• 为每个源文件添加注释,说明其中声明的共同点、手册引用、程序员姓名、维护的一般提示等。

• 为每个类、模板和命名空间添加注释

• 为每个非平凡函数添加注释,说明其用途、使用的算法(除非很明显),以及可能关于其对环境所做的假设

• 为每个全局和命名空间变量和常量添加注释

• 代码不明显和/或不可移植时添加一些注释

• 其他内容很少

例如:

// tbl.c: Implementation of the symbol table.

/*

Gaussian elimination with partial pivoting.

See Ralston: "A first course ..." pg 411.

*/

// scan(p,n,c) requires that p points to an array of at least n elements

// sor t(p,q) sorts the elements of the sequence [p:q) using < for comparison.

// Revised to handle invalid dates. Bjar ne Stroustr up, Feb 29 2013

精心挑选和精心编写的注释是优秀程序的重要组成部分。编写优秀的注释可能与编写程序本身一样困难。这是一门值得培养的艺术。

请注意,/ / 样式的注释不能嵌套。例如:

/*

remove expensive check

if (check(p,q)) error("bad p q") /* should never happen */

/

对于不匹配的最终 ∗/,此嵌套将产生错误。

9.8  建议

[1] 在没有初始化变量的值之前不要声明变量;§9.3、§9.4.3、§9.5.2。

[2] 如果可选,则优先使用 switch 语句而不是 if 语句;§9.4.2。

[3] 如果可选,则优先使用范围for 语句而不是 for 语句;§9.5.1。

[4] 如果有明显的循环变量,则优先使用 for 语句而不是 while 语句;§9.5.2。

[5] 如果有明显的循环变量,则优先使用 while 语句而不是 for 语句;§9.5.3。

[6] 避免使用 do 语句;§9.5。

[7] 避免使用 goto;§9.6。

[8] 保持注释简洁;§9.7。

[9] 不要在注释中说代码中可以清楚说明的内容;§9.7。

[10] 在注释中表述意图;§9.7。

[11] 保持一致的缩进样式;§9.7。

内容来源:

<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部