久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

C 編程的 42 條建議(一)

 刀首木 2017-03-17

前 言

關(guān)于作者,,我叫 Andrey Karpov。我的興趣就是 C/C++以及改進(jìn)代碼分析的方法論,。在 VisualC++的五年里,,我獲得了微軟最具價(jià)值專家獎(jiǎng)(Microsoft MVP)。我的這些文章和工作的首要目的就是讓代碼更安全,。若這些建議能夠讓你避免一些常見的錯(cuò)誤并寫出更好的代碼,,我將不勝榮幸。那些為企業(yè)寫代碼規(guī)范的人也可從本文中得到一些有用的信息,。 

有個(gè)小背景,。不久前,我創(chuàng)建了一個(gè)資源文件夾,,在那里我分享了一些對(duì) C++編程比較有用的技巧,。但這個(gè)資源文件夾的訂閱用戶并沒有我預(yù)料的那么多,所以我覺得沒有在這里放鏈接的必要,。這個(gè)文件夾應(yīng)該會(huì)在網(wǎng)上放一段時(shí)間,,但最終,,會(huì)被刪掉。然而,,文件夾中的技巧依舊有價(jià)值,。這也是為什么我要更新它們,增加了幾條新的技巧,,然后把它們整合在一個(gè)單獨(dú)的文本里,。閱讀愉快。 

1. 別 做 編 譯 器 的 活 

來(lái)看一段選自 MySQL 項(xiàng)目代碼塊,,這段代碼包含有一個(gè)錯(cuò)誤,,在 PVS-Studio 代碼分析器中表現(xiàn)為:V525 代碼包含一系列相同的代碼塊。檢查在 680,、682,、684、684,、689,、691、693,、695 行的“0”,、“1”、“2”,、“3”、“4”,、“1”,、“6”項(xiàng)。 

解 釋 

這是一個(gè)關(guān)于代碼復(fù)制粘貼的典型錯(cuò)誤,。很顯然,,程序員復(fù)制代碼“ if (a[1] != b[1]) return (int) a[1] - (int) b[1];”,然后開始改變下標(biāo),,但是忘了將“1”改為“5”,。結(jié)果導(dǎo)致在這個(gè)比較函數(shù)中會(huì)返回不正確的值。這個(gè)問題比較不容易注意到,,而且很難被檢測(cè)到,,因?yàn)樵谖覀兪褂?PVS-Studio 瀏覽 MySQL 之前所有的測(cè)死用力都沒有檢測(cè)出來(lái)。 

正 確 代 碼 

if (a[5] != b[5]) 

 return (int) a[5] - (int) b[5]; 

建 議 

盡管這段代碼簡(jiǎn)潔,、易讀,,但是開發(fā)人員還是比較容易忽視掉這個(gè)錯(cuò)誤。在閱讀類似這樣的代碼的時(shí)候你會(huì)比較難集中精力,,因?yàn)槟闼吹亩际窍嗨频拇a,。 

這些相似的語(yǔ)句塊經(jīng)常會(huì)導(dǎo)致一個(gè)結(jié)果,就是程序員會(huì)想要盡可能的優(yōu)化代碼。他手動(dòng)“展開循環(huán)”,。我認(rèn)為在這里,,循環(huán)展開并不是一個(gè)好法子。 

首先,,我會(huì)懷疑程序員能否真的從這里獲得任何益處?,F(xiàn)代的編譯器都非常的智能,也非常善于在可以提升代碼能力的情況下自動(dòng)展開循環(huán),。 

其次,,這段代碼塊的 bug 是因?yàn)樗胍獌?yōu)化代碼。如果你把它寫成一段比較簡(jiǎn)單的循環(huán)的話,,犯錯(cuò)的幾率應(yīng)該會(huì)小一點(diǎn),。 

我會(huì)建議用下面的方式重寫這個(gè)函數(shù): 

static int rr_cmp(uchar *a,uchar *b) 

   for (size_t i = 0; i < 7;="">

  { 

     if (a[i] != b[i]) 

       return a[i] - b[i]; 

   } 

    return a[7] - b[7]; 


優(yōu) 點(diǎn) 

  • 函數(shù)更易于閱讀和理解 

  • 犯錯(cuò)的幾率比較小 

我敢肯定這個(gè)函數(shù)不會(huì)比原來(lái)那個(gè)版本運(yùn)行慢。 

所以,,我的建議就是——寫簡(jiǎn)單和易于理解的代碼,。具體來(lái)說,簡(jiǎn)單的代碼就是正確的代碼,。不要嘗試去做編譯器的活——比方說,,展開循環(huán)。如果展開會(huì)比較好的話,,編譯器會(huì)去做的,。做這種手動(dòng)優(yōu)化的工作只有在一些比較重要的代碼塊那里才會(huì)有意義,而且這只有在profiler 已經(jīng)將這些代碼塊判斷為有問題(慢)的時(shí)候才成立,。 

2. 大于 0 并不意味著是 1 

下面這個(gè)代碼塊選自 CoreCLR 項(xiàng)目,。這個(gè)代碼包含一個(gè)錯(cuò)誤,在 PVS-Studio 分析器中診斷為:V698 表達(dá)式“memcmp(....) == -1”不正確,。這個(gè)函數(shù)的返回值不是只有“-1”,,可以是任意的負(fù)數(shù)值??梢钥紤]用“memcmp(....) <>”來(lái)代替,。 

bool operator( )(const GUID& _Key1, const GUID& _Key2) const 

    { return memcmp(&_Key1, &_Key2, sizeof(GUID)) == -1; } 

解 釋 

讓我們來(lái)看一下 memcmp() 函數(shù)的描述: 

int memcmp ( const void * ptr1, const void * ptr2, size_t num ); 

