0%

C++ Primer - 第 5 章 语句

  1. 一个表达式,末尾加上分号就变成了表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃掉求值结果。最简单的语句是空语句(null statement),空语句中只含有一个单独的分号。如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。例如,我们想读取输入流的内容直到遇到一个特定的值为止,除此之外什么事情也不做:

    1
    2
    3
    // 重复读入数据直至到达文件末尾或某次输入的值等于 sought
    while (cin >> s && s != sought)
    ; // 空语句

    使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略。

    下面的片段包含两条语句:表达式语句和空语句。

    1
    ival = v1 + v2;;  // 正确:第二个分号表示一条多余的空语句

    多余的空语句一般来说是无害的,但是如果在 if或者 while 的条件后面跟了一个额外的分号就可能完全改变我们的初衷。例如,下面的代码将无休止地循环下去:

    1
    2
    3
    // 出现了糟糕的情况:额外的分号,循环体是那条空语句
    while (iter != svec.end()) ; // while 循环体是那条空语句
    ++iter; // 递增运算不属于循环的一部分

    虽然从形式上来看执行递增运算的语句前面有缩进,但它并不是循环的一部分。循环条件后面跟着的分号构成了一条空语句,它才是真正的循环体。

    多余的空语句并非总是无害的。

  2. 复合语句(compound statement)是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作(block)。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。 通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。所谓空块,是指内部没有任何语句的一对花括号。空块的作用等价于空语句,空语句显然没有空块更加直白。

  3. 定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了,如果其他代码也需要访问控制变量,则变量必须定义在语句的外部。

  4. C++ 为解决悬垂 else(dangling else)问题,规定 else 与离它最近的尚未匹配的 if 匹配,从而消除了程序的二义性。

  5. switch 语句首先对括号里的表达式求值,该表达式紧跟在关键字 switch 的后面,可以是一个初始化的变量声明。表达式的值转换成整数类型,然后与每个 case 标签的值比较。

  6. case 关键字和它对应的值一起被称为 case 标签(case label)。case 标签必须是整型常量表达式。任何两个 case 标签的值不能相同,否则就会引发错误。另外,default 也是一种特殊的 case 标签。

  7. 如果某个 case 标签匹配成功,将从该标签开始往后顺序执行所有 case 分支,除非程序显式地中断了这一过程,否则直到 switch 的结尾处才会停下来。要想避免执行后续 case 分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个 case 标签之前应该有一条 break 语句。每个 case 标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉 break 语句,使得程序能够连续执行若干个 case 标签。例如,我们想统计所有元音字母出现的总次数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    unsigned vowelCnt = 0;
    // ...
    switch (ch)
    {
    // 出现了 a、e、i、o 或 u 中的任意一个都会将 vowelCnt 的值加 1
    case 'a':
    case 'e':
    case 'i':
    case 'o':
    case 'u':
    ++vowelCnt;
    break;
    }

    上述代码片段中 switch 语句还可以写成下面这种较为简洁的形式:

    1
    2
    3
    4
    5
    6
    switch (ch)
    {
    case 'a': case 'e': case 'i': case 'o': case 'u':
    ++vowelCnt;
    break;
    }

    一般不要省略 case 分支最后的 break 语句。如果没写 break 语句,最好加一段注释说清楚程序的逻辑。尽管 switch 语句不是非得在最后一个标签后面写上break,但是为了安全起见,最好这么做。因为这样的话,即使以后再增加新的 case 分支,也不用再在前面补充 break 语句了。

  8. 如果没有任何一个 case 标签能匹配上 switch 表达式的值,程序将执行紧跟在 default标签(default label)后面的语句。

    即使不准备在 default 标签下做任何工作,定义一个 default 标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。

  9. 如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。C++ 语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。如果需要为某个 case 分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有 case 标签都在变量的作用域之外。

  10. while 语句(while statement)中,定义在 while 条件部分或者 while 循环体内的变量每次选代都经历从创建到销毁的过程。

  11. for 语句中对循环控制变量的修改发生在每次循环迭代之后。

  12. for 语句头中定义的对象只在 for 循环体内可见。

  13. 和其他的声明一样,for 语句的 init_statement 也可以定义多个对象。但是 init_statement 只能有一条声明语句,因此,所有变量的基础类型必须相同。举个例子,我们用下面的循环把 vector 的元素拷贝一份添加到原来的元素后面:

    1
    2
    3
    // 记录 v 的大小,当到达原来的最后一个元素后结束循环
    for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
    v.push_back(v[i]);

    在这个循环中,我们在 init_statement 里同时定义了索引 i 和循环控制变量 sz。

  14. for 语句头能省略掉 init_statementconditionexpression 中的任何一个(或者全部),省略掉 expression 时,就要求条件部分或者循环体必须改变迭代变量的值。

  15. 范围 for 语句(range for statement)的语法形式是:

    1
    2
    for (declaration : expression)
    statement

    expression 表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者 vectorstring 等类型的对象,这些类型的共同特点时拥有能返回迭代器 beginend 成员。declaration 定义一个变量,序列中的每个元素都得能转换成该变量的类型,确保类型相容最简单的办法是使用 auto 类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。 每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行 statement。

  16. 不能通过范围 for 语句增加 vector 对象(或者其他容器)的元素,因为在范围 for 语句中,预存了 end() 的值。一旦在序列中添加(删除)元素,end 函数的值就可能变得无效了。

  17. do while语句应该在括号包围起来的条件后面用一个分号表示语句结束。

  18. break 语句(break statement)负责终止离它最近的 whiledo whileforswitch 语句,并从这些语句之后的第一条语句开始继续执行。break 语句只能出现在迭代语句或者 switch 语句内部(包括嵌套在此类循环里的语句或块的内部)。break 语句的作用范围仅限于最近的循环或者 switch

  19. continue 语句(continue statement)终止最近的循环中的当前迭代并立即开始下一次迭代。continue 语句只能出现在 forwhiledo while 循环的内部,或者嵌套在此类循环里的语句或块的内部。break 语句类似的是,出现在嵌套循环中的 continue 语句也仅作用于离它最近的循环。和 break 语句不同的是,只有当 switch 语句嵌套在迭代语句内部时,才能在 switch 里使用 continuecontinue 语句中断当前的迭代,但是仍然继续执行循环。对于 while 或者 do while 语句来说,继续判断条件的值;对于传统的 for 循环来说,继续执行 for 语句头的 expression;而对于范围 for 语句来说,则是用序列中的下一个元素初始化循环控制变量。

  20. goto 语句(goto statement)的作用是从 goto 语句无条件跳转到同一函数内的另一条语句。

  21. goto 语句的语法形式是:

    1
    goto label;

    其中,label 是用于标识一条语句的标示符。带标签语句(labeled statement)是一种特殊的语句,在它之前有一个标示符以及一个冒号:

    1
    end: return;  // 带标签语句,可以作为 goto 的目标

    标签标示符独立于变量或其他标示符的名字,因此,标签标示符可以和程序中其他实体的标示符使用同一个名字而不会相互干扰。goto 语句和控制权转向的那条带标签的语句必须位于同一个函数之内。switch 语句类似,goto 语句也不能将程序的控制权从变量的作用域之外转移到作用域之内

    1
    2
    3
    4
    5
    6
        // ...
    goto end;
    int ix = 10; // 错误:goto 语句绕过了一个带初始化的变量定义
    end:
    // 错误:此处的代码需要使用 ix,但是 goto 语句绕过了它的声明
    ix = 42;

    向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它:

    1
    2
    3
    4
    5
    6
    7
    // 向后跳过一个带初始化的变量定义是合法的
    begin:
    int sz = get_size();
    if (sz <= 0)
    {
    goto begin;
    }

    在上面的代码中,goto 语句执行后将销毁 sz。因为跳回到 begin 的动作跨过了 sz 的定义语句,所以 sz 将重新定义并初始化。

  22. 在 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 子句之间传递异常的具体信息。

  23. 程序的异常检测部分使用 throw 表达式引发一个异常。throw 表达式包含关键字 throw 和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw 表达式后面通常紧跟一个分号,从而构成一条表达式语句。

  24. 类型 runtime_error 是标准库异常类型的一种,定义在 stdexcept 头文件中。必须初始化runtime_error 的对象,方式是给它提供一个 string 对象或者一个 C 风格的字符串,例如:

    1
    throw runtime_error("Data must refer to same ISBN");
  25. try 语句块的通用语法形式是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    try
    {
    program-statements
    }
    catch (exception-declaration)
    {
    handler-statements
    }
    catch (exception-declaration)
    {
    handler-statements
    } // ...

    跟在 try 块之后的是一个或多个 catch 子句。catch 子句包括三部分:关键字 catch、括号内一个(可能未命名的)对象的声明(称作异常声明,exception declaration)以及一个块。当选中了某个 catch 子句处理异常之后,执行与之对应的块。catch 一旦完成,程序跳转到 try 语句块最后一个 catch 子句之后的那条语句继续执行。try 语句块内声明的变量在外部无法访问,特别是在 catch 子句内也无法访问。

  26. 每个标准库异常类都定义了名为 what 的成员函数,这些函数没有参数,返回值是 C 风格字符串(即 const char*)。其中,runtime_errorwhat 成员返回的是初始化一个具体对象时所用的 string 对象的副本。

  27. 寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的 catch 子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的 catch 子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的 catch 子句为止。如果最终还是没能找到任何匹配的 catch 子句,程序转到名为 terminate 的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。对于那些没有任何 try 语句块定义的异常,也按照类似的方式处理:毕竟,没有 try 语句块也就意味着没有匹配的 catch 子句。如果一段程序没有 try 语句块且发生了异常,系统会调用 terminate 函数并终止当前程序的执行。

  28. C++ 标准库定义了一组类,用于报告标准库函数遇到的问题,它们分别定义在 4 个头文件中:

    • exception 头文件定义了最通用的异常类 exception。它只报告异常的发生,不提供任何额外信息。
    • stdexcept 头文件定义了几种常用的异常类,如下表所示。
    • new 头文件定义了 bad_alloc 异常类型。
    • type_info 头文件定义了 bad_cast 异常类型。

    <stdexcept>定义的异常类
    exception 最常见的问题
    runtime_error 只有在运行时才能检测出的问题
    range_error 运行时错误:生成的结果超出了有意义的值域范围
    overflow_error 运行时错误:计算上溢
    underflow_error 运行时错误:计算下溢
    logic_error 程序逻辑错误
    domain_error 逻辑错误:参数对应的结果值不存在
    invalid_argument 逻辑错误:无效参数
    length_error 逻辑错误:试图创建一个超出该类型最大长度的对象
    out_of_range 逻辑错误:使用一个超出有效范围的值
  29. 标准库异常类只定义了几种运算,包括创建或接贝异常类型的对象,以及为异常类型的对象赋值。只能以默认初始化的方式初始化 exceptionbad_allocbad_cast 对象,不允许为这些对象提供初始值。其他异常类型的行为则恰好相反:应该使用 string 对象或者 C 风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。异常类型只定义了一个名为 what 的成员函数,该函数没有任何参数,返回值是一个指向 C 风格字符串的 const char*。该字符串的目的是提供关于异常的一些文本信息。what 函数返回的 C 风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则 what 返回该字符串。对于其他无初始值的异常类型来说,what 返回的内容由编译器决定。

  30. 编写一段程序,从标准输入读取两个整数,当第二个数是 0 时抛出异常,使用 try 语句块捕获异常,catch 子句为用户输出一条提示信息,询问是否输入新数并重新执行 try 语句块的内容。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int a, b;
    std::invalid_argument err("Wrong input for the 2nd argument!");
    while (cin >> a >> b)
    {
    try
    {
    if (0 == b)
    throw err;
    std::cout << "a/b = " << a / b;
    }
    catch (const std::invalid_argument &e)
    {
    std::cerr << e.what() << '\n';
    std::cout << "Try again? Enter y or n" << std::endl;
    char c;
    cin >> c;
    if (!cin || 'n' == c)
    break;
    }
    }

Thank you for your donate!