《3、PC-Lint的代碼檢查功能》
PC-Lint能夠檢查出很多語法錯誤和文法上正確的邏輯錯誤,PC-Lint為大部分錯誤訊息都分配了一個錯誤號碼,編號小於1000的錯誤號碼是分配給C 語言的,編號大於1000的錯誤號碼則用來說明C++的錯誤訊息。表 1 列出了PC-Lint警示訊息的詳細分類:
表 1 列出了PC-Lint警示訊息分類
| 錯誤說明 |
C |
C++ |
警示層級 |
| 語法錯誤 |
1-199 |
1001-1199 |
1 |
| 內部錯誤 |
200-299 |
|
0 |
| 致命錯誤 |
300-399 |
|
0 |
| 警示 |
400-699 |
1400-1699 |
2 |
| 訊息 |
700-800 |
1700-1899 |
3 |
| 可選資訊 |
900-999 |
1900-1999 |
4 |
以C 語言為例,其中的編號1-199指的是一般編譯器也會產生的語法錯誤;編號200-299是PC-Lint程式內部的錯誤,這類錯誤不會出現在代碼中的;編號300-399指的是由於記憶體限制等導致的系統致命錯誤。編號400-999中出現的提示資訊,是根據隱藏代碼問題的可能性進行分類的:其中編號 400-699指的是被檢查代碼中很可能存在問題而產生的警示資訊;編號700-899中出現的資訊,產生錯誤的可能性相比警示資訊來說層級要低,但仍然可能是因為代碼問題導致的問題。編號900-999是可選資訊,他們不會被預設檢查,除非你在選項中指定檢查他們。
PC-Lint/FelexLint提供了和許多編譯器類似的警示層級設定選項-wLevel,它的警示層級分為以下幾個層級,預設警示層級為3級:
-w0 不產生資訊(除了遇到致命的錯誤)
-w1 只建置錯誤資訊 -- 沒有警示資訊和其它提示資訊
-w2 只有錯誤和警示資訊
-w3 建置錯誤、警示和其它提示資訊(這是預設設定)
-w4 產生所有資訊
PC-Lint/FelexLint還提供了用於處理函數庫的標頭檔的警示層級設定選項-wlib(Level),這個選項不會影響處理C/C++原始碼模組的警示層級。它有和-wLevel相同的警示層級,預設警示層級為3級:
-wlib(0) 不產生任何庫資訊
-wlib(1) 只建置錯誤資訊(當處理庫的原始碼時)
-wlib(2) 建置錯誤和警示資訊
-wlib(3) 建置錯誤、警示和其它資訊(這是預設設定)
-wlib(4) 產生所有資訊
PC-Lint的檢查分很多種類,有強型別檢查、變數值跟蹤、語義資訊、賦值順序檢查、弱定義檢查、格式檢查、縮排檢查、const變數檢查和 volatile變數檢查等等。對每一種檢查類型,PC-Lint都有很多詳細的選項,用以控制PC-Lint的檢查效果。PC-Lint的選項有300 多種,這些選項可以放在注釋中(以注釋的形式插入代碼中),例如:
/*lint option1 option2 ... optional commentary */ 選項可以有多行
//lint option1 option2 ... optional commentary 選項僅為一行(適用於C++)
選項間要以空格分開,lint命令一定要小寫,並且緊跟在/*或//後面,不能有空格。如果選項由類似於操作符和運算元的部分組成,例如-esym (534, printf, scanf, operator new),其中最後一個選項是operator new,那麼在operator和new中間只能有一個空格。PC-Lint的選項還可以放在宏定義中,當宏被展開時選項才生效。例如:
#define DIVZERO(x) /*lint -save -e54 */ ((x) /0) /*lint -restore */ 允許除數為0而不警示
下面將分別介紹PC-Lint常用的,也是比較重要的代碼檢查類型,並舉例介紹了各個檢查類型下可能出現的警示資訊以及常用選項的用法:
3.1 強型別檢查
強型別檢查選項“-strong”和它的輔助(補充)選項“-index”可以對typedef定義的資料類型進行強型別檢查,以保證只有相同類型之間的變數才能互相賦值,強型別檢查選項strong的用法是:
-strong( flags[, name] ... )
strong選項必須在typedef定義類型之前開啟,否則PC-Lint就不能識別typedef定義的資料類型,類型檢查就會失效。flags參數可以是A、J、X、B、b、l和f,相應的解釋和弱化字元在表 2 中列出:
表 2 強型別檢查strong選項和參數表
| A |
對強型別變數賦值時進行類型檢查,這些指派陳述式包括:直接賦值、傳回值、參數傳遞、初始化 。 A參數後面可以跟以下字元,用來弱化A的檢查強度: i 忽略初始化 r 忽略Return語句 p 忽略參數傳遞 a 忽略賦值操作 c 忽略將常量賦值(包括整數常量、常量字串等)給強型別的情況 z 忽略Zero賦值,Zero定義為任何非強制轉換為強型別的0常量。例如:0L和(int)0都是Zero, 但是(HANDLE)0當HANDLE是一個強型別的時候就不是Zero。(HANDLE *)0也不是例如使用-strong(Ai,BITS)設定,PC-Lint將會對從非BITS類型資料向BITS類型資料賦值的代碼發出警示,但是忽略變數初始化時的此類賦值。 |
| X |
當把強型別的變數賦指給其他變數的時候進行類型檢查。弱化參數i, r, p, a, c, z同樣適用於X並起相同的作用。 |
| J |
選項是當強型別與其它類型進行如下的二進位操作時進行檢查,下面是J的參數: e 忽略==、!=和?:操作符 r 忽略>、>=、<和<= o 忽略+、-、*、/、%、|、&和^ c 忽略該強型別與常量進行以上操作時的檢查 z 忽略該強型別與Zero進行以上操作時的檢查 使用忽略意味著不會產生警示資訊。舉個例子,如果Meters是個強型別,那麼它只在判斷相等和其他關係操作時才會被正確地檢查,其它情況則不檢查,在這個例子中使用J選項是正確的。 |
| B |
B選項有兩個效果: 1. 出於強型別檢查的目的,假設所有的Boolean操作返回一個和Type相容的類型,所謂Boolean操作就是那些指示結果為true或false的操作,包括前面提到的四種關係運算子和兩種等於判斷符,取反操作符!,二元操作符&&和||。 2. 在所有需要判斷Bolean值的地方,如if語句和while語句,都要檢查結果是否符合這個強型別,否則警示。 例如if(a)...當a為int時,將產生警示,因為int與Bolean類不相容,所以必須改為if(a != 0)。 |
| b |
僅僅假定每一個Bolean類操作符都將返回一個與Type類型相容的傳回值。與B選項相比,b選項的限制比較寬鬆。 |
| l |
庫標誌,當強型別的值作為參數傳遞給庫函數等情況下,不產生警示。 |
| f |
與B或b連用,表示抑止對1bit長度的位域是Boolean類型的假定,如果不選該項表示1bit長度的位域被預設假定為Boolean類型。 |
這些選項字元的順序對功能沒有影響。但是A和J選項的弱化字元必須緊跟在它們之後。B選項和b選項不能同時使用,f選項必須搭配B選項或b選項使用,如果不指定這些選項,-strong的作用就是僅僅聲明type為強型別而不作任何檢查。下面用一段代碼示範-strong選項的用法:
//lint -strong(Ab,Bool) <選項是以注釋的形式插入代碼中>
typedef int Bool;
Bool gt(int a, b)
{
if(a) return a > b; // OK
else return 0; // Warning
}
例子代碼中Bool被聲明成強型別,如果沒有指定b選項,第一個return語句中的比較操作就會被認為與函數類型不符。第二個return語句導致警示是因為0不是各Bool類型,如果添加c選項,例如-strong(Acb,Bool),這個警示就會被抑制。再看一個例子:
/*lint -strong( AJXl, STRING ) */
typedef char *STRING;
STRING s;
...
s = malloc(20);
strcpy( s, "abc" );
由於malloc和strcpy是庫函數,將malloc的傳回值賦給強型別變數s或將強型別變數s傳遞給strcpy時會產生強型別衝突,不過l選項抑制了這個警示。
強型別也可用於位域,出於強型別檢查的目的,先假定位域中最長的一個欄位是優勢Boolean類型,如果沒有優勢Boolean或位域中沒有哪個欄位比其它欄位長,這個類型從位域被切開的位置開始成為“散”類型,例如:
//lint -strong( AJXb, Bool )
//lint -strong( AJX, BitField )
typedef int Bool;
typedef unsigned BitField;
struct foo
{
unsigned a:1, b:2;
BitField c:1, d:2, e:3;
} x;
void f()
{
x.a = (Bool) 1; // OK
x.b = (Bool) 0; // strong type violation
x.a = 0; // strong type violation
x.b = 2; // OK
x.c = x.a; // OK
118
x.e = 1; // strong type violation
x.e = x.d; // OK
}
上面例子中,成員a和c是強型別Bool,成員d和e是BitField類型,b不是強型別。為了避免將只有一位的位域假設成Boolean類型,需要在聲明Boolean的-strong中使用f選項,上面的例子就應該改成這樣:-strong(AJXbf,Bool)。
另一個強型別檢查選項是index,index的用法是:
-index( flags, ixtype, sitype [, sitype] ... )
這個選項是對strong選項的補充,它可以和strong選項一起使用。這個選項指定ixtype是一個排除索引類型,它可以和Strongly Indexed類型sitype的數組(或指標)一起使用,ixtype和sitype被假設是使用typedef聲明的類型名稱。flags可以是c或 d,c允許將ixtype和常量作為索引使用,而d允許在不使用ixtype的情況下指定數組的長度(Dimensions)。下面是一個使用index 的例子:
//lint -strong( AzJX, Count, Temperature )
//lint -index( d, Count, Temperature )
// Only Count can index a Temperature
typedef float Temperature;
typedef int Count;
Temperature t[100]; // OK because of d flag
Temperature *pt = t; // pointers are also checked
// ... within a function
Count i;
t[0] = t[1]; // Warnings, no c flag
for( i = 0; i < 100; i++ )
t[i] = 0.0; // OK, i is a Count
119
pt[1] = 2.0; // Warning
i = pt - t; // OK, pt-t is a Count
上面的例子中,Temperature是被強索引類型,Count是強索引類型。如果沒有使用d選項,數組的長度將被映射成固有的類型:
Temperature t[ (Count) 100 ];
但是,這是個小麻煩,像下面那樣將數組長度定義成常量更好一些:
#define MAX_T (Count) 100
Temperature t[MAX_T];
這樣做還有一個好處就是同樣的MAX_T還可以用在for語句中,用於限制for語句的範圍。需要注意的是,指向強被索引類型的指標(例如上面的pt)如果用在[]符號(數組符號)中也會被檢查類型。其實,無論何時,只要將一個值加到一個指向強被索引類型的指標時,這個值就會被檢查以確認它是一個強索引類型。此外,強被索引指標如果減去一個值,其結果被認為是平常的強索引,所以下面的例子就不會產生警示:
i = pt - t;
3.2 變數值跟蹤
3.2.1 變數值初始化跟蹤
早期的變數值跟蹤技術主要是對變數值的初始化進行跟蹤,和變數初始化相關的LINT訊息主要是644, 645 ("變數可能沒有初始化"), 771, 772 ("不可靠的初始化"), 530 ("未初始化的"), and 1401 - 1403 ("成員 ... 未初始化")。以下面的代碼為例:
if( a ) b = 6;
else c = b; // 530 message
a = c; // 645 message
假設b和c在之前都沒有初始化,PC-Lint就會報告b沒有初始化(在給c賦值的時候)和c可能沒有被初始化(在給a賦值的時候)的訊息。而while和for迴圈語句和上面的if語句稍微有所不同,比較下面的代碼:
while ( n-- )
{
b = 6;
...
}
c = b; //772 message
假設b在使用之前沒有被初始化,這裡會報告b可能沒有初始化的訊息(當給c賦值時)。之所以會有這樣的區別,是因為程式設計者可能知道這樣的迴圈體總是會被至少執行一次。相反,前面的if語句,對於程式設計者來說比較難以確定if語句是否總會被執行,因為如果是這樣的話,這樣的if語句就是多餘的,應該被去掉。While語句和if比較相似,看下面的例子:
switch ( k )
{
case 1: b = 2; break;
case 2: b = 3;
/* Fall Through */
case 3: a = 4; break;
default: error();
}
c = b; //645 message
儘管b在兩個不同的地方被賦值,但是仍然存在b沒有被初始化的可能。因此,當b賦值給c的時候,就會產生可能沒有初始化的訊息。為瞭解決這個問題,你可以在 switch語句之前給b賦一個預設值。這樣PC-Lint就不會產生警示訊息,但是我們也失去了讓PC-Lint檢查後續的代碼修改引起的變數初始化問題的機會。更好的方法是修改沒有給b賦值的case語句。
如果error()語句代表那些“不可能發生”的事情發生了,那麼我們可以讓PC-Lint知道這一段其實是不可能執行的,下面的代碼錶明了這一點:
switch ( k )
{
case 1: b = 2; break;
case 2:
case 3: b = 3; a = 4; break;
default: error();
/*lint -unreachable */
}
c = b;
注意:這裡的-unreachable應該放在error()後面,break的前面。另外一個產生”沒有初始化”警示的方式是傳遞一個指標給free(或者採用相似的方法)。比如:
if( n ) free( p );
...
p->value = 3;
在訪問p的時候會產生p可能沒有被初始化的訊息。對於goto語句,前向的goto可能產生沒有初始化訊息,而向後的goto 會被忽略掉這種檢查。
if ( a ) goto label;
b = 0;
label: c = b;
當在一個大的項目中使用未初始設定變數檢查時,可能會產生一些錯誤的報告。這種報告的產生,很大一部分來自於不好的程式設計風格,或者包括下面的結構:
if( x ) initialize y
...
if( x ) use y
當出現這種情況時,可以採用給y賦初始值的方式,或者利用選項-esym(644,y)關掉變數y上面的初始化檢查。
3.2.2 變數值跟蹤
變數值跟蹤技術從指派陳述式、初始化和條件陳述式中收集資訊,而函數的參數被預設為在正確的範圍內,只有在從函數中可以收集到的資訊與此不符的情況下才產生警示。與變數值跟蹤相關的訊息有:
(1) 訪問地址越界訊息(訊息415,661,796)
(2) 被0除訊息(54,414,795)
(3) NULL指標的錯誤使用(413,613,794)
(4) 非法指標的建立錯誤(416,662,797)
(5) 冗餘的布爾值測試(774)
看下面的例子:
int a[10];
int f()
{
int k;
k = 10;
return a[k]; // Warning 415
}
這個語句會產生警告415(通過 '[' 訪問越界的指標),因為PC-Lint儲存了賦給k的值,然後在使用k的時候進行了判斷。如果我們把上面的例子稍加修改:
int a[10];
int f( int n )
{
int k;
if ( n ) k = 10;
else k = 0;
return a[k]; // Warning 661
}
這樣就會產生警示 661 (可能訪問越界指標)。 使用“可能”是因為不是所有的路徑都會把10賦值給k。PC-Lint不僅收集指派陳述式和初始化,還從條件陳述式中收集值的資訊。比如下面的例子:
int a[10];
int f( int k, int n )
{
if ( k >= 10 ) a[0] = n;
return a[k]; // Warning 661 -- k could be 10
}
這裡仍然產生661警示,因為PC-Lint檢測到,在使用k的時候,k的值>=10。另外,對於函數來說,它總是假設K是正確的,程式使用者知道他們要做些什麼,所以下面的語句不會產生警示:
int a[10];
int f( int k, int n )
{ return a[k+n]; } // no warning
和檢查變數沒有初始化一樣,還可以檢查變數的值是否正確。比如,如果下面例子中的迴圈一次都沒有運行,k可能會超出範圍。這時候會產生訊息796 (可預見的地址訪問越界).
int a[10];
int f(int n, int k)
{
int m = 2;
if( k >= 10 ) m++; // Hmm -- So k could be 10, eh?
while( n-- )
{ m++; k = 0; }
return a[k]; // Info 796 - - k could still be 10
}
下面的例子示範了可能使用NULL指標的問題:
int *f( int *p )
{
if ( p ) printf( "/n" ); // So -- p could be NULL
printf( "%d", *p ); // Warning
return p + 2; // Warning
}
這裡會產生兩個警示,因為可能使用了NULL指標,很明顯,這兩個語句應該在if語句的範圍內。為了使你的程式更加健壯,你可能需要開啟Pointer- parameter-may-be-NULL這個開關(+fpn)。這個選項假設所有傳遞到函數中的指標都有可能是NULL的。數組邊界值在高位被檢測,也就是說
int a[10]; ... a[10] = 0;
被檢測了,而a[-1]卻檢測不到。PC-Lint中有兩個訊息是和指標的越界檢查有關的,一個是越界指標的建立,另外一個是越界指標的訪問,也就是通過越界指標擷取值。在ANSI C([1]3.3.6)中,允許建立指向超過數組末尾一個單元的指標,比如:
int a[10];
f( a + 10 ); // OK
f( a + 11 ); // error
但是上面建立的兩個指標,都是不能訪問的,比如:
int a[10], *p, *q;
p = a + 10; // OK
*p = 0; // Warning (access error)
p[-1] = 0; // No Warning
q = p + 1; // Warning (creation error)
q[0] = 0; // Warning (access error)
布爾條件檢查不象指標檢查那麼嚴格,但是它會對恒真的布爾條件產生警示,比如:
if ( n > 0 ) n = 0;
else if ( n <= 0 ) n = -1; // Info 774
上面的代碼會產生警示(774),因為第二個條件檢查是恒真的,可以忽略。這種冗餘代碼不會導致問題,但它的產生通常是因為邏輯錯誤或一種錯誤可能發生的徵兆,需要詳細的檢查。
3.2.3 使用assert(斷言)進行補救
在某些情況下,雖然根據代碼我們可以知道確切的值,但是PC-Lint卻無法擷取所有情況下變數的值的範圍,這時候會產生一些錯誤的警示資訊,我們可以使用assert語句增加變數取值範圍資訊的方法,來抑制這些錯誤的警示資訊的產生。下面舉例來說明:
char buf[4];
char *p;
strcpy( buf, "a" );
p = buf + strlen( buf ); // p is 'possibly' (buf+3)
p++; // p is 'possibly' (buf+4)
*p = 'a'; // Warning 661 - possible out-of-bounds reference
PC-Lint無法知道在所有情況下變數的值是多少。在上面的例子中,產生警示的語句其實並不會帶來什麼危害。我們可以直接使用
*p = 'a'; //lint !e661
來抑制警示。另外,我們還可以使用assert工具來修正這個問題:
#include <assert.h>
...
char buf[4];
char *p;
strcpy( buf, "a" );
p = buf + strlen( buf );
assert( p < buf + 3 ); // p is 'possibly' (buf+2)
p++; // p is 'possibly' (buf+3)
*p = 'a'; // no problem
由於assert在NDEBUG被定義時是一個空操作,所以要保證Lint進行的時候這個宏沒有被定義。
為了使assert()和你的編譯器內建的assert.h一起產生上面的效果,你需要在編譯選項檔案中添加一個選項。例如,假設assert 是通過以下的編譯器宏定義實現的:
#define assert(p) ((p) ? (void)0 : __A(...))
考慮到__A()會彈出一個訊息並且不會返回,所以這個需要添加的選項就是:
-function( exit, __A )
這個選項將exit函數的一些非返回特徵傳遞給__A函數。做為選擇結果,編譯器可能將assert實現成一個函數,例如:
#define assert(k) _Assert(k,...)
為了讓PC-lint知道_Assert是一個assert函數,你需要使用-function( __assert, _Assert )選項或-function( __assert(1), _Assert(1) )選項複製__assert()函數的語義
許多編譯器的編譯選項檔案中已經存在這些選項了,如果沒有的話,你可以複製一個assert.h檔案到PC-lint目錄下(這個目錄由於使用了-i選項,檔案搜尋的順序優先於編譯器的標頭檔目錄)。
3.2.4 函數內變數跟蹤
PC-Lint的函數值跟蹤功能會跟蹤那些將要傳遞給函數(作為函數參數)變數值,當發生函數調用時,這些值被用來初始化函數參數。這種跟蹤功能被用來測定傳回值,記錄額外的函數調用,當然還可以用來偵測錯誤。考察下面的例子代碼:
t1.cpp:
1 int f(int);
2 int g()
3 { return f(0); }
4 int f( int n )
5 { return 10 / n; }
在這個例子中,f()被調用的時候使用0作為參數,這將導致原本沒有問題的10/n語句產生被0除錯誤,使用命令lin -u t1.cpp可以得到以下輸出:
--- Module: t1.cpp
During Specific Walk:
File t1.cpp line 3: f(0)
t1.cpp 5 Warning 414: Possible division by 0 [Reference:File t1.cpp: line 3]
你第一個注意到的事情是短語“During Specific Walk”,緊接著是函數調用發生的位置,函數名稱以及參數,再下來就是錯誤資訊。如果錯誤資訊中缺少了錯誤再現時的錯誤行和用來標記錯誤位置的指示資訊,這是因為檢查到錯誤的時候代碼(被調用函數的代碼)已經走過了。如果像下面一樣調換一下兩個函數的位置:
t2.cpp:
1 int f( int n )
2 { return 10 / n; }
3 int g()
4 { return f(0); }
這種情況下就不會出現被0除的警示,因為此時f(0)在第四行,函數f()的代碼已經過了,在這種情況下就需要引入multi-pass選項。如果在剛才的例子中使用lin -u -passes(2) t2.cpp命令,那麼輸出就變成:
--- Module: t2.cpp
/// Start of Pass 2 ///
--- Module: t2.cpp
During Specific Walk:
File t2.cpp line 4: f(0)
t2.cpp 2 Warning 414: Possible division by 0 [Reference:File t2.cpp: line 4]
使用-passes(2)選項將會檢查代碼兩遍,一些作業系統不支援在命令列中使用-passes(2),對於這樣的系統,可以使用-passes=2 或 -passes[2]代替。通過冗長的資訊可以看出來,以pass 2開始表示第一次檢查沒有產生警示資訊。這一次得到的錯誤資訊和前一次不同,在某種情況下我們可以推斷出指定函數調用的傳回值,至少可以得到一些傳回值的屬性。以下面的模組為例:
t3.cpp:
1 int f( int n )
2 { return n - 1; }
3 int g( int n )
4 { return n / f(1); }
使用命令 lin -u -passes(2) t3.cpp,可以得到以下輸出資訊:
--- Module: t3.cpp
/// Start of Pass 2 ///
--- Module: t3.cpp
{ return n / f(1); }
t3.cpp 4 Warning 414: Possible division by 0 [Reference:File t3.cpp: lines 2, 4]
第一遍檢查我們知道調用函數f()傳遞的參數是1,第二遍檢查先處理了函數f(),我們推斷出這個參數將導致返回結果是0,當第二遍檢查開始處理函數g() 的時候,產生了被0除錯誤。應該注意到這個資訊並不是在短語“During Specific Walk”之前出現的,這是因為錯誤是在對函數g()進行正常的處理過程中檢測到的,此時並沒有使用為函數g()的參數指定的值。指定的函數調用能夠產生附加的函數調用,如果我們pass足夠多的檢測次數,這個過程可能會重複發生,參考下面的代碼:
t4.cpp:
1 int f(int);
2 int g( int n )
3 { return f(2); }
4 int f( int n )
5 { return n / f(n - 1); }
第五行的分母f(n-1)並不會引起懷疑,直到我們意識到f(2)調用將導致f(1)調用,最終會調用f(0),迫使最終的傳回值是0。使用下面的命令列:
lin -u -passes(3) t4.cpp,
輸出結果如下:
--- Module: t4.cpp
{ return f(2); }
t4.cpp 3 Info 715: Symbol 'n' (line 2) not referenced
/// Start of Pass 2 ///
--- Module: t4.cpp
/// Start of Pass 3 ///
--- Module: t4.cpp
During Specific Walk:
File t4.cpp line 3: f(2)
File t4.cpp line 5: f(1)
t4.cpp 5 Warning 414: Possible division by 0 [Reference:File t4.cpp: lines 3, 5]
到這裡已經處理了三遍才檢測到可能的被0除錯誤,想瞭解為什麼需要處理三遍可以看看這個選項-specific_wlimit(n)。需要注意的是,指定的調用序列,f(2),f(2),是作為警示資訊的序言出現的。
3.3 賦值順序檢查
當一個運算式的值依賴於賦值的順序的時候,會產生警示564。這是C/C++語言中非常普遍的一個問題,但是很少有編譯器會分析這種情況。比如
n++ + n
這個語句是有歧義的,當左邊的+操作先執行的話,它的值會比右邊的先執行的值大一,更普遍的例子是這樣的:
a[i] = i++;
f( i++, n + i );
第一個例子,看起來好像自加操作應該在數組索引計算以後執行,但是如果右邊的賦值操作是在左邊賦值操作之前執行的話,那麼自加一操作就會在數組索引計算之前執行。雖然,賦值操作看起來應該指明一種操作順序,但實際上是沒有的。第二個例子是有歧義的,是因為函數的參數值的計算順序也是沒有保證的。能保證賦值順序的操作符是布爾與(&&)或(||)和條件賦值(? :)以及逗號(,),因此:
if( (n = f()) && n > 10 ) ...
這條語句是正確的,而:
if( (n = f()) & n > 10 ) ...
將產生一條警示。
3.4 弱定義檢查
這裡的弱定義包含是以下內容:宏定義、typedef名字、聲明、結構、聯合和枚舉類型。因為這些東西可能在模組中被過多定義且不被使用,PC-Lint 有很多訊息用來檢查這些問題。PC-Lint的訊息749-769 和1749-1769都是保留用來作為弱定義提示的。
(1) 當一個檔案#include的標頭檔中沒有任何引用被該檔案使用,PC-Lint會發出766警示。
(2) 為了避免一個標頭檔變得過於大而臃腫,防止其中存在冗餘的聲明,當一個標頭檔中的對象聲明沒有被外部模組引用到時,PC-Lint會發出759警示。
(3) 當變數或者函數只在模組內部使用的時候,PC-Lint會產生765警示,來提示該變數或者函數應該被聲明為static。
如果你想用PC-Lint檢查以前沒有檢查過的代碼,你可能更想將這些警示資訊關閉,當然,如果你只想查看標頭檔的異常,可以試試這個命令:
lint -w1 +e749 +e?75? +e?76? ...
3.5 格式檢查
PC-Lint會檢查printf和scanf(及其家族)中的格式衝突,例如:
printf( "%+c", ... )
將產生566警示,因為加號只在數字轉換時有用,有超過一百個這樣的組合會產生警示,編譯器通常不標記這些矛盾,其他的警示還有對壞的格式的抱怨,它們是 557和567。我們遵循ANSI C建立的規則,可能更重要的是我們還對大小不正確的格式進行標記(包括警示558, 559, 560 和 561)。比如 %d 格式,允許使用int和unsigned int,但是不支援double和long(如果long比int長),同樣,scanf需要參數指向的對象大小正確。如果只是參數的類型(不是大小)與格式不一致,那將產生626和627警示。-printf 和 -scanf選項允許使用者指定與printf或scanf函數族類似的函數,-printf_code 和 -scanf_code也可以被用來描述非標準的 % 碼。
3.6 縮排檢查
根據代碼中的縮排問題,PC-Lint也會產生相應的警示,因為縮排的問題有很大一部分是由於代碼結構不良或者大括弧的遺漏造成的。比如下面的例子:
if( ... )
if( ... )
statement
else statement
很明顯這裡的else是和第一個if語句對應的,而在這裡編譯器則把它和第二個if對應起來。PC-Lint會對這種情況產生警示。和這樣的縮排檢查相關的警示主要有三個725(no positive indentation)、525(negatively indented from)、539(Did not expect positive indentation from Location)要進行縮排檢查,我們首先要設定檔案中的tab鍵所對應的空格數,預設的是佔用8個空格,這個參數可以用-t#選項進行修改。比如- t4表示tab鍵佔用4個空格長度。另外,縮排檢查還和代碼的編碼格式策略相關,需要進行必要的調整。
3.7 const變數檢查
對於const變數的檢查,PC-Lint是完全支援的。使用const變數,對於提高代碼的品質非常有好處,看一下下面的例子:
char *strcpy( char *, const char * );
const char c = 'a';
const char *p = &c;
void main()
{
char buf[100];
c = 'b';
*p = 'c';
strcpy( p, buf );
...
這裡的c和*P指向的內容都是靜態變數,不可修改。上面的代碼明顯違反了這個規定,會產生Error(11),另外,把P作為第一個參數傳入strcpy 中,會產生警示605(Increase in pointer capability),而把buf作為第二個參數傳入strcpy函數中,會產生警示603(Symbol 'Symbol' (Location) not initialized),因為buf沒有初始化,而作為靜態變數的第二個參數,是不能在strcpy函數中再被初始化的。
3.8 volatile變數檢查
對於volatile變數的檢查,在PC-Lint中有這樣的規定,如果一個運算式中同時使用了兩次相同的volatile變數,那麼就會給出564警示,因為這時候會產生賦值順序的問題。
volatile char *p;
volatile char f();
n = (f() << 8) | f(); /* Warning 564 */
n = (*p << 8) | *p; /* Warning 564 */