C 语言基础好代码教程(我的C之旅开始了)[九]

24 +、-、*、 、= 的优先级 1 优先级 和数学一样,C 语言规定先乘除后加减。也就是说,乘法运算符和除法运算符的优先级(Precedence

24. +、-、*、/、= 的优先级

1. 优先级

    和数学一样,C 语言规定先乘除后加减。也就是说,乘法运算符和除法运算符的优先级Precedence)比加法运算符和减法运算符高。同时,C 语言也规定,如果两个运算符的优先级相同,并且它们之间没有被优先级比它们高或者低的运算符隔开,则它们的运算顺序根据它们在语句中出现的先后而定。大多数运算符都是从左向右进行运算的,不过也有从右向左进行运算的(例如赋值运算符)。乘法运算符和除法运算符的优先级相同,加法运算符和减法运算符的优先级相同。因此,以下语句

        var = 8.0 + 20.0 / 4.0 * 2.0;

运算顺序为:

        20.0 / 4.0
        5.0 * 2.0  (20.0 / 4.0 得 5.0)
        8.0 + 10.0
        var = 18.0

在这个表达式中,/ 和 * 优先级相同,而且是从左向右进行运算的,所以先运算 20.0 / 4.0,然后才轮到 5.0 * 2.0。

    如果我们想让加法先进行,可以给 8.0 + 20.0 加上括号

        var = (8.0 + 20.0) / 4.0 * 2.0;

这个语句的运算顺序为:

        8.0 + 20.0
        28.0 / 4.0
        7.0 * 2.0
        var = 14.0

    C 语言规定,先进行括号里面的运算,后进行括号外面的运算。在括号里面,运算顺序和上面讨论的一样。例如:

        var = (8.0 + 20.0 / 4.0 * 2.0) / 3.0;

运算顺序为:

        20.0 / 4.0
        5.0 * 2.0
        8.0 + 10.0
        18.0 / 3.0
        var = 6.0

下表总结了这几个运算符的优先级以及它们的结合律,按优先级从高到低进行排列

        运算符             结合律

          ()               从左向右
       + -(单目)         从右向左
         * /               从左向右
       + -(二目)         从左向右
          =                从右向左


2. 优先级和运算顺序

    运算符优先级Operator precedence)是决定运算顺序的重要规则,但不能完全(也没必要完全)确定运算顺序。例如:

        5 * 3 + 8 * 4;

根据运算符优先级,我们知道,乘法运算先于加法运算。但是 5 * 3 和 8 * 4 谁先谁后,我们并不能确定。它们运算的先后是由编译器决定的。这是因为某种运算顺序在某种系统中效率更高,而另一种运算顺序在另一种系统中效率更高。无论它们的运算先后如何,最终得到的结果都是 47。

    您可能会说:“乘法不是从左向右进行运算的吗?这不是说明最左边的乘法最先进行吗?”是的,乘法的确是从左向右进行运算,但是您也要看到,这两个乘法运算符之间被加法运算符隔开了!我们举一个例子来说明乘法从左向右进行运算的意思。以下语句

        5 * 2 * 9 * 4;

运算顺序为:

        5 * 2
        10 * 9
        90 * 4


下面我们来看一个小程序。

        /* precedence.c -- 优先级测试 */
        #include <stdio.h>

        int main(void)
        {
            int var1, var2;

            var1 = var2 = -(9 + 4) * 5 + (6 + 8 * (7 - 1));
            printf("var1 = var2 = %d\n", var1);

            return 0;
        }

请认真阅读以上程序,想想会出现什么结果,然后编译运行,看看运行结果和您想象的是否一样。

    首先,括号运算符优先级最高。但是 (9 + 4) 和 (6 + 8 * (7 - 1)) 运算的先后是由编译器决定的。假设 (9 + 4) 先进行,则运算后得 13,然后负号运算符作用于 13 得 -13。于是我们得到:

        var1 = var2 = -13 * 5 + (6 + 8 * (7 - 1));

