在表達一些比較復雜的條件判斷時,在同一個表達式中,有時可能會存在多個操作符。比如,我們在判斷要不要買某個西瓜時,不僅要判斷它的總價(單價8.2元/斤,一共10.3斤)是否小於100塊錢(因為兜裡只有這麼多錢),同時還要判斷這個西瓜是否有壞掉的地方。要表達這個復雜的條件判斷,我們不得不把前面學過的算術操作符、關系操作符和邏輯操作符全都派上場:
bool bBad = false; // 是否有壞掉的地方 float fPrice = 8.2; // 單價 float fWeight = 10.3; // 重量 // 判斷總價是否小於100且是否壞掉 if(fPrice * fWeight < 100 && !bBad) { cout<<"買西瓜"<<endl; } else { cout<<"算了,不買了"<<endl; }
在“fPrice * fWeight < 100 && !bBad”這個表達式中,有算術操作符“*”,有關系操作符“<”,同時也還有邏輯操作符“!”和“&&”。那麼,這麼多操作符在同一個表達式中,到底該從哪一個操作開始呢?這個表達式的最終結果又是什麼呢?
要想搞清楚一個表達式是按照什麼順序計算的,就得先搞清楚各個操作符之間的計算優先級。按照正確的計算順序進行計算,才可以得出正確的結果。在C++中,各個操作符的優先級如表4-1所示。
表4-1 操作符的優先級
級別
操 作 符
說 明
1
( )
括號是所有操作符中的領導,具有最高的優先級。如果括號內部還有括號,內部括號的優先級更高
2
!、+(正號)、-(負號)、++、--
它們都是一元操作符,往往是對操作數進行計算得到結果後繼續參與下一個計算
注意,這裡的+、-指的是改變數值正負屬性的符號,而不是加減操作的符號
3
*、/、%
乘、除、取余運算
4
+、-
加、減運算
5
>、>=、<、<=、==、!=
關系運算
6
&&
邏輯與運算
7
||
邏輯或運算
8
=、+=、*=、/=、%=
賦值操作
表達式的計算順序規則是:總是優先計算優先級較高的操作符;同一優先級的操作符,則按照從左到右的順序進行計算。在清楚了各操作符的優先級及表達式的計算規則後,那就可以計算上面這個復雜表達式的結果了。在這個表達式中,優先級最高的操作符是對bBad變量進行邏輯非運算的“!”符號,所以它優先得到運算,形成這樣的中間結果:
fPrice * fWeight < 100 && true // bBad的值為false,取非運算後的結果是true
在這個中間結果表達式中,優先級最高的是計算總價的乘法算術操作符“*”,接著對其進行計算,得到一個中間結果:
84.46 < 100 && true // fPrice*fWeight的結果是84.46
經過前面兩步的計算,整個表達式就清晰多了。在剩下的兩個操作符中,比較大小的關系操作符“<”的優先級較高,應該得到優先計算,得到的中間結果是:
true && true
現在,剩下唯一的邏輯與操作符“&&”,最終結果一目了然,對兩個true值進行邏輯與運算,表達式的最終結果是true。計算機在對表達式進行計算時,是按照各個操作符的優先級確定的計算順序進行的。反過來,這也就要求我們在設計表達式的時候,也同樣必須遵守操作符的優先順序,按照這個順序來設計表達式。否則,實際的計算順序跟我們設想的計算順序不同,得到的計算結果自然也就跟我們的設想大相徑庭了。從這個意義上講,熟悉和掌握操作符的優先級十分必要。
最佳實踐:合理使用括號標示表達式的計算順序
從上面這個例子我們可以看到,過於復雜的表達式計算起來非常麻煩。雖然表達式是由計算機負責計算,我們不用擔心計算機怕麻煩。但是,表達式卻是由程序員進行設計,並且也是要提供給他人閱讀的。設計過於復雜的表達式很容易出錯,且代碼的可讀性非常差。所以我們應當盡量避免在同一表達式中混合使用多個操作符,盡量保持表達式的短小精悍。必要的時候,可以將復雜的表達式拆分成多個較小的表達式分別計算得到中間結果,最後再將中間結果組合起來得到最終結果。例如,我們可以把上面的復雜表達式拆分成兩個較小的表達式,分別判斷是否有壞掉的地方以及總價是否小於100塊,然後再將這兩個中間結果進行“與”運算,得到最終結果:
// 將復雜表達式拆分成兩個較小的表達式 bool bFresh = !bBad; // 表示是否新鮮 float fTotal = fPrice * fWeight; // 計算總價 bool bMoney = fTotal < 100; // 判斷總價是否小於100塊 // 對中間結果進行比較 if( bFresh && bMoney) // …
經過這樣的拆分,每個表達式的計算都清楚明了,減少了出錯的可能,可讀性也得到了提升。但是它同時也帶來一個不便之處,那就是代碼變的過於繁瑣。既想得到拆分表達式帶來的清楚明了的好處,又想避免代碼繁瑣的不便,那就只有使用“()”了。
“()”的優先級是所有操作符中最高的,使用它,可以人為地按照設計者的意圖標示表達式中的計算順序。比如,可以改寫上面的表達式,用括號來表達我們希望的計算順序,讓其表達的意義更加清晰:
// … if(((fPrice * fWeight) < 100) && (!bBad)) // …
使用括號後,整個表達式的計算順序變得一目了然:按照括號確定的計算順序,首先計算最裡層的(fPrice * fWeight) 得到中間結果84.46,然後計算(84.46 < 100)得到中間結果true,接著計算(!bBad)得到中間結果true,最後計算“true && true”得到最終結果true。使用括號後,計算順序跟默認順序相同,但是卻增加了代碼的可讀性,讓我們對計算順序一目了然,同時也避免了讓代碼變得過於繁瑣。另外,在某些特殊情況下需要改變表達式的默認計算順序時,括號成為一種必須。
總結起來,使用“()”後,我們想讓表達式按照什麼順序計算就按照什麼順序計算,媽媽再也不用擔心我記不住各個操作符的優先級。
學習C++編程,實際上也就是學習如何使用這門特殊的語言來描述和表達現實世界,就如同我們學習英語是為了用它來描述和表達現實世界一樣。在前面的章節中,我們學習了操作符,學習了由操作符連接操作數而構成的各種表達式,而這些只能算是這門語言中的“短語”,它們可以表達一定的意義,但卻是不完整的:
// 短語式的表達式 a // 一個單獨的變量,什麼都不做 3 + 2 // 用算術操作符“+”計算3和2的和
這些表達式可以被執行,但它們並不改變程序的狀態,也沒有計算結果保留下來,所以沒有任何實際的意義。就像在英語中我們需要給短語加上主謂賓才能構成一個完整的句子一樣,在C++中,我們也同樣需要把一些表達零散意義的表達式組合起來,最後再加一個英文分號表示結束,以此來形成一個語句,用以完成某個相對獨立而完整的功能。例如,把上面兩個表達式通過賦值操作符組合起來,就形成了一條完整的賦值語句:
// 賦值語句 a = 3 + 2;
形成語句後,它表達了一個完整的意義:用算術操作符“+”計算3和2的和,然後將其賦值給變量a。
在C++中,語句和表達式並沒有嚴格的區分。很多時候,一個表達式加上一個分號就可以直接形成一條語句。語句強調它所完成的功能,而表達式關注它所描述的運算和最終的結果。在此之前,我們已經接觸過兩種最常見的語句類型:變量定義語句和賦值語句。
知道更多:使用“{}”表示的語句塊
當連續的多條語句屬於同一個控制結構時,可以用一對花括號“{}”將這些語句括起來,從而形成一個語句塊,共同表達一個相對獨立的意義。在使用上,語句塊與單獨的語句並無太大區別,但是它的意義在於,它可以將多條語句打包成一個語句塊,從而可以在for循環等控制結構中執行多條語句。例如,在for循環結構中,我們可以這樣來統計從1到100間所有整數的和:
int nTotal = 0; for(int i = 1; i <= 100; ++i) nTotal += i;
這個統計只需要一條語句就可以完成,自然可以把這條語句直接放在for循環結構之後完成,可是如果我們只需要統計這個區間中所有偶數的和,那麼就需要加上條件判斷,這就不是單獨一條語句可以完成的了。我們必須用“{}”將所有判斷偶數、統計偶數的語句打包成一個語句塊,然後放在for循環結構之後才能完成統計:
for(int i = 1; i <= 100; ++i) { // for循環語句塊開始 if(0 == i%2) // 判斷語句 nTotal += i; // 統計語句 } // for循環語句塊結束
除了打包語句之外,語句塊的另外一個意義是,它代表了C++中的作用域的起訖位置。關於作用域的具體介紹可以參考後繼的7.3.3小節。