31. 在 C 和C++中,數(shù)組不是按值傳遞的 下面的代碼來自游戲'Wolf'.代碼中包含的錯誤被 PVS-Studio診斷為:V511在'sizeof (src)'這個表達式中,sizeof()返回的是指針的大小,而不是數(shù)組的大小。 解釋 有時候,程序員會忘記在C/C++中,你不可以把一個數(shù)組值傳遞給一個函數(shù)。因為傳數(shù)組給一個函數(shù),數(shù)組類型自動轉(zhuǎn)換為指針類型。方括號里里的數(shù)字沒什么意思,它們只是用來告訴程序員,多大的數(shù)組被傳進去了。事實上,你可以傳任意大小的數(shù)組。比如,下面的代碼也能編譯成功:
類似的,sizeof(src) 不是計算數(shù)組的大小,而是指針的大小。然后結(jié)果就是,memcpy() 只是復制了一部分數(shù)組而已。也就是,4或者8字節(jié),這都取決于指針的大?。ú凰闫婀值慕Y(jié)構(gòu))。 正確代碼 最簡單的修正是這樣的:
建議 有多種方法可以讓你的代碼更安全。 知道數(shù)組大小。你可以在函數(shù)中用數(shù)組的引用做參數(shù)。但并不是所有人都知道可以這么做。甚至更少的人知道怎么寫。所以我希望下面的例子能夠有用,有趣:
現(xiàn)在,就可以在傳遞數(shù)組的時候只用右邊的部分。而且最重要的是,sizeof()計算的是數(shù)組的大小了,不再是一個指針的。 解決這個問題的另一個方法是用std::array類。 不知道數(shù)組大小。有些編程書的作者建議使用std::vector類和其他相似的類。但是,這實際應用做,并不總是那么方便。 有時,你也會想要用指針來解決這個問題。在這個例子中,你可以傳兩個參數(shù)給那個函數(shù):一個指針,元素個數(shù)。然而,一般來說,這樣做都不太好,因為它會導致很多bug。 在這樣的例子中,'C++ Core Guidelines'這篇文章里的觀點蠻有用的。我建議讀'Do not pass an array as a single pointer'。總的來說,在你有空的時候讀'C++ Core Guidelines'真的不失為一件有益的事。它里面包含了很多有用的觀點。 32. 危險的 printf 下面的代碼選自 TortoiseSVN 項目。代碼中包含的錯誤被 PVS-Studio 診斷為:V618 以一種危險的方式調(diào)用 printf 函數(shù),因為傳遞進去的那一行應該包含格式化說明。安全使用 printf 的例子:: printf('%s', str);
解釋 當你打算打印或者,比如說,寫一個字符串到文件中,很多程序員會這樣寫:
一個優(yōu)秀的程序員應該時刻記得,這樣組織代碼是非常不安全的。事情是這樣的,如果格式化說明不知怎樣進入一個字符串里,將會導致無法預測的后果。 讓我們回過頭來看最初的例子。如果文件名是'file%s%i%s.txt',那么這個程序會崩潰,或者輸出一些不知所云的東西。但這不還是問題的全部。事實上,這樣的函數(shù)調(diào)用是一個真正的漏洞。在它的幫助下,我們能發(fā)動攻擊。用一些刻意的字符串,就可以打印內(nèi)存里的私有數(shù)據(jù)。 更多關(guān)于這個漏洞的信息可以查看這篇文章?;c時間去通讀一遍,我保證,會很有趣的。你不僅能看到理論基礎(chǔ),還可以看到實際的例子。 正確代碼
建議 像 printf() 這樣的函數(shù)會引起很多有關(guān)安全的問題。最好一點也不要使用它們,你可以用其他的來代替啊。比如說,你會發(fā)現(xiàn) boost::format 或者 std::stringstream 也很有用。 一般來說,草率地使用 printf(), sprintf(), fprintf(),等等函數(shù)不僅會導致運行不當,而且會引發(fā)潛在的漏洞,然后別人就可以利用這個漏洞來攻擊你。 33. 永遠不要間接引用空指針 bug 是在 GIT 的源代碼中發(fā)現(xiàn)的。代碼中包含的錯誤被 PVS-Studio 診斷為:V595 在還沒有驗證‘tree‘指針是否為空之前就使用它。檢查134,136行。
解釋 無疑,這是一個糟糕的做法,因為它間接引用了空指針,而這樣間接引用的結(jié)果就是未定義行為。我們都同意這背后的理論基礎(chǔ)。 但是,當具體運用的時候,程序員們就開始爭論不休了??傆腥寺暦Q,這段代碼能夠正確運行。他們甚至以項上人頭做擔?!獙λ麄儊碚f,它也總是能運行。所以,我要給出更多的理由來證明我的觀點。這就是為什么這篇文章是改變他們觀點的又一嘗試。 我故意選了這么一個能夠引發(fā)更多討論的例子。當tree指針被調(diào)用,類成員不僅是在使用,也在計算該成員的地址。那么,如果(tree == nullptr),就永遠不會用到成員的地址,而且函數(shù)已經(jīng)退出了。很多人都認為這段代碼是正確的。 但并不是。你不應該這么寫代碼。未定義行為不一定造成程序崩潰,比如賦值給空地址,或者諸如此類的行為。只要你調(diào)用了一個等于null的指針,未定義行為可以是任何操作。這個時候再討論這段代碼會如何運行已經(jīng)沒有意義了,因為此時它可以做它任何想做的操作。 未定義行為的一個標志是,編譯器會把'if (!tree) return;'刪掉——編譯器看到指針已經(jīng)被調(diào)用了,而指針不是空的,那么這一行檢查就會被編譯器移除。這只是眾多版本中的一個,而這個版本會引起程序崩潰。 我建議閱讀這一篇文章:http://www./en/b/0306/,里面給出了更多細節(jié)。 正確代碼
要注意未定義行為,即使一切看上去都沒什么問題。沒必要冒險。即使我已經(jīng)寫了,但還是很難表現(xiàn)出它的價值。嘗試著去避免未定義行為,即使一切看上來都沒問題。 有人會想,他清楚的知道,未定義行為是怎樣運作的。而且,他可能會想,這意味著,他可以做一些其他人不能做的事,還能保證代碼不出錯。但并非如此。下一章節(jié)將會說明未定義行為真的非常危險。 34. 未定義行為比你想象的要貼近我們的生活 這次很難給出實際應用的例子。但是,我經(jīng)常有看到會導致下面要所描述問題的可疑代碼。這個錯誤是在處理大數(shù)組的時候出現(xiàn)的,而我不知道哪個項目會用到那么大的數(shù)組。我們沒有真的收集到64位的錯誤,所以今天的例子是刻意的。 讓我們來看一段刻意出錯的代碼:
解釋 如果你構(gòu)建的是這個項目的32位版本,代碼是可以正確運行的。但是如果我們要編譯64位版本,情況就會變得很復雜。 這個64位項目開始的時候申請5GB的緩沖區(qū),然后初始化為0.接著,用循環(huán)修改為非0值:用“|1”來保證非0. 現(xiàn)在來猜一下,如果在x64位版本下用Visual Studio 2015編譯的時候,這段代碼會如何運行?你有答案嗎?如果有,那我們繼續(xù)。 如果你運行的是這個項目的調(diào)試版本,它會因為下標溢出而崩潰。在一定程度上,下標會溢出,而且其值會變成?2147483648 (INT_MIN). 聽上去很有邏輯,對不對?事情并不是這樣的。這是一個未定義行為,任何事情都有可能發(fā)生。 為獲得更多內(nèi)容,我推薦下面的鏈接: 好玩的是——當我或者其他人說這段代碼會引發(fā)未定義行為的時候,就會有人抱怨。我不知道為啥,但看起來好像是,他們覺得自己對C++有絕對的了解,而且知道編譯器是怎樣運作的。 但事實上他們都沒有注意到它。如果他們知道,他們不會這么說的(大眾的觀點): 這在理論上沒什么意義。好吧,是,‘int’溢出會導致未定義行為。但是這沒什么,不過老生常談而已。在實際應用中,我們知道代碼運行后能得到什么。1 加 INT_MAX 等于 INT_MIN 嘛。但是,可能在宇宙中的某一個角落存在著能讓這種情況(整形一出也能得我們想要的結(jié)果)發(fā)生的結(jié)構(gòu),但就是我的 Visual C++/GCC 沒能給出正確的結(jié)果而已。 現(xiàn)在,沒有任何魔法。我會用一個簡單的例子來證明未定義行為,而且沒有用到什么奇怪的結(jié)構(gòu),只是一個 win64 的項目。 把上面的例子構(gòu)建成發(fā)布版本并運行就已經(jīng)足夠了。代碼會崩潰結(jié)束,而且“最后一個數(shù)組元素包含0”的提示也不會出現(xiàn)。 未定義行為一般以如下的方式出現(xiàn)。所有的數(shù)組元素都會被賦值,盡管數(shù)組下標的類型 int 并不足以覆蓋所有的元素。那些至今還在懷疑我的人,可以看一下下面的代碼:
這里就有一個未定義行為。而且沒有用到什么特殊的編譯器,用的就是VS2015. 如果你用 unsigned 代替 int,就不會有未定義行為。而數(shù)組就只有一部分被填充,最后我們會收到一條信息——“最后一個數(shù)組元素包含0”。 相似的代碼用 unsigned 的情況:
正確代碼 在程序中你要用對正確的類型,這樣才能確保它順利運行。如果你要處理大數(shù)組,忘了 int 和 unsigned 吧。正確的類型有 ptrdiff_t, intptr_t, size_t, DWORD_PTR, std::vector::size_type 等等,在這里用 size_t.
建議 如果根據(jù) C/C++ 的語言機制會導致未定義行為的話,就別跟它們爭了,也不要嘗試去預測它們會在將來做出什么表現(xiàn)。只要不寫危險的代碼就好了啊。 還是有很多固執(zhí)的程序員不愿意在轉(zhuǎn)換負數(shù)、比較 this 和 null 或者有符號類型溢出時看任何表示懷疑的言論。 不要這樣。就算現(xiàn)在代碼運行得好好的也不意味著一切都是好的。未定義行為是不可預測的??深A測的程序行為是未定義行為的一個變體。 35. 在枚舉中加了新的枚舉常量后別忘了修改switch運算 下面的代碼來自 Appleseed 項目。代碼中包含的錯誤被 PVS-Studio 診斷為:V719 switch 語句沒有覆蓋枚舉“InputFormat”的所有值,少了InputFormatEntity.
解釋 有的時候我們需要在已經(jīng)存在的枚舉里加入新的元素,當我們做這個操作的時候,我們需要特別的謹慎——因為我們要檢查全部代碼看看哪里有用到這個枚舉,比如說在 switch 和 if 中。上面給出的代碼就是這種情況。 在 InputFormat 中加了 InputFormatEntity——這里是我想象的,因為確實在 InputFormat 的后面有加入這個常量。很多時候,程序員在枚舉后面加了新的常量,然后忘了檢查代碼以確保他們有正確處理這個常量,也沒有修改 switch 操作。 最后的結(jié)果就是,在這個例子中,并沒有處理到'm_format==InputFormatEntity'的情況。 正確代碼
建議 讓我們想想,怎樣在代碼重構(gòu)的時候避免這種錯誤?最簡單,但不那么有效的解決方法就是加一個‘default’,它可以輸出一個信息,像這樣:
現(xiàn)在,如果變量 m_format 是 InputFormatEntity,我們就可以看到一個異常。這樣的處理方法有兩個不好的地方:
我建議用以下的方法來解決這個問題,我不敢說這個方法很完美,但至少它有解決到問題。 當你定義一個枚舉的時候,要確保你也加了一條特殊的注釋。你也可以用關(guān)鍵詞和枚舉名。 例子:
在上面的代碼中,當你要改變枚舉InputFormat,你就可以直接在項目的源代碼中查找“ENUM:InputFormat”。 如果你是在一個開發(fā)者團隊中,你可以告訴你的小伙伴們這個約定,然后把它加入到你們的編程標準和風格指引中。如果有人沒能遵守這條原則,真遺憾。
相 關(guān) 閱 讀: .... 更多優(yōu)秀文章點擊左下角“關(guān)注原文”查看! 看雪論壇:http://bbs./ |
|