PC-LINT
介紹
PC-Lint for C/C++是由Gimpel軟件公司于1985年開發的代碼靜態分析工具,它能有效地發現程序語法錯誤、潛在的錯誤隱患、不合理的編程習慣等。
C語言的靈活性帶來了代碼效率的提升,但相應帶來了代碼編寫的隨意性,另外C編譯器不進行強制類型檢查,也帶來了代碼編寫的隱患。PC-Lint能識別并報告C語言中的編程陷阱和格式缺陷的發生。它進行程序的全局分析,能識別沒有被適當檢驗的數組下標,報告未被初始化的變量,警告使用空指針,冗余的代碼,等等。軟件除錯是軟件項目開發成本和延誤的主要因素。PC-lint能夠幫你在程序動態測試之前發現編碼錯誤,這樣消除錯誤的成本更低。使用PC-Lint在代碼走讀和單元測試之前進行檢查,可以提前發現程序隱藏錯誤,提高代碼質量,節省測試時間。并提供編碼規則檢查,規范軟件人員的編碼行為。
告警信息
PC-Lint能夠檢查出很多語法錯誤和語法上正確的邏輯錯誤,PC-Lint為大部分錯誤消息都分配了一個錯誤號,編號小于1000的錯誤號是分配給C語言的,編號大于1000的錯誤號則用來說明C++的錯誤消息。如下列出了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級:
l -w0 不產生信息(除了遇到致命的錯誤)
l -w1 只生成錯誤信息 -- 沒有告警信息和其它提示信息
l -w2 只有錯誤和告警信息
l -w3 生成錯誤、告警和其它提示信息(這是默認設置)
l -w4 生成所有信息
PC-Lint/FelexLint還提供了用于處理函數庫的頭文件的告警級別設置選項-wlib(Level),這個選項不會影響處理C/C++源代碼模塊的告警級別。它有和-wLevel相同的告警級別,缺省告警級別為3級:
l -wlib(0) 不生成任何庫信息
l -wlib(1) 只生成錯誤信息(當處理庫的源代碼時)
l -wlib(2) 生成錯誤和告警信息
l -wlib(3) 生成錯誤、告警和其它信息(這是默認設置)
l -wlib(4) 產生所有信息
選項
選項規則
PC-Lint的檢查分很多種類,有強類型檢查、變量值跟蹤、語義信息、賦值順序檢查、弱定義檢查、格式檢查、縮進檢查、const變量檢查和volatile變量檢查等等。對每一種檢查類型,PC-Lint都有很多詳細的選項,用以控制PC-Lint的檢查效果。PC-Lint的選項有300多種,通過使用加號"+"和減號"-",以注釋的形式插入代碼中,來恢復和屏蔽指定的被檢查的選項。格式如下:
/*lint option1 option2 ... optional commentary */
或者
//lint option1 option2 ... optional commentary
注意:選項間要以空格分開,lint命令一定要小寫,并且緊跟在/*或//后面,不能有空格。選項的一行不能超過80個字符,否則導致致命的錯誤,錯誤信息的編號就是323。如果選項確實有很長,可以通過換行的方式來實現。另外屏蔽和恢復的選項的代碼可以放在宏定義中,宏被展開后,這些選項會生效。例如:
#define DIVZERO(x) /*lint -save -e54 */ ((x) /0) /*lint -restore */ 允許除數為0而不告警。
禁止錯誤消息
選項開頭使用"-e"可以禁止指定的錯誤消息,使用"+e"恢復指定的錯誤消息。
支持通配符,'?'匹配單個字符,"*"匹配多個字符。比如:
l -e7???, 則關閉了700~799這個范圍內的錯誤消息。
l -e1*, 則關閉了所有以1開頭的編號的錯誤消息。
同樣通配符也能使用在-esym, -elib, -elibsym, -efile, -efunc, -emacro, -etemplate, -e(#), --e(#), -e{#} and –e{#}。
1. 禁止多行代碼錯誤消息
-e# 禁止指定的錯誤消息,#代表數字或是數字匹配符,錯誤消息的編號為#。
+e# 恢復指定的錯誤消息,錯誤消息的編號為#。
例如:
/*lint -504*/
...Code.....
/*lint +504*/
第一行關閉了編號為504的錯誤消息,最后一個行則重新打開了編號為504的錯誤消息。
2. 禁止單行代碼錯誤消息
!e# [!e#]… 僅對其所在行有效。
例如:
if( x = f(34) ) //lint !e720
y = y / x;
或
if( x = f(34) ) /*lint !e720 */
y = y / x;
在這個例子中,僅對那一行禁止編號為720 的錯誤消息。
如果有更多的錯誤信息要禁止,而又無法使用通配符,則可以使用下面的方法:
n = u / -1; //lint !e573 !e721
3. 禁止單個表達式錯誤消息
-e(#[,#]...) 為下一個表達式禁止指定的錯誤消息,在這個表達式結束后被禁止的錯誤消息自動恢復,#代表數字或是數字匹配符,錯誤消息的編號為#。例如:
a = /*lint -e(413) */ *(char *)0;
它等價于下面的語句:
a = /*lint -save -e413 */ *(char *)0
/*lint -restore */;
4. 禁止整個表達式錯誤消息
--e( # [,#]... ) 比上面的那個管的更寬一些,它對整個表達式有效,舉個例子就明白它與上面的區別了。
例如:
a = /*lint --e(413) */ *(int *)0 + *(char *)0;
整個表示式是指*(int *)0 + *(char *)0,將禁止兩個編號為413 的錯誤消息, 如果使用 -e(413) ,則只禁止第一個編號為 413 的錯誤消息。
5. 禁止代碼塊錯誤消息
-e{ # [, #] …} 對后面的代碼塊會禁止錯誤消息,代碼塊可以是一個函數、一句聲明或賦值語句或者if或while。在C++的類定義或命名空間聲明前放這個選項,則將可以將禁止整個類或命名空間內的代碼中指定的錯誤消息。例如:
//lint -e{715} suppress "k not referenced"
void f( int n, unsigned u, int k )
{
//lint -e{732} suppress "loss of sign"
u = n; // 732 not issued
//lint -e{713} suppress "loss of precision"
if(n)
{
n = u; // 713 not issued
}
} // 715 not issued
6. 禁止{}代碼塊錯誤消息
--e{ # [, #] … } 對于其所處的 {} 號區域內的整個代碼體有效。 {} 號區域可能是復雜的語句、函數體、C++的類,結構體或聯合體的定義、C++的命名空間等。如果這個選項放在一個模塊前,而模塊前沒有 {},則對整個模塊生效。
7. 參數不匹配禁止
-ealetter 參數不匹配禁止。
8. 禁止文件錯誤消息
-efile( #, file [, file] ... )
+efile( #, file [, file] ... )
9. 禁止函數錯誤消息
-efunc( #, Symbol [, Symbol] ... )
+efunc( #, Symbol [, Symbol] ... )
10. 禁止lib庫錯誤消息
-elib( # [, #] ... )
+elib( # [, #] ... )
11. 禁止lib庫符號錯誤消息
-elibsym( # [, # ] ... )
+elibsym( # [, # ] ... )
12. 禁止宏錯誤消息
-emacro( #, symbol, ... )
+emacro( #, symbol, ... )
-emacro( (#), symbol, ... )
--emacro( (#), symbol, ... )
-emacro( {#}, symbol, … )
--emacro( {#}, symbol, … )
13. 禁止符號錯誤消息
-esym( #, Symbol [, Symbol] ... )
+esym( #, Symbol [, Symbol] ... )
例如:
class X
{
void f(double, int);
};
如果member X::f(double, int)沒有被引用,為了屏蔽這個消息,可以使用:
-esym( 754, X::f )
另外,-esym和-e#選項是獨立的,例如:
-e714 +esym( 714,alpha )
第二個選項并不會恢復編號為714的錯誤消息,除非前面有個對應的-esym(714,alpha)。
14. 禁止和恢復在擴展模板(expanding templates)時的錯誤消息
-etemplate( # [,#] ... )
+etemplate( # [,#] ... )
數據類型大小
不同架構數據類型的大小可能不一樣。下面的列表,#號代表要設置的大小:
l -sb#:指定Byte的位數,默認的是-sb8
l -sbo#:指定bool的大小,默認值為1
l -sc#:指定char的大小,默認值為1
l -slc#:指定long char的大小,默認值為2
l -si#:指定int的大小,默認為2
l -sp#:指定指針的大小,默認為2
例如:設置int為16bit,pointers為32bit那么應該指定:
lint -si2 -sp4 ...
庫文件檢查
代碼中一般都會包含第三方庫文件,而源碼往往無法獲得,比如標準庫,如果直接使用PC-LINT檢查代碼會報718錯,提示庫文件中的函數沒有聲明和定義。因此可以通過選項對指定庫文件不做檢查。
1) 對指定頭文件不做檢查
+libclass( identifier[, identifier] ... )
identifier是其中下面之一:
angle: 所有尖括號包含起來的頭文件
foreign:所有在搜索列表中目錄下的頭文件
ansi:標準ANSI C/C++ 的頭文件
all:所有頭文件
默認情況下,+libclass(angle,foreign) 是有效的,所以代碼可以直接使用標準庫函數而不會報錯。
2) 對指定目錄下的文件不做檢查
+libdir( directory [, directory] ... )
-libdir( directory [, directory] ... )
3) 對指定頭文件不做檢查
+libh( file [, file] ... )
-libh( file [, file] ... )
強類型檢查
強類型檢查選項“-strong”和它的輔助(補充)選項“-index”可以對typedef定義的數據類型進行強類型檢查,以保證只有相同類型之間的變量才能互相賦值。
l 強類型檢查選項strong的用法是:
-strong( flags[, name] ... )
strong選項必須在typedef定義類型之前打開,否則PC-Lint就不能識別typedef定義的數據類型,類型檢查就會失效。flags參數可以是A、J、X、B、b、l和f,相應的解釋和弱化字符如下:
|
flag |
說明 |
|
A |
對強類型變量賦值時進行類型檢查,這些賦值語句包括:直接賦值、返回值、參數傳遞、初始化 A參數后面可以跟以下字符,用來弱化A的檢查強度: i 忽略初始化 r 忽略return語句 p 忽略參數傳遞 a 忽略賦值操作 c 忽略將常量賦值(包括整數常量、常量字符串等)給強類型的情況 z 忽略Zero賦值,Zero定義為任何非強制轉換為強類型的0常量。例如:0L和(int)0都是Zero, 例如:-strong(Ai,BITS),PC-Lint將會對從非BITS類型數據向BITS類型數據賦值的代碼發出告警,但是忽略變量初始化時的此類賦值。 |
|
X |
當把強類型的變量賦指給其他變量的時候進行類型檢查。弱化參數i, r, p, a, c, z同樣適用于X并起相同的作用。 |
|
J |
當強類型與其它類型進行如下的二進制操作時進行檢查,下面是J的參數: e 忽略==、!=和?:操作符 r 忽略>、>=、<和<= o 忽略+、-、*、/、%、|、&和^ c 忽略該強類型與常量進行以上操作時的檢查 z 忽略該強類型與Zero進行以上操作時的檢查 |
|
B |
1. 對所有的Boolean操作進行檢查,所謂Boolean操作就是四種關系運算符(>、>=、<、<=)和兩種等于判斷符(==、!=),取反操作符!,二元操作符&&和||; 2. 在所有需要判斷Bolean值的地方,如if語句和while語句,都要檢查結果是否符合這個強類型,否則告警。 例如:if(a)...當a為int時,將產生告警,因為int與Bolean類不兼容,所以必須改為if(a != 0)。 |
|
b |
僅對所有的Boolean操作進行檢查 |
|
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被聲明成強類型,第二個return語句告警是因為0不是Bool類型,如果添加c選項,例如-strong(Acb,Bool),這個告警就會被抑制。如果沒有指定b選項,第一個return語句也會告警,比較操作的結果是bool類型與函數返回值類型不匹配。
例如:
/*lint -strong( AJXl, STRING ) */
typedef char *STRING;
STRING s;
...
s = malloc(20);
strcpy( s, "abc" );
由于malloc和strcpy是庫函數,將malloc的返回值賦給強類型變量s或將強類型變量s傳遞給strcpy時會產生強類型沖突,不過l選項抑制了這個告警。
例如:
//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; // 強類型轉換
x.a = 0; // 強類型轉換
x.b = 2; // OK
x.c = x.a; // OK
x.e = 1; // 強類型轉換
x.e = x.d; // OK
}
上面例子中,成員a和c是強類型Bool,成員d和e是BitField類型,b不是強類型。為了避免將只有一位的位域假設成Boolean類型,需要在聲明Boolean的-strong中使用f選項,上面的例子就應該改成這樣:-strong(AJXbf,Bool)。
l 強類型選項index的用法:
-index( flags, ixtype, sitype [, sitype] ... )
index是對strong選項的補充,它可以和strong選項一起使用。這個選項指定ixtype是一個排除索引類型,ixtype和sitype被假設是使用typedef聲明的類型名稱。flags可以是c或d,c允許將ixtype和常量作為索引使用,而d允許在不使用ixtype的情況下指定數組的長度。
例如:
//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];
變量值跟蹤
變量值跟蹤技術從賦值語句、初始化和條件語句中收集信息,而函數的參數被默認為在正確的范圍內,只有在從函數中可以收集到的信息與此不符的情況下才產生告警。與變量值跟蹤相關的消息有:
(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的時候進行了判斷。
函數內變量跟蹤
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),是作為告警信息的序言出現的。
賦值順序檢查
當一個表達式的值依賴于賦值的順序的時候,會產生告警564。這是C/C++語言中非常普遍的一個問題,但是很少有編譯器會分析這種情況。比如
n++ + n
這個語句是有歧義的,當左邊的+操作先執行的話,它的值會比右邊的先執行的值大一,更普遍的例子是這樣的:
a[i] = i++;
f( i++, n + i );
第一個例子,看起來好像自加操作應該在數組索引計算以后執行,但是如果右邊的賦值操作是在左邊賦值操作之前執行的話,那么自加一操作就會在數組索引計算之前執行。雖然,賦值操作看起來應該指明一種操作順序,但實際上是沒有的。第二個例子是有歧義的,是因為函數的參數值的計算順序也是沒有保證的。能保證賦值順序的操作符是布爾與(&&)或(||)和條件賦值(? :)以及逗號(,),因此:if( (n = f()) && n > 10 ) ...這條語句是正確的,而:if( (n = f()) & n > 10 ) ...將產生一條告警。
弱定義檢查
這里的弱定義包含是以下內容:宏定義、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? ...
格式檢查
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也可以被用來描述非標準的 % 碼。
縮進檢查
根據代碼中的縮進問題,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個空格長度。另外,縮進檢查還和代碼的編碼格式策略相關,需要進行必要的調整。
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函數中再被初始化的。
volatile變量檢查
對于volatile變量的檢查,在PC-Lint中有這樣的規定,如果一個表達式中同時使用了兩次相同的volatile變量,那么就會給出564告警,因為這時候會產生賦值順序的問題。
volatile char *p;
volatile char f();
n = (f() << 8) | f(); /* Warning 564 */
n = (*p << 8) | *p; /* Warning 564 */
安裝與配置
PC-lint的安裝非常簡單,以PC-lint 8.0為例,運行安裝程序將其釋放到指定的安裝目錄即可,比如c:/pclint8。然后需要運行PC-lint的配置工具config.exe生成選項和檢查配置文件,以剛才的安裝路徑為例,config.exe應該位于:C:/pclint8/config.exe。配置文件是代碼檢查的依據,PC-lint自帶了一個標準配置文件std.lnt,但是這個文件沒有目錄包含信息(頭文件目錄),通常對代碼檢查的時候都需要指定一些特殊的包含目錄,所以要在標準配置的基礎上生成針對某個項目代碼檢查的定制配置。下面就以Microsoft Visual C++ 6的開發環境為例,介紹一下定制配置的過程。
(1) 運行C:/pclint8/config.exe后出現一個歡迎界面,提示版權信息;
(2) 點擊“下一步”按鈕出現pc-lint.exe命令行使用說明窗口;
(3) 點擊“下一步”按鈕繼續,接著是選擇創建或修改已有配置文件STD.LNT的選項:
因為我們是第一次配置,所以選擇上面一個選項“Create a new STD.LNT”,這樣做不會修改已有配置文件STD.LNT的內容,而是創建一個新的STD_x.LNT文件,文件名中的x是從“a”到“z”26個英文字符中的任意一個,一般是按順序排列,從“a”開始。STD_x.LNT文件的內容被初始化為STD.LNT內容的拷貝。使用默認的PC-Lint路徑,然后點擊“下一步”按鈕選擇編譯器;
(4) 接下來是選擇編譯器,在下拉框中選擇自己使用的編譯器。這里我們選擇“Microsoft Visual C++ 6.x (co-msc60.lnt)”。如果沒有自己使用的編譯器,可選擇通用編譯器“Generic Compilers”。這個選項會體現在co-xxx.lnt文件中,并存放在前面我們選擇的配置路徑(C:/PCLint8)下,在后面配置選項我們所選擇的***.LNT均會被存放到這個路徑下。點擊“下一步”按鈕選擇內存模式;
(5) 可以根據自己程序區和數據區的實際大小選擇一個恰當的內存模型,內存模型的選項會體現在STD.LNT文件或新創建的STD_x.LNT中。因為我們的開發環境是32位的Windows,所以選擇“32-bit Flat Model”,然后點擊“下一步”按鈕選擇所要的支持庫的配置信息;
(6) PC-Lint對現在常用的一些軟件庫都提供了定制的配置信息,選擇這些定制信息有助于開發人員將錯誤或信息的注意力集中在自己的代碼中,選擇的支持庫配置將被引入到STD.LNT文件或新創建的STD_x.LNT文件中。選擇常用的ATL、MFC、STL等配置,然后點擊“下一步”按鈕選擇軟件名人的編程建議;
(7) 這是一個比較有意思的選項,就是讓你選擇是否支持為使用C/C++編程提出過重要建議的作者的一些關于編程方面的個人意見。如果選擇某作者的建議,那么他提出的編程建議方面的選項將被打開,作者建議的配置名為AU-xxx.LNT,建議全部選擇,然后點擊“下一步”按鈕選擇是否現在設置包含文件目錄;
(8) 接下來是選擇用何種方式設置包含文件目錄,如果選擇使用-i方式協助設置包含文件選項,下一步就會要求輸入一個或多個包含路徑。也可以跳過這一步,以后手工修改配置文件,-i選項體現在STD.LNT文件或新創建的STD_x.LNT文件中,每個目錄前以-i引導,目錄間以空格分隔,如果目錄名中有長文件名或包含空格,使用時要加上雙引號,如-i“E:/Program Files/Microsoft Visual C++/VC98/indlue”。這里我們選擇用-i方式協助我們來設置,然后點擊“下一步”按鈕選擇是否現在設置包含文件目錄;
(9) 這一步就是在下面的文本框里可手工輸入文件包含路徑,用分號“;”或用ctrl+Enter換行來分割多個包含路徑,或者可以點中Brows,在目錄樹中直接選擇。填完后點擊“下一步”按鈕
(10) 提示std_x.lnt已經被創建,因為第三步選擇了“Create a new STD.LNT”選項,所以出現對話框,表示std_x.lnt,std.lnt在配置路徑下已被創建,這里的std_a.lnt實際上包含了std.lnt的信息,除此之外還有我們選擇的包含路徑和庫配置信息。單擊“確定”按鈕繼續;
(11) 提示是否為其它編譯環境創建配置文件,選擇“確定”后,會接著提示是否為其它編譯環境創建配置文件,如果選擇“是”將從第四步開始創建一個新的配置文件。這里選擇“否”;
(12) 接下來會提示是否使用現在生成的std_x.lnt文件取代std.lnt文件。如果選擇“是”將會用std_x.lnt文件的內容覆蓋std.lnt文件的內容,使得當前創建的配置選項成為以后創建新的配置文件時的缺省配置。通常我們選擇“否”繼續下一步;
(13) 生成全局代碼檢查選項文件OPTIONS.LNT,接下來將會準備產生一個控制全局編譯信息顯示情況的選項文件OPTIONS.LNT,該文件的產生方式有兩種,一種是安裝程序對幾個核心選項逐一解釋并提問你是否取消該選項,如果你選擇取消,則會體現在OPTIONS.LNT文件中,具體體現方式是在該類信息編碼前加-e,后面有一系列逐一選擇核心選項的過程。如果選擇第二種選擇方式,安裝文件會先生成一個空的OPTIONS.LNT文件,等你以后在實際應用時加入必要的選項。這里選擇“No”選項,即不取消這些選項,然后單擊“下一步”;
(14) 選擇所支持的集成開發環境,接著選擇所支持的集成開發環境選項,可選多個或一個也不選,PC-Lint提供了集成在多種開發環境中工作的功能,例如可集成在VC、BC、Source Insight中。這里我們選擇Microsift Visual C++ 6.0,這樣env-v6.lnt就會被拷貝到配置路徑中。然后單擊“下一步”;
(15) 選擇LIN.BAT文件的使用方式,安裝程序會生成一個LIN.BAT文件,該文件是運行PC-Lint的批處理文件,為了使該文件能在任何路徑下運行,安裝程序提供了兩種方法供你選擇。第一種方法是讓你選擇把LIN.BAT拷貝到任何一個PATH目錄下。第二種方法是生成一個LSET.BAT文件,在每次使用PC-LINT前先運行它來設置路徑,或者把LSET.BAT文件的內容拷貝到AUTOEXEC.BAT文件中。建議選擇第一種方法,指定的目錄為當前PC-Lint的安裝目錄。我們選擇第一種方式:“copy LIN.BAT to one of my PATH directory”,然后單擊“下一步”輸入PATH目錄;
(16) 輸入安裝目錄C:/PCLint8作為PATH目錄,然后單擊“下一步”按鈕進入最后的確認窗口;
(17) 到此就完成了PC-Lint的安裝配置工作,單擊“完成”按鈕就可以使用PC-Lint了。
以上配置過程中在配置路徑下產生的多個*.lnt文件,除了std.lnt、std_x.lnt和option.lnt為配置向導所生成,其它co-xxx.lnt、lib-xxx.lnt、env-xxx.lnt均是從原始安裝目錄中拷貝出來的,在這個目錄下還有其它PCLint所支持的編譯器、庫及集成開發環境的lnt配置文件,所有的lnt文件均為文本文件。
使用
PC-Lint的使用方法很簡單,可以用命令行方式進行,也可以集成到開發環境中,下面就分別介紹這些用法。
命令行方式
命令行的使用方式是PC-lint最基本的使用方式,也是其他各種集成使用方式的基礎,通過命令行可以完成PC-lint的全部代碼分析工作。PC-lint的命令行有下列形式:
Lint-nt option file1 [file1 file3 …]
其中的Lint-nt是PC-lint在Windows NT/2000/XP平臺上的可執行程序Lint-nt.exe,它完成PC-lint的基本功能;option代表PC-lint可接受的各種選項,這是PC-lint最為復雜的部分,它的選項有300多種,可以分為:錯誤信息禁止選項、變量類型大小選項、冗余信息選項、標志選項、輸出格式選項和其他選項等幾類。
與Visual C++集成
在所有集成開發環境中,PC-Lint 8.0對VC++6和VC++7.0的支持是最完善的,甚至支持直接從VC的工程文件(VC6是*.dsp,VC7是*.vcproj)導出對應工程的.Lnt文件,此文件包含了工程設置中的預編譯宏,頭文件包含路徑,源文件名,無需人工編寫工程的.Lnt文件。
PC-Lint與VC集成的方式就是在VC的集成開發環境中添加幾個定制的命令,添加定制命令的方法是選擇“Tools”的“Customize...”命令,在彈出的Customize窗口中選擇“Tools”標簽,在定制工具命令的標簽頁中添加定制命令。
(1) 首先要為VC的集成開發環境添加一個導出當前工程的.Lnt配置文件的功能,導出.Lnt文件的命令行是:
lint-nt.exe +linebuf $(TargetName).dsp>$(TargetName).lnt
參數+linebuf表示加倍行緩沖的大小,最初是600 bytes,行緩沖用于存放當前行和你讀到的最長行的信息。$(TargetName)是VC集成開發環境的環境變量,表示當前激活的Project名字,注意要選中“Use Output Window”選項,這樣PC-Lint就會將信息輸出到Output窗口中。
(2) 接著添加一個檢查當前文件的定制命令,檢查文件的命令行為:
lint-nt.exe -i"C:/PCLint8" -u std_g.lnt env-vc6.lnt "$(FileName)$(FileExt)"
第一個參數-i"C:/PCLint8"為PC-Lint搜索*.lnt文件的目錄,這里就是我們的配置路徑。std_g.lnt是為VC編譯環境定制的配置文件,$(FileName)和$(FileExt)是VC集成開發環境的環境變量,"$(FileName)$(FileExt)"表示當前文件的文件名。和導出.Lnt命令一樣,這個命令也要使用VC集成環境的Output窗口輸出檢查信息,所以要選中“Use Output Window”選項;
(3) 最后要添加一個檢查整個工程的定制命令,檢查整個工程的命令行是:
lint-nt.exe +ffn -i"C:/PCLint8" std_g.lnt env-vc6.lnt $(TargetName).lnt>$(TargetName).chk
這個命令的結果就是將整個工程的檢查結果輸出到與工程同名的.chk文件中。參數中+ffn表示Full File Names,可被用于控制是否使用的完整路徑名稱表示。
下面就以一個簡單的例子介紹一下如何在VC集成開發環境中使用PC-Lint。
首先新建一個“Win32 Console Application”類型的工程(輸出“Hello World”),然后將本文第二章引用的例子代碼添加到工程的代碼中,最后將這個工程代碼所倚賴的包含目錄手工添加到配置文件中,因為代碼檢查要搜索stdafx.h這個預編譯文件,所以本例要手工添加工程代碼所在的目錄。本文的例子生成的配置文件是std_g.lnt,用文本文件打開std_g.lnt,在文件中添加一行:
-iC:/unzipped/test
“C:/unzipped/test”就是例子工程所在的目錄(stdafx.h就在這個目錄)。如果你的工程比較龐大,有很多頭文件包含目錄,就需要將這些目錄一一添加到配置文件。在確保代碼輸入沒有錯誤之后(有錯誤頁沒關系,PC-Lint會檢查出錯誤),就可以開始代碼檢查了。例子工程,打開要檢查的代碼文件,本例是test.cpp,然后選擇“Tools”菜單下的“PC_LINT 8.0 Check Current File”命令,Output窗口輸出對本文件的檢查結果;
與source insight集成
PC-Lint與source insight的集成也是通過添加定制命令實現的。
從“Options”菜單中選擇“Custom Commands”命令項。點擊“Add…”按鈕,在彈出的“Custom Commands”窗口中完成以下輸入:
l 在Name欄中輸入“PC-lint Check Current File”,原則上這個名稱可以隨便起,只要你能搞清楚它的含義就可以了;
l 在Run欄中輸入“C:/PcLint/lint-nt -u -iC:/PcLint/Lint std_f env-si %f”其中C:/PcLint是你PC-LINT的安裝目錄,std_f表示為Source Insight定制的配置文件std_f.lnt;
l 在Output欄中選擇“Iconic Window”、“Capture Output”選項;
l 在Control欄中選擇“Save Files First”;
l 在Source Links in Output欄中選擇“Parse Links in Output”、“File,then Line”;
l 在Pattern欄中輸入“^/([^ ]*/) /([0-9]+/)”;
命令添加完成后就可以點擊“Run”按鈕就可以對當前文件執行PC-Lint檢查。為了方便使用,還可以點擊“Menu...”按鈕將這個定制命令添加到Source Insight的菜單中。
與UltraEdit集成
在UltraEdit中集成PC-Lint的方法和Source Insight類似,也是添加一個定制命令菜單,具體實現方法是先單擊UltraEdit的“高級”菜單中的“工具配置”命令,在打開的配置窗口中依次輸入以下內容:
在“菜單項目名”欄輸入“PC-lint Check Current File”;
l 在“命令行”欄輸入以下命令:C:/PCLint/lint-nt –u -iC:/PCLint std env-si %f 其中,C:/PCLint是PC-Lint的安裝目錄,使用std.lnt中的配置,由于UltraEdit和Source Insightde 的檢查環境類似,所以借用env-si中的環境配置;
l 在“工作目錄”欄輸入以下路徑:E:/code,這是代碼所在目錄;
l 選中“先保存所有文件”選項;
l 在“命令輸出”欄中,選中“輸出到列表”和“捕捉輸出”兩個選項;
l 點“插入”將命令行插入UltraEdit的菜單中;
PC-Lint 重要文件說明
l int-nt.exe:PC-lint的可執行程序。
l config.exe: PC-lint的配置文件程序。
l pc-lint.pdf:PC-lint的PDF格式的在線手冊。
l msg.txt :解釋告警的內容。
l options.lnt :反映全局編譯信息顯示情況的選項文件,通常需要添加自定選項以使代碼檢查更為嚴格。
l env-xx.lnt :講述如何將PC-lint與對應的編輯環境結合起來,xx是si表示是為Source Insight配置的檢查環境,xx是vc6則表示是為Visual C++ 6.0準備的檢查環境。
l co-xxx.lnt :選定的編譯器與庫選項。
l std.lnt :標準配置文件,包含內存模型等全局性東西。
l lib-xxx.lnt :庫類型的列表,包括標準C/C++庫,MFC庫,OWL庫等等。
l au-xxx.LNT :C++編程提出過重要建議的作者,選擇某作者后,他提出的編程建議方面的選項將被打開。
常見錯誤
|
錯誤編碼 |
說明 |
舉例 |
|
40 |
變量未聲明 |
|
|
506 |
固定的Boolean值 |
char c=3; if(c<300){} |
|
525 |
縮排格式錯誤 |
|
|
527 |
無法執行到的語句 |
if(a > B) return TRUE; else return FALSE; return FALSE; |
|
529 |
變量未引用 |
|
|
530 |
使用未初始化的變量 |
|
|
534 |
忽略函數返回值 |
|
|
539 |
縮排格式錯誤 |
|
|
545 |
對數組變量使用& |
char arr[100], *p; p=&arr; |
|
603 |
指針未初始化 |
void print_str(const char *p); … char *sz; print_str(sz); |
|
605 |
指針能力增強 |
void write_str(char *lpsz); … write_str(“string”); |
|
613 |
可能使用了空指針 |
|
|
616 |
在switch語句中未使用break |
|
|
650 |
比較數值時,常量的范圍超過了變量范圍 |
if( ch == 0xFF ) |
|
713 |
把有符號型數值賦給了無符號型數值 |
|
|
715 |
變量未引用 |
|
|
725 |
Indentation錯誤 |
|
|
734 |
在賦值時發生變量越界 |
int a, b, c; … c=a*b;
|
|
737 |
無符號型變/常量和有符號型變量/常量存在于同一個表達式中 |
|
|
744 |
在switch語句中沒有default |
|
|
752 |
本地聲明的函數未被使用 |
|
|
762 |
函數重復聲明 |
|
|
774 |
Boolean表達式始終返回真/假 |
|