因爲我(wǒ)(wǒ)從事的Windows編程,開(kāi)發工(gōng)具主要是VC6.0,所以以下(xià)的觀點有可能有Windows編程習慣的痕迹,但我(wǒ)(wǒ)會盡量是以C/C++的觀點來說明。除非進行了特别注明,否則與具體(tǐ)的開(kāi)發工(gōng)具無關。
關于編程規範,大(dà)家可以參考編程規範。本文的内容是我(wǒ)(wǒ)認爲編程規範需要強調的和一(yī)些個人的編程經驗。由于C++/C編程是衆所周知(zhī)的技術,沒有秘密可言。編程的好經驗應該大(dà)家共享,若有自認爲好的編程習慣或方法請告訴我(wǒ)(wǒ),我(wǒ)(wǒ)會收錄到這篇文章或後續的文章中(zhōng)。謝謝!
如果有時間,我(wǒ)(wǒ)也願意再繼續寫一(yī)些東西。
因爲個人的水平有限,所以會存在錯誤或不當之處。如果有任何問題請與我(wǒ)(wǒ)聯系。
對文中(zhōng)提到的<編程規範>,指<XXXX>和<XX編程規範>。
1 引言
軟件質量是被大(dà)多數程序員(yuán)挂在嘴上而不是放(fàng)在心上的東西!
作爲軟件工(gōng)程師,我(wǒ)(wǒ)們首先要樹(shù)立質量和責任意識。我(wǒ)(wǒ)認爲在公司現有的開(kāi)發流程下(xià)(質量管理部主要做功能測試)測試最終軟件産品的質量主要取決于開(kāi)發人員(yuán)。也可以說,産品出現BUG的主要責任應由開(kāi)發人員(yuán)來承擔。這不是爲測試工(gōng)程師和其他人員(yuán)開(kāi)脫責任,而是說我(wǒ)(wǒ)們一(yī)定要有這個意識。
編程質量差往往是由于不良習慣造成的,與人的智力、能力沒有多大(dà)關系。
關于C++/C編程風格,難度不高,但是細節比較多。别小(xiǎo)看了,提高質量就是要從這些點點滴滴做起。世上不存在最好的編程風格,一(yī)切因需求而定。團隊開(kāi)發講究風格一(yī)緻,如果制定了大(dà)家認可的編程風格,那麽所有組員(yuán)都要遵守。如果你覺得某些編程風格比較合你的工(gōng)作,那麽就采用它,不要隻看不做。人在小(xiǎo)時候說話(huà)發音不準,寫字潦草,如果不改正,總有後悔的時候。編程也是同樣道理。
與代碼質量的相關的重要因素有:
l 設計文檔
l 編程風格
l 編程經驗
l 編程語言
l 編程工(gōng)具
l 操作系統
l 錯誤處理
l 安全處理
參考資(zī)料:
<高質量C++編程指南(nán)>
<應用程序調試技術>
<C語言編程常見問題解答>
<Effective C++中(zhōng)文版>
<C++ Primer>
...
2 注意注釋
作爲軟件工(gōng)程師,我(wǒ)(wǒ)們的工(gōng)作是雙重的,即爲用戶開(kāi)發解決方案,同是還要使該解決方案在将來是可維護的。使我(wǒ)(wǒ)們編寫的代碼是可維護的唯一(yī)方法是對代碼進行注釋。對于“注釋”,我(wǒ)(wǒ)的意思不隻是簡單地編寫代碼功用的注釋,而是将假設、方法以及選擇所使用的方法的原因寫進文件。同時,注釋必需與代碼相符。通常情況下(xià),當維護程序設計者們試圖更新那些功用與注釋描述的功能不一(yī)緻的代碼時,工(gōng)作效率往往是極低的。
對于關于注釋的規範,請參見<編程規範>。
2.1 單入口單出口原則
結構化程序設計技術的定義是結構化程序設計技術是一(yī)種程序設計技術,它采用自頂向下(xià)逐步求精的設計方法和單入口單出口的控制結構,并且隻包含順序、選擇和循環三種結構。結構化程序設計的核心思想是Algorithms+Data Structures=Programs。結構化程序設計雖然不是最好的的程序設計方法并且好像已經過時。我(wǒ)(wǒ)認爲結構化程序設計應該和其它的程序設計技術結合起來使用。但對于它的單入口和單出口原則無論是在詳細設計和編碼階段都應該堅決貫徹的。
2.1.1 詳細設計
在詳細設計階段基本上确定了程序的數據和控制的流向。在進行詳細設計時一(yī)定要确保程序執行相同的功能調用的函數和順序是一(yī)緻的,也就是單入口和單出口,這有利于程序的實現、測試和維護。另外(wài),在程序維護時也要确保不要爲了增加某個功能而另外(wài)建立一(yī)條數據流通道,而是要仔細分(fēn)析是否可利用現有的入口和出口。
有關例子此處不列舉了,但相信每一(yī)個編程老手都會受到在程序結構不是單入口和單出口的問題的困擾。
2.1.2 函數設計
2.1.2.1 函數隻有一(yī)個出口
以下(xià)是從WebKeeper中(zhōng)摘錄的一(yī)段代碼,爲了保證函數隻有一(yī)個出口,利用了tryfinally終止處理結構,這避免了函數内部的多個return語句。試想一(yī)下(xià),如果改用return或其它方式來改寫以下(xià)的模塊,會是多麽複雜(zá)和難以維護。假設函數體(tǐ)中(zhōng)再有對同步量的占有和釋放(fàng)過程,則擁有多個出口的函數将是更難編寫和維護。
__try
{
//創建目錄内存映射文件
{
HANDLE hFileMap = NULL;
……
g_hDir = CreateFileW(lpszFileName, ….);
if(INVALID_HANDLE_VALUE == g_hDir)
__leave;
if((GetFileSize(g_hDir, NULL) == 0)
&& (AddFileHeader(g_hDir) != ERROR_SUCCESS))
__leave;
if(!(hFileMap = CreateFileMapping(g_hDir, …, 0, 0, NULL)))
__leave;
if(!(g_pDir = (char*)MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)))
__leave;
CloseHandle(hFileMap);
}
//創建索引内存映射文件
{
…
}
//創建數據内存映射文件
{
}
g_bLoaded = TRUE;
}
__finally
{
if(!g_bLoaded)
CloseGlobalObject();
else if(IsBadBackupLib())
RepairBackupLib();
}
2.1.2.2 語句塊隻有一(yī)個出口
程序流應該清晰,避免使用goto語句和其它(比如break,continue)跳轉語句
例如:
for(int a = 0; a < 100; a++)
{
Func1(a);
if(a == 2)
continue;
Func2(a);
}
它可以被改寫成如下(xià)的形式:
for(int a = 0; a < 100; a++)
{
Func1(a);
if(a != 2)
Func2(a);
}
這段程序更易于調試,因爲循環體(tǐ)内的代碼清楚地顯示了應該執行和不應該執行什麽。假設現在要加入一(yī)些在每次循環的最後都要被執行的代碼,在第一(yī)段程序中(zhōng)如果維護者注意到了continue語句,就不得不對這段程序做的修改;如果沒有注意到continue語句,那麽恐怕就要犯一(yī)個難以發現的錯誤了。在第二段代碼中(zhōng),要做的修改很簡單,隻需把新的代碼加到循環體(tǐ)的末尾。
當使用break語句時,可能會發生(shēng)另外(wài)兩種錯誤。
for(int a = 0; a < 100; a++)
{
if(Func1(a) == 2)
break;
Func2(a);
}
假設Func1()的返回值永遠不會等于2,上面循環就會從1進行到100;反之,循環在到達100以前就會結束。假設因維護的需要要在循環體(tǐ)中(zhōng)加入代碼,一(yī)種危險是我(wǒ)(wǒ)們可能認爲它确實能從0循環到99;另一(yī)種危險可能來自對a值的使用,因爲當循環結束後,a的值并不一(yī)定就是100。
但我(wǒ)(wǒ)們可能按以下(xià)形式編寫這個for循環:
for(a = 0; (a < 100) && (Func1(a) != 2); a++)
這樣當維護這段代碼時就很難犯前面的那些錯誤了。
2.1.2.3 多個出口還可能導緻程序代碼膨脹
内聯析構函數可能是程序代碼膨脹的一(yī)個源泉,因爲它被插入到函數中(zhōng)的每個退出點,以析構每一(yī)個活動的局部類對象。《C++Primer 3 P579》。
3 命名規則
比較著名的命名規則當推Microsoft公司的“匈牙利”法,該命名規則的主要思想是“在變量和函數名中(zhōng)加入前綴以增進人們對程序的理解”。例如所有的字符變量均以ch爲前綴,若是指針變量則追加前綴p。如果一(yī)個變量由ppch開(kāi)頭,則表明它是指向字符指針的指針。
但是也沒有必要對每個變量的命名都用這個方法,在某些情況下(xià)單字符的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通常可用作函數内的局部變量。
1. 命名規則盡量與所采用的操作系統或開(kāi)發工(gōng)具的風格保持一(yī)緻。例如Windows應用程序的标識符通常采用“大(dà)小(xiǎo)寫”混排的方式,如AddChild。而Unix應用程序的标識符通常采用“小(xiǎo)寫加下(xià)劃線”的方式,如add_child。别把這兩類風格混在一(yī)起用。
2. 爲了防止某一(yī)軟件庫中(zhōng)的一(yī)些标識符和其它軟件庫中(zhōng)的沖突,可以爲各種标識符加上能反映軟件性質的前綴。例如三維圖形标準OpenGL的所有庫函數均以gl開(kāi)頭,所有常量(或宏定義)均以GL開(kāi)頭;又(yòu)如動态鏈接庫AutoRecover.dll的所有引出函數名均以ar開(kāi)頭。
4 雜(zá)項
4.1 new和delete操作符
4.1.1 盡量以new和delete取代malloc和free
原因很簡單:malloc和free函數對構造函數和析構函數一(yī)無所知(zhī)。
4.1.2 new
很多程序員(yuán)習慣這樣用new操作符:
#include
……
if(char* p = new char[100])
{
…;
}
這樣寫的目的是判斷分(fēn)配内存是否成功,然後針對分(fēn)配成功與否進行相應的處理。但這樣寫代碼達不到我(wǒ)(wǒ)們的目的。因爲當分(fēn)配不成功時是觸發異常(std::bad_alloc)而不是返回NULL(這依賴于編譯器和相關編譯設置),當分(fēn)配不成功的情況出現以後如果沒有異常處理将會彈出異常對話(huà)框,導緻程序終止運行。有關這個問題在很多書(shū)籍和資(zī)料中(zhōng)都認爲new在失敗後返回NULL,其實這種觀點是不正确的。
若是讓new在分(fēn)配失敗時返回NULL,可用如下(xià)形式:
#include
……
if(char* p = new (std::nothrow) char[100])
{
…;
}
說明:在用VC6.0的main和WinMain項目時在DEBUG和RELEASE兩種方式都沒有問題。在MFC的項目下(xià)使用時在RELEASE下(xià)編譯和運行都沒有問題,但在DEBUG下(xià)編譯通不過。通過研究發現在源文件的開(kāi)頭有這樣幾行代碼:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
原來是DEBUG環境下(xià)已經把new定義成了DEBUG_NEW,問題就出在這裏。通過進一(yī)步的研究發現在AFX.H中(zhōng)有如下(xià)代碼:
// Memory tracking allocation
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#define DEBUG_NEW new(THIS_FILE, __LINE__)
原來是爲了内存跟蹤和保存調試信息,AFX小(xiǎo)組已經把new進行了重載,而這個重載過的new是不支持nothrow的。難道是AFX開(kāi)發小(xiǎo)組忽略了與C++标準的兼容嗎(ma)?也許是,但也許這樣做也可能是該小(xiǎo)組成員(yuán)認爲這樣可以在DEBUG環境下(xià)更快的發現問題,而不允許使用new的nothrow版本。
其實CObject類也把new進行了重載了三個版本,但沒有重載nothrow版本。這說明了分(fēn)配一(yī)個以CObject爲基類的時,不能使用nothrow版本,隻能使用抛出異常版本。以下(xià)是CObject的聲明片斷:
class CObject
{
public:
……
// Diagnostic allocations
void* PASCAL operator new(size_t nSize);
void* PASCAL operator new(size_t, void* p);
void PASCAL operator delete(void* p);
#if _MSC_VER >= 1200
void PASCAL operator delete(void* p, void* pPlace);
#endif
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
// for file name/line number tracking using DEBUG_NEW
void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#if _MSC_VER >= 1200
void PASCAL operator delete(void *p, LPCSTR lpszFileName, int nLine);
#endif
#endif
……
};
不論使用正常形式(抛出exception)的new,或是“nothrow”形式的new,重要的是我(wǒ)(wǒ)們已經準備好處理内存配置失敗的情況了。最簡單的辦法就是利用set_new_handler,因爲它在兩種形式中(zhōng)都可用。
4.1.3 delete
delete的作用是确保每個一(yī)對象的析構函數被調用。且new[]和delete[]一(yī)定要成對使用。
例如:
…
CObject* pObj = new CObject[5];
…
delete oObj;
這段代碼能夠把所分(fēn)配的内存全部釋放(fàng)掉,不會出現内存洩漏問題。但隻調用了第一(yī)個對象的析構函數,而不是全部的5個。對基本類型(比如分(fēn)配字符串數組),沒有析構函數可調用,但爲了規範和維護的不出問題,也一(yī)定要保證delete和new的形式要匹配。
4.2 邊編碼邊調試
被動式的程序設計是提示有錯誤出現的錯誤處理代碼,而主動式的程序設計則會告訴我(wǒ)(wǒ)們錯誤出現的原因。被動式的編碼隻是修改和解決錯誤工(gōng)作的一(yī)部分(fēn)。軟件工(gōng)程師們通常會努力制定出明确的被動策略,例如,校驗指向某一(yī)字符串的指針不是NULL,但是通常不采納主動式程序設計所要示的另外(wài)一(yī)個步驟,即檢查這個相同的參數,查看是否有足夠的内存來支持字符串所允許的最大(dà)字符數。主動式程序設計同時這意味着在編寫代碼的同時,查找存在問題的部分(fēn),因此編碼一(yī)開(kāi)始,調試過程也就開(kāi)始了。
實際上,錯誤并不會無緣無故地出現在代碼中(zhōng)。其原因實際上是我(wǒ)(wǒ)們在編寫代碼的同時也輸入了錯誤,而且這些令人讨厭(yàn)的錯誤可以來源于任何地方。但是,必需接受這樣一(yī)個事實:錯誤是由人爲引起的。
由于開(kāi)發者要對代碼中(zhōng)的任何錯誤負責,問題就成了找出一(yī)種方法來創建一(yī)個檢查和對比的系統,以便在編碼的過程中(zhōng)找出錯誤。這種方法稱爲“信任,但要校驗”。爲了避免錯誤我(wǒ)(wǒ)檢驗其它開(kāi)發者傳送到我(wǒ)(wǒ)編寫的代碼中(zhōng)的數據、校驗我(wǒ)(wǒ)編寫的代碼的内部處理功能、校驗我(wǒ)(wǒ)在代碼中(zhōng)所作的任何假設、校驗我(wǒ)(wǒ)編寫的代碼傳送給他人的數據、并且校驗我(wǒ)(wǒ)編寫的代碼進行的調用所返回的數據。
需要強調軟件開(kāi)發哲學中(zhōng)的一(yī)個重要原則:代碼質量僅是軟件開(kāi)發工(gōng)程師的責任,而不是測試工(gōng)程師、技術資(zī)料編寫者或管理者的責任。
4.2.1 聲明
聲明有很多函數,大(dà)家可以先用自己喜歡的一(yī)個或幾個。例如ASSERT、assert、_ASSERT、_ASSERTE、ASSERT_KINDOF、ASSERT_VALID和VERIFY。
使用聲明除了能幫助我(wǒ)(wǒ)們更快地發現問題并定位BUG點以外(wài)還有一(yī)個附加的好處:就是它起到了注釋的作用,幫助自己以後或其他維護工(gōng)程師更快更清楚的知(zhī)道該函數的假設條件。
使用聲明的注意事項:
1.聲明的第一(yī)個原則是每次隻檢查單獨的一(yī)項。如果隻用一(yī)個聲明檢查多個條件,我(wǒ)(wǒ)們将無法知(zhī)道故障是哪個條件造成的。
2. 聲明一(yī)個條件時,需要盡可能全面地檢查該條件。
C/C++和API提供了各種各樣的函數幫助我(wǒ)(wǒ)們盡可能以描述性的語言編寫聲明。
a) GetObjectType
b) IsBadCodePtr
c) IsBadReadPtr
d) IsBadStringPtr
e) IsBadWritePtr
f) IsWindow
g) AfxIsValidAddress
h) AfxIsMemoryBlock
i) AfxIsValidString
注意:IsBadStringPtrt和IsBadWritePtr不是線程安全的。詳見。
3. 需要聲明所有出現在函數中(zhōng)的參數。對于接口函數和開(kāi)發組中(zhōng)其他成員(yuán)調用的公用類來說,聲明參數是尤其重要的。因爲這是進入自己所編寫代碼的入口點,需要确保各個參數和假設是有效的。進入到自己的模塊中(zhōng)以後,對模塊的專用函數的參數不用進行太多的檢查,因爲問題主要取決于參數的起源地。隻需一(yī)些經驗即可判斷需要聲明哪些參數。
4. 另一(yī)個經常使用聲明的地方,是在普通的處理流程中(zhōng),調用的函數的返回值。
5. 使用聲明的最後一(yī)個地方是需要對假設進行檢查的地方。例如,如果某一(yī)函數需要3MB的磁盤空間,就使用一(yī)個聲明檢查該假設。另一(yī)個例子是,如果函數使用了一(yī)個指向某一(yī)特定數據結構的指針數組,應遍曆該數據結構,驗證每個獨立的項都是有效的。
6. 對每一(yī)個聲明還要進行if語句判斷,聲明不能代替判斷。
一(yī)個使用聲明的代碼片段:
BOOL BUGSUTIL_DLLINTERFACE __stdcall
HookOrdinalExport ( HMODULE hModule ,
LPCTSTR szImportMod ,
DWORD dwOrdinal ,
PROC pHookFunc ,
PROC * ppOrigAddr )
{
ASSERT ( NULL != hModule ) ;
ASSERT ( FALSE == IsBadStringPtr ( szImportMod , MAX_PATH ) ) ;
ASSERT ( 0 != dwOrdinal ) ;
ASSERT ( FALSE == IsBadCodePtr ( pHookFunc ) ) ;
// Perform the error checking for the parameters.
if ( ( NULL == hModule ) ||
( TRUE == IsBadStringPtr ( szImportMod , MAX_PATH ) ) ||
( 0 == dwOrdinal ) ||
( TRUE == IsBadCodePtr ( pHookFunc ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
if ( NULL != ppOrigAddr )
{
ASSERT ( FALSE ==
IsBadWritePtr ( ppOrigAddr , sizeof ( PROC ) ) ) ;
if ( TRUE == IsBadWritePtr ( ppOrigAddr , sizeof ( PROC ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
}
}
4.3 線程
一(yī)般情況下(xià)創建線程應該用_beginthreadex而不是CreateThread,因爲後一(yī)個函數爲API,當線程終結時在大(dà)多數情況下(xià)會有内存和資(zī)源洩漏。
隻有在萬不得已的情況下(xià)才用TerminateThread,但一(yī)定要注意,除了内存和資(zī)源會釋放(fàng)不了以外(wài),還可能會導緻某些同步量釋放(fàng)不了。經過測試被TerminateThread掉線程占有的臨界段(CRITICAL_SECTION)和事件(Event)釋放(fàng)不了,但互斥量(Mutex)能夠釋放(fàng)。但不管怎樣,殺掉線程以前知(zhī)道将要發生(shēng)的結果是有益的。詳見<MSDN>關于TerminateThread的說明。
4.4 MFC的數據類
CString、CList和CArray等最好隻在一(yī)個模塊内部使用,若在不同模塊間進行通信時的接口參數或結構中(zhōng)的成員(yuán)是這些類型則很可能發生(shēng)内存(運行不正常)問題。原因是對這些數據類的賦值等操作都伴随着内存的釋放(fàng)和分(fēn)配等操作,問題就轉化成了在一(yī)個模塊分(fēn)配内存而在另一(yī)個模塊釋放(fàng)。而在一(yī)個二進制模塊分(fēn)配内存又(yòu)在另一(yī)個二進制模塊釋放(fàng)内存是我(wǒ)(wǒ)們要盡量避免的。原因是:在一(yī)個項目的不同模塊中(zhōng)分(fēn)别鏈接不同的C運行庫是可以的,但不同的C運行庫對内存的處理是不一(yī)樣的(比如RELEASE和DEBUG,MultiThreaded和MultiThreaded DLL等)。這就是在一(yī)個模塊中(zhōng)分(fēn)配的内存而在另一(yī)個模塊釋放(fàng)是不安全的原因,也是我(wǒ)(wǒ)們要統一(yī)各個模塊的編譯設置的原因之一(yī)。
另外(wài),在結構中(zhōng)也最好不要使用這些類作爲成員(yuán)。因爲有些程序員(yuán)爲了操作方便和認爲這樣不會浪費(fèi)空間而習慣這樣做。不要讓它們作爲結構成員(yuán)的原因之一(yī)是,因爲要常常利用結構對象進行模塊間的通信。原因之二是我(wǒ)(wǒ)們常常把對其他結構慣用的一(yī)些操作應用到采用了CString等作爲成員(yuán)之一(yī)的結構上面,例如賦值、内存拷貝等。因爲CString等數據成員(yuán)隻是一(yī)個指針,而它們的析構函數一(yī)般都要進行内存釋放(fàng)操作所以會非常容易造成對一(yī)塊内存釋放(fàng)兩次的内存錯誤。當然,第二種錯誤可以通過重載賦值操作符和拷貝構造函數來避免。但爲什麽不用其他方法替代呢?現在的程序運行環境下(xià),空間對我(wǒ)(wǒ)們來說非常重要嗎(ma)?使用CString一(yī)定比char [MAX_PATH]節省空間嗎(ma),不一(yī)定,這要視具體(tǐ)情況而定,因爲每一(yī)個new操作都會占用額外(wài)的空間(大(dà)概至少是12-32字節,這和具體(tǐ)的庫實現版本有關),當然也要多花費(fèi)一(yī)定的時間。這樣做了至少造成兩個後果:a)這樣開(kāi)發出來的産品運行易出錯;b)難于開(kāi)發、調試和維護。
4.5 表達式和基本語句
1. 如果代碼行中(zhōng)的運算符比較多,用括号确定表達式的操作順序,避免使用默認的優先級。
由于将運算符的優先級熟記是比較困難的,爲了防止産生(shēng)歧義并提高可讀性,應當用括号确定表達式的操作順序。例如:
word = (high << 8) | low
if ((a | b) && (a & c))
2.不可将布爾變量直接與TRUE、FALSE或者1、0進行比較。
根據布爾類型的語義,零值爲“假”(記爲FALSE),任何非零值都是“真”(記爲TRUE)。TRUE的值究竟是什麽并沒有統一(yī)的标準。例如Visual C++ 将TRUE定義爲1,而Visual Basic則将TRUE定義爲-1。
假設布爾變量名字爲flag,它與零值比較的标準if語句如下(xià):
if (flag) // 表示flag爲真
if (!flag) // 表示flag爲假
其它的用法都屬于不良風格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)
3.if語句
有時候我(wǒ)(wǒ)們可能會看到 if (NULL == p) 這樣古怪的格式。不是程序寫錯了,是程序員(yuán)爲了防止将 if (p == NULL) 誤寫成 if (p = NULL),而有意把p和NULL颠倒。編譯器認爲 if (p = NULL) 是合法的,但是會指出 if (NULL = p)是錯誤的,因爲NULL不能被賦值。
把if(p == NULL)誤寫成if(p = NULL)的情形是很可能發生(shēng)的,也可能成爲一(yī)個難以發現的BUG。依賴編譯器也許能夠發現這一(yī)點,比如把VC的警告設置成4級,就可以發現這個問題,但你得有這樣的習慣。總而言之,if(NULL == p)這樣的寫法應是我(wǒ)(wǒ)們提倡的。
4. case語句的結尾不要忘了加break,否則将導緻多個分(fēn)支重疊(除非有意使多個分(fēn)支重疊)。
5. 不要忘記最後那個default分(fēn)支。即使程序真的不需要default處理,也應該保留語句 default : break; 這樣做并非多此一(yī)舉,而是爲了防止别人誤以爲你忘了default處理。
6. 盡管switch語句的最後一(yī)個分(fēn)支不一(yī)定需要break語句,但最好還是在switch的每個分(fēn)支後面加上break語句,包括最後一(yī)個分(fēn)支。這樣做的原因是:程序很可能要讓另一(yī)個來維護,他可能要增加一(yī)些新的分(fēn)支,但沒有注意到最後一(yī)個分(fēn)支沒有break語句,結果使原來的最後一(yī)個分(fēn)支受到其後新增分(fēn)支的幹擾而失效。在每個分(fēn)支後面加上break語句将防止發生(shēng)這種錯誤并增強程序的安全性。此外(wài),目前大(dà)多數優化編譯程序都會忽略最後一(yī)條break語句,所以加入這條語句不會影響程序的性能。
7. for語句
可能的情況下(xià),要用某一(yī)标志(zhì)而不是固定的次數作爲循環終止條件。這有利于程序的擴展和維護。例如,爲了遍曆某一(yī)個結構數組(最後一(yī)個元素全0或結構中(zhōng)的重要值是0代表結構數組的終結)則終止條件可爲判斷相應值是否标志(zhì)結束。這要比用固定次數爲終止條件有更好的擴展和維護性能。
爲了保證for語句隻有一(yī)個出口,不要在循環體(tǐ)内部使用跳轉(continue,break)語句。
4.6 使用const常量
1. C++ 程序中(zhōng)隻使用const常量而不使用宏常量,即const常量完全取代宏常量。
2. 如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體(tǐ)内被意外(wài)修改。例如char *strcpy( char *strDestination, const char *strSource )。
這樣做還會有附加的好處,例如:
{
char szNewFileName[MAX_PATH];
CString strOldFileName;
strOldFileName = “abc.doc”;
strcpy(szNewFileName, strOldFileName);
}
但若strcpy的原型是char *strcpy( char *strDestination, char *strSource ),則要完成類似功能需編寫如下(xià)代碼:
{
char szNewFileName[MAX_PATH];
CString strOldFileName;
strOldFileName = “abc.doc”;
strOldFileName.GetBuffer(strOldFileName.GetLength());
strcpy(szNewFileName, strOldFileName);
strOldFileName.ReleaseBuffer();
}
3. 建議對函數内部不進行修改的所有參數聲明爲const。這有利于編譯器幫助我(wǒ)(wǒ)們發現錯誤,并可生(shēng)成效率稍高的二進制文件。
例如:假如strcpy函數内部實現若不更改指針值則可聲明爲:char *strcpy( char *const strDestination, const char * const strSource ),但C函數庫的聲明不是這樣的,這也說明了在這個函數實現中(zhōng)應該對這兩個指針值進行了更改。以下(xià)是該函數的一(yī)種可能的實現方式:
char *strcpy(char *strDest, const char *strSrc);
{
assert(strDest!=NULL);
assert(strSrc !=NULL);
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘0’ );
return address;
}
請大(dà)家注意:const char* const psz這樣寫法的含義,并習慣這樣的寫法。
4. 如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去(qù)臨時對象的構造和析構過程,從而提高效率。
4.7 函數設計
1. 對C++程序而言,函數内部的局部變量應該最近定義。這會使程序看起來更直觀,同時也方便維護和排錯。
2. 避免函數有太多的參數,參數個數盡量控制在5個以内。如果參數太多,在使用時容易将參數類型或順序搞錯。
3. 在函數體(tǐ)的“入口處”,對參數的有效性進行檢查。
很多程序錯誤是由非法參數引起的,我(wǒ)(wǒ)們應該充分(fēn)理解并正确使用“斷言”(assert)來防止此類錯誤。詳見“邊編碼邊調試”。
4. 在函數體(tǐ)的“出口處”,對return語句的正确性和效率進行檢查。
如果函數有返回值,那麽函數的“出口處”是return語句。我(wǒ)(wǒ)們不要輕視return語句。如果return語句寫得不好,函數要麽出錯,要麽效率低下(xià)。
注意事項如下(xià):
(1)return語句不可返回指向“棧内存”的“指針”或者“引用”,因爲該内存在函數體(tǐ)結束時被自動銷毀。例如
char * Func(void)
{
char str[] = “hello world”; // str的内存位于棧上
…
return str; // 将導緻錯誤
}
(2)要搞清楚返回的究竟是“值”、“指針”還是“引用”。
(3)如果函數返回值是一(yī)個對象,要考慮return語句的效率。例如
return String(s1 + s2);
這是臨時對象的語法,表示“創建一(yī)個臨時對象并返回它”。不要以爲它與“先創建一(yī)個局部對象temp并返回它的結果”是等價的,如
String temp(s1 + s2);
return temp;
實質不然,上述代碼将發生(shēng)三件事。首先,temp對象被創建,同時完成初始化;然後拷貝構造函數把temp拷貝到保存返回值的外(wài)部存儲單元中(zhōng);最後,temp在函數結束時被銷毀(調用析構函數)。然而“創建一(yī)個臨時對象并返回它”的過程是不同的,編譯器直接把臨時對象創建并初始化在外(wài)部存儲單元中(zhōng),省去(qù)了拷貝和析構的化費(fèi),提高了效率。
類似地,我(wǒ)(wǒ)們不要将
return int(x + y); // 創建一(yī)個臨時變量并返回它
寫成
int temp = x + y;
return temp;
由于内部數據類型如int,float,double的變量不存在構造函數與析構函數,雖然該“臨時變量的語法”不會提高多少效率,但是程序更加簡潔易讀。
5. 僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入函數體(tǐ)内的變量的有效性,例如全局變量、文件句柄等。
5.用于出錯處理的返回值一(yī)定要清楚,讓使用者不容易忽視或誤解錯誤情況。
4.8 使用安全的C/C++庫函數和Windows API
使用strcpy,strcat,sprintf,fgets等函數有緩沖區溢出的危險,每一(yī)個合格的程序員(yuán)都會非常謹慎地使用它們。但最好禁用這些函數。
爲了自己會在某一(yī)天忘記了這條約束,可在公共頭文件中(zhōng)進行如下(xià)定義:
#define strcpy Unsafe_strcpy
這樣如果使用了strcpy,編譯器會提醒我(wǒ)(wǒ)們。
可用strncpy版本的函數替代strcpy函數:
有如下(xià)的語句塊:
{
char szBuf[MAX_PATH];
strcpy(szBuf, lpszInput); //lpszInput爲用戶鍵盤輸入的字符串
}
上面的代碼是不安全的,可用如下(xià)語語句代替:
{
char szBuf[MAX_PATH];
strncpy(szBuf, lpszInput, sizeof(szBuf) / sizeof(szBuf[0]));
if(szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1])
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
應該用_snprintf代替sprintf,用法與strncpy有點不同,如下(xià):
{
char szBuf[MAX_PATH];
szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1] = 0;
_snprintf(szBuf, sizeof(szBuf) / sizeof(szBuf[0]), “%s%d”, lpszInout, 0x233323);
if(szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1])
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
或
{
char szBuf[MAX_PATH];
if(_snprintf(szBuf, sizeof(szBuf) / sizeof(szBuf[0]), “%s%d”, lpszInout, 0x233323) < 0)
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
不安全的C/C++庫函數和API還有很多,大(dà)家可以多看一(yī)下(xià)這方面的書(shū)籍或資(zī)料。
CreateProcess,WinExec等使用不當也可能造成一(yī)些漏洞,參見<MSDN>。
4.9 杜絕“野指針”和“野句柄”
“野指針”不是NULL指針,是指向“垃圾”内存的指針。人們一(yī)般不會錯用NULL指針,因爲用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。
在使用任何一(yī)個指針或句柄等前先進行判斷是非常有益的。否則你認爲目前的程序代碼中(zhōng)某個指針一(yī)定是有效且可用的,于是就沒有進行判斷而直接使用,但随着時間的推移和代碼量的增大(dà),在維護程序時你有很大(dà)的可能注意不到這一(yī)點,而出現一(yī)個難以發現的BUG。這也是改正了一(yī)個BUG而又(yòu)增加了一(yī)個或幾個BUG的原因之一(yī)。也有可能是自己沒有考慮到某一(yī)個邊界情況,而自認爲該指針或句柄等一(yī)定是有效的。出現了這種情況更難調試,有可能還要依靠測試工(gōng)程師的幫助。因爲這個邊界條件是很難重現的,外(wài)在表現是程序在絕大(dà)多數情況下(xià)運行正常,而某刻又(yòu)不正常。尋找這樣的BUG真是太困難了。爲什麽不在開(kāi)始寫代碼時就做嚴格的檢查呢?
“野指針”的成因主要有兩種:
(1)指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針,它的缺省值是随機的,它會亂指一(yī)氣。所以,指針變量在創建的同時應當被初始化,要麽将指針設置爲NULL,要麽讓它指向合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指針p被free或者delete之後,沒有置爲NULL,讓人誤以爲p是個合法的指針。
(3)指針操作超越了變量的作用範圍。這種情況讓人防不勝防,示例程序如下(xià):
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生(shēng)命期
}
p->Func(); // p是“野指針”
}
函數Test在執行語句p->Func()時,對象a已經消失,而p是指向a的,所以p就成了“野指針”。
4.10 使用const提高函數的健壯性
看到const關鍵字,C++程序員(yuán)首先想到的可能是const常量。這可不是良好的條件反射。如果隻知(zhī)道用const定義常量,那麽相當于把火(huǒ)藥僅用于制作鞭炮。const更大(dà)的魅力是它可以修飾函數的參數、返回值,甚至函數的定義體(tǐ)。
const是constant的縮寫,“恒定不變”的意思。被const修飾的東西都受到強制保護,可以預防意外(wài)的變動,能提高程序的健壯性。所以很多C++程序設計書(shū)籍建議:“Use const whenever you need”。
11.1.1 用const修飾函數的參數
如果參數作輸出用,不論它是什麽數據類型,也不論它采用“指針傳遞”還是“引用傳遞”,都不能加const修飾,否則該參數将失去(qù)輸出功能。
const隻能修飾輸入參數:
u 如果輸入參數采用“指針傳遞”,那麽加const修飾可以防止意外(wài)地改動該指針,起到保護作用。
例如StringCopy函數:
void StringCopy(char *strDestination, const char *strSource);
其中(zhōng)strSource是輸入參數,strDestination是輸出參數。給strSource加上const修飾後,如果函數體(tǐ)内的語句試圖改動strSource的内容,編譯器将指出錯誤。
u 如果輸入參數采用“值傳遞”,由于函數将自動産生(shēng)臨時變量用于複制該參數,該輸入參數本來就無需保護,所以不要加const修飾。
例如不要将函數void Func1(int x) 寫成void Func1(const int x)。同理不要将函數void Func2(A a) 寫成void Func2(const A a)。其中(zhōng)A爲用戶自定義的數據類型。
u 對于非内部數據類型的參數而言,象void Func(A a) 這樣聲明的函數注定效率比較底。因爲函數體(tǐ)内将産生(shēng)A類型的臨時對象用于複制參數a,而臨時對象的構造、複制、析構過程都将消耗時間。
爲了提高效率,可以将函數聲明改爲void Func(A &a),因爲“引用傳遞”僅借用一(yī)下(xià)參數的别名而已,不需要産生(shēng)臨時對象。但是函數void Func(A &a) 存在一(yī)個缺點:“引用傳遞”有可能改變參數a,這是我(wǒ)(wǒ)們不期望的。解決這個問題很容易,加const修飾即可,因此函數最終成爲void Func(const A &a)。
以此類推,是否應将void Func(int x) 改寫爲void Func(const int &x),以便提高效率?完全沒有必要,因爲内部數據類型的參數不存在構造、析構的過程,而複制也非常快,“值傳遞”和“引用傳遞”的效率幾乎相當。
問題是如此的纏綿,我(wǒ)(wǒ)隻好将“const &”修飾輸入參數的用法總結一(yī)下(xià),如下(xià)表:
對于非内部數據類型的輸入參數,應該将“值傳遞”的方式改爲“const引用傳遞”,目的是提高效率。例如将void Func(A a) 改爲void Func(const A &a)。
對于内部數據類型的輸入參數,不要将“值傳遞”的方式改爲“const引用傳遞”。否則既達不到提高效率的目的,又(yòu)降低了函數的可理解性。例如void Func(int x) 不應該改爲void Func(const int &x)。
“const &”修飾輸入參數的規則
11.1.2 用const修飾函數的返回值
u 如果給以“指針傳遞”方式的函數返回值加const修飾,那麽函數返回值(即指針)的内容不能被修改,該返回值隻能被賦給加const修飾的同類型指針。
例如函數
const char * GetString(void);
如下(xià)語句将出現編譯錯誤:
char *str = GetString();
正确的用法是
const char *str = GetString();
u 如果函數返回值采用“值傳遞方式”,由于函數會把返回值複制到外(wài)部臨時的存儲單元中(zhōng),加const修飾沒有任何價值。
例如不要把函數int GetInt(void) 寫成const int GetInt(void)。
同理不要把函數A GetA(void) 寫成const A GetA(void),其中(zhōng)A爲用戶自定義的數據類型。
如果返回值不是内部數據類型,将函數A GetA(void) 改寫爲const A & GetA(void)的确能提高效率。但此時千萬千萬要小(xiǎo)心,一(yī)定要搞清楚函數究竟是想返回一(yī)個對象的“拷貝”還是僅返回“别名”就可以了,否則程序會出錯。見6.2節“返回值的規則”。
u 函數返回值采用“引用傳遞”的場合并不多,這種方式一(yī)般隻出現在類的賦值函數中(zhōng),目的是爲了實現鏈式表達。
例如
class A
{…
A & operate = (const A &other); // 賦值函數
};
A a, b, c; // a, b, c 爲A的對象
…
a = b = c; // 正常的鏈式賦值
(a = b) = c; // 不正常的鏈式賦值,但合法
如果将賦值函數的返回值加const修飾,那麽該返回值的内容不允許被改動。上例中(zhōng),語句 a = b = c仍然正确,但是語句 (a = b) = c 則是非法的。
11.1.3 const成員(yuán)函數
任何不會修改數據成員(yuán)的函數都應該聲明爲const類型。如果在編寫const成員(yuán)函數時,不慎修改了數據成員(yuán),或者調用了其它非const成員(yuán)函數,編譯器将指出錯誤,這無疑會提高程序的健壯性。
以下(xià)程序中(zhōng),類stack的成員(yuán)函數GetCount僅用于計數,從邏輯上講GetCount應當爲const函數。編譯器将指出GetCount函數中(zhōng)的錯誤。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const成員(yuán)函數
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 編譯錯誤,企圖修改數據成員(yuán)m_num
Pop(); // 編譯錯誤,企圖調用非const函數
return m_num;
}
const成員(yuán)函數的聲明看起來怪怪的:const關鍵字隻能放(fàng)在函數聲明的尾部,大(dà)概是因爲其它地方都已經被占用了。
4.11 BOOL的使用
1. 如果可能應使用int代替 BOOL作爲函數傳入參數。說明:原因有二,其一(yī)是BOOL參數值TRUE/FALSE的含義是非常模糊的,在調用時很難知(zhī)道該參數到底傳達的是什麽意思;其二是BOOL參數值不利于擴充。
2. bool占用一(yī)個字節,BOOL占用4個字節。所以在一(yī)個系統中(zhōng)不同模塊間進行通信或參數傳遞時應保證一(yī)緻,否則在某些情況下(xià)會出現緩沖區溢出問題。建議使用BOOL,因爲:a)BOOL是一(yī)個宏,便于移植;b)把bool變量作爲結構成員(yuán)時,因爲結構對齊的需要,不一(yī)定會節省空間。
3. 一(yī)個項目的開(kāi)發中(zhōng)是使用bool還是BOOL一(yī)定要統一(yī)。否則極易産生(shēng)緩沖區溢出。
4.12 提高程序的效率
程序的時間效率是指運行速度,空間效率是指程序占用内存或者外(wài)存的狀況。
全局效率是指站在整個系統的角度上考慮的效率,局部效率是指站在模塊或函數角度上考慮的效率。
1. 不要一(yī)味地追求程序的效率,應當在滿足正确性、可靠性、健壯性、可讀性等質量因素的前提下(xià),設法提高程序的效率。
2. 以提高程序的全局效率爲主,提高局部效率爲輔。
3. 在優化程序的效率時,應當先找出限制效率的“瓶頸”,不要在無關緊要之處優化。
4. 先優化數據結構和算法,再優化執行代碼。
5. 有時候時間效率和空間效率可能對立,此時應當分(fēn)析那個更重要,作出适當的折衷。例如多花費(fèi)一(yī)些内存來提高性能。
6. 不要追求緊湊的代碼,因爲緊湊的代碼并不一(yī)定能産生(shēng)高效的機器碼。
4.13 一(yī)些有益的建議
1.當心那些視覺上不易分(fēn)辨的操作符發生(shēng)書(shū)寫錯誤。
我(wǒ)(wǒ)們經常會把“==”誤寫成“=”,象“||”、“&&”、“<=”、“>=”這類符号也很容易發生(shēng)“丢1”失誤。然而編譯器卻不一(yī)定能自動指出這類錯誤。
2. 變量(指針、數組)被創建之後應當及時把它們初始化,以防止把未被初始化的變量當成右值使用。
3. 當心變量的初值、缺省值錯誤,或者精度不夠。
4. 當心數據類型轉換發生(shēng)錯誤。盡量使用顯式的數據類型轉換(讓人們知(zhī)道發生(shēng)了什麽事),避免讓編譯器輕悄悄地進行隐式的數據類型轉換。
5. 當心變量發生(shēng)上溢或下(xià)溢,數組的下(xià)标越界。
6. 當心忘記編寫錯誤處理程序,當心錯誤處理程序本身有誤。
7. 當心文件I/O有錯誤。
8. 避免編寫技巧性很高代碼。
9. 不要設計面面俱到、非常靈活的數據結構。
10. 代碼質量比較好,盡量複用它。但是不要修補很差勁的代碼,應當重新編寫。
11. 盡量使用标準庫函數,不要“發明”已經存在的庫函數。
12. 盡量不要使用與具體(tǐ)硬件或軟件環境關系密切的變量。
13. 把編譯器的選擇項設置爲最嚴格狀态。
14. 如果可能的話(huà),使用PC-Lint、LogiScope、BoundsChecker/SmartCheck(一(yī)種錯誤檢測工(gōng)具)、TrueTime(一(yī)種性能工(gōng)具)、TureCoverage(一(yī)種代碼覆蓋工(gōng)具)等工(gōng)具進行代碼審查。我(wǒ)(wǒ)這兒隻有BoundsChecker,如果誰有上面提到的工(gōng)具或其它編程工(gōng)程工(gōng)具請共享一(yī)下(xià)。
5 調試
1. 當我(wǒ)(wǒ)們發現一(yī)個BUG後,應該先嘗試用多種方法複制這一(yī)錯誤,然後再開(kāi)始調試。以多種方式複制錯誤以保證你看到的是它本身,而不是多個錯誤相互掩蓋和混合。如果繁重的調試工(gōng)作以前,先進行“創見性的思考”,則情況會好些。
2. 一(yī)般情況下(xià)(特殊情況除外(wài)),邊編碼邊測試,在代碼覆蓋率達到80%以前提交給質量管理部是一(yī)種不負責任的做法。
3. 進行項目開(kāi)發時,若項目被劃分(fēn)成若幹個二進制模塊,把開(kāi)發時使用的編譯鏈接設置設置成完全一(yī)樣(尤其是其中(zhōng)的某些關鍵項)的是有利的,這能避免一(yī)些難以調試的莫名其妙的問題。
4. 把警告設置成最高級别且設置Warnings as errors,并把每一(yī)個警告消滅掉是一(yī)個非常好的習慣。很多程序在VC的3級警告下(xià)甚至還有幾個警告,很難讓人相信這是一(yī)個質量合格的程序。以下(xià)是摘選代碼,它們對警告設置進行操作,在編譯選項中(zhōng)已經設置成最高級别4級(以下(xià)示例均以VC6.0爲例):
a)某些庫函數的頭文件在最高警告級别下(xià)編譯出來警告較多,以下(xià)爲處理方式
……
#pragma warning(push, 3)
#include
#pragma warning(pop)
….
b)有未使用的函數參數但以後會用到,或是系統提供的函數的參數目前未用
……
UNREFERENCED_PARAMETER(hModule);
UNREFERENCED_PARAMETER(lpReserved);
……
c)對某一(yī)程序段有類或幾類警告我(wǒ)(wǒ)們認爲是合理的,但要用以下(xià)方式進行顯式處理
……
#pragma warning(push)
#pragma warning(disable:4706)
……
#pragma warning(pop)
…
相關警告的更多用法和解釋說明,請參考<MSDN>。
5. 在基于MFC的開(kāi)發中(zhōng),派生(shēng)自CObject的類都會繼承下(xià)來AssertValid和Dump兩個虛函數。這兩個函數是MFC的内嵌調試函數。
a) AssertValid()。是一(yī)個虛成員(yuán)函數,檢查傳給它的類對象中(zhōng)的每個成員(yuán),從而判斷該對象是否合法。直接使用AssertValid存在兩個問題:其一(yī)是隻有定義了_DEBUG之後,AssertValid才存在;其二是因爲AsserValid是虛成員(yuán)函數。使用非法指針調用虛拟成員(yuán)函數總是緻命的錯誤。使用ASSERT_VALID宏可以解決這兩個問題。
b) Dump()。也是一(yī)個虛成員(yuán)函數,當一(yī)個對象存在時該函數提供給程序員(yuán)盡可能多的信息,但必須準備好輸出。MFC會傾盡所有已知(zhī)的關于出錯對象的信息,僅僅一(yī)個對象的信息就有幾十行。調用的方式可爲Dump(AfxDump)。
值得一(yī)提的是要利用這兩個函數,需要我(wǒ)(wǒ)們在自己的類中(zhōng)進行重載。把自己想檢查的内容和想輸出的内容寫成代碼,這樣才有實用性。例如:
void CTgetsView::AssertValid() const
{
CView::AssertValid();
ASSERT_VALID(&m_cMousePositions);
ASSERT_VALID(&m_cMouseButtons);
ASSERT_VALID(m_cMousePositions.GetSize() == m_cMouseButtons.GetSize());
for(int i = 0; i < m_cMousePositions.GetSize(); i++)
{
...;
ASSERT(...);
}
}
void CTgetsView::Dump(CDumpContext& dc) const
{
dc << ' Dump of CTgetsView@' << (void*)this << ' ';
dc << 'm_nPrevIndex = ' << m_nPrevIndex << ' ';
CView::Dump(dc);
}
6. CMemoryState是MFC的内存洩漏調試類。有興趣的話(huà)可以學習并應用一(yī)下(xià)。例如:
CMemoryState msObject;
//…Code Continues…
msObject.CheckPoint(); //update the msObject
//…Keep on tracking…
msObject.DumpAllObjectsSince();
Dump()現在被限定在CheckPoint()調用後的範圍内。可以用Difference()比較一(yī)下(xià)兩個檢查CmemoryState對象的區别。
7. 很多内容隻能點到爲止,因爲a)每一(yī)個主題都可以闡述成某個書(shū)籍一(yī)章,而這方面的書(shū)籍和資(zī)料多得很且都比要我(wǒ)(wǒ)認識的要全面和深刻,好多内容我(wǒ)(wǒ)也是東拼西湊出來的。;b)我(wǒ)(wǒ)揀了一(yī)點有些書(shū)籍不太注意的細節進行了較詳細的解釋和說明,比如有關警告的部分(fēn);c)好多内容我(wǒ)(wǒ)也隻能用到了或看書(shū)時才會想到。d)本文的任務主要是對編程過程中(zhōng)的一(yī)些易忽視的方面引起大(dà)家的重視,“勿以善小(xiǎo)而不爲”,否則不管是自己或他人維護你的程序是非常困難的。
8. 對本文的某些觀點大(dà)家同意,以後的工(gōng)作中(zhōng)可以多加注意。如果認爲沒必要,則可以置之不理。但若發現有任何錯誤或不當之處請一(yī)定要告訴我(wǒ)(wǒ)。
9. 謝謝大(dà)家能有耐心看完這篇文章!
6 其它
1. 使用MFC向導創建的MFC 共享方式的DLL工(gōng)程中(zhōng)編譯設置中(zhōng),兩個宏_MBCS,_USRDLL默認中(zhōng)有的,但使用RESET功能後會沒有,沒有_USRDLL會導緻問題,所以要手動加上。這應該是MS的一(yī)個BUG。
2. 以下(xià)是ScanMain工(gōng)程中(zhōng)出現的一(yī)個鏈接沖突的問題描述及解決方法,解決的途徑是修改預編譯頭文件的設置。在這個工(gōng)程中(zhōng)的StdAfx.h文件中(zhōng)也有相應的文字說明。
關于編程規範,大(dà)家可以參考編程規範。本文的内容是我(wǒ)(wǒ)認爲編程規範需要強調的和一(yī)些個人的編程經驗。由于C++/C編程是衆所周知(zhī)的技術,沒有秘密可言。編程的好經驗應該大(dà)家共享,若有自認爲好的編程習慣或方法請告訴我(wǒ)(wǒ),我(wǒ)(wǒ)會收錄到這篇文章或後續的文章中(zhōng)。謝謝!
如果有時間,我(wǒ)(wǒ)也願意再繼續寫一(yī)些東西。
因爲個人的水平有限,所以會存在錯誤或不當之處。如果有任何問題請與我(wǒ)(wǒ)聯系。
對文中(zhōng)提到的<編程規範>,指<XXXX>和<XX編程規範>。
1 引言
軟件質量是被大(dà)多數程序員(yuán)挂在嘴上而不是放(fàng)在心上的東西!
作爲軟件工(gōng)程師,我(wǒ)(wǒ)們首先要樹(shù)立質量和責任意識。我(wǒ)(wǒ)認爲在公司現有的開(kāi)發流程下(xià)(質量管理部主要做功能測試)測試最終軟件産品的質量主要取決于開(kāi)發人員(yuán)。也可以說,産品出現BUG的主要責任應由開(kāi)發人員(yuán)來承擔。這不是爲測試工(gōng)程師和其他人員(yuán)開(kāi)脫責任,而是說我(wǒ)(wǒ)們一(yī)定要有這個意識。
編程質量差往往是由于不良習慣造成的,與人的智力、能力沒有多大(dà)關系。
關于C++/C編程風格,難度不高,但是細節比較多。别小(xiǎo)看了,提高質量就是要從這些點點滴滴做起。世上不存在最好的編程風格,一(yī)切因需求而定。團隊開(kāi)發講究風格一(yī)緻,如果制定了大(dà)家認可的編程風格,那麽所有組員(yuán)都要遵守。如果你覺得某些編程風格比較合你的工(gōng)作,那麽就采用它,不要隻看不做。人在小(xiǎo)時候說話(huà)發音不準,寫字潦草,如果不改正,總有後悔的時候。編程也是同樣道理。
與代碼質量的相關的重要因素有:
l 設計文檔
l 編程風格
l 編程經驗
l 編程語言
l 編程工(gōng)具
l 操作系統
l 錯誤處理
l 安全處理
參考資(zī)料:
<高質量C++編程指南(nán)>
<應用程序調試技術>
<C語言編程常見問題解答>
<Effective C++中(zhōng)文版>
<C++ Primer>
...
2 注意注釋
作爲軟件工(gōng)程師,我(wǒ)(wǒ)們的工(gōng)作是雙重的,即爲用戶開(kāi)發解決方案,同是還要使該解決方案在将來是可維護的。使我(wǒ)(wǒ)們編寫的代碼是可維護的唯一(yī)方法是對代碼進行注釋。對于“注釋”,我(wǒ)(wǒ)的意思不隻是簡單地編寫代碼功用的注釋,而是将假設、方法以及選擇所使用的方法的原因寫進文件。同時,注釋必需與代碼相符。通常情況下(xià),當維護程序設計者們試圖更新那些功用與注釋描述的功能不一(yī)緻的代碼時,工(gōng)作效率往往是極低的。
對于關于注釋的規範,請參見<編程規範>。
2.1 單入口單出口原則
結構化程序設計技術的定義是結構化程序設計技術是一(yī)種程序設計技術,它采用自頂向下(xià)逐步求精的設計方法和單入口單出口的控制結構,并且隻包含順序、選擇和循環三種結構。結構化程序設計的核心思想是Algorithms+Data Structures=Programs。結構化程序設計雖然不是最好的的程序設計方法并且好像已經過時。我(wǒ)(wǒ)認爲結構化程序設計應該和其它的程序設計技術結合起來使用。但對于它的單入口和單出口原則無論是在詳細設計和編碼階段都應該堅決貫徹的。
2.1.1 詳細設計
在詳細設計階段基本上确定了程序的數據和控制的流向。在進行詳細設計時一(yī)定要确保程序執行相同的功能調用的函數和順序是一(yī)緻的,也就是單入口和單出口,這有利于程序的實現、測試和維護。另外(wài),在程序維護時也要确保不要爲了增加某個功能而另外(wài)建立一(yī)條數據流通道,而是要仔細分(fēn)析是否可利用現有的入口和出口。
有關例子此處不列舉了,但相信每一(yī)個編程老手都會受到在程序結構不是單入口和單出口的問題的困擾。
2.1.2 函數設計
2.1.2.1 函數隻有一(yī)個出口
以下(xià)是從WebKeeper中(zhōng)摘錄的一(yī)段代碼,爲了保證函數隻有一(yī)個出口,利用了tryfinally終止處理結構,這避免了函數内部的多個return語句。試想一(yī)下(xià),如果改用return或其它方式來改寫以下(xià)的模塊,會是多麽複雜(zá)和難以維護。假設函數體(tǐ)中(zhōng)再有對同步量的占有和釋放(fàng)過程,則擁有多個出口的函數将是更難編寫和維護。
__try
{
//創建目錄内存映射文件
{
HANDLE hFileMap = NULL;
……
g_hDir = CreateFileW(lpszFileName, ….);
if(INVALID_HANDLE_VALUE == g_hDir)
__leave;
if((GetFileSize(g_hDir, NULL) == 0)
&& (AddFileHeader(g_hDir) != ERROR_SUCCESS))
__leave;
if(!(hFileMap = CreateFileMapping(g_hDir, …, 0, 0, NULL)))
__leave;
if(!(g_pDir = (char*)MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0)))
__leave;
CloseHandle(hFileMap);
}
//創建索引内存映射文件
{
…
}
//創建數據内存映射文件
{
}
g_bLoaded = TRUE;
}
__finally
{
if(!g_bLoaded)
CloseGlobalObject();
else if(IsBadBackupLib())
RepairBackupLib();
}
2.1.2.2 語句塊隻有一(yī)個出口
程序流應該清晰,避免使用goto語句和其它(比如break,continue)跳轉語句
例如:
for(int a = 0; a < 100; a++)
{
Func1(a);
if(a == 2)
continue;
Func2(a);
}
它可以被改寫成如下(xià)的形式:
for(int a = 0; a < 100; a++)
{
Func1(a);
if(a != 2)
Func2(a);
}
這段程序更易于調試,因爲循環體(tǐ)内的代碼清楚地顯示了應該執行和不應該執行什麽。假設現在要加入一(yī)些在每次循環的最後都要被執行的代碼,在第一(yī)段程序中(zhōng)如果維護者注意到了continue語句,就不得不對這段程序做的修改;如果沒有注意到continue語句,那麽恐怕就要犯一(yī)個難以發現的錯誤了。在第二段代碼中(zhōng),要做的修改很簡單,隻需把新的代碼加到循環體(tǐ)的末尾。
當使用break語句時,可能會發生(shēng)另外(wài)兩種錯誤。
for(int a = 0; a < 100; a++)
{
if(Func1(a) == 2)
break;
Func2(a);
}
假設Func1()的返回值永遠不會等于2,上面循環就會從1進行到100;反之,循環在到達100以前就會結束。假設因維護的需要要在循環體(tǐ)中(zhōng)加入代碼,一(yī)種危險是我(wǒ)(wǒ)們可能認爲它确實能從0循環到99;另一(yī)種危險可能來自對a值的使用,因爲當循環結束後,a的值并不一(yī)定就是100。
但我(wǒ)(wǒ)們可能按以下(xià)形式編寫這個for循環:
for(a = 0; (a < 100) && (Func1(a) != 2); a++)
這樣當維護這段代碼時就很難犯前面的那些錯誤了。
2.1.2.3 多個出口還可能導緻程序代碼膨脹
内聯析構函數可能是程序代碼膨脹的一(yī)個源泉,因爲它被插入到函數中(zhōng)的每個退出點,以析構每一(yī)個活動的局部類對象。《C++Primer 3 P579》。
3 命名規則
比較著名的命名規則當推Microsoft公司的“匈牙利”法,該命名規則的主要思想是“在變量和函數名中(zhōng)加入前綴以增進人們對程序的理解”。例如所有的字符變量均以ch爲前綴,若是指針變量則追加前綴p。如果一(yī)個變量由ppch開(kāi)頭,則表明它是指向字符指針的指針。
但是也沒有必要對每個變量的命名都用這個方法,在某些情況下(xià)單字符的名字也是有用的,常見的如i,j,k,m,n,x,y,z等,它們通常可用作函數内的局部變量。
1. 命名規則盡量與所采用的操作系統或開(kāi)發工(gōng)具的風格保持一(yī)緻。例如Windows應用程序的标識符通常采用“大(dà)小(xiǎo)寫”混排的方式,如AddChild。而Unix應用程序的标識符通常采用“小(xiǎo)寫加下(xià)劃線”的方式,如add_child。别把這兩類風格混在一(yī)起用。
2. 爲了防止某一(yī)軟件庫中(zhōng)的一(yī)些标識符和其它軟件庫中(zhōng)的沖突,可以爲各種标識符加上能反映軟件性質的前綴。例如三維圖形标準OpenGL的所有庫函數均以gl開(kāi)頭,所有常量(或宏定義)均以GL開(kāi)頭;又(yòu)如動态鏈接庫AutoRecover.dll的所有引出函數名均以ar開(kāi)頭。
4 雜(zá)項
4.1 new和delete操作符
4.1.1 盡量以new和delete取代malloc和free
原因很簡單:malloc和free函數對構造函數和析構函數一(yī)無所知(zhī)。
4.1.2 new
很多程序員(yuán)習慣這樣用new操作符:
#include
……
if(char* p = new char[100])
{
…;
}
這樣寫的目的是判斷分(fēn)配内存是否成功,然後針對分(fēn)配成功與否進行相應的處理。但這樣寫代碼達不到我(wǒ)(wǒ)們的目的。因爲當分(fēn)配不成功時是觸發異常(std::bad_alloc)而不是返回NULL(這依賴于編譯器和相關編譯設置),當分(fēn)配不成功的情況出現以後如果沒有異常處理将會彈出異常對話(huà)框,導緻程序終止運行。有關這個問題在很多書(shū)籍和資(zī)料中(zhōng)都認爲new在失敗後返回NULL,其實這種觀點是不正确的。
若是讓new在分(fēn)配失敗時返回NULL,可用如下(xià)形式:
#include
……
if(char* p = new (std::nothrow) char[100])
{
…;
}
說明:在用VC6.0的main和WinMain項目時在DEBUG和RELEASE兩種方式都沒有問題。在MFC的項目下(xià)使用時在RELEASE下(xià)編譯和運行都沒有問題,但在DEBUG下(xià)編譯通不過。通過研究發現在源文件的開(kāi)頭有這樣幾行代碼:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
原來是DEBUG環境下(xià)已經把new定義成了DEBUG_NEW,問題就出在這裏。通過進一(yī)步的研究發現在AFX.H中(zhōng)有如下(xià)代碼:
// Memory tracking allocation
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#define DEBUG_NEW new(THIS_FILE, __LINE__)
原來是爲了内存跟蹤和保存調試信息,AFX小(xiǎo)組已經把new進行了重載,而這個重載過的new是不支持nothrow的。難道是AFX開(kāi)發小(xiǎo)組忽略了與C++标準的兼容嗎(ma)?也許是,但也許這樣做也可能是該小(xiǎo)組成員(yuán)認爲這樣可以在DEBUG環境下(xià)更快的發現問題,而不允許使用new的nothrow版本。
其實CObject類也把new進行了重載了三個版本,但沒有重載nothrow版本。這說明了分(fēn)配一(yī)個以CObject爲基類的時,不能使用nothrow版本,隻能使用抛出異常版本。以下(xià)是CObject的聲明片斷:
class CObject
{
public:
……
// Diagnostic allocations
void* PASCAL operator new(size_t nSize);
void* PASCAL operator new(size_t, void* p);
void PASCAL operator delete(void* p);
#if _MSC_VER >= 1200
void PASCAL operator delete(void* p, void* pPlace);
#endif
#if defined(_DEBUG) && !defined(_AFX_NO_DEBUG_CRT)
// for file name/line number tracking using DEBUG_NEW
void* PASCAL operator new(size_t nSize, LPCSTR lpszFileName, int nLine);
#if _MSC_VER >= 1200
void PASCAL operator delete(void *p, LPCSTR lpszFileName, int nLine);
#endif
#endif
……
};
不論使用正常形式(抛出exception)的new,或是“nothrow”形式的new,重要的是我(wǒ)(wǒ)們已經準備好處理内存配置失敗的情況了。最簡單的辦法就是利用set_new_handler,因爲它在兩種形式中(zhōng)都可用。
4.1.3 delete
delete的作用是确保每個一(yī)對象的析構函數被調用。且new[]和delete[]一(yī)定要成對使用。
例如:
…
CObject* pObj = new CObject[5];
…
delete oObj;
這段代碼能夠把所分(fēn)配的内存全部釋放(fàng)掉,不會出現内存洩漏問題。但隻調用了第一(yī)個對象的析構函數,而不是全部的5個。對基本類型(比如分(fēn)配字符串數組),沒有析構函數可調用,但爲了規範和維護的不出問題,也一(yī)定要保證delete和new的形式要匹配。
4.2 邊編碼邊調試
被動式的程序設計是提示有錯誤出現的錯誤處理代碼,而主動式的程序設計則會告訴我(wǒ)(wǒ)們錯誤出現的原因。被動式的編碼隻是修改和解決錯誤工(gōng)作的一(yī)部分(fēn)。軟件工(gōng)程師們通常會努力制定出明确的被動策略,例如,校驗指向某一(yī)字符串的指針不是NULL,但是通常不采納主動式程序設計所要示的另外(wài)一(yī)個步驟,即檢查這個相同的參數,查看是否有足夠的内存來支持字符串所允許的最大(dà)字符數。主動式程序設計同時這意味着在編寫代碼的同時,查找存在問題的部分(fēn),因此編碼一(yī)開(kāi)始,調試過程也就開(kāi)始了。
實際上,錯誤并不會無緣無故地出現在代碼中(zhōng)。其原因實際上是我(wǒ)(wǒ)們在編寫代碼的同時也輸入了錯誤,而且這些令人讨厭(yàn)的錯誤可以來源于任何地方。但是,必需接受這樣一(yī)個事實:錯誤是由人爲引起的。
由于開(kāi)發者要對代碼中(zhōng)的任何錯誤負責,問題就成了找出一(yī)種方法來創建一(yī)個檢查和對比的系統,以便在編碼的過程中(zhōng)找出錯誤。這種方法稱爲“信任,但要校驗”。爲了避免錯誤我(wǒ)(wǒ)檢驗其它開(kāi)發者傳送到我(wǒ)(wǒ)編寫的代碼中(zhōng)的數據、校驗我(wǒ)(wǒ)編寫的代碼的内部處理功能、校驗我(wǒ)(wǒ)在代碼中(zhōng)所作的任何假設、校驗我(wǒ)(wǒ)編寫的代碼傳送給他人的數據、并且校驗我(wǒ)(wǒ)編寫的代碼進行的調用所返回的數據。
需要強調軟件開(kāi)發哲學中(zhōng)的一(yī)個重要原則:代碼質量僅是軟件開(kāi)發工(gōng)程師的責任,而不是測試工(gōng)程師、技術資(zī)料編寫者或管理者的責任。
4.2.1 聲明
聲明有很多函數,大(dà)家可以先用自己喜歡的一(yī)個或幾個。例如ASSERT、assert、_ASSERT、_ASSERTE、ASSERT_KINDOF、ASSERT_VALID和VERIFY。
使用聲明除了能幫助我(wǒ)(wǒ)們更快地發現問題并定位BUG點以外(wài)還有一(yī)個附加的好處:就是它起到了注釋的作用,幫助自己以後或其他維護工(gōng)程師更快更清楚的知(zhī)道該函數的假設條件。
使用聲明的注意事項:
1.聲明的第一(yī)個原則是每次隻檢查單獨的一(yī)項。如果隻用一(yī)個聲明檢查多個條件,我(wǒ)(wǒ)們将無法知(zhī)道故障是哪個條件造成的。
2. 聲明一(yī)個條件時,需要盡可能全面地檢查該條件。
C/C++和API提供了各種各樣的函數幫助我(wǒ)(wǒ)們盡可能以描述性的語言編寫聲明。
a) GetObjectType
b) IsBadCodePtr
c) IsBadReadPtr
d) IsBadStringPtr
e) IsBadWritePtr
f) IsWindow
g) AfxIsValidAddress
h) AfxIsMemoryBlock
i) AfxIsValidString
注意:IsBadStringPtrt和IsBadWritePtr不是線程安全的。詳見。
3. 需要聲明所有出現在函數中(zhōng)的參數。對于接口函數和開(kāi)發組中(zhōng)其他成員(yuán)調用的公用類來說,聲明參數是尤其重要的。因爲這是進入自己所編寫代碼的入口點,需要确保各個參數和假設是有效的。進入到自己的模塊中(zhōng)以後,對模塊的專用函數的參數不用進行太多的檢查,因爲問題主要取決于參數的起源地。隻需一(yī)些經驗即可判斷需要聲明哪些參數。
4. 另一(yī)個經常使用聲明的地方,是在普通的處理流程中(zhōng),調用的函數的返回值。
5. 使用聲明的最後一(yī)個地方是需要對假設進行檢查的地方。例如,如果某一(yī)函數需要3MB的磁盤空間,就使用一(yī)個聲明檢查該假設。另一(yī)個例子是,如果函數使用了一(yī)個指向某一(yī)特定數據結構的指針數組,應遍曆該數據結構,驗證每個獨立的項都是有效的。
6. 對每一(yī)個聲明還要進行if語句判斷,聲明不能代替判斷。
一(yī)個使用聲明的代碼片段:
BOOL BUGSUTIL_DLLINTERFACE __stdcall
HookOrdinalExport ( HMODULE hModule ,
LPCTSTR szImportMod ,
DWORD dwOrdinal ,
PROC pHookFunc ,
PROC * ppOrigAddr )
{
ASSERT ( NULL != hModule ) ;
ASSERT ( FALSE == IsBadStringPtr ( szImportMod , MAX_PATH ) ) ;
ASSERT ( 0 != dwOrdinal ) ;
ASSERT ( FALSE == IsBadCodePtr ( pHookFunc ) ) ;
// Perform the error checking for the parameters.
if ( ( NULL == hModule ) ||
( TRUE == IsBadStringPtr ( szImportMod , MAX_PATH ) ) ||
( 0 == dwOrdinal ) ||
( TRUE == IsBadCodePtr ( pHookFunc ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
if ( NULL != ppOrigAddr )
{
ASSERT ( FALSE ==
IsBadWritePtr ( ppOrigAddr , sizeof ( PROC ) ) ) ;
if ( TRUE == IsBadWritePtr ( ppOrigAddr , sizeof ( PROC ) ) )
{
SetLastErrorEx ( ERROR_INVALID_PARAMETER , SLE_ERROR ) ;
return ( FALSE ) ;
}
}
}
4.3 線程
一(yī)般情況下(xià)創建線程應該用_beginthreadex而不是CreateThread,因爲後一(yī)個函數爲API,當線程終結時在大(dà)多數情況下(xià)會有内存和資(zī)源洩漏。
隻有在萬不得已的情況下(xià)才用TerminateThread,但一(yī)定要注意,除了内存和資(zī)源會釋放(fàng)不了以外(wài),還可能會導緻某些同步量釋放(fàng)不了。經過測試被TerminateThread掉線程占有的臨界段(CRITICAL_SECTION)和事件(Event)釋放(fàng)不了,但互斥量(Mutex)能夠釋放(fàng)。但不管怎樣,殺掉線程以前知(zhī)道将要發生(shēng)的結果是有益的。詳見<MSDN>關于TerminateThread的說明。
4.4 MFC的數據類
CString、CList和CArray等最好隻在一(yī)個模塊内部使用,若在不同模塊間進行通信時的接口參數或結構中(zhōng)的成員(yuán)是這些類型則很可能發生(shēng)内存(運行不正常)問題。原因是對這些數據類的賦值等操作都伴随着内存的釋放(fàng)和分(fēn)配等操作,問題就轉化成了在一(yī)個模塊分(fēn)配内存而在另一(yī)個模塊釋放(fàng)。而在一(yī)個二進制模塊分(fēn)配内存又(yòu)在另一(yī)個二進制模塊釋放(fàng)内存是我(wǒ)(wǒ)們要盡量避免的。原因是:在一(yī)個項目的不同模塊中(zhōng)分(fēn)别鏈接不同的C運行庫是可以的,但不同的C運行庫對内存的處理是不一(yī)樣的(比如RELEASE和DEBUG,MultiThreaded和MultiThreaded DLL等)。這就是在一(yī)個模塊中(zhōng)分(fēn)配的内存而在另一(yī)個模塊釋放(fàng)是不安全的原因,也是我(wǒ)(wǒ)們要統一(yī)各個模塊的編譯設置的原因之一(yī)。
另外(wài),在結構中(zhōng)也最好不要使用這些類作爲成員(yuán)。因爲有些程序員(yuán)爲了操作方便和認爲這樣不會浪費(fèi)空間而習慣這樣做。不要讓它們作爲結構成員(yuán)的原因之一(yī)是,因爲要常常利用結構對象進行模塊間的通信。原因之二是我(wǒ)(wǒ)們常常把對其他結構慣用的一(yī)些操作應用到采用了CString等作爲成員(yuán)之一(yī)的結構上面,例如賦值、内存拷貝等。因爲CString等數據成員(yuán)隻是一(yī)個指針,而它們的析構函數一(yī)般都要進行内存釋放(fàng)操作所以會非常容易造成對一(yī)塊内存釋放(fàng)兩次的内存錯誤。當然,第二種錯誤可以通過重載賦值操作符和拷貝構造函數來避免。但爲什麽不用其他方法替代呢?現在的程序運行環境下(xià),空間對我(wǒ)(wǒ)們來說非常重要嗎(ma)?使用CString一(yī)定比char [MAX_PATH]節省空間嗎(ma),不一(yī)定,這要視具體(tǐ)情況而定,因爲每一(yī)個new操作都會占用額外(wài)的空間(大(dà)概至少是12-32字節,這和具體(tǐ)的庫實現版本有關),當然也要多花費(fèi)一(yī)定的時間。這樣做了至少造成兩個後果:a)這樣開(kāi)發出來的産品運行易出錯;b)難于開(kāi)發、調試和維護。
4.5 表達式和基本語句
1. 如果代碼行中(zhōng)的運算符比較多,用括号确定表達式的操作順序,避免使用默認的優先級。
由于将運算符的優先級熟記是比較困難的,爲了防止産生(shēng)歧義并提高可讀性,應當用括号确定表達式的操作順序。例如:
word = (high << 8) | low
if ((a | b) && (a & c))
2.不可将布爾變量直接與TRUE、FALSE或者1、0進行比較。
根據布爾類型的語義,零值爲“假”(記爲FALSE),任何非零值都是“真”(記爲TRUE)。TRUE的值究竟是什麽并沒有統一(yī)的标準。例如Visual C++ 将TRUE定義爲1,而Visual Basic則将TRUE定義爲-1。
假設布爾變量名字爲flag,它與零值比較的标準if語句如下(xià):
if (flag) // 表示flag爲真
if (!flag) // 表示flag爲假
其它的用法都屬于不良風格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)
3.if語句
有時候我(wǒ)(wǒ)們可能會看到 if (NULL == p) 這樣古怪的格式。不是程序寫錯了,是程序員(yuán)爲了防止将 if (p == NULL) 誤寫成 if (p = NULL),而有意把p和NULL颠倒。編譯器認爲 if (p = NULL) 是合法的,但是會指出 if (NULL = p)是錯誤的,因爲NULL不能被賦值。
把if(p == NULL)誤寫成if(p = NULL)的情形是很可能發生(shēng)的,也可能成爲一(yī)個難以發現的BUG。依賴編譯器也許能夠發現這一(yī)點,比如把VC的警告設置成4級,就可以發現這個問題,但你得有這樣的習慣。總而言之,if(NULL == p)這樣的寫法應是我(wǒ)(wǒ)們提倡的。
4. case語句的結尾不要忘了加break,否則将導緻多個分(fēn)支重疊(除非有意使多個分(fēn)支重疊)。
5. 不要忘記最後那個default分(fēn)支。即使程序真的不需要default處理,也應該保留語句 default : break; 這樣做并非多此一(yī)舉,而是爲了防止别人誤以爲你忘了default處理。
6. 盡管switch語句的最後一(yī)個分(fēn)支不一(yī)定需要break語句,但最好還是在switch的每個分(fēn)支後面加上break語句,包括最後一(yī)個分(fēn)支。這樣做的原因是:程序很可能要讓另一(yī)個來維護,他可能要增加一(yī)些新的分(fēn)支,但沒有注意到最後一(yī)個分(fēn)支沒有break語句,結果使原來的最後一(yī)個分(fēn)支受到其後新增分(fēn)支的幹擾而失效。在每個分(fēn)支後面加上break語句将防止發生(shēng)這種錯誤并增強程序的安全性。此外(wài),目前大(dà)多數優化編譯程序都會忽略最後一(yī)條break語句,所以加入這條語句不會影響程序的性能。
7. for語句
可能的情況下(xià),要用某一(yī)标志(zhì)而不是固定的次數作爲循環終止條件。這有利于程序的擴展和維護。例如,爲了遍曆某一(yī)個結構數組(最後一(yī)個元素全0或結構中(zhōng)的重要值是0代表結構數組的終結)則終止條件可爲判斷相應值是否标志(zhì)結束。這要比用固定次數爲終止條件有更好的擴展和維護性能。
爲了保證for語句隻有一(yī)個出口,不要在循環體(tǐ)内部使用跳轉(continue,break)語句。
4.6 使用const常量
1. C++ 程序中(zhōng)隻使用const常量而不使用宏常量,即const常量完全取代宏常量。
2. 如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體(tǐ)内被意外(wài)修改。例如char *strcpy( char *strDestination, const char *strSource )。
這樣做還會有附加的好處,例如:
{
char szNewFileName[MAX_PATH];
CString strOldFileName;
strOldFileName = “abc.doc”;
strcpy(szNewFileName, strOldFileName);
}
但若strcpy的原型是char *strcpy( char *strDestination, char *strSource ),則要完成類似功能需編寫如下(xià)代碼:
{
char szNewFileName[MAX_PATH];
CString strOldFileName;
strOldFileName = “abc.doc”;
strOldFileName.GetBuffer(strOldFileName.GetLength());
strcpy(szNewFileName, strOldFileName);
strOldFileName.ReleaseBuffer();
}
3. 建議對函數内部不進行修改的所有參數聲明爲const。這有利于編譯器幫助我(wǒ)(wǒ)們發現錯誤,并可生(shēng)成效率稍高的二進制文件。
例如:假如strcpy函數内部實現若不更改指針值則可聲明爲:char *strcpy( char *const strDestination, const char * const strSource ),但C函數庫的聲明不是這樣的,這也說明了在這個函數實現中(zhōng)應該對這兩個指針值進行了更改。以下(xià)是該函數的一(yī)種可能的實現方式:
char *strcpy(char *strDest, const char *strSrc);
{
assert(strDest!=NULL);
assert(strSrc !=NULL);
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘0’ );
return address;
}
請大(dà)家注意:const char* const psz這樣寫法的含義,并習慣這樣的寫法。
4. 如果輸入參數以值傳遞的方式傳遞對象,則宜改用“const &”方式來傳遞,這樣可以省去(qù)臨時對象的構造和析構過程,從而提高效率。
4.7 函數設計
1. 對C++程序而言,函數内部的局部變量應該最近定義。這會使程序看起來更直觀,同時也方便維護和排錯。
2. 避免函數有太多的參數,參數個數盡量控制在5個以内。如果參數太多,在使用時容易将參數類型或順序搞錯。
3. 在函數體(tǐ)的“入口處”,對參數的有效性進行檢查。
很多程序錯誤是由非法參數引起的,我(wǒ)(wǒ)們應該充分(fēn)理解并正确使用“斷言”(assert)來防止此類錯誤。詳見“邊編碼邊調試”。
4. 在函數體(tǐ)的“出口處”,對return語句的正确性和效率進行檢查。
如果函數有返回值,那麽函數的“出口處”是return語句。我(wǒ)(wǒ)們不要輕視return語句。如果return語句寫得不好,函數要麽出錯,要麽效率低下(xià)。
注意事項如下(xià):
(1)return語句不可返回指向“棧内存”的“指針”或者“引用”,因爲該内存在函數體(tǐ)結束時被自動銷毀。例如
char * Func(void)
{
char str[] = “hello world”; // str的内存位于棧上
…
return str; // 将導緻錯誤
}
(2)要搞清楚返回的究竟是“值”、“指針”還是“引用”。
(3)如果函數返回值是一(yī)個對象,要考慮return語句的效率。例如
return String(s1 + s2);
這是臨時對象的語法,表示“創建一(yī)個臨時對象并返回它”。不要以爲它與“先創建一(yī)個局部對象temp并返回它的結果”是等價的,如
String temp(s1 + s2);
return temp;
實質不然,上述代碼将發生(shēng)三件事。首先,temp對象被創建,同時完成初始化;然後拷貝構造函數把temp拷貝到保存返回值的外(wài)部存儲單元中(zhōng);最後,temp在函數結束時被銷毀(調用析構函數)。然而“創建一(yī)個臨時對象并返回它”的過程是不同的,編譯器直接把臨時對象創建并初始化在外(wài)部存儲單元中(zhōng),省去(qù)了拷貝和析構的化費(fèi),提高了效率。
類似地,我(wǒ)(wǒ)們不要将
return int(x + y); // 創建一(yī)個臨時變量并返回它
寫成
int temp = x + y;
return temp;
由于内部數據類型如int,float,double的變量不存在構造函數與析構函數,雖然該“臨時變量的語法”不會提高多少效率,但是程序更加簡潔易讀。
5. 僅要檢查輸入參數的有效性,還要檢查通過其它途徑進入函數體(tǐ)内的變量的有效性,例如全局變量、文件句柄等。
5.用于出錯處理的返回值一(yī)定要清楚,讓使用者不容易忽視或誤解錯誤情況。
4.8 使用安全的C/C++庫函數和Windows API
使用strcpy,strcat,sprintf,fgets等函數有緩沖區溢出的危險,每一(yī)個合格的程序員(yuán)都會非常謹慎地使用它們。但最好禁用這些函數。
爲了自己會在某一(yī)天忘記了這條約束,可在公共頭文件中(zhōng)進行如下(xià)定義:
#define strcpy Unsafe_strcpy
這樣如果使用了strcpy,編譯器會提醒我(wǒ)(wǒ)們。
可用strncpy版本的函數替代strcpy函數:
有如下(xià)的語句塊:
{
char szBuf[MAX_PATH];
strcpy(szBuf, lpszInput); //lpszInput爲用戶鍵盤輸入的字符串
}
上面的代碼是不安全的,可用如下(xià)語語句代替:
{
char szBuf[MAX_PATH];
strncpy(szBuf, lpszInput, sizeof(szBuf) / sizeof(szBuf[0]));
if(szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1])
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
應該用_snprintf代替sprintf,用法與strncpy有點不同,如下(xià):
{
char szBuf[MAX_PATH];
szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1] = 0;
_snprintf(szBuf, sizeof(szBuf) / sizeof(szBuf[0]), “%s%d”, lpszInout, 0x233323);
if(szBuf[sizeof(szBuf) / sizeof(szBuf[0]) – 1])
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
或
{
char szBuf[MAX_PATH];
if(_snprintf(szBuf, sizeof(szBuf) / sizeof(szBuf[0]), “%s%d”, lpszInout, 0x233323) < 0)
{
//szBuf緩沖區長度不夠,進行合适的處理
szBuf[0] = 0;
}
}
不安全的C/C++庫函數和API還有很多,大(dà)家可以多看一(yī)下(xià)這方面的書(shū)籍或資(zī)料。
CreateProcess,WinExec等使用不當也可能造成一(yī)些漏洞,參見<MSDN>。
4.9 杜絕“野指針”和“野句柄”
“野指針”不是NULL指針,是指向“垃圾”内存的指針。人們一(yī)般不會錯用NULL指針,因爲用if語句很容易判斷。但是“野指針”是很危險的,if語句對它不起作用。
在使用任何一(yī)個指針或句柄等前先進行判斷是非常有益的。否則你認爲目前的程序代碼中(zhōng)某個指針一(yī)定是有效且可用的,于是就沒有進行判斷而直接使用,但随着時間的推移和代碼量的增大(dà),在維護程序時你有很大(dà)的可能注意不到這一(yī)點,而出現一(yī)個難以發現的BUG。這也是改正了一(yī)個BUG而又(yòu)增加了一(yī)個或幾個BUG的原因之一(yī)。也有可能是自己沒有考慮到某一(yī)個邊界情況,而自認爲該指針或句柄等一(yī)定是有效的。出現了這種情況更難調試,有可能還要依靠測試工(gōng)程師的幫助。因爲這個邊界條件是很難重現的,外(wài)在表現是程序在絕大(dà)多數情況下(xià)運行正常,而某刻又(yòu)不正常。尋找這樣的BUG真是太困難了。爲什麽不在開(kāi)始寫代碼時就做嚴格的檢查呢?
“野指針”的成因主要有兩種:
(1)指針變量沒有被初始化。任何指針變量剛被創建時不會自動成爲NULL指針,它的缺省值是随機的,它會亂指一(yī)氣。所以,指針變量在創建的同時應當被初始化,要麽将指針設置爲NULL,要麽讓它指向合法的内存。例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指針p被free或者delete之後,沒有置爲NULL,讓人誤以爲p是個合法的指針。
(3)指針操作超越了變量的作用範圍。這種情況讓人防不勝防,示例程序如下(xià):
class A
{
public:
void Func(void){ cout << “Func of class A” << endl; }
};
void Test(void)
{
A *p;
{
A a;
p = &a; // 注意 a 的生(shēng)命期
}
p->Func(); // p是“野指針”
}
函數Test在執行語句p->Func()時,對象a已經消失,而p是指向a的,所以p就成了“野指針”。
4.10 使用const提高函數的健壯性
看到const關鍵字,C++程序員(yuán)首先想到的可能是const常量。這可不是良好的條件反射。如果隻知(zhī)道用const定義常量,那麽相當于把火(huǒ)藥僅用于制作鞭炮。const更大(dà)的魅力是它可以修飾函數的參數、返回值,甚至函數的定義體(tǐ)。
const是constant的縮寫,“恒定不變”的意思。被const修飾的東西都受到強制保護,可以預防意外(wài)的變動,能提高程序的健壯性。所以很多C++程序設計書(shū)籍建議:“Use const whenever you need”。
11.1.1 用const修飾函數的參數
如果參數作輸出用,不論它是什麽數據類型,也不論它采用“指針傳遞”還是“引用傳遞”,都不能加const修飾,否則該參數将失去(qù)輸出功能。
const隻能修飾輸入參數:
u 如果輸入參數采用“指針傳遞”,那麽加const修飾可以防止意外(wài)地改動該指針,起到保護作用。
例如StringCopy函數:
void StringCopy(char *strDestination, const char *strSource);
其中(zhōng)strSource是輸入參數,strDestination是輸出參數。給strSource加上const修飾後,如果函數體(tǐ)内的語句試圖改動strSource的内容,編譯器将指出錯誤。
u 如果輸入參數采用“值傳遞”,由于函數将自動産生(shēng)臨時變量用于複制該參數,該輸入參數本來就無需保護,所以不要加const修飾。
例如不要将函數void Func1(int x) 寫成void Func1(const int x)。同理不要将函數void Func2(A a) 寫成void Func2(const A a)。其中(zhōng)A爲用戶自定義的數據類型。
u 對于非内部數據類型的參數而言,象void Func(A a) 這樣聲明的函數注定效率比較底。因爲函數體(tǐ)内将産生(shēng)A類型的臨時對象用于複制參數a,而臨時對象的構造、複制、析構過程都将消耗時間。
爲了提高效率,可以将函數聲明改爲void Func(A &a),因爲“引用傳遞”僅借用一(yī)下(xià)參數的别名而已,不需要産生(shēng)臨時對象。但是函數void Func(A &a) 存在一(yī)個缺點:“引用傳遞”有可能改變參數a,這是我(wǒ)(wǒ)們不期望的。解決這個問題很容易,加const修飾即可,因此函數最終成爲void Func(const A &a)。
以此類推,是否應将void Func(int x) 改寫爲void Func(const int &x),以便提高效率?完全沒有必要,因爲内部數據類型的參數不存在構造、析構的過程,而複制也非常快,“值傳遞”和“引用傳遞”的效率幾乎相當。
問題是如此的纏綿,我(wǒ)(wǒ)隻好将“const &”修飾輸入參數的用法總結一(yī)下(xià),如下(xià)表:
對于非内部數據類型的輸入參數,應該将“值傳遞”的方式改爲“const引用傳遞”,目的是提高效率。例如将void Func(A a) 改爲void Func(const A &a)。
對于内部數據類型的輸入參數,不要将“值傳遞”的方式改爲“const引用傳遞”。否則既達不到提高效率的目的,又(yòu)降低了函數的可理解性。例如void Func(int x) 不應該改爲void Func(const int &x)。
“const &”修飾輸入參數的規則
11.1.2 用const修飾函數的返回值
u 如果給以“指針傳遞”方式的函數返回值加const修飾,那麽函數返回值(即指針)的内容不能被修改,該返回值隻能被賦給加const修飾的同類型指針。
例如函數
const char * GetString(void);
如下(xià)語句将出現編譯錯誤:
char *str = GetString();
正确的用法是
const char *str = GetString();
u 如果函數返回值采用“值傳遞方式”,由于函數會把返回值複制到外(wài)部臨時的存儲單元中(zhōng),加const修飾沒有任何價值。
例如不要把函數int GetInt(void) 寫成const int GetInt(void)。
同理不要把函數A GetA(void) 寫成const A GetA(void),其中(zhōng)A爲用戶自定義的數據類型。
如果返回值不是内部數據類型,将函數A GetA(void) 改寫爲const A & GetA(void)的确能提高效率。但此時千萬千萬要小(xiǎo)心,一(yī)定要搞清楚函數究竟是想返回一(yī)個對象的“拷貝”還是僅返回“别名”就可以了,否則程序會出錯。見6.2節“返回值的規則”。
u 函數返回值采用“引用傳遞”的場合并不多,這種方式一(yī)般隻出現在類的賦值函數中(zhōng),目的是爲了實現鏈式表達。
例如
class A
{…
A & operate = (const A &other); // 賦值函數
};
A a, b, c; // a, b, c 爲A的對象
…
a = b = c; // 正常的鏈式賦值
(a = b) = c; // 不正常的鏈式賦值,但合法
如果将賦值函數的返回值加const修飾,那麽該返回值的内容不允許被改動。上例中(zhōng),語句 a = b = c仍然正确,但是語句 (a = b) = c 則是非法的。
11.1.3 const成員(yuán)函數
任何不會修改數據成員(yuán)的函數都應該聲明爲const類型。如果在編寫const成員(yuán)函數時,不慎修改了數據成員(yuán),或者調用了其它非const成員(yuán)函數,編譯器将指出錯誤,這無疑會提高程序的健壯性。
以下(xià)程序中(zhōng),類stack的成員(yuán)函數GetCount僅用于計數,從邏輯上講GetCount應當爲const函數。編譯器将指出GetCount函數中(zhōng)的錯誤。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const成員(yuán)函數
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 編譯錯誤,企圖修改數據成員(yuán)m_num
Pop(); // 編譯錯誤,企圖調用非const函數
return m_num;
}
const成員(yuán)函數的聲明看起來怪怪的:const關鍵字隻能放(fàng)在函數聲明的尾部,大(dà)概是因爲其它地方都已經被占用了。
4.11 BOOL的使用
1. 如果可能應使用int代替 BOOL作爲函數傳入參數。說明:原因有二,其一(yī)是BOOL參數值TRUE/FALSE的含義是非常模糊的,在調用時很難知(zhī)道該參數到底傳達的是什麽意思;其二是BOOL參數值不利于擴充。
2. bool占用一(yī)個字節,BOOL占用4個字節。所以在一(yī)個系統中(zhōng)不同模塊間進行通信或參數傳遞時應保證一(yī)緻,否則在某些情況下(xià)會出現緩沖區溢出問題。建議使用BOOL,因爲:a)BOOL是一(yī)個宏,便于移植;b)把bool變量作爲結構成員(yuán)時,因爲結構對齊的需要,不一(yī)定會節省空間。
3. 一(yī)個項目的開(kāi)發中(zhōng)是使用bool還是BOOL一(yī)定要統一(yī)。否則極易産生(shēng)緩沖區溢出。
4.12 提高程序的效率
程序的時間效率是指運行速度,空間效率是指程序占用内存或者外(wài)存的狀況。
全局效率是指站在整個系統的角度上考慮的效率,局部效率是指站在模塊或函數角度上考慮的效率。
1. 不要一(yī)味地追求程序的效率,應當在滿足正确性、可靠性、健壯性、可讀性等質量因素的前提下(xià),設法提高程序的效率。
2. 以提高程序的全局效率爲主,提高局部效率爲輔。
3. 在優化程序的效率時,應當先找出限制效率的“瓶頸”,不要在無關緊要之處優化。
4. 先優化數據結構和算法,再優化執行代碼。
5. 有時候時間效率和空間效率可能對立,此時應當分(fēn)析那個更重要,作出适當的折衷。例如多花費(fèi)一(yī)些内存來提高性能。
6. 不要追求緊湊的代碼,因爲緊湊的代碼并不一(yī)定能産生(shēng)高效的機器碼。
4.13 一(yī)些有益的建議
1.當心那些視覺上不易分(fēn)辨的操作符發生(shēng)書(shū)寫錯誤。
我(wǒ)(wǒ)們經常會把“==”誤寫成“=”,象“||”、“&&”、“<=”、“>=”這類符号也很容易發生(shēng)“丢1”失誤。然而編譯器卻不一(yī)定能自動指出這類錯誤。
2. 變量(指針、數組)被創建之後應當及時把它們初始化,以防止把未被初始化的變量當成右值使用。
3. 當心變量的初值、缺省值錯誤,或者精度不夠。
4. 當心數據類型轉換發生(shēng)錯誤。盡量使用顯式的數據類型轉換(讓人們知(zhī)道發生(shēng)了什麽事),避免讓編譯器輕悄悄地進行隐式的數據類型轉換。
5. 當心變量發生(shēng)上溢或下(xià)溢,數組的下(xià)标越界。
6. 當心忘記編寫錯誤處理程序,當心錯誤處理程序本身有誤。
7. 當心文件I/O有錯誤。
8. 避免編寫技巧性很高代碼。
9. 不要設計面面俱到、非常靈活的數據結構。
10. 代碼質量比較好,盡量複用它。但是不要修補很差勁的代碼,應當重新編寫。
11. 盡量使用标準庫函數,不要“發明”已經存在的庫函數。
12. 盡量不要使用與具體(tǐ)硬件或軟件環境關系密切的變量。
13. 把編譯器的選擇項設置爲最嚴格狀态。
14. 如果可能的話(huà),使用PC-Lint、LogiScope、BoundsChecker/SmartCheck(一(yī)種錯誤檢測工(gōng)具)、TrueTime(一(yī)種性能工(gōng)具)、TureCoverage(一(yī)種代碼覆蓋工(gōng)具)等工(gōng)具進行代碼審查。我(wǒ)(wǒ)這兒隻有BoundsChecker,如果誰有上面提到的工(gōng)具或其它編程工(gōng)程工(gōng)具請共享一(yī)下(xià)。
5 調試
1. 當我(wǒ)(wǒ)們發現一(yī)個BUG後,應該先嘗試用多種方法複制這一(yī)錯誤,然後再開(kāi)始調試。以多種方式複制錯誤以保證你看到的是它本身,而不是多個錯誤相互掩蓋和混合。如果繁重的調試工(gōng)作以前,先進行“創見性的思考”,則情況會好些。
2. 一(yī)般情況下(xià)(特殊情況除外(wài)),邊編碼邊測試,在代碼覆蓋率達到80%以前提交給質量管理部是一(yī)種不負責任的做法。
3. 進行項目開(kāi)發時,若項目被劃分(fēn)成若幹個二進制模塊,把開(kāi)發時使用的編譯鏈接設置設置成完全一(yī)樣(尤其是其中(zhōng)的某些關鍵項)的是有利的,這能避免一(yī)些難以調試的莫名其妙的問題。
4. 把警告設置成最高級别且設置Warnings as errors,并把每一(yī)個警告消滅掉是一(yī)個非常好的習慣。很多程序在VC的3級警告下(xià)甚至還有幾個警告,很難讓人相信這是一(yī)個質量合格的程序。以下(xià)是摘選代碼,它們對警告設置進行操作,在編譯選項中(zhōng)已經設置成最高級别4級(以下(xià)示例均以VC6.0爲例):
a)某些庫函數的頭文件在最高警告級别下(xià)編譯出來警告較多,以下(xià)爲處理方式
……
#pragma warning(push, 3)
#include
#pragma warning(pop)
….
b)有未使用的函數參數但以後會用到,或是系統提供的函數的參數目前未用
……
UNREFERENCED_PARAMETER(hModule);
UNREFERENCED_PARAMETER(lpReserved);
……
c)對某一(yī)程序段有類或幾類警告我(wǒ)(wǒ)們認爲是合理的,但要用以下(xià)方式進行顯式處理
……
#pragma warning(push)
#pragma warning(disable:4706)
……
#pragma warning(pop)
…
相關警告的更多用法和解釋說明,請參考<MSDN>。
5. 在基于MFC的開(kāi)發中(zhōng),派生(shēng)自CObject的類都會繼承下(xià)來AssertValid和Dump兩個虛函數。這兩個函數是MFC的内嵌調試函數。
a) AssertValid()。是一(yī)個虛成員(yuán)函數,檢查傳給它的類對象中(zhōng)的每個成員(yuán),從而判斷該對象是否合法。直接使用AssertValid存在兩個問題:其一(yī)是隻有定義了_DEBUG之後,AssertValid才存在;其二是因爲AsserValid是虛成員(yuán)函數。使用非法指針調用虛拟成員(yuán)函數總是緻命的錯誤。使用ASSERT_VALID宏可以解決這兩個問題。
b) Dump()。也是一(yī)個虛成員(yuán)函數,當一(yī)個對象存在時該函數提供給程序員(yuán)盡可能多的信息,但必須準備好輸出。MFC會傾盡所有已知(zhī)的關于出錯對象的信息,僅僅一(yī)個對象的信息就有幾十行。調用的方式可爲Dump(AfxDump)。
值得一(yī)提的是要利用這兩個函數,需要我(wǒ)(wǒ)們在自己的類中(zhōng)進行重載。把自己想檢查的内容和想輸出的内容寫成代碼,這樣才有實用性。例如:
void CTgetsView::AssertValid() const
{
CView::AssertValid();
ASSERT_VALID(&m_cMousePositions);
ASSERT_VALID(&m_cMouseButtons);
ASSERT_VALID(m_cMousePositions.GetSize() == m_cMouseButtons.GetSize());
for(int i = 0; i < m_cMousePositions.GetSize(); i++)
{
...;
ASSERT(...);
}
}
void CTgetsView::Dump(CDumpContext& dc) const
{
dc << ' Dump of CTgetsView@' << (void*)this << ' ';
dc << 'm_nPrevIndex = ' << m_nPrevIndex << ' ';
CView::Dump(dc);
}
6. CMemoryState是MFC的内存洩漏調試類。有興趣的話(huà)可以學習并應用一(yī)下(xià)。例如:
CMemoryState msObject;
//…Code Continues…
msObject.CheckPoint(); //update the msObject
//…Keep on tracking…
msObject.DumpAllObjectsSince();
Dump()現在被限定在CheckPoint()調用後的範圍内。可以用Difference()比較一(yī)下(xià)兩個檢查CmemoryState對象的區别。
7. 很多内容隻能點到爲止,因爲a)每一(yī)個主題都可以闡述成某個書(shū)籍一(yī)章,而這方面的書(shū)籍和資(zī)料多得很且都比要我(wǒ)(wǒ)認識的要全面和深刻,好多内容我(wǒ)(wǒ)也是東拼西湊出來的。;b)我(wǒ)(wǒ)揀了一(yī)點有些書(shū)籍不太注意的細節進行了較詳細的解釋和說明,比如有關警告的部分(fēn);c)好多内容我(wǒ)(wǒ)也隻能用到了或看書(shū)時才會想到。d)本文的任務主要是對編程過程中(zhōng)的一(yī)些易忽視的方面引起大(dà)家的重視,“勿以善小(xiǎo)而不爲”,否則不管是自己或他人維護你的程序是非常困難的。
8. 對本文的某些觀點大(dà)家同意,以後的工(gōng)作中(zhōng)可以多加注意。如果認爲沒必要,則可以置之不理。但若發現有任何錯誤或不當之處請一(yī)定要告訴我(wǒ)(wǒ)。
9. 謝謝大(dà)家能有耐心看完這篇文章!
6 其它
1. 使用MFC向導創建的MFC 共享方式的DLL工(gōng)程中(zhōng)編譯設置中(zhōng),兩個宏_MBCS,_USRDLL默認中(zhōng)有的,但使用RESET功能後會沒有,沒有_USRDLL會導緻問題,所以要手動加上。這應該是MS的一(yī)個BUG。
2. 以下(xià)是ScanMain工(gōng)程中(zhōng)出現的一(yī)個鏈接沖突的問題描述及解決方法,解決的途徑是修改預編譯頭文件的設置。在這個工(gōng)程中(zhōng)的StdAfx.h文件中(zhōng)也有相應的文字說明。