在 (6 + 8 * (7 - 1)) 中,先运算 (7 - 1),得:

        var1 = var2 = -13 * 5 + (6 + 8 * 6);

因为 * 优先级高于 +,于是我们得到:

        var1 = var2 = -13 * 5 + (6 + 48);

进而

        var1 = var2 = -13 * 5 + 54;
        var1 = var2 = -65 + 54;
        var1 = var2 = -11;

因为赋值运算是从右向左的,所以 -11 被赋值给 var2,接着 var2 被赋值给 var1。最终的结果是,var1 和 var2 相等,它们的值都是 -11。


 

25. 模除运算符 %

 

    %模除运算符Modulus Operator),用于求余数。% 只可用于对整数进行模除,不可用于浮点数。例如:

          15 % 2       // 正确。余数为 1
          15.2 % 3     // 错误!

C99 以前,并没有规定如果操作数中有负数,模除的结果会是什么。C99 规定,如果 % 左边的操作数是正数,模除的结果也是正数;如果 % 左边的操作数是负数,模除的结果就是负数。例如:

          15 % 2       // 余 1
          15 % -2      // 余 1
          -15 % 2      // 余 -1
          -15 % -2     // 余 -1

标准规定,如果 a 和 b 都是整数,则 a % b 可以用公式 a - (a / b) * b 算出。例如:

          -15 % 2 == -15 - (-15 / 2) * 2 == -15 - (-7) * 2 == -1

最后,我们看一个小程序。

        /* months_to_year.c -- 将用户输入的月数转换成年数和月数 */

        #include <stdio.h>

        int main(void)
        {
            int months, years, months_left, months_per_year = 12;

            printf("Enter the number of months: ");
            scanf("%d", &months);

            years = months / months_per_year;         /* 算出年数 */
            months_left = months % months_per_year;   /* 算出剩余的月数 */

            printf("%d months is %d years, %d months.\n", months, years, months_left);

            return 0;
        }

26. 自增运算符和自减运算符
 

1. 自增运算符(Increment Operator)

    自增运算符 ++ 使操作数的值增 1。++ 可以置于操作数前面,也可以放在后面。例如:

        ++n ;
        n++ ;

这两个语句产生的结果都是使 n 增 1,可以说没什么区别。使用以下语句得到的效果也是一样的:

        n = n + 1 ;

    尽管上面两个语句中,++ 前置和后置没有区别。但是,++ 前置和后置其实是有区别的。例如:

        int n = 1, post, pre;

        post = n++;
        pre = ++n;

对于 post = n++; 这个语句,n 的值被赋予 post 后,n 才增 1。也就是说,这个语句执行完后,post 的值是 1,而 n 的值变成 2。而 pre = ++n; 这个语句,n 先增 1,然后再把自增后的值赋予 pre。也就是说,这个语句执行完后,pre 的值是 3,n 的值也是 3。

    由此可得,如果 ++ 前置,则 ++ 的操作数先增 1,然后再参与其它运算;如果 ++ 后置,则 ++ 的操作数先参与其它运算,然后才增 1。严格地说,前置 ++ 的操作数的值在被使用之前增 1,而后置 ++ 的操作数的值在被使用之后增 1。例如:

        int n = 5, post = 1, pre = 1;
        pre = ++n + pre;    // 运算结束后 pre 为 7
        n = 5;
        post = n++ + post;  // 运算结束后 post 为 6

 

2. 自减运算符(Decrement Operator)

    自减运算符 -- 使操作数的值减 1。-- 可以置于操作数前面,也可以放在后面。例如:

        --n ;
        n-- ;

自减运算符和自增运算符非常相似,区别只在于自减运算符使操作数减 1,而自增运算符使操作数增 1。例如:

        int n = 5, post = 1, pre = 1;
        pre = --n + pre;    // 运算结束后 pre 为 5
        n = 5;
        post = n-- + post;  // 运算结束后 post 为 6

 

