Section outline

  • 带标签语句(Labeled Statements)

    在 K&R 中,标签名与“普通”标识符共享同一个命名空间。也就是说,如果在一个内层代码块中声明了一个与标签同名的标识符,那么该标签名就会被隐藏。例如:

    void f()
    {
        label: ;
        {
            int label;
            …
            goto label;
        }
    }
    

    这个代码会在编译时报错,因为 goto 所跳转的目标 label 被视为一个整型变量而不是标签。

    而在标准 C 中,标签有自己的独立命名空间,因此上面的代码可以正常编译,不会报错。

    K&R 指定内部标识符(如标签)的有效长度为 8 个字符。

    而标准 C 要求内部标识符(包括标签)的有效长度至少为 63 个字符。


    复合语句(Compound Statement / Block)

    在 C99 之前,代码块中的所有声明必须位于所有语句之前。但从 C99 开始,声明和语句可以交错出现。

    C++ 注意事项:C++ 也允许声明和语句交错使用。

    使用 gotoswitch 可以跳转进入一个代码块。虽然这样是可移植的,但是否能正确初始化被“跳过”的自动变量则未作保证。

    K&R 允许嵌套代码块,但未说明嵌套的深度限制。

    C89 要求复合语句至少支持嵌套 15 层,C99 将此限制提升到了 127 层。


    表达式语句和空语句(Expression and Null Statements)

    以下是一个使用 volatile 类型限定符的示例(此限定符由 C89 引入):

    extern volatile int vi[5];
    void test2()
    {
        volatile int *pvi = &vi[2];
    
        vi[0];
        pvi;
        *pvi;
        *pvi++;
    }
    

    当处理带有 volatile 限定的对象时,优化器必须非常小心,因为不能对这些对象的状态作出任何假设。即使像 *pvi; 这样的语句,看似没有实际用途,编译器也可能会生成访问 vi[2] 的代码,这可能涉及到硬件同步。但 pvi; 语句不会被执行任何访问操作,因为 pvi 本身不是 volatile

    建议:不要依赖表达式语句如 i[0];pi;*pi; 来生成代码。即使 i 是一个 volatile 对象,也不能保证一定会进行访问。


    选择语句(Selection Statements)

    if 语句

    if 控制表达式是完整表达式,因此在其后有一个序列点(sequence point)

    switch 语句

    K&R 要求控制表达式及每个 case 表达式的类型必须是 int

    标准 C 放宽了限制,要求它们是某种整型。case 表达式会在必要时转换为与控制表达式相同的类型。

    由于标准 C 支持枚举类型(以整数形式表示),可以在 switchcase 中使用枚举常量(K&R 中没有枚举类型)。某些实现支持 case 表达式中的区间语法,但因语法不统一,标准 C 不支持该特性。

    标准 C 允许多字符字符常量(如 'ab''abcd')用于 case 中。

    建议:由于多字符常量的内部表示是实现定义的,不应case 中使用多字符字符常量。

    K&R 没有规定 switch 中最多允许多少个 case。C89 要求至少支持 257 个,C99 将此数量提升至 1023 个。

    switch 控制表达式是完整表达式,因此在其后存在一个序列点。

    关于 switch 中跳转到复合语句的问题,见上文“复合语句”。


    循环语句(Iteration Statements)

    whiledofor 语句中的控制表达式可能包含如 expr1 == expr2 的浮点比较表达式。由于浮点运算涉及舍入等实现定义行为,表达式结果可能难以精确相等

    建议:控制表达式中如需比较浮点值,考虑使用类似 fabs(expr1 - expr2) < 0.1e-5 这样的方式代替 expr1 == expr2

    有些程序中会出现“空转循环”(idle loop),例如:

    for (i = 0; i <= 1000000; ++i) { }
    

    为解决这类空循环的问题,C11 引入以下规则:如果循环控制表达式不是常量表达式,且其循环体中没有输入/输出操作、未访问 volatile 对象、未进行同步或原子操作,那么编译器可以假定该循环会终止,并可以将其优化掉。比如上述示例中,编译器可以省略整个循环,只保留使 i 最终变为 1000001 的效果。

    建议:不要使用“空转循环”来实现延时。这类循环即使没有被优化掉,其执行时间也极易受系统任务优先级和处理器速度影响。

    标准 C 保证控制结构和复合语句至少支持 15 层嵌套,C99 提升到 127 层。K&R 未说明嵌套层数下限。

    while 语句

    控制表达式是完整表达式,后有序列点。

    do 语句

    控制表达式是完整表达式,后有序列点。

    for 语句

    三部分表达式均为完整表达式,三者之后均有序列点。

    C99 支持在 for 的第一个表达式中进行声明,例如 int i = 0;,而不再要求变量 i 事先定义。

    C++ 注意事项:C++ 也支持这一 C99 特性,但在 for 中声明的变量作用域规则与 C 不同。


    跳转语句(Jump Statements)

    goto 语句

    关于标签命名空间的说明,见前文“带标签语句”。关于跳转进入代码块的问题,见“复合语句”部分。

    return 语句

    当使用 return expression; 形式时,expression 是完整表达式,之后存在序列点。

    若函数声明有返回值,但实际未返回任何值,行为未定义(C99 起,对 main 函数除外,若无明确返回则视为 return 0;)。

    标准 C 支持 void 函数类型,允许编译器验证函数不返回值(K&R 不支持 void 类型)。

    标准 C 还支持按值返回结构体与联合体,对返回对象大小无强制限制,虽然传值的大小可能受限。K&R 不支持这种返回方式。

    K&R 第 68 页和第 70 页示例中使用 return(expression);,而第 203 页的正式语法为 return expression;。这看似矛盾,实则不然:第 203 页是正确的,括号只是表达式中的冗余分组符号,不属于语法的一部分。示例中使用括号是出于风格考虑,便于将 return 与表达式分开。第二版 K&R 已移除括号,并通常使用 return 0; 结束 main 函数。

    C99 还添加了一条限制:“不带表达式的 return 语句只能出现在返回类型为 void 的函数中。”