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

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

(0)

相关推荐

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

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

  • C 语言基础教程(我的C之旅开始了)[八]

    19. 基本数据类型:复数类型和虚数类型 C99 新增了复数类型(_Complex)和虚数类型(_Imaginary).简单来说,C99 提供了三种复数类型:float _Complex,double _Complex,和 long double _Complex.对于 float _Complex 类型的变量来说,它包含两个 float 类型的值,一个用于表示复数的实部(real part),另一个用于表示虚部(imaginary part).类似地,double _Complex 包含两个

  • C 语言基础教程(我的C之旅开始了)[十]

    27. 表达式(Expression) 表达式由运算符和操作数组成.单独一个操作数是最简单的表达式.请看以下例子: 9                    -4                    +5                 3 + 6       a * ( b/c - d )             e = 9 / 3           f = ++e % 3 表达式中的表达式称为子表达式.例如:b/c 是 a * ( b/c - d ) 的子表达式. 每个表达式都有一个值,

  • C 语言基础教程(我的C之旅开始了)[六]

    14. 基本数据类型:字符型(上) 1.字符型(char)简介 字符型(char)用于储存字符(character),如英文字母或标点.严格来说,char 其实也是整数类型(integer type),因为 char 类型储存的实际上是整数,而不是字符.计算机使用特定的整数编码来表示特定的字符.美国普遍使用的编码是 ASCII(American    Standard    Code   for    Information   Interchange  美国信息交换标准编码).例如:ASCII

  • C 语言基础教程(我的C之旅开始了)[五]

    12. 基本数据类型:整型(下) 1. 输出各种整数类型的变量 输出不同类型的整数,需要使用不用的格式限定符.输出 unsigned int 类型的整数,要用 %u .输出 long ,要用 %ld:如果要以十六进制或者八进制形式输出,那就用 %lx(或者%lX)或者 %lo.注意:虽然整数常量的后缀使用大写或者小写英文字母都没关系,但是它们格式限定符必须使用小写!如果我们要输出 short 类型的整数,可以在 %d 中间加上前缀 h,也就是%hd:同理,%ho 和 %hx(或者 %hX )分别

  • C 语言基础教程(我的C之旅开始了)[四]

    10. 基本数据类型:整型(上) 1. 整型 int C 语言提供了很多整数类型(整型),这些整型的区别在于它们的取值范围的大小,以及是否可以为负.int 是整型之一,一般被称为整型.    int 代表有符号整数,也就是说,用 int 声明的变量可以是正数,可以是负数,也可以是零,但是只能是整数.标准规定 int 的最小取值范围是 -32767 到 32767.int 的取值范围因机器而异,但是一定要大于或者等于-32767到 32767.一般来说,int 占用一个字的内存空间.因此,字长为

  • C 语言基础教程(我的C之旅开始了)[三]

    7. 第三个 C 程序 首先请看下面这个小程序,试着自己编译运行一下.如果不懂怎么编译,请点击下面的超链接:      编译器使用方法    编译器Dev-C++下载&使用教程 /* circle.c  --  计算圆的面积 */ #include <stdio.h> int main( void )      {          float radius;    /* 圆的半径 */ printf( "Please enter the radius: " ); 

  • C 语言基础教程(我的C之旅开始了)[二]

    3. C 程序的结构 C 程序由一个以上的函数组成,而且必须有 main 函数.此外,C 程序一般还有一些预处理指令.例如 #include 指令.当然并不是必须要有 #include 指令.函数由函数头和函数体组成.函数头由返回值.函数名以及参数列表(可以是void)组成.函数体从 { 开始,以 } 结束.函数体里可以有一系列的语句,每个语句以分号(;)结束.例如: 预处理指令     --〉   #include <stdio.h> 函数头         --〉  int main( v

  • C 语言基础教程(我的C之旅开始了)[七]

    17. 基本数据类型:布尔型(_Bool type) _Bool 型是 C99 添加的,用于表示布尔值,亦即是表示逻辑真(true)和逻辑假(false).因为 C 用 1 表示 true ,0 表示 false ,所以 _Bool 实际上是整数类型.理论上 _Bool 只需要 1 bit 存储单元,因为1 bit 就足以表示 0 和 1 .事实上,_Bool 是无符号整型,一般占用 1 字节.例如: _Bool flag = 1; flag = 0; 包含标准头文件 stdbool.h 后,我

  • C 语言基础教程(一)颜色加亮

    首先请看下面这个简单的 C 程序,猜猜它会做些什么.猜不出来也没关系,后面会有详细的解说.         #include <stdio.h> int main( void )            /* 一个简单的程序 */         {             int num;                  /* 定义一个名叫 num 的变量 */             num = 1;                  /* 将 1 赋值给 num         */ p

随机推荐