ptr1 指向的存儲(chǔ)單元里的前 num 個(gè)字節(jié)和 ptr2 指向的存儲(chǔ)單元里的前 num 個(gè)字節(jié)做比較。如果它們都相等就返回 0,,如果不等,,返回一個(gè)非 0 的數(shù)來(lái)表示大于。 

返回值: 

  • <> —— 兩個(gè)存儲(chǔ)單元里的字節(jié)不相等,,ptr1 里的值小于 ptr2 里的值(如果以無(wú)符號(hào)char 值作為比較) 

  • ==0 ——兩個(gè)存儲(chǔ)單元里的內(nèi)容相等 

  • >0 ——兩個(gè)存儲(chǔ)單元里的字節(jié)不相等,,ptr1 里的值大于 ptr2 里的值(如果以無(wú)符號(hào)char 值作為比較) 

注意到如果存儲(chǔ)塊里的東西不一樣的話,函數(shù)的返回值會(huì)大于或小于 0 ,。大于或小于,。這很重要,。你不能將 memcmp(), strcmp(), strncmp(),等等這些函數(shù)的返回值與常數(shù) 1 或 -1 進(jìn)行比較。 

有趣的是,,這段將返回值和 1/-1 進(jìn)行比較的錯(cuò)誤代碼,,多年來(lái)也能返回程序員所期望的值。但只是運(yùn)氣而已,,別無(wú)其他,。函數(shù)的行為可能產(chǎn)生意想不到的變化。比如說,,你可能換了一個(gè)編譯器,,或者開發(fā)人員會(huì)以一種新的方式優(yōu)化 memcmp() 函數(shù),那你的代碼就不起作用了,。 

正 確 代 碼 

bool operator( )(const GUID& _Key1, const GUID& _Key2) const 

   { return memcmp(&_Key1, &_Key2, sizeof(GUID)) < 0;="">


建 議 

不要覺得函數(shù)現(xiàn)在起作用就好,。如果文檔說一個(gè)函數(shù)可以返回大于和小于 0 的值,那么它就真的會(huì),。它意味著這個(gè)函數(shù)可以返回-10 ,,2 ,或 1024. 你經(jīng)常見到它返回-1 ,,0 ,,1 并不意味著什么。 

還有,,函數(shù)能返回類似于 1024 的事實(shí)表明了 memcmp() 的運(yùn)行結(jié)果不能以 char 變量進(jìn)行排序,。這是一個(gè)比較多人會(huì)犯的一個(gè)錯(cuò)誤,其后果很嚴(yán)重,。這個(gè)錯(cuò)誤就是 MySQL/MariaDB5.1.61,、5.2.11, 5.3.5 ,,5.5.22 版本以前一些高危漏洞的根源。當(dāng)用戶連上 MySQL/MariaDB的時(shí)候,,代碼會(huì)記錄一個(gè)值(對(duì) hash 和密碼進(jìn)行 SHA 安全哈希算法后的值),,然后這個(gè)值將會(huì)用來(lái)和 memcmp() 函數(shù)的返回值進(jìn)行比較。但是在一些平臺(tái)下,,返回值會(huì)超出 {-128…127},。結(jié)果就是,有 1/256 的幾率在比較 hash 值與預(yù)期值的時(shí)候總返回 true,,不管 hash 值是怎樣的,。因此,只要一條簡(jiǎn)單的命令行黑客就能無(wú)須密碼黑進(jìn) MySQL 服務(wù)器,。原因就是“sql/password.c”文件里的這部分代碼:


typedef char my_bool; 

... 

my_bool check(...) { 

 return memcmp(...); 

3. 一次復(fù)制,,多次檢查 

下面這段代碼塊選自 Audacity 項(xiàng)目,。PVS-Studio 檢測(cè)到的錯(cuò)誤是:V501 在“-”的左邊和右邊有相同的子表達(dá)式。 

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]”這個(gè)表達(dá)式減去它本身,。出現(xiàn)這個(gè)錯(cuò)誤是因?yàn)樵趶?fù)制代碼塊( 復(fù)制 -粘貼)的時(shí)候,,程序員忘了把 2 改為 1。

這真是一個(gè)很不起眼的錯(cuò)誤,,但它終究是個(gè)錯(cuò)誤,。這樣的錯(cuò)誤于程序員而言確實(shí)是一個(gè)殘酷的事實(shí),所以我們要在這里多講幾遍,。我要向它們宣戰(zhàn),。 

正 確 代 碼 

int ztrend = sgn(buffer[samplesleft - WindowSizeInt-2]- 

                         buffer[samplesleft - WindowSizeInt-1]); 


建 議 

在復(fù)制代碼的時(shí)候須謹(jǐn)慎小心。 

讓程序員拒絕使用復(fù)制粘貼是沒意義的,。因?yàn)樗奖?,太有用了,我們?cè)跄軛壷挥?。所以,,要?jǐn)慎小心,不要急——凡事欲則立,。 

記得復(fù)制代碼可能會(huì)引起很多錯(cuò)誤,。大部分被診斷為 V501 錯(cuò)誤的代碼都是因復(fù)制粘貼引起的。 

如果你要復(fù)制代碼后編輯它,,記得檢查一下代碼,,不要偷懶。 

我們稍后會(huì)詳談復(fù)制粘貼,。我希望你能記住,,這個(gè)問題比我們想象的要嚴(yán)重。 

4. 當(dāng)心三元運(yùn)算符“,?:”,,用括號(hào)把它括起來(lái) 

下面這段代碼塊選自 Haiku 項(xiàng)目(BeOS 的繼承者)。這段代碼中的錯(cuò)誤被 PVS-Studio 診斷為:V502,有可能“,?:”不能返回你所期望的值,。“,?:”的優(yōu)先級(jí)低于“-”,。 

