C語言陷阱與缺陷 之 詞法
前言
本文主要參考Andrew Koenig 的《C Traps and Pitfalls》,重新編排整理,歸納總結了C語言程式設計中常見的問題點,並提出了一些簡單的意見和建議,以期讀者閱讀本文後,能夠在程式設計實踐中做出預防性的設計,減少因語言理解不到位而產生的Bug。讓我們站在巨人的肩膀上前進,而不是陷在巨人的腳印裡掙扎,願與諸君共勉。
目錄
一、相似符號
在C語言中有幾對長得很相似符號,如:= 和 ==、| 和 ||、& 和 &&,其潛在的問題在於,程式設計師很容易無意中將這些符號誤寫成對方的模樣,而且大多數情況是無法通過編譯報錯發現的。
程式碼1.1 誤寫判等符
int x = 2;
int y = 3;
bool func(int x, int y)
{
bool res = false;
if (x = y) {
res = true;
}
return res;
}
可以看出上邊這段程式碼的原意是要在引數相等的時候返回真,但由於將 == 誤寫了 =,實際就成了判斷y是不是0。現在的編譯器一般可以識別出判斷表示式中 = 和 == 的區別,並通過告警資訊顯示出來,但如果考慮相容編譯器版本,則還是應改做一些規定和處理:
- 判斷表示式中不要進行賦值操作
- 變數和常量判等時,常量放在符號左邊
- 兩個變數判等時,生命週期長的變數放在符號右邊
擴充套件的講,最好不要在判斷表示式中進行帶有“副作用”的操作,比如自增、自減甚至是運算賦值,這會增加程式理解的複雜度,同時很容易引入問題。
常量放在 == 左邊可以避免誤寫為 = 的情況,此時編譯器一定會報錯。不同於 > 和 < 判斷,判等符的左、右值是什麼,並不會影響程式的閱讀。
生命週期長的變數放在 == 右邊,是考慮在誤寫時降低影響面,便於定位問題;但有些情形下,也可能會掩蓋問題的發現,因此還要結合實際業務場景,區別對待。
| 和 ||、& 和 && 與 = 和 == 的問題大致相同。要想避免誤寫,除了仔細仔細再仔細外,可以做的一點是:
- 用 or 替換 ||
- 用 and 替換 &&
如同用 uint 替換 unsigned int 一樣,借鑑其他語言,使用文字替換,降低誤寫的概率。
另外,/ 和 // 在形式上似乎和上述幾種情形相似,但實際上卻有一個明顯的差別,即誤寫時很容易在編譯階段發現,因而不再作進一步討論。
二、貪心法
C編譯器讀取多字元符號(//、/*、==)時,為了準確的識別出符號,會從左到右逐個讀入字元,直到讀取的字元不能再組成一個有意義的符號時停止,稍有不慎就會產生一句程式碼兩個意思的情況。
程式碼2.2 準二義性語句
int func(int x, int *p)
{
if (*p == null) {
return -1;
}
int res = x/*p /* p指向除數 */;
return res;
}
程式碼2.2展示了一種準二義性的語句,其原意是要求商。但由於編譯器識別到第一個 / 後,會根據貪心法繼續向後讀取,一直到最後一個 / 才結束,對編譯器而言就變成了返回x的值。針對這種情況:
- 運算子號左右需要各空一個空格
- 使用括號明確運算優先順序
在運算子左右各空一個空格,除了使程式碼美觀、易讀之外,還具有分割字元的作用,如將程式碼2.2改為程式碼2.3的形式,語義就正確了。
程式碼2.3
int func(int x, int *p)
{
if (*p == null) {
return -1;
}
int res = x / *p /* p指向除數 */;
return res;
}
使用括號顯式的標明運算子的優先順序,而不過度依賴運算子自身的結合性和優先順序,可以減少錯誤,並且易於閱讀,將程式碼2.2之改為程式碼2.4的形式,語義也能正確。
程式碼2.4
int func(int x, int *p)
{
if (*p == null) {
return -1;
}
int res = x/(*p) /* p指向除數 */;
return res;
}
針對程式碼2.2這個具體的例子,使用帶有主題顏色區分功能的程式碼編輯器也能明顯的發現問題,當然也還可以有另外一些規則,如:
- 註釋只能加在語句上邊或右邊
三、八進位制
在C語言中整型常量有八進位制、十進位制和十六進位制三種表示方法,為了上下文對齊會無意中將十進位制的數改成八進位制形式,如:
程式碼3.1
int array[] = {
123,
125,
024,
};
其中024是八進位制數,對應十進位制的值是20,這種情況下一般可以採用空格對齊。
四、字元與字串
在C語言中,用單引號引起來的一個字元實際代表一個整數,而用雙引號引起來的字串,代表的是一個指向無名陣列起始字元的指標。兩者混用,會引發一些顯式的錯誤,如:
程式碼4.1
char *buf = '3';
此時編譯器會報錯,因為 '3' 是一個整數;而 "3" 就是正確的,它代表的是一個包含結束符的字串常量。
五、優先順序與結合性
附上C語言運算子優先順序與結合性明細表,以備查覽。
表5.1 運算子優先順序與結合性
| 運算子 | 名稱或含義 | 優先順序 | 結合性 |
| [] | 陣列下標 | 1 | 從左到右 |
| () | 圓括號 | ||
| . | 成員運算子 | ||
| -> | 成員運算子 | ||
| - | 負號 | 2 | 從右到左 |
| ~ | 按位取反 | ||
| ++ | 自增 | ||
| -- | 自減 | ||
| * | 取值 | ||
| & | 取地址 | ||
| ! | 邏輯非 | ||
| (型別) | 強制型別轉換 | ||
| sizeof | 獲取位元組大小 | ||
| / | 除 | 3 | 從左到右 |
| * | 乘 | ||
| % | 取餘 | ||
| + | 加 | 4 | 從左到右 |
| - | 減 | ||
| << | 左移 | 5 | 從左到右 |
| >> | 右移 | ||
| > | 大於 | 6 | 從左到右 |
| >= | 大於等於 | ||
| < | 小於 | ||
| <= | 小於等於 | ||
| == | 等於 | 7 | 從左到右 |
| != | 不等於 | ||
| & | 按位與 | 8 | 從左到右 |
| ^ | 按位異或 | 9 | 從左到右 |
| | | 按位或 | 10 | 從左到右 |
| && | 邏輯與 | 11 | 從左到右 |
| || | 邏輯或 | 12 | 從左到右 |
| ?: | 條件運算子 | 13 | 從右到左 |
| = | 賦值 | 14 | 從右到左 |
| /= | 除後賦值 | ||
| *= | 乘後賦值 | ||
| %= | 取餘後賦值 | ||
| += | 加後賦值 | ||
| -= | 減後賦值 | ||
| <<= | 左移後賦值 | ||
| >>= | 右移後賦值 | ||
| &= | 按位與後賦值 | ||
| ^= | 按位異或後賦值 | ||
| |= | 按位或後賦值 | ||
| , | 逗號運算子 | 15 | 從左到右 |
參考資料
- 《C Traps and Pitfalls》[美]Andrew Koenig著
- C語言運算子優先順序(超詳細)_良師,益友-CSDN部落格_c語言優先順序運算子
