映月读书网 > C语言解惑 > 16.5 优先级和求值顺序错误 >

16.5 优先级和求值顺序错误

优先级和求值顺序不是一回事,使用不当都能产生错误。尤其是两者混在一起,更不容易掌握。

优先级用来解决计算顺序,与四则运算和逻辑运算类似。C语言运算符繁多,优先级最高为17,其中15级就有8个运算符。附录A给出了它们的关系(相同级别在一个框内),要记住它们并非易事。求值顺序就是规定运算符的求值顺序,C语言仅对4个运算符规定了求值顺序(它们按优先级由高到低排列为&&、||、?:和,),即“&&”运算符最高(4级)而逗号运算符最低(1级),所以在使用时千万不能对其他的运算符假定求值顺序,例如要对表达式a<b求值,编译器可能先对a求值,也可能先对b求值,甚至可能同时对a和b并行求值。

1.优先级

因为逻辑运算符只是控制语句的组成部分之一,所以这里说的优先级,并不是单指逻辑运算符,这也就是控制语句显得很复杂并容易出错的原因。

所有的组合赋值操作符都有相同的较低的优先级,并且是自右至左结合的。这就意味着无论使用什么操作符,一组操作符的序列按照自右至左顺序进行语法分析和执行。

【例16.22】演示算术组合赋值的语法、优先级和结合性的例子。


#include <stdio.h>
void main
(void
)
{
      double k =10.0
, m =5.0
, n = 64.0
, t = -63.0
;
      printf
(\"k = %.2g m =%.2g n = %.2g t=%.2gn\"
,k
,m
,n
,t
);
      t /=n -= m *=k += 7
;
      printf
(\"
执行 t /= n -= m *= k += 7 
后的结果为: n\"
);
      printf
(\"k =%.2g m = %.2g n = %.2g t = %.2g n\"
, k
, m
, n
, t 
);
}
  

程序运行结果如下。


k = 10 m =5 n = 64 t=-63
执行 t /= n -= m *= k += 7 
后的结果为:
k =17 m = 85 n = -21 t = 3
  

(1)这个长表达式显示了所有的组合操作符的优先级相同,按从右向左的顺序进行语法分析和执行。

(2)因为“+=”在“*=”的右边,因此它在“*=”之前解释。尽管单独的“*”操作符比“+”操作符的优先级高,但这同组合操作符的优先级没有关系。

其实,优先级最高者并不是真正意义上的运算符。最高的4个运算符是数组下标“”、函数调用操作符“()”、成员结构选择操作符“指针->”和“成员.”。因此,对成员选择表达式a.b.c的含义是(a.b).c,而不是a.(b.c)。

【例16.23】一个人使用语句


double 
(*p
)( 
);
  

声明函数指针,另一个使用语句


double  *p
( 
);
  

声明函数指针,哪一个的语句正确?

【分析】因为函数调用的优先级要高于单目运算符的优先级,p是一个函数指针,第1种写法是正确的。第2种将被编译器解释成*(p()),所以是错误的。

【例16.24】下面的程序输出数组内容,找出程序中的错误。


#include <stdio.h>
int main
( 
)
{
    int a[3]={1
,3
,5}
, i=0
, *p=a
;
    for
(i=0
;i<3
;i++
)
           printf
(\"%d\"
,(*p
)++
);
    return 0
;
}
  

(*p)++是先取指针p所指地址中的值,使用后再将其加1作为新的*p存入该地址供下次使用。这相当于语句


*p=*p+1
;    //i=0
,1
,2
  

所以输出的都是a[0]的值(1 2 3)。执行完之后,将a[0]里的值修改为4。单目运算符是自右至左结合的,将其改为*p++,才会被编译器解释为*(p++),即取指针指向地址里的值,然后将p的地址变为下一个地址。相当于下面的等效语句


*p=*
(p+i
);   //i=0
,1
,2
  

【例16.25】分析下面程序的输出。


#include <stdio.h>
int main
( 
)
{
     int a[3]={1
,3
,5}
, i=0
, *p=a
;
     char b[8]=\"1234\"
;
     for
(i=0
;i<3
;i++
)
          printf
(\"%d \"
,++*p
);
     p--
;
     for
(i=0
;i<3
;i++
)
          printf
(\"%d \"
,*++p
);
     p=a
;
     p=
(int *
)b
;
     printf
((char *
)p
);
     return 0
;
}
  