bool IsVisible(bool ancestorsVisible) const 

     int16 showLevel = BView::Private(view).ShowLevel(); 

     return (showLevel - (ancestorsVisible) ? 0 : 1) <=>

解 釋 

讓我們來(lái)看一下 C/C++運(yùn)算符的優(yōu)先級(jí).三元運(yùn)算符“?:”的優(yōu)先級(jí)非常低,,比/,,+,<>

程序員認(rèn)為那些操作會(huì)以如下順序執(zhí)行: 

(showLevel - (ancestorsVisible ? 0 : 1) ) <=>

但事實(shí)上,,它的順序是這樣的: 

((showLevel - ancestorsVisible) ? 0 : 1) <=>

在這樣簡(jiǎn)單的代碼發(fā)生這樣的錯(cuò)誤,,也說明了“,?:”有多么難搞。在使用它的時(shí)候很容易出錯(cuò),,尤其是當(dāng)這個(gè)三元運(yùn)算符的判斷條件比較復(fù)雜的時(shí)候,,它可能就是代碼中唯一的錯(cuò)誤。而且不是你會(huì)出錯(cuò)這么簡(jiǎn)單,,這樣的表達(dá)式也比較不易讀,。 

真的,要當(dāng)心“,?:”運(yùn)算,。我看到過蠻多在使用這個(gè)運(yùn)算符的時(shí)候出現(xiàn)的錯(cuò)誤。 

正 確 代 碼 

return showLevel - (ancestorsVisible ? 0 : 1) <=>

建 議 

在之前的文章中,,我們已經(jīng)討論過關(guān)于三元運(yùn)算符所引起的錯(cuò)誤,,但自從那以后我就變得更多疑了。上面給的那個(gè)例子說明了在使用三元運(yùn)算符時(shí)是多么容易出錯(cuò),,就算在簡(jiǎn)短簡(jiǎn)單的表達(dá)式中也不例外,,這也是為什么我要修改我之前的那些技巧。我不建議完全不用“,?:”,。有時(shí)它也很有用很必要的。然而,,請(qǐng)勿濫用,。如果你已經(jīng)決定要使用它,那我的建議就是:要加括號(hào),。 

假設(shè)你有這樣一個(gè)表達(dá)式: 

A = B ? 10 : 20; 


那我建議你這樣寫: 

A = (B ? 10 : 20); 

好吧,,括號(hào)在這里有點(diǎn)多余…… 

但是, 當(dāng)以后你或者你的同事要重構(gòu)函數(shù)時(shí),,比如說在 10 或 20 那里加一個(gè)變量 x:A = X + (B ? 10 : 20); 

沒有括號(hào),,你可能會(huì)忘記“?:”的優(yōu)先級(jí)非常低,,然后就會(huì)出錯(cuò)哦,。 

當(dāng)然啦,你也可以把“x+”寫在括號(hào)里,,然后也會(huì)導(dǎo)致同樣的錯(cuò)誤。

雖然看上去好像有保護(hù)到它,,但并沒有,。 

5. 使用可利用的工具去分析你的代碼 

下面的代碼來(lái)自 LibreOffice 項(xiàng)目。其錯(cuò)誤在 PVS-Studio 中診斷為:V718 “Create Thread”不能被“DllMain”調(diào)用,。 

BOOL WINAPI DllMain( HINSTANCE hinstDLL, 

         DWORD fdwReason, LPVOID lpvReserved ) 

      .... 

      CreateThread( NULL, 0, ParentMonitorThreadProc,                                                                                    (LPVOID)dwParentProcessId, 0, &dwThreadId ); 

       .... 

解 釋 

過去有段比較長(zhǎng)的時(shí)間我都有在兼職網(wǎng)站找兼職做,。有次,,我沒能完成我接的任務(wù)。任務(wù)本身就沒表述正確,,但是我那個(gè)時(shí)候并沒有意識(shí)到,。畢竟它乍一看挺簡(jiǎn)單清晰的。 

在 DllMain 里的特定條件下,,我要使用 Windows API 的函數(shù)來(lái)做一些操作,,具體什么操作我忘了,但是應(yīng)該不難,。 

我花了蠻多時(shí)間在上面,,然而代碼就是運(yùn)行不了。更離奇的是,,當(dāng)我新做一個(gè)標(biāo)準(zhǔn)化應(yīng)用的時(shí)候,,它又運(yùn)行得了,但在 DllMain 函數(shù)里面就是運(yùn)行不了,。好神奇,,對(duì)不對(duì)?那個(gè)時(shí)候我沒能找出問題的根源,。 

直到多年后的現(xiàn)在,,我在 PVS-Studio 部門工作,我才突然意識(shí)到當(dāng)年我代碼運(yùn)行不了的原因,。在 DllMain 這個(gè)函數(shù)里,,你所能做的動(dòng)作是有限的。因?yàn)橛行﹦?dòng)態(tài)鏈接庫(kù)還沒有加載進(jìn)來(lái),,那當(dāng)然你就不可以調(diào)用它們啦,。 

現(xiàn)在,當(dāng)程序員要在 DllMain 里刪除一些危險(xiǎn)操作的時(shí)候,,我們就會(huì)有一個(gè)診斷程序來(lái)提醒程序員,。也就是這個(gè),來(lái)源于我當(dāng)年的一個(gè)任務(wù),。 

細(xì) 節(jié) 

更多關(guān)于 DllMain 的使用都可以在 MSDN 的這篇文章找到:Dynamic-Link Library BestPractices ,,在這里我放一些那篇文章的摘要: 