3. 优先级

    自增运算符和自减运算符的优先级很高,只有圆括号的优先级比它们高。因此,n*m++; 表示 n*(m++); 而不是 (n * m)++; 。而且 (n * m)++; 是错误的。因为 ++ 和 -- 的操作数只能是可变左值(modifiable lvalue),而 n * m 不是。

    注意,不要把优先级和取值顺序混淆了。例如:

        int x = 1, y = 2, z;

        z = (x + y++) * 3;   // 运算结束后 z 为 9,y 为 3

用数字代替上面的语句得:

        z = (1 + 2) * 3;

仅当 y 的值被使用后,y 才会增 1。优先级表明的是 ++ 仅作用于 y,而不是 (x + y)。优先级也表明 y 的值何时被使用,但是 y 的值何时增 1 是由自增运算符的本质决定的。

    当 y++ 是某个算术表达式的一部分时,您可以认为它表示“先使用 y 的值,然后自增”。类似地,++y 表示“先自增,然后使用自增后的值”。

==========================================================================

以下内容引自《C 语言常见问题集》 原著:Steve Summit 翻译:朱群英, 孙 云

http://c-faq-chn.sourceforge.net/ccfaq/index.html

http://www.eskimo.com/~scs/C-faq/top.html

==========================================================================

4.3 对于代码  int i = 3; i = i++; 不同编译器给出不同的结果, 有的为 3, 有的为 4, 哪个是正确的?

    没有正确答案;这个表达式无定义。参见问题 3.1, 3.7 和  11.32。 同时注意, i++ 和 ++i 都不同于 i+1。如果你要使 i 自增 1, 使用 i=i+1, i+=1, i++ 或 ++i, 而不是任何组合, 参见问题 3.10

12.35 有人说 i = i++ 的行为是未定义的, 但是我刚在一个兼容 ANSI  的编译器上测试, 得到了我希望的结果。

    面对未定义行为的时候, 包括范围内的实现定义行为和未确定行为, 编译器可以做任何实现, 其中也包括你所有期望的结果。但是依靠这个实现却不明智。参加问题 7.4, 11.31, 11.32 和 11.34

4.2 使用我的编译器,下面的代码  int i=7; printf("%d\n", i++ * i++); 返回 49?不管按什么顺序计算, 难道不该打印出56吗?

    尽管后缀自加和后缀自减操作符 ++ 和 -- 在输出其旧值之后才会执行运算, 但这里的``之后"常常被误解。没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式 ``完成" (按照 ANSI C 的术语, 在下一个 ``序列点" 之前, 参见问题 3.7) 之前的某个时刻进行。本例中, 编译器选择使用变量的旧值相乘以后再对二者进行自增运算。

    包含多个不确定的副作用的代码的行为总是被认为未定义。(简单而言, ``多个不确定副作用" 是指在同一个表达式中使用导致同一对象修改两次或修改以后又被引用的自增, 自减和赋值操作符的任何组合。这是一个粗略的定义; 严格的定义参见问题 3.7, ``未定义" 的含义参见问题 11.32。) 甚至都不要试图探究这些东西在你的编译器中是如何实现的 (这与许多 C 教科书上的弱智练习正好相反); 正如  K&R 明智地指出, ``如果你不知道它们在不同的机器上如何实现, 这样的无知可能恰恰会有助于保护你。

4.7 我怎样才能理解复杂表达式?``序列点" 是什么?

    序列点是一个时间点(在整个表达式全部计算完毕之后或在 ||、  &&、 ? : 或逗号 运算符处, 或在函数调用之前), 此刻尘埃落定, 所有的副作用都已确保结束。 ANSI/ISO C 标准这样描述:

在上一个和下一个序列点之间, 一个对象所保存的值至多只能被表达式的计算修改一次。而且前一个值只能用于决定将要保存的值。

第二句话比较费解。它说在一个表达式中如果某个对象需要写入, 则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值。这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。例如 i = i+1 合法, 而 a[i] = i++ 则非法 (参见问题 3.1)。

参见下边的问题 3.8