讀古今文學網 > 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)對有副作用的操作符,應保持它們的獨立性,以免引起錯誤。