在加載器鎖被掛起的時(shí)候就會(huì)調(diào)用 DllMain。因此那些能調(diào)用 DllMain 的函數(shù)會(huì)有比較多的限制,。結(jié)果就是,,DllMain 被設(shè)計(jì)為只能使用小部分的微軟的 Windows API 來(lái)執(zhí)行很少的初始化任務(wù)。你不能在 DllMain 里直接或間接的調(diào)用任何函數(shù)來(lái)嘗試申請(qǐng)加載器鎖,。不然,,你的應(yīng)用程序會(huì)有可能死鎖或崩潰。DllMain 里的一個(gè)錯(cuò)誤可能危及整個(gè)進(jìn)程以及它的所有線程,。 

最理想的 DllMain 應(yīng)該是一個(gè)空的樁代碼(stub),。但是考慮到很多應(yīng)用程序的復(fù)雜性,,這樣太受限了。所以使用 DllMain 的一條比較好的原則就是盡可能的延長(zhǎng)初始化,。晚點(diǎn)初始化能增強(qiáng)應(yīng)用程序的魯棒性,,因?yàn)楫?dāng)加載器鎖被掛起的時(shí)候,初始化無(wú)法完成,。而且,,晚點(diǎn)初始化能讓你更安全地使用比較多的 Windows API。 

一些初始化任務(wù)無(wú)法推遲,。舉個(gè)例子,,如果布局文件(configuration file)出錯(cuò)了或者包含有垃圾,那么依賴于布局文件的動(dòng)態(tài)鏈接庫(kù)(DLL)就無(wú)法加載,。對(duì)于這種類型的初始化,,那些動(dòng)態(tài)鏈接庫(kù)應(yīng)該嘗試著去加載,如果加載失敗就立馬退出,,而不是浪費(fèi)其他資源去做其他事,。 

在 DllMain 里面你不應(yīng)該做以下這些事: 

  • 調(diào)用 LoadLibrary 或者 LoadLibraryEx(不管是直接還是間接)。這樣會(huì)引起死鎖或崩潰,。 

  • 調(diào)用 GetStringTypeA, GetStringTypeEx,或者 GetStringTypeW(不管是直接還是間接),,這樣會(huì)引起死鎖或崩潰。? 于其他進(jìn)程同步,。這樣會(huì)引起死鎖,。 

  • 申請(qǐng)一個(gè)同步對(duì)象,而該產(chǎn)生該對(duì)象的代碼正在等待申請(qǐng)加載器鎖,。這樣會(huì)引起死鎖,。 

  • 在一定條件下用 CoInitializeEx 初始化 COM 進(jìn)程。這個(gè)函數(shù)會(huì)調(diào)用 LoadLibraryEx. 

  • 調(diào)用注冊(cè)函數(shù),。這些注冊(cè)函數(shù)是由 Advapi32.dll 來(lái)實(shí)現(xiàn)的,。如果 Advapi32.dll 沒有在你的動(dòng)態(tài)鏈接庫(kù)之前初始化完成,那你的動(dòng)態(tài)鏈接庫(kù)可能就會(huì)訪問非初始化的內(nèi)存,,然后引起進(jìn)程崩潰,。 

  • 調(diào)用 CreateProcess. 創(chuàng)建一個(gè)進(jìn)程會(huì)加載其他動(dòng)態(tài)鏈接庫(kù)。 

  • 調(diào)用 ExitThread. 在動(dòng)態(tài)鏈接庫(kù)沒加載的情況下退出一個(gè)進(jìn)程需要再次申請(qǐng)加載器鎖,,然后就會(huì)引起死鎖或者進(jìn)程崩潰,。 

  • 調(diào)用 CreateThread. 新建一個(gè)進(jìn)程后就算你不把它與其他進(jìn)程同步它可能也能運(yùn)行,但是很危險(xiǎn),。 

  • 創(chuàng)建一個(gè)命名 pipe 或者其他命名對(duì)象(僅針對(duì) Windows 2000),。在 Windows 2000 中,命名對(duì)象由終端服務(wù)的動(dòng)態(tài)鏈接庫(kù)(Terminal Services DLL).如果這個(gè)動(dòng)態(tài)鏈接庫(kù)還沒有初始化,調(diào)用它就會(huì)引起進(jìn)程崩潰,。 

  • 使用 C 語(yǔ)言的動(dòng)態(tài)運(yùn)行時(shí)庫(kù)(CRT)里的內(nèi)存管理函數(shù)。如果 CRT DLL 還沒有初始化,,調(diào)用這些函數(shù)就會(huì)引起進(jìn)程崩潰,。 

  • 調(diào)用 User32.dll 或 Gdi32.dll 里的函數(shù)。有些函數(shù)會(huì)加載其他 DLL,,而這些 DLL 可能還沒有初始化,。 

  • 使用受監(jiān)督代碼 

正 確 代 碼 

上面那段引自 LibreOffice 項(xiàng)目的代碼塊有的時(shí)候能運(yùn)行,有的時(shí)候不能運(yùn)行,?!珣{運(yùn)氣。 

要解決類似這樣的問題比較麻煩,。你需要盡可能地重組你的代碼以使 DllMain 函數(shù)更簡(jiǎn)單,,更簡(jiǎn)短。 

建 議 

很難給出建議,。你不可能萬(wàn)事皆知,,每個(gè)人都有可能遇到這種離奇的錯(cuò)誤。所能給的建議無(wú)非是:對(duì)于你所負(fù)責(zé)的項(xiàng)目,,要仔細(xì)閱讀所有文檔,。但你也知道,我們無(wú)法預(yù)見所有的問題,。當(dāng)你把所有的時(shí)間都用來(lái)閱讀文檔,,那就沒有時(shí)間來(lái)編程了。還有,,就算你已經(jīng)看了 N 篇文章,,你也無(wú)法確定你有沒有漏掉那些告訴你如何應(yīng)付其他問題的文章。

我希望我可以給你一些有用的小貼上,,但很遺憾,,我只能想到一個(gè):使用靜態(tài)分析器。不,,這也不能保證你的代碼沒有任何 bug,。如果多年前有一個(gè)代碼分析器告訴我說,我不能再DllMain 調(diào)用 foo 函數(shù),,我可能會(huì)節(jié)省很多時(shí)間和精力:無(wú)法完成那個(gè)任務(wù)真的讓我蠻生氣的,,快要?dú)庹恕?nbsp;