自右至左结合,++*p就是++(*p),是先执行加1操作,修改a[0]的值之后再使用,因此输出序列为{2 3 4},并使a[0]=4。

*++p就是*(++p),因为是先改变p指向的地址,所以先执行p--;以便在循环语句里,指针p从&a[0]依次循环到&a[3],从a[0]开始输出数组a的内容{4 3 5}。

类型转换也是单目运算符,它的优先级也和其他单目运算符的优先级相同。将字符串b的地址赋给整型指针,需要使用(int*)转换,打印字符串则要将指针p再次转换为指针类型的指针,保证输出字符串“1234”。

程序最终输出为:2 3 4 4 3 5 1234

双目运算符的优先级比单目运算符的优先级低。在双目运算符中,算术运算符的优先级最高,移位运算符次之,然后依次是关系运算符、逻辑运算符、赋值运算符。优先级比双目运算符低的是条件运算符(三目运算符),而逗号运算符的优先级最低。

C语言中的逗号操作符用来连接两个表达式,使其能够作为一个表达式出现。例如,下面的循环实现求数组data中的前n项的和,用逗号操作符来初始化两个变量,循环计数器和累计器:


for 
(sum=0
, k=n-1
; k >= 0
;  --k 
)  sum += data[k]
;
  

逗号操作符的优先级比分号(;)低,作用虽然类似,但有如下重要的区别。

(1)逗号操作符前后必须是非void类型的表达式。而分号的前后可以是语句也可以是表达式。

(2)在表达式后写上分号表示表达式结束,整个单元成为一条语句。用逗号不代表表达式结束而表示将其同后面的表达式一起构成一个更大的表达式。如表达式“a=3*4,a*5”,先要解得a的值为12,再进行a*5(但a的值不变)。如在之后加“,a+8”,即


( a=3*4
, a*5 
) 
, a+8
  

就构成新的表达式,整个表达式的值为20。

(3)逗号右边的操作数值为整个封装表达式的值,可用来做进一步的计算。

【例16.26】分析下面程序的输出结果。


#include <stdio.h>
int main
( 
)
{
    int a=0
,b=0
,c=0
;
    c=
((a=3*4
, b=a*5
),a+8
);
    printf
(\"%d
,%d
,%dn\"
,a
,b
,c
);     //
值相等
    return 0
;
 }
  

变量a由计算得12,逗号表示继续计算b=60,第2个逗号继续计算12+8=20,这是表达式的值,即c的值,所以输出为:12,60,20。

逗号运算符一般用在for循环和宏定义中。

除逗号运算符之外,三目条件运算符优先级最低。这就允许在三目条件运算符的表达式中包括关系运算符的逻辑组合。下面给出一个实例。

【例16.27】分析下面程序的输出结果。


#include <stdio.h>
int main
( 
)
{
    int a
, b
, c
, i
;
    for
(i=0
,a=5
,b=6
,c=2
;i<2
;i++
)
    {
        a+=
(a<b||a<c
)?10
:2
;
        printf
(\"%d \"
,a
);
    }
    return 0
;
 }
  

三目条件运算符有3个操作数和2个运算符号(?和:)。条件操作符同if~else的作用相同,仅有一点不同:if是一个语句,没有值;而?:是操作符,同其他操作符一样计算并返回一个值。所以在上述程序中,第一次循环,用逗号取得各个变量的初值,5<6成立,不需要再判断另一个表达式,对“10:2”以真值自右至左操作,应取10,执行a+=10得15。第二次循环时,因为a=15,15<6不成立,判断a<c,也不成立,对“10:2”以假值自右至左操作,应取2,执行a+=2得17。程序输出“15 17”。

【例16.28】要求在下面程序运行后,先输出9.000000,再输入字符$结束运行。分析程序中的错误。


#include <stdio.h>
int main
( 
)
{
    double a=2
,b=0
,c=8
;
    char ch
;
    b=1/2*a+c
;
    printf
(\"%lfn\"
,b
);          //
输出b
的值
    while
(ch=getchar
()!=\'$\'
)     //
以$
号结束
        putchar
(ch
);          //
在屏幕上显示出来
    printf
(\"n\"
);
    return 0
;
}
  

注意:1/2*a含义不是1/(2*a),而是(1/2)*a。写的语法是对的,但数字1/2代表整数相除,所以是0,0*a=0,输出是8.000000。将1或2写成实数即可,例如


