1. 程式人生 >實用技巧 >其它 >C語言陷阱與缺陷 之 詞法

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

從左到右


參考資料

  1. 《C Traps and Pitfalls》[美]Andrew Koenig著
  2. C語言運算子優先順序(超詳細)_良師,益友-CSDN部落格_c語言優先順序運算子