6. 檢查所有指針強(qiáng)制轉(zhuǎn)換成 integer 類型的代碼快 

下面的代碼選自 IPP Samples 項(xiàng)目。這里的錯(cuò)誤被 PVS-Studio 診斷為:V205 強(qiáng)制轉(zhuǎn)換指針類型為 32 位 integer 類型:(unsigned long)(img) 

void write_output_image(...., const Ipp32f *img, 

                                                          ...., const Ipp32s iStep) { 

    ... 

    img = (Ipp32f*)((unsigned long)(img) + iStep); 

    ... 

注意,。有人會(huì)說因?yàn)橐恍┰?,這段代碼并不是最好的例子。我們并不關(guān)注為什么一個(gè)程序員會(huì)需要以這樣奇怪的方式進(jìn)入數(shù)據(jù)緩沖區(qū)。我們?cè)谝獾膬H僅是,,指針被轉(zhuǎn)換成了“unsignedlong”類型,。我選擇這個(gè)例子只是因?yàn)樗?jiǎn)潔。

解 釋 

一個(gè)程序員想要把一個(gè)指針轉(zhuǎn)換為特定數(shù)目的字節(jié),。這段代碼在 win32 下能夠正確運(yùn)行,,因?yàn)橹羔樀拇笮≌玫扔?long 類型的大小。但是如果我們編譯的是這段代碼的 64 位版本,,指針就會(huì)變成 64 位,,將它強(qiáng)制轉(zhuǎn)換成 long 類型會(huì)確實(shí)高位。 

注意,。Linux 使用不同的數(shù)據(jù)模型,。在 64 位的 Linux 項(xiàng)目中,“long”類型也是 64 位,。雖然如此,,但用“long”來(lái)存儲(chǔ)指針委實(shí)不是一個(gè)好主意。首先,,這樣的代碼比較常用于 Windows 應(yīng)用程序中,,但是在 Windows 下面,它是出錯(cuò)的,。其次,,有專門用來(lái)存儲(chǔ)指針類型的數(shù)據(jù)類型,比如說,,intptr_t. 用這些類型可以讓程序看上去更干凈一些,。 

在上面的例子中,我們可以看到一個(gè)發(fā)生于 64 位程序中的典型錯(cuò)誤,。更直白一點(diǎn)說,,在 64位編程的道路上,程序員還會(huì)遇到很多其他的錯(cuò)誤,。但是把指針轉(zhuǎn)換為 32 位 integer 變量是一個(gè)最常見也最隱秘的問題,。這個(gè)錯(cuò)誤可以用下面的圖來(lái)表示: 

圖 1 A)32 位程序 B)指向低位地址的 64 位指針。C)被破壞的 64 位指針,。 

講一下它的隱秘性,,這個(gè)錯(cuò)誤有時(shí)候真的很難注意到。程序大多數(shù)都運(yùn)行正確,。在使用這個(gè)函數(shù)時(shí),,丟失指針里的重要位數(shù)這種事可能只會(huì)出現(xiàn)在少有的幾個(gè)小時(shí)里。因?yàn)榉峙涞降膬?nèi)存是來(lái)自低位地址的,,也就是為什么所有的對(duì)象和數(shù)組都保存在前 4G 內(nèi)存里,。一切都很順利,。 

當(dāng)程序持續(xù)運(yùn)行,內(nèi)存變得四分五裂的,,這樣即使這個(gè)程序并沒有用到那么多內(nèi)存,,新建的對(duì)象也可能會(huì)被創(chuàng)建在前 4GB 以外。然后,,問題開始產(chǎn)生了,。要重現(xiàn)這樣的錯(cuò)誤非常難。 

正 確 代 碼 

你可以使用 size_t, INT_PTR, DWORD_PTR, intrptr_t, 等等這些類型來(lái)存儲(chǔ)指針,。 

img = (Ipp32f*)((uintptr_t)(img) + iStep); 

事實(shí)上,我們可以不強(qiáng)制轉(zhuǎn)換指針的,。并沒有見到在哪里有提到過說格式化不同于標(biāo)準(zhǔn)化的,,也就是為什么使用__declspec(align( # ))這樣函數(shù)也沒產(chǎn)生作用。

所以,,除非要轉(zhuǎn)換成數(shù)字類型的指針能被 Ipp32f 整除,,不然我們會(huì)觸發(fā)一些沒有定義過的行為。(詳情看:EXP36-C) 

所以,,我們可以這樣寫: 

img += iStep / sizeof(*img); 

建 議 

使用特定的類型來(lái)存儲(chǔ)指針——把 intlong 忘掉吧,。最常用來(lái)存儲(chǔ)指針的類型是:intptr_tuintptr_t。在 Visual C++ 里面,,也可以用這些類型:INT_PTR, UINT_PTR, LONG_PTR,ULONG_PTR, DWORD_PTR,。從它們的名字你就可以看出來(lái),用它們來(lái)存儲(chǔ)指針很安全,。 

size_tptrdiff_t 也可以用來(lái)存儲(chǔ)指針,,但是我不建議用,因?yàn)樗鼈冊(cè)镜哪康氖怯脕?lái)存儲(chǔ)大小和下標(biāo)的,。 