b=1./2*a+c
;
  

这就保证1./2=0.5,1./2*a=1.000000,从而得到预期结果。

while循环语句的表达式不对。因为赋值运算符的优先级最低,因此ch的值实际上是函数getchar()的返回值与字符$比较的结果。不相等为1,这时就将这个比较值(所得的结果值为1)赋给ch。执行的是


putchar
(1
);
  

这是个图形符号。应该保证ch取得函数getchar()的返回值,再用ch与$比较,即


while
((ch = getchar
()) 
!= \'$\'
)
  

可以将优先级总结如下(参见附录A):

(1)任何一个逻辑运算符的优先级低于任何一个关系运算符。

(2)移位运算符的优先级比算术运算符要低,但比关系运算符要高。

(3)6个关系运算符的优先级并不相同。运算符“==”和“!=”的优先级要低于其他关系运算符的优先级。

(4)任何两个逻辑运算符都具有不同的优先级。

(5)所有的按位运算符优先级要比顺序运算符的优先级高,每个“与”运算符要比相应的“或”运算符优先级高,而按位异或运算符(“^”运算符)优先级介于按位与运算符和按位或运算符之间。

由于运算符“==”和“!=”的优先级要低于其他关系运算符的优先级,如果要比较a与b相对大小顺序是否和c与b的相对大小顺序一样,就可以写成


a < b  ==  c < d
  

对于整数a和b,有人将语句


if
(a & b
)
  

写成if(a&b!=0)。因为“!=”运算符的优先级高于“&”运算符优先级,实际上被解释为


if
(a &
(b 
!=0
))
  

这与原来的含义


if
((a & b 
)!=0
)
  

是不同的。同理,加法运算的优先级比移位运算符的优先级高。表达式


r=hi<<4 + low
;
  

的含义是


r=hi<<
(4 + low
);
  

如果本意是先移位,应该使用括号避免这类问题,即


r=
(hi<<4 
)+ low
;
  

也可以将加号改为按位逻辑或,即


r=hi<<4 | low
;
  

使用时一定要仔细,以免用错。

2.求值顺序

C语言仅对4个运算符(&&、||、?:和,)规定了求值顺序,“&&”优先级最高,依次是“||”,逗号运算符最低,分析时注意,除了“?:”是自右至左分析之外,其他三个均是自左向右分析。

运算符“&&”和运算符“||”首先对左侧操作数求值,只在需要时才对右侧操作数求值。这对于保证检查操作按照正确的顺序执行至关重要。例如,在语句


  if 
( y 
!= 0 && x/y > max
)
  

中,就必须保证仅当y非0时才对x/y求值。

运算符“?:”有三个操作数,假设为a?b:c,操作数a首先被求值,根据a的值再求操作数b或c的值。

【例16.29】分析下面程序的输出结果。


#include <stdio.h>
int main
( 
)
{
     int a=5
,a1=15
,a2=2
;
     a+=
(a<a1||a<a2
)?a1+a2
:a1-a2
;
     printf
(\"%dn\"
,a
);
     return 0
;
 }
  

【分析】三个表达式是(a<a1||a<a2)、a1+a2和a1-a2。第1个表示式中的a<a1满足,无需去求右侧的a<a2,即第1个表达式为真。为真只需要求表达式a1+a2=17,带入


a+=17
;
  

即a=5+17=22,程序输出22。由此可见,冒号前后的表达式必须能够求值且应该是相同数据类型的值。

逗号表达式首先对左操作数求值,然后“丢弃”该值,再对右操作数求值。

C语言其他所有运算符对其操作数求值的顺序是未定义的。特别是赋值运算符,并不保证任何求值顺序。有时称为操作符的副作用。尤其是要注意++和--,因为这类操作符的副作用明显,所以有时又把它们称为副作用操作符。编程时要保持副作用操作符的独立。因为多数表达式的计算顺序是不确定的,如果对变量V使用自增或自减操作符,就不要在同一个表达式的其他地方再使用变量V。如果再次使用V,就无法预先确定V的值在自增前后是否改变了。

组合赋值的优先级较低,无论使用哪种复合运算,都要严格按照自右至左进行分析。

3.多操作符简便运算

在简便计算的情况下,总是忽略右边的操作数。当右边操作数是简单变量时,不会引起混淆。但是,有时右边是一个包含几个操作符的表达式。假设有如下程序:

