原文來自: The Ultimate Question of PRogramming, Refactoring, and Everything https://software.intel.com/en-us/articles/the-ultimate-question-of-programming-refactoring-and-everything
譯文來自:http://blog.csdn.net/huanglong8/article/details/56029783
主要說了在編程方面的42條建議,這些建議可以幫助你避免錯誤,節(jié)省時間和精力。原作者是 Andrey Karpov - “Program Verification Systems”的技術(shù)總監(jiān),它的團(tuán)隊致力于研發(fā)PVS-Studio靜態(tài)代碼分析器。檢查了大量的開源項目后,整理出了一些建議,并且每個建議都給出了一個實際的例子,以此說明這個問題的現(xiàn)實性。以 C / C ++程序員為主,但通常可以應(yīng)用所有范例中。 
作者是C/C++語言的牛人,是微軟VC++ MVP,作者寫作的意圖就是讓你寫出更安全,更高效的C代碼,作者也喜歡分享,才有了這42條建議。
此內(nèi)容來自MySQL項目,在PVS-Studio分析器中診斷出如下錯誤:
static int rr_cmp(uchar * a,uchar * b){ if(a [0]!= b [0]) return(int)a [0] - (int)b [0]; if(a [1]!= b [1]) return(int)a [1] - (int)b [1]; if(a [2]!= b [2]) return(int)a [2] - (int)b [2]; if(a [3]!= b [3]) return(int)a [3] - (int)b [3]; if(a [4]!= b [4]) return(int)a [4] - (int)b [4]; if(a [5]!= b [5]) return(int)a [1] - (int)b [5]; <<<< ==== if(a [6]!= b [6]) return(int)a [6] - (int)b [6]; return(int)a [7] - (int)b [7];}}說明 觀察上述代碼,其實外人也不敢保證它是對的還是錯的,但經(jīng)過mysql上下文分析后,發(fā)現(xiàn),偶爾返回錯誤的值,原因就是,程序員復(fù)制了代碼塊“if(a [1]!= b [1])return(int)a [1] - (int)b [1];”。然后他開始更改索引,忘記將“1”替換為“5”。這種小的且偶發(fā)性的邏輯錯誤很難找到。
正確代碼
if(a [5]!= b [5]) return(int)a [5] - (int)b [5];建議 我們當(dāng)初在思考為什么不寫成循環(huán)時,我們得知,程序員為了優(yōu)化代碼,幫助編譯器做了展開循環(huán)的工作。當(dāng)然在實際開發(fā)中,我們可能也會這樣做,來提升輕微的速度或是減少內(nèi)存。但現(xiàn)在的編譯器都很聰明,為什么非要這么做。如果一個簡單的循環(huán)可以幫助理解,并減少犯錯的機(jī)會,那不是很好?
static int rr_cmp(uchar * a,uchar * b){ for(size_t i = 0; i <7; ++ i) { if(a [i]!= b [i]) return a [i] - b [i]; }} return a [7] - b [7];}}它的優(yōu)點就是易于閱讀和理解,并且不太可能會寫錯。通常來講簡單的代碼通常才是正確的代碼,不要嘗試去做編譯器做的工作。
代碼片段來自 CoreCLR項目,其中針對memcmp(…) == -1做了說明,當(dāng)然還有諸如此類的函數(shù)。
bool Operator()(const GUID&_Key1,const GUID&_Key2)const {return memcmp(&_ Key1,&_Key2,sizeof(GUID))== -1; }}說明 int memcmp(const void * ptr1,const void * ptr2,size_t num); 是將ptr1和ptr2中的前num字節(jié)進(jìn)行比較,內(nèi)存內(nèi)容相等返回0,不等則返回非0。 <0 - 前num的第一個不同字節(jié)的數(shù)值比較,ptr1的小(如果計算為unsigned char值)。 == 0 - 兩個內(nèi)存塊的內(nèi)容相等。 >0 - 前num的第一個不同字節(jié)的數(shù)值比較,ptr1的大(如果計算為unsigned char值)。 不要將諸如memcmp(),strcmp(),strncmp()等函數(shù)的結(jié)果與常量1和-1進(jìn)行比較。 有趣的是,很多代碼都這么寫,并且還長期的運(yùn)行著,從來沒有出錯,那我認(rèn)為那是運(yùn)氣,因為當(dāng)這些函數(shù)的行為被更改。例如換編譯器,或重寫事先,將導(dǎo)致你的代碼出bug。 正確代碼
bool operator()(const GUID&_Key1,const GUID&_Key2)const {return memcmp(&_Key1,&_Key2,sizeof(GUID))<0; }}建議 遵從函數(shù)確定的實現(xiàn)及返回,不要依賴于經(jīng)驗等,如果文檔說返回大于小于0,也不要將他和特定的數(shù)字進(jìn)行比較。 順便講一下,這個函數(shù)返回1024的實際情況,這個錯誤是mysql/MariaDB中導(dǎo)致的,主要是由于訪問后返回的token對其256位內(nèi)進(jìn)行比較,一旦超出,則永遠(yuǎn)都會返回true,哪怕這個人不知道密碼。在passWord.c中
my_bool check(...){ return memcmp(...);有關(guān)這個問題更詳述的信息參考:MySQL / MariaDB中的安全漏洞。
這個錯誤來自Audacity項目,代碼如下:
sampleCount VoiceKey :: OnBackward(....){ ... ... int atrend = sgn(buffer [samplesleft-2] - buffer [samplesleft - 1]); int ztrend = sgn(buffer [samplesleft-WindowSizeInt-2] - buffer [samplesleft - WindowSizeInt-2]); ... ...}}說明 其實一眼看去,如果你不是這個項目的參與者,或者是剛接手這個項目,你很難明白錯誤在哪里,即便指出你也不能保證這就是錯誤。buffer [samplesleft-WindowSizeInt-2]出現(xiàn)此錯誤同樣因為復(fù)制粘貼,程序員復(fù)制了代碼字符串,但忘記將2替換為1。這是一個很low的錯誤,但我們每個人都會犯,這也就是為什么強(qiáng)調(diào)的原因。 正確的代碼
int ztrend = sgn(buffer [samplesleft-WindowSizeInt-2] - buffer [samplesleft - WindowSizeInt-1]);建議 復(fù)制代碼快可以幫助你高效的完成敲碼工作,但請你要多檢查他幾次,因為那些實體,數(shù)字一旦反生bug,則將很難被找出,稍后我們還是會討論復(fù)制粘貼,讓你印象深刻。
片段來自Haiku項目,?:運(yùn)算符優(yōu)先級低于-運(yùn)算符導(dǎo)致。
bool IsVisible(bool ancestorsVisible)const{ int16 showLevel = BView :: Private(view).ShowLevel(); return(showLevel - (ancestorsVisible)?0:1)<= 0;}}說明 一同來看看C/C++運(yùn)算符優(yōu)先級。三目運(yùn)算符具有非常低的優(yōu)先級,低于/+<等等,它也比負(fù)優(yōu)先級低,因此,當(dāng)混合使用時,需要更加注意。 程序員認(rèn)為運(yùn)算順序是:
(showLevel - (ancestorsVisible?0:1))<= 0然后呵呵
((showLevel - ancestorsVisible)?0:1)<= 0錯誤明顯,這也是很簡單的代碼,卻很容易犯錯,如果你有包含三目的非常復(fù)雜的算式,那。。。這么難讀就不要搞這么長的算式。 正確的代碼
return showLevel - (ancestorsVisible?0:1)<= 0;建議 如果你的三目混合其中,那么請加上括號,以避免這種錯誤,也幫助程序員去閱讀你的代碼。當(dāng)然我也不排斥直接使用三目,當(dāng)然,只有你需要一個表達(dá)式,表達(dá)式中只有三目時,你可以不寫括號,這樣看起來更簡潔。
此段來自LibreOffice項目,由PVS檢測出,CreateThread函數(shù)不應(yīng)該由DllMain函數(shù)調(diào)用。
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason,LPVOID lpvReserved){ .... CreateThread(NULL,0,ParentMonitorThreadProc, (LPVOID)dwParentProcessId,0,&dwThreadId); ....}}說明 在一定條件下,DllMain不得不做一些事情,當(dāng)我在這個主函數(shù)下調(diào)用Win API時,莫名奇妙的發(fā)現(xiàn)它并不工作,我也沒設(shè)法弄清楚當(dāng)時的問題。 再進(jìn)行PVS開發(fā)工作時,我突然意識到之前失敗的原因。在DllMain函數(shù)中,你只能執(zhí)行非常有限的操作,原因就是Dll可能還沒有完全被加載到內(nèi)存中,你無法在這里面調(diào)用其他的API。當(dāng)然,現(xiàn)在診斷出的結(jié)果是警告,以此來提醒程序員,是不是應(yīng)該放到其他中去。 細(xì)節(jié) 使用DllMain更多的細(xì)節(jié)應(yīng)該參考MSDN上的文章,動態(tài)鏈接庫實踐。這里列舉一些摘要。
DllMain被調(diào)用時內(nèi)存加載處于鎖定狀態(tài),這意味著在它之中調(diào)用的函數(shù)有一定的限制,因此,在主函數(shù)入口中,應(yīng)該只進(jìn)行一小部分的初始化任務(wù),如果你調(diào)用了任何直接或間接嘗試獲取加載器鎖的函數(shù),那么程序可能會導(dǎo)致死鎖或崩潰,而這種錯誤可能直接影響到整個進(jìn)程或線程中。某DllMain的一個好的初始化方法就是盡可能延長初始化,這樣使您更安全的使用API。當(dāng)然不是所有任務(wù)都可以延長初始化的,如果某些不正確的資源或配置,應(yīng)該立即退出,而不是通過做其他的工作繼續(xù)浪費時間。
不要在DllMain中執(zhí)行以下任務(wù):
不要直接或間接的調(diào)用LoadLibrary或LoadLibraryEx,導(dǎo)致死鎖或崩潰滴。不要直接或間接的調(diào)用GetStringTypeA,GetStringTypeEx或GetStringTypeW,同上滴。與其他線程同步,同上滴。獲取或等待同步對象,鎖啊這類的釋放,同上滴。使用ColnitializeEx初始化COM線程。調(diào)用注冊表函數(shù)。這個是加載advapi32.dll后才能用的。調(diào)用CreateProcess。如果這個進(jìn)程又加載另一個DLL。調(diào)用ExitThread。在卸載期間如果推出線程,哦哦。調(diào)用CreateThread。如果不同步,是可以的,就是有點風(fēng)險。創(chuàng)建命名管道或其他對象(限win2000),命名對象由終端服務(wù)DLL提供。如果這個Dll沒有初始化,則可能哦崩潰。使用CRT的內(nèi)存管理。如果CRT的DLL未初始化,也有問題。調(diào)用User32.dll或Gdi32.dll中的函數(shù),因為這里面的函數(shù)可能加載了另一個dll。使用托管代碼。 正確的代碼 LibreOffice的代碼可能不工作,這是偶發(fā)的。修復(fù)這個不簡單,因為要重構(gòu),以使DllMain函數(shù)盡可能短。 建議 很難建議,因為誰知道突然發(fā)生個莫名其妙的bug,但讀完這里,你至少知道,在寫這樣的代碼時,應(yīng)該注意哪些問題,從而避免。還有不要告訴我在dllmain要干嘛干嘛,我也不知道,這一切都得靠你。此片段來自ipP Samples項目,看這代碼
void write_output_image(....,const Ipp32f * img, ....,const Ipp32s iStep){ ... ... img =(Ipp32f *)((unsigned long)(img)+ iStep); ... ...}}說明 程序員將指針移動一定數(shù)量的字節(jié),此代碼在win32下正確,因為指針大小和long類型同樣。但是如果換成64位版本,就會造成高位丟失。 linux使用不同的數(shù)據(jù)類型。可能也會有類似問題,但很多l(xiāng)inux程序也都會進(jìn)入到windows中,所以,還是使用intprt_t這樣的類型來定義比較妥。 此錯誤如圖:
這種錯誤有時也很難注意到,因為導(dǎo)致指針高位丟失也只能是在程序運(yùn)行了好長時間后才偶然出現(xiàn)的問題。 正確的代碼 使用size_t,INT_PTR,DWORD_PTR,intrptr_t來定義。
參見EXP36-C 建議 使用指針類型來存儲指針。如果要編譯64位版本,首先需要檢查所有代碼,尤其是使用指針強(qiáng)轉(zhuǎn)的地方,來避免麻煩。 這里還有一個學(xué)習(xí)資源,去看吧。64位C/C++應(yīng)用開發(fā)的經(jīng)驗教訓(xùn)
新聞熱點
疑難解答