你不能用 uintptr_t 類里的成員函數(shù)來(lái)存儲(chǔ)指針,。成員函數(shù)跟標(biāo)準(zhǔn)函數(shù)還是有些許不同的。除去指針本身,,成員函數(shù)會(huì)隱藏掉指向其他類對(duì)象的 this 指針的值,。然而,這也不重要——在32 位程序中,,你無(wú)法把這樣一個(gè)指針賦值給 unsigned int 類型,。這些指針一般會(huì)以一些特殊的方式處理的,這就是為什么 64 位程序里沒那么多錯(cuò)誤,。至少,,我沒有看到這樣的錯(cuò)誤。 

如果你打算把你的程序編譯成 64 位,,第一件事就是,,你需要檢查并修改所有把指針強(qiáng)制轉(zhuǎn)換為 32 位 integer 類型的代碼塊,。記住——可能程序里還是有其他錯(cuò)誤,但是你應(yīng)該從指針開始檢查,。 

對(duì)于那些在開發(fā)或者打算開發(fā) 64 位應(yīng)用程序的,,我建議你們看一下這個(gè)材料:Lessons ondevelopment of 64-bit C/C++ applications. 

7. 別在循環(huán)里調(diào)用 alloca() 函數(shù) 

在 Pixie 項(xiàng)目中發(fā)現(xiàn)了這個(gè) bug。該錯(cuò)誤被 PVS-Studio 診斷為:V505 在循環(huán)里使用了“alloca”函數(shù),。這樣很快就會(huì)導(dǎo)致棧溢出,。 

inline void triangulatePolygon(....) { 

   ... 

   for (i=1;i

   ...

   do{

       ...

       do{

          ...

          CTriVertex *snvertex=

         (CTriVertex *) alloca(2*sizeof(ctrivertex));

          ...

       }while(dvertex != loops[0]);

     ...

    }while(svertex=!= loops[i]);

     ...

  }

    ...

 }

解 釋

alloca(size_t) 函數(shù)使用棧來(lái)分配內(nèi)存。由 alloca() 分配到的內(nèi)存要在函數(shù)執(zhí)行完的時(shí)候才被回收,。

分給程序的棧內(nèi)存通常不多,。當(dāng)你在 visual c++里新建一個(gè)項(xiàng)目的時(shí)候,你可以看到默認(rèn)就分配 1mb 的棧內(nèi)存,,這也就是為什么如果 alloca() 函數(shù)是在循環(huán)里的話,,它很快就把可用的棧內(nèi)存耗光了。

在上面那個(gè)例子中,,一次就有三個(gè)循環(huán),,所以會(huì)造成棧溢出。

在循環(huán)里使用類似 a2w 的宏也不安全,,因?yàn)樗鼈冎卸加姓{(diào)用 alloca() 的函數(shù),。

就像我們前面所說的,windows 程序默認(rèn)使用的棧內(nèi)存,。我們可以修改這個(gè)值,,在項(xiàng)目設(shè)置里找到并“stack reserve size”和“stack commit size”的參數(shù)值。詳情:“stack (stack allocations)”,。然而我們應(yīng)該明白,,擴(kuò)大棧內(nèi)存并不能解決這個(gè)問題——你只是推遲了項(xiàng)目棧溢出的時(shí)間。

建 議

不要在循環(huán)里調(diào)用函數(shù),。如果你有一個(gè)循環(huán),,然后你又需要開辟一塊臨時(shí)緩沖區(qū),用下面的 3 個(gè)法子中任意一個(gè)來(lái)解決:

  1. 提前申請(qǐng)內(nèi)存,,然后使用一個(gè)緩沖區(qū)來(lái)進(jìn)行所有操作,。如果你每次需要的緩沖區(qū)大小都不一樣,申請(qǐng)最大塊的內(nèi)存,。如果不行(你不知道具體它會(huì)需要多少內(nèi)存),,使用法2。

  2. 把循環(huán)單獨(dú)放在一個(gè)函數(shù)里,。這樣,,緩沖區(qū)在每一次迭代的時(shí)候都會(huì)申請(qǐng)、釋放,。如果這樣也不行,,就只剩下法3了,。

  3. malloc()m 或者其他操作來(lái)代替 alloca(),或者使用類似 std::vector 函數(shù),。但是這樣分配內(nèi)存會(huì)比較耗時(shí)間,。如果使用 malloc/new 你就要考慮到它的釋放。另一方面,,這樣你在用比較大的數(shù)給客戶演示程序的時(shí)候不會(huì)發(fā)生棧溢出,。

8. 記住析構(gòu)函數(shù)里的異常很危險(xiǎn)

這個(gè)問題是在 libreoffice 項(xiàng)目里發(fā)現(xiàn)的。在 pvs-studio 中診斷為:v509 應(yīng)該在 try……catch 里放‘dynamic_cast

virtual ~LazyFieldmarkDeleter() 

  dynamic_cast

      (*m_pFieldmark.get()).ReleaseDoc(m_pDoc); 

解 釋 

析構(gòu)函數(shù)可以在異常發(fā)生后在清理?xiàng)?nèi)存的過程中調(diào)用,,也可以在對(duì)象生命周期結(jié)束時(shí)被調(diào)用,。如果在異常已經(jīng)發(fā)生時(shí)調(diào)用析構(gòu)函數(shù),而在清理?xiàng)?nèi)存時(shí)又發(fā)生異常,,異常之上又有異常,,C++庫(kù)只能調(diào)用 terminate() 來(lái)終止程序。所以從中我們可以知道,,不要在析構(gòu)函數(shù)中拋出異常。析構(gòu)函數(shù)中的異常要在同一個(gè)析構(gòu)函數(shù)中處理,。 

上面引用的代碼不只是危險(xiǎn)那么簡(jiǎn)單,。動(dòng)態(tài)強(qiáng)制轉(zhuǎn)換操作失敗的話會(huì)生成一個(gè)名為“std::bad_cast”的異常。 