【例16.30】多操作符简便运算的例子。


#include <stdio.h>
void main
(void
)
{
    int b
,y
,a
;
    scanf
(\"%d%d\"
,&a
,&b
);
    y=a < 10 || a >= 2*b && b
!=1
;
    printf
(\"%dn\"
,y
);
}
  

它的||操作符左边是表达式a<10,右边是表达式a>=2*b&&b!=1。当a=7,b为任意值时,赋值语句y对||产生忽略。当a=17,b=20时,对&&产生忽略。

计算逻辑表达式的顺序是从左向右,其中可以省略某些子表达式。首先计算最左边逻辑运算符左边的操作数的值。根据这个值决定是继续计算表达式右边的数还是将其忽略。在这个例子中,先计算a<10;如果是true,就忽略其余(包括“&&”操作符的表达式)。

对此通常会有一种看法:认为应该先计算“&&”,因为它的优先级高。其实,由于“&&”操作符的优先级高,因此它“截获”a>=2*b。然而逻辑表达式是自左向右计算,而“||”操作符是在“&&”操作符的左边。因此必须从“||”开始。只有“||”左边的操作数为false时,才会继续计算“&&”。在这种情况下,“&&”左边的操作数是false,意味着可以忽略其右边的操作数。

当发生忽略时,无论右边多么复杂,是什么样的操作符,所有的工作都被忽略。在这个例子中,左边的操作数计算的是一个简单的比较,而右边是一个很长且复杂的表达式。只有a<10为true时,才可以忽略操作符右边的所有工作,并将1存储在变量y中。

如果计算表达式


y=a<0 || a++<b
  

在a=-3时的值,则要注意自增操作在“||”的操作符的右边,因为“||”左边的操作数为true,因此不计算自增操作。同样也忽略了比较之后的所有计算。不过,总的来说可能忽略剩余表达式的一部分。

有时,简便计算可以提高程序的效率。但是效率的提高是很微小的,简便计算的更大的好处是避免由于计算表达式的其余部分而产生的机器崩溃或其他问题。例如,假定希望用某个数除以x,并将结果同一个极小值比较,如果结果比极小值小,程序就会发生错误。但是,x是可能等于0的,因此必须进行检查。为了避免除0错,可在一个表达式中同时进行计算和比较,在除之前使用哨兵。哨兵表达式是由错误条件测试后跟“&&”操作符构成。完整的C语言表达式如下:


if 
(x
!=0 && cotal /x < minimum
)  do_error
();
  

带哨兵的表达式的应用非常广泛。

C语言中另一种常见的错误是,一旦开始进行忽略,则表达式右边的全部都被忽略。这是不对的。应该是仅仅忽略引起忽略的操作符的右操作数。如果在表达式中有多个逻辑操作符,可能需要计算开头和结尾的分支而忽略中间部分。如计算表达式


y=a < 0 || a > b && b > c || b >10
  

当a=3,b=17时的值。注意只忽略“&&”的右操作数,其他的表达式没有被忽略。任何情况下,都要确定应该忽略什么。

4.总结

本节涉及一些关于C语言的非直接语义方面的问题,这些问题引起很多编程错误。为了更好地使用C语言,应该了解如下问题。

(1)使用简便求值。逻辑操作符的左操作数总是要计算的,但有时候可能会忽略右边的操作数。当左边操作数足够确定整个表达式的值时,就会产生忽略现象。

(2)使用哨兵表达式。由于有了简便计算,于是可以写一个复合条件,令左边部分为“哨兵表达式”,用来检查和限制可能会导致右边崩溃的条件。如果哨兵表达式检测到了“致命”条件则忽略右边部分。

(3)求值顺序不同于优先级顺序。虽然首先分析高优先级的操作符,但并不一定首先计算它们,在逻辑表达式中也可能根本就不计算它们。逻辑表达式按自左到右的顺序计算,中间可能会忽略一些部分。处于语法分析树中被忽略部分的操作符都不会被执行。因此,尽管自增操作符的优先级很高,逻辑操作符的优先级很低,也可能不执行自增操作符。

(4)求值顺序不确定。只有逻辑与(||)、逻辑或(&&)、逗号和条件操作符是按照自左到右的顺序计算。其他二元操作符,既可以先算左边的,也可以先算右边的。

(5)对有副作用的操作符,应保持它们的独立性,以免引起错误。