有個國外團隊檢測了 200 多個 C/C++ 開源專案,包括了 Php、Qt 和 Linux 核心等知名專案。於是他們每天分享一個錯誤案例,並給出相應建議。本篇案例來自 Miranda NG 原始碼。
錯誤程式碼:
1 2 3 4 5 6 7 8 |
#define MF_BYCOMMAND 0x00000000L void CMenuBar::updateState(const HMENU hMenu) const { .... ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR, MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED); .... } |
問題解釋:
我想提出一個發人深思的主題來討論:有時候,我們會看到完全不正確的程式碼卻能工作得很好!
經驗豐富的程式猿不會對這個問題感到驚訝(這又是另一個故事),但對那些最近剛開始學習 C/C++ 的人來說,這個問題也許有點讓人困惑。那麼,今天我們就來看看這樣一個示例。
在上面顯示的程式碼中,必須在呼叫 CheckMenuItem() 時設定標誌位;然後,先看 bShowAvatar 是否為 true,如果成立MF_BYCOMMAND 就和 MF_CHECKED 按位或運算,反之則和 MF_UNCHECKED 做按位或運算。就這麼簡單!
1 |
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED |
在上面的程式碼中,這位程式猿很自然地選擇三元運算子來表示這個過程(這個運算子是 if-then-else 的簡便版):
1 |
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED |
問題是,按位或運算子 | 比運算子 ?: 的優先順序更高(詳細看 C/C++ 運算優先順序)。很明顯這裡有兩個錯誤。
第一個錯誤是條件變了,不再是“dat->bShowAvatar”,而變成了“MF_BYCOMMAND | dat->bShowAvatar”。
第二個錯誤是隻選擇了一個標誌位 —— MF_CHECKED 或 MF_UNCHECKED,標誌位 MF_BYCOMMAND 不見了。
儘管存在這些錯誤,這條程式碼仍然能正常工作!這純粹是因為運氣。這位程式猿很走運,因為 MF_BYCOMMAND 等於 0x0000000L。
當標誌位 MF_BYCOMMAND 等於 0,也不會對程式碼有任何影響。可能某些經驗豐富的程式猿已然明瞭,但萬一這裡有新手,我還是詳細解釋一下其中的緣由。
先來看看加了括號的正確表示式:
1 |
MF_BYCOMMAND | (dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED) |
用數值替換巨集:
1 |
0x00000000L | (dat->bShowAvatar ? 0x00000008L : 0x00000000L) |
如果運算子“|”兩邊的運算元之一為0,那我們可以將表示式簡化為:
1 |
dat->bShowAvatar ? 0x00000008L : 0x00000000L |
現在來進一步看看錯誤程式碼的變體:
1 |
MF_BYCOMMAND | dat->bShowAvatar ? MF_CHECKED : MF_UNCHECKED |
用數值代替巨集:
1 |
0x00000000L | dat->bShowAvatar ? 0x00000008L : 0x00000000L |
在子表示式“0x0000000L | dat->bShowAvatar”中,運算子 | 的運算元之一為0,將表示式簡化為:
1 |
dat->bShowAvatar ? 0x00000008L : 0x00000000L |
結果就是,我們得到了相同的表示式,這就是錯誤程式碼能正確工作的原因。又發生了一個程式設計奇蹟。
正確程式碼這麼寫:
有很多種方法可以修正這段程式碼,其中一個方法就是加上圓括號,另外一個就是增加一箇中間變數。用大家熟悉的操作符“if”在這裡也會有幫助:
1 2 3 4 |
if (dat->bShowAvatar) ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR,MF_BYCOMMAND | MF_CHECKED); else ::CheckMenuItem(hMenu, ID_VIEW_SHOWAVATAR,MF_BYCOMMAND | MF_UNCHECKED); |
我真的不是強調非用這種方式修正程式碼不可。這樣也許更易閱讀,但稍微有點長,所以這更多的是個人偏好。
建議:
我的建議很簡單 —— 設法避免複雜的表示式,尤其是在使用三元運算子的時候。還有,別忘了圓括號。
要記住,操作符 “?:”很危險!有時你會忘記它的優先順序比較低,並且容易寫出錯誤的表示式。人們常常在塞滿一個字串時使用這個操作符,因此儘量不要那麼做。