類似的,,任何可以拋出異常的構(gòu)造函數(shù)也是危險(xiǎn)的,。比如說,在析夠函數(shù)中用 new 操作來(lái)分配內(nèi)存就不安全,,因?yàn)槿绻峙洳怀晒?huì)拋出“std::bad_alloc”異常,。 

正 確 代 碼 

要解決 dynamic_cast 可以不用引用類型而是用指針類型。這樣,,就算轉(zhuǎn)換類型失敗也不會(huì)拋出異常,。 

virtual ~LazyFieldmarkDeleter() 

    auto p = dynamic_castm_pFieldmark.get(); 

    if (p) 

       p->ReleaseDoc(m_pDoc); 

建 議 

讓析夠函數(shù)盡可能的簡(jiǎn)單。析夠函數(shù)不是用來(lái)分配內(nèi)存和文件讀取的,。 

當(dāng)然,,有時(shí)候我們無(wú)法做到讓析夠函數(shù)變得簡(jiǎn)單,但是我們要盡力達(dá)到這一點(diǎn),。此外,,復(fù)雜的析夠函數(shù)也說明類的設(shè)計(jì)不合理,以及這個(gè)方案不是最簡(jiǎn)便的方案,。你在析夠函數(shù)里寫的代碼越多,,它越難把問題解決好。因?yàn)檫@樣,,更難指出那個(gè)代碼塊可以或者不可以拋出異常,。 

如果有一定幾率會(huì)發(fā)生異常的話,,我們可以把它都最后寫在 catch(...) 里: 

virtual ~LazyFieldmarkDeleter() 

    try 

   { 

     dynamic_cast

     (*m_pFieldmark.get()).ReleaseDoc(m_pDoc); 

  } 

  catch (...) 

  { 

      assert(false); 

   } 

沒錯(cuò),這樣使用可能會(huì)掩蓋住一些發(fā)生在析構(gòu)函數(shù)里的錯(cuò)誤,,但這也有助于程序更穩(wěn)定的運(yùn)行,。 

我并不堅(jiān)持說一定不要在構(gòu)造函數(shù)里拋出異常——視情況而定吧,。有時(shí)在析構(gòu)函數(shù)里拋出異常會(huì)更有用,。我也看到過一下專門的類中是可以拋出異常的,但這些情況很少,,而且那些類就是被設(shè)計(jì)為可以在析夠函數(shù)里拋出異常的,。如果是比較常見的類如:'own string','dot','brush' 'triangle', 'document'等等,就不應(yīng)該在析構(gòu)函數(shù)里拋出異常,。 

只要記住,,在一個(gè)異常之上又拋出異常會(huì)讓程序終止,。所以,,一切都取決于你想不想讓這種事發(fā)生在你的程序身上。 

9. 用 '\0' 來(lái)結(jié)束空字符 

片段選自 Notepad++項(xiàng)目,。其錯(cuò)誤被 PVS-Studio 診斷為:V528 'char' 指針要跟'\0' 比較值很奇怪,。 可能是想表達(dá):*headerM != '\0'。 

TCHAR headerM[headerSize] = TEXT(''); 

... 

size_t Printer::doPrint(bool justDoIt) 

   ... 

   if (headerM != '\0') 

   ... 


解 釋 

感謝這段代碼的作者,,使用'\0'來(lái)作為結(jié)束空字符,,這讓我們很容易定位和修正這個(gè)錯(cuò)誤。作者大大做得好,,但還不夠好,。 

假設(shè)代碼是這樣寫的: 

if (headerM != 0) 


數(shù)組地址和 0 比較,這其實(shí)沒什么意義,,因?yàn)楸容^結(jié)果總是 true,。所以這是什么——一個(gè)錯(cuò)誤或者多余的檢查?很難說,,尤其是如果這個(gè)代碼是別人的代碼或者說這個(gè)代碼寫了很久,。 

但既然程序員在這段代碼中用了'\0',我們就假設(shè)程序員是想要檢查某一個(gè)字符的值,。然后我們又知道,,比較 headerM 指針和 NULL 沒意義。所以我們認(rèn)為作者是想知道那個(gè)字符串是否為空或者在寫檢測(cè)的時(shí)候沒出錯(cuò),。要修改這段代碼,,我們需要增加一個(gè)指針間接引用操作。 

正 確 代 碼 

TCHAR headerM[headerSize] = TEXT(''); 

... 

size_t Printer::doPrint(bool justDoIt) 

    ... 

    if (*headerM != _T('\0')) 

    ... 

建 議 

0 可以表示 NULL,,false,空字符'\0',,或者僅僅是 0,。所以請(qǐng)不要偷懶——避免在任一個(gè)案例中使用 0 作為簡(jiǎn)寫的符號(hào)。 

用下面的符號(hào): 

  • 0——整數(shù) 0; 

  • nullptr——C++中的空指針; 

  • NULL——C 中的空指針 

  • '\0', L'\0', _T('\0')——空結(jié)束 

  • 0.0, 0.0f——浮點(diǎn)類型的 0 

  • false,,F(xiàn)ALSE——’false‘值 

用這種方法會(huì)讓你的代碼更干凈,也讓你和其他程序員在重新檢查代碼時(shí)更容易定位 bug,。 

10.  避免使用過多的小塊 #ifdef 代碼 

代碼選自 CoreCLR 項(xiàng)目,。其錯(cuò)誤被 PVS-Studio 診斷為:V522 可能會(huì)發(fā)生空指針‘hp’的間接引用,。

解 釋 

我總覺得#ifdef/#endif 是有害的——一個(gè)無(wú)可避免的害處。不幸的是,,它們是必須的,我們不得不使用它們,。所以我不主張讓你停止使用#ifdef,,這么做沒意義。但我想讓你注意別“濫用”它,。 

我猜,你們中很多人都有見到過那種代碼,,滿屏的#ifdef. 要看那種每十行就來(lái)一個(gè)#ifdef,或者更頻繁的代碼,,真的很痛苦,。這種代碼是依賴于系統(tǒng)的,而且你不能不用#ifdef,。盡管它的存在沒有讓你更快樂一些,。 

你看,要閱讀上面那段代碼是多么難,!有些程序員的基本工作就是閱讀代碼,。是的,你沒看錯(cuò),。我們花了非常非常多的時(shí)間來(lái)檢查和學(xué)習(xí)已經(jīng)寫好的代碼,,比花在寫新代碼的時(shí)間上要多得多,。這也就是為什么閱讀難讀的代碼會(huì)降低我們的效率,,而且也比較容易出錯(cuò)。 

回過頭來(lái)看我們的代碼,,錯(cuò)誤是在空指針間接引用的時(shí)候發(fā)現(xiàn)的,,在沒有聲明MULTIPLE_HEAPS 宏的時(shí)候發(fā)生的,。為讓你更容易理解,我們來(lái)展開那個(gè)宏: 

heap_segment* gc_heap::get_segment_for_loh (size_t size) 

    gc_heap* hp = 0; 

    heap_segment* res = hp->get_segment (size, TRUE); 

    .... 

程序員聲明了 hp 變量,,初始化為 null,,然后立刻就間接引用它了。如果此時(shí)MULTIPLE_HEAPS 還沒有定義,,自然會(huì)出錯(cuò),。 

正 確 代 碼 

雖然我的同時(shí)已經(jīng)在“25 Suspicious Code Fragments in CoreCLR”這篇文章中報(bào)告了這個(gè)錯(cuò)誤,但是這個(gè)問題依然存在于 CoreCLR(12.04.2016)中,。所以我也不知道解決這個(gè)錯(cuò)誤的最好的法子是什么,。 

我的想法是,既然(hp == nullptr), 那變量‘res’就應(yīng)該也初始化為其他值——但是我不知道具體是什么值,。所以這次我們什么也不用做,。 

建 議 

從你的代碼中消除小塊#ifdef/#endif——它們的存在真的讓代碼閱讀與理解變得很困難。過多的 ifdef 更容易出現(xiàn)錯(cuò)誤,。 

并沒有適用于任何情況的建議——都得視具體情況而定,。無(wú)論如何,記住,,ifdef 是麻煩之源,。所以你應(yīng)該努力使你的代碼盡可能的干凈整潔。 

Tip N1: 試著拒絕#ifdef 

#ifdef 有時(shí)可以用常量和 if 代替,。比較一下下面兩段代碼:一個(gè)宏的變體: 


#define DO 1 

#ifdef DO 

static void foo1() 

    zzz(); 

#endif //DO 

void F(){#ifdef DO 

   foo1(); 

#endif // DO 

   foo2();

這段代碼不好讀,,你不會(huì)愿意讀的。我打賭,,你直接略過了,,有沒有? 現(xiàn)在來(lái)看下面的代碼: 

const bool DO = true; 

static void foo1() 

     if (!DO) 

        return; 

     zzz(); 

void F() 

    foo1(); 

    foo2(); 

現(xiàn)在更好讀一點(diǎn)了,。有人會(huì)說這樣的話代碼效率就沒那么好了,,因?yàn)樗{(diào)用函數(shù),然后還要在函數(shù)里檢查它有沒有定義過,。我不同意這樣的說法,。首先,當(dāng)代的編譯器都非常智能,,你非常有可能會(huì)在 release 版本看到這樣的代碼:沒有任何外部檢查和函數(shù)調(diào)用,。其次,這一點(diǎn)點(diǎn)損失就沒必要過分糾結(jié)了,,好嗎,。代碼的干凈整潔更重要。 

Tip N2 :讓你的#ifdef 代碼塊更大一點(diǎn) 

如果我要寫那個(gè) get_segment_for_loh() 函數(shù),我不會(huì)在其中使用很多#ifdef,,我會(huì)用兩個(gè)版本的函數(shù)來(lái)代替,。對(duì),字可能會(huì)比較多一點(diǎn),,但是函數(shù)很容易閱讀和編輯了,。 

再者,有人會(huì)說,,這是復(fù)制代碼,,既然每個(gè)#ifdef 都有很多長(zhǎng)度比較長(zhǎng)的函數(shù)。每個(gè)函數(shù)都有兩個(gè)版本容易讓人在修改其中一個(gè)版本時(shí)忘記修改另一個(gè)版本,。 

嘿,,等一下。為什么你的函數(shù)長(zhǎng)度那么長(zhǎng),?把所有的邏輯放到額外的備用函數(shù)——這樣你兩個(gè)版本的函數(shù)都會(huì)變得更段一些,。我敢肯定這樣能讓你更容易找出它們之間的不同。 

我知道這個(gè) Tip 并不能包治百病,,但也要考慮這么做,。 

Tip N3. 考慮使用模板(templates)——有時(shí)它們也能幫到你。 

Tip N4.在使用#ifdef 之前花點(diǎn)時(shí)間來(lái)考慮:你可以不用它嗎,?或者,,你可以少用它并且只在一個(gè)地方使用它嗎?


原文:https://software.intel.com/en-us/articles/the-ultimate-question-of-programming-refactoring-and-everything

本文由看雪翻譯小組 lumou 編譯


相 關(guān) 閱 讀:


從逆向工程的角度來(lái)看C++ (一) C++ 之?dāng)?shù)據(jù)類型

從逆向工程的角度來(lái)看C++ (二) C++ 之 友元關(guān)系

從逆向工程的角度來(lái)看C++ (四)

......

更多優(yōu)秀文章點(diǎn)擊左下角“關(guān)注原文”查看,!

看雪論壇:http://bbs./

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn),。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào),。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多