• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            隨筆-341  評論-2670  文章-0  trackbacks-0
                復雜的東西寫多了,如今寫點簡單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發維護的遍歷、減少粗心犯下的錯誤以及增強單元測試、回歸測試和測試工具,因此記錄下一些開發上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。

                上一篇文章講到了如何檢查內存泄露。其實只要肯用C++的STL里面的高級功能的話,內存泄露是很容易避免的。我在開發Vczh Library++ 3.0的時候,所有的測試用例都保證跑完了沒有內存泄露。但是很可惜有些C++團隊不能使用異常,更甚者不允許寫構造函數析構函數之類,前一個還好,后一個簡直就是在用C。當然有這些變態規定的地方STL都是用不了的,所以我們更加需要扎實的基礎來開發C++程序。

                今天這一篇主要還是講指針的問題。因為上一篇文章一筆帶過,今天就來詳細講內存泄漏或者野指針發生的各種情況。當然我不可能一下子舉出全部的例子,只能說一些常見的。

                一、錯誤覆蓋內存。

                之前提到的不能隨便亂memset其實就是為了避免這個問題的。其實memcpy也不能亂用,我們來看一個例子,最簡單的:
             1 #define MAX_STRING 20;
             2 
             3 struct Student
             4 {
             5   char name[MAX_STRING];
             6   char id[MAX_STRING];
             7   int chinese;
             8   int math;
             9   int english;
            10 };

                大家對這種結構肯定十分熟悉,畢竟是大學時候經常要寫的作業題……好了,大家很容易看得出來這其實是C語言的經典寫法。我們拿到手之后,一般會先初始化一下,然后賦值。
            1 Student vczh;
            2 memset(&vczh, 0sizeof(vczh));
            3 strcpy(vczh.name, "vczh");
            4 strcpy(vczh.id, "VCZH'S ID");
            5 vczh.chinese=70;
            6 vczh.math=90;
            7 vczh.english=80;

                為什么要在這里使用memset呢?memset的用處是將一段內存的每一個字節都設置成同一個數字。這里是0,因此兩個字符串成員的所有字節都會變成0。因此在memset了Student之后,我們通過正常方法來訪問name和id的時候都會得到空串。而且如果Student里面有指針的話,0指針代表的是沒有指向任何有效對象,因此這個時候對指針指向的對象進行讀寫就會立刻崩潰。對于其他數值,0一般作為初始值也不會有什么問題(double什么的要小心)。這就是我們寫程序的時候使用memset的原因。

                好了,如今社會進步,人民當家做主了,死程們再也不需要受到可惡的C語言剝削了,我們可以使用C++!因此我們借助STL的力量把Student改寫成下面這種帶有C++味道的形式:
            1 struct Student
            2 {
            3   std::string name;
            4   std::string id;
            5   int chinese;
            6   int math;
            7   int english;
            8 };

                我們仍然需要對Student進行初始化,不然三個分數還是隨機值。但是我們又不想每一次創建的時候都對他們分別進行賦值初始化城0。這個時候你心里可能還是想著memset,這就錯了!在memset的時候,你會把std::string內部的不知道什么東西也給memset掉。假如一個空的std::string里面存放的指針指向的是一個空的字符串而不是用0來代表空的時候,一下子內部的指針就被你刷成0,等下std::string的析構函數就沒辦法delete掉指針了,于是內存泄露就出現了。有些朋友可能不知道上面那句話說的是什么意思,我們現在來模擬一下不能memset的std::string要怎么實現。

                為了讓memset一定出現內存泄露,那么std::string里面的指針必須永遠都指向一個有效的東西。當然我們還需要在字符串進行復制的時候復制指針。我們這里不考慮各種優化技術,用最簡單的方法做一個字符串出來:
             1 class String
             2 {
             3 private:
             4   char* buffer;
             5 
             6 public:
             7   String()
             8   {
             9     buffer=new char[1];
            10     buffer[0]=0;
            11   }
            12 
            13   String(const char* s)
            14   {
            15     buffer=new char[strlen(s)+1];
            16     strcpy(buffer, s);
            17   }
            18 
            19   String(const String& s)
            20   {
            21     buffer=new char[strlen(s.buffer)+1];
            22     strcpy(buffer, s.buffer);
            23   }
            24 
            25   ~String()
            26   {
            27     delete[] buffer;
            28   }
            29 
            30   String& operator=(const String& s)
            31   {
            32     delete[] buffer;
            33     buffer=new char[strlen(s.buffer)+1];
            34     strcpy(buffer, s.buffer);
            35   }
            36 };

                于是我們來做一下memset。首先定義一個字符串變量,其次memset掉,讓我們看看會發生什么事情:
            1 string s;
            2 memset(&s, 0sizeof(s));

                第一行我們構造了一個字符串s。這個時候字符串的構造函數就會開始運行,因此strcmp(s.buffer, "")==0。第二行我們把那個字符串給memset掉了。這個時候s.buffer==0。于是函數結束了,字符串的析構函數嘗試delete這個指針。我們知道delete一個0是不會有問題的,因此程序不會發生錯誤。我們活生生把構造函數賦值給buffer的new char[1]給丟了!鐵定發生內存泄露!

                好了,提出問題總要解決問題,我們不使用memset的話,怎么初始化Student呢?這個十分好做,我們只需要為Student加上構造函數即可:
            1 struct Student
            2 {
            3   .//不重復那些聲明
            4 
            5   Student():chinese(0),math(0),english(0)
            6   {
            7   }
            8 };

                這樣就容易多了。每當我們定義一個Student變量的時候,所有的成員都初始化好了。name和id因為string的構造函數也自己初始化了,因此所有的成員也都初始化了。加入Student用了一半我們想再初始化一下怎么辦呢?也很容易:
            1 Student vczh;
            2 .//各種使用
            3 vczh=Student();

                經過一個等號操作符的調用,舊Student的所有成員就被一個新的初始化過的Student給覆蓋了,就如同我們對一個int變量重新賦值一樣常見。當然因為各種復制經常會出現,因此我們也要跟上面貼出來的string的例子一樣,實現好那4個函數。至此我十分不理解為什么某些團隊不允許使用構造函數,我猜就是為了可以memset,其實是很沒道理的。

                二、異常。

                咋一看內存泄露跟異常好像沒什么關系,但實際上這種情況更容易發生。我們來看一個例子:
             1 char* strA=new char[MAX_PATH];
             2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
             3 char* strB=new char[MAX_PATH];
             4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
             5 
             6 DoSomething(strA, strB);
             7 
             8 RELEASE_STRB:
             9 delete[] strB;
            10 RELEASE_STRA:
            11 delete[] strA;

                相信這肯定是大家的常用模式。我在這里也不是教唆大家使用goto,不過對于這種例子來說,用goto是最優美的解決辦法了。但是大家可以看出來,我們用的是C++,因為這里有new。如果DoSomething發生了異常怎么辦呢?如果GetXXX發生了異常怎么辦呢?我們這里沒有任何的try-catch,一有異常,函數里克結束,兩行可憐的delete就不會被執行到了,于是內存泄漏發生了!

                那我們如何避免這種情況下的內存泄露呢?一些可愛的小盆友可能會想到,既然是因為沒有catch異常才發生的內存泄露,那我們來catch吧:
             1 char* strA=new char[MAX_PATH];
             2 try
             3 {
             4   if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
             5   char* strB=new char[MAX_PATH];
             6   try
             7   {
             8     if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
             9     DoSomething(strA, strB);
            10   }
            11   catch()
            12   {
            13     delete[] strB;
            14     throw;
            15   }
            16 }
            17 catch()
            18 {
            19   delete[] strA;
            20   throw;
            21 }
            22 
            23 RELEASE_STRB:
            24 delete[] strB;
            25 RELEASE_STRA:
            26 delete[] strA;

                你能接受嗎?當然是不能的。問題出在哪里呢?因為C++沒有try-finally。你看這些代碼到處都是雷同的東西,顯然我們需要編譯器幫我們把這些問題搞定。最好的解決方法是什么呢?顯然還是構造函數和析構函數。總之記住,如果想要事情成對發生,那么使用構造函數和析構函數。

                第一步,GetXXX顯然只能支持C模式的東西,因此我們要寫一個支持C++的:
             1 bool GetXXX2(string& s)
             2 {
             3   char* str=new char[MAX_PATH];
             4   bool result;
             5   try
             6   {
             7     result=GetXXX(str, MAX_PATH);
             8     if(result)s=str;
             9   }
            10   catch()
            11   {
            12     delete[] str;
            13     throw;
            14   }
            15   delete[] str;
            16   return result;
            17 }

                借助這個函數我們可以看到,因為有了GetXXX這種C的東西,導致我們多了多少麻煩。不過這總是一勞永逸的,有了GetXXX2和修改之后的DoSomething2之后,我們就可以用更簡單的方法來做了:
            1 string a,b;
            2 if(GetXXX2(a) && GetXXX2(b))
            3 {
            4   DoSomething2(a, b);
            5 }

                多么簡單易懂。這個代碼在任何地方發生了異常,所有new的東西都會被delete。這就是析構函數的一個好處。一個變量的析構函數在這個變量超出了作用域的時候一定會被調用,無論代碼是怎么走出去的。

                今天就說到這里了。說了這么多還是想讓大家不要小看構造函數和析構函數。那種微不足道的因為一小部分不是瓶頸的性能問題而放棄構造函數和析構函數的做法,終究是要為了修bug而加班的。只要明白并用好了構造函數、析構函數和異常,那么C++的特性也可以跟C一樣清楚明白便于理解,而且寫出來的代碼更好看的。大家期待第三篇哈。
            posted on 2010-06-23 10:12 陳梓瀚(vczh) 閱讀(11711) 評論(23)  編輯 收藏 引用 所屬分類: C++實用技巧

            評論:
            # re: C++實用技巧(二) 2010-06-23 12:53 | OwnWaterloo
            >>0指針代表的是沒有指向任何有效對象

            空指針的二進制表示并不一定是全0 。
            浮點數也一樣, 0.0f, 0.0, 0.0lf的二進制表示都不一定是全0。
            所以, 即使是C語言, 欲使用memset去將指針初始化為空, 或者將浮點初始化為0, 都是不可移植的。


            >>30 String& operator=(const String& s)
            >>31 {
            >>32 delete[] buffer;
            >>33 buffer=new char[strlen(s.buffer)+1];
            >>34 strcpy(buffer, s.buffer);
            >>35 }

            這個實現有問題, 當出現自賦值的時候:
            String s;
            s = s;
            this->buffer和s.buffer是同一個指針。
            32行delete之后, 已經是dangling pointer。
            33行傳遞給strlen, 34行傳遞給strcpy都是錯誤的。


            要么判斷自賦值的情況:
            if (this!=&s)
            {
            delete[] buffer;
            size_t len = strlen(s.buffer)+1;
            buffer = new char[ len ];
            memcpy(buffer, s.buffer, len );
            }

            但是, 如果new新的buffer時出現異常, 就會導致this有一個dangling pointer。
            為了安全, 可以先new, 再delete:
            size_t len = strlen(s.buffer)+1;
            char* p = new char[len]; // 之后操作都不會產生異常
            memcpy(p, s.buffer, len );
            delete[] buffer;
            buffer = p;

            先new再delete也可以不用判斷自賦值的情況了。
              回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 17:55 | zuhd
            @OwnWaterloo
            哥,你總是這么犀利,我給你留言看到沒?  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 18:33 | OwnWaterloo
            @zuhd
            剛發現…… 那一天收到的通知太多, 被我直接全部標記為已讀, 然后忘了……
            通常用同名gmail郵箱, qq不怎么用……
              回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 18:42 | 陳梓瀚(vczh)
            @OwnWaterloo
            為了迅速說明問題,那個string其實還有很多其他的缺陷的……因為在之后講構造函數和析構函數的一篇上會著重處理這種case。

            話說回來,我上一篇提到了,這個系列是默認你使用Visual C++的。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 18:46 | 陳梓瀚(vczh)
            @OwnWaterloo
            話說回來,因為在C++里面,字面量“0”的確是代表空指針,因此如果空指針的二進制真的不是0的話,我可以認為是編譯器的bug。因為從語法上講,既然void* p=0;是對的,那么void* p=(void*)(int)0;也必須是對的。

            除非從一開始就有C++0x的nullptr。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 19:25 | OwnWaterloo
            @陳梓瀚(vczh)
            無論是C還是C++, 說的都是"編譯時可求出0值的常數是空指針常量"。
            可以將空指針賦值給任意指針類型的變量。
            但并不保證賦值之后指針變量的二進制表示也是全0。


            從語法上講:
            void* p = 0;
            void* p=(void*)(int)0;
            都是對的, 就像:

            float f = 0;
            float f = (float)(int)0;
            而通常f的二進制表示并不是全0。
            這是因為編譯器是能看到這里的轉型動作, 并加入適當的操作。


            但這樣寫就是沒有保證的:
            void* p;
            memset(&p, 0, sizeof p);
            memset函數中已經不知道p的類型, 只能將p當作byte 數組。
            就沒有機會為指針類型作適當的調整。


            再舉個例子:
            class A {};
            class B {};
            class C : public A, public B {};

            C c;
            C* pc = &c;
            A* pa = pc;
            B* pb = pc;
            assert( pc==pa && pc==pb);

            因為編譯器知道pc,pa,pb的類型。
            所以無論是賦值還是比較, 都會插入適當的操作。

            而如果使用memset或者memcmp, 就掛了。


            最后, 標準中有句話, 在calloc的腳注里。
            The calloc function allocates space for an array
            of nmemb objects, each of whose size is size.
            The space is initialized to all bits zero.

            Note that this need not be the same as the representation
            of floating-point zero or a null pointer constant.
              回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 20:40 | 付翔
            指出個 小錯誤
            #define MAX_STRING 20; 不要分號   回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 21:49 | 陳梓瀚(vczh)
            @OwnWaterloo
            話說回來,那什么地方的空指針的二進制不是0?  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 21:50 | 陳梓瀚(vczh)
            @付翔
            嗯嗯,這是個錯誤。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 21:52 | 陳梓瀚(vczh)
            @OwnWaterloo
            不過這個二進制位不是0的事實更加反映出了構造函數的重要性,memset更加不能隨便來了(當然指的是非Visual C++的情況)  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 22:12 | OwnWaterloo
            @陳梓瀚(vczh)
            http://linuxdude.com/Steve_Sumit/C-faq/q5.17.html
            整個第5章都是講null pointer的。
              回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-23 22:35 | 陳梓瀚(vczh)
            @OwnWaterloo
            都是些從現在開始見都沒見過的機器……  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-24 01:18 | 匿名
            delete null
            確定程序不會拋異常?  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-24 01:46 | 匿名(還是俺)
            char* strA=new char[MAX_PATH];
            這個也不能保證它一定不會拋異常。
            new這個數組的時候,加入new到第M個(M小于MAX_PATH)時候拋出異常,這時候也會有內存泄露咯  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-24 03:28 | 陳梓瀚(vczh)
            @匿名(還是俺)
            如果new一個東西還是敗了,那程序就直接log了崩潰了吧  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-24 10:25 | paul_simon
            1 string s;
            2 memset(&s, 0, sizeof(s));
            //這里有個問題想和博主探討,下文中提到的buffer是String的一個private成
            //員,memset函數是不是該在String的成員函數里面調用,比如:
            String::mems()
            {
            memset(buffer,0,sizeof(buffer));
            }
            //或者可否考慮在String::String()里加入memset函數
            //QQ:29975723,謝謝  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-24 21:00 | 陳梓瀚(vczh)
            @paul_simon
            String當然要為自己負責了,如果String自己覺得memset合適,那當然可以。不過你那么搞有問題,除非buffer是一個數組而不是指針。

            但是外部不能memset掉string。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-25 01:07 | paul_simon
            @陳梓瀚(vczh)
            String的構造函數決定了buffer是個數組,這里的String和student是兩個不同的對象,String里的buffer成員不能被外部的memset所清零吧?
            所以我說的是
            1 string s;
            2 memset(&s, 0, sizeof(s));//這里的實質不是是對buffer進行清零嗎?  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-25 05:43 | 陳梓瀚(vczh)
            @paul_simon
            不是,因為類的私有部分是不透明的,所以你在外部操作的時候,不能以知道string的實現作為前提,人家改了怎么辦,你就要加班了。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-28 02:11 | 球球
            如果memset用在子類的初始化時...  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-28 03:22 | 陳梓瀚(vczh)
            @球球
            子類也不能假設父類是如何實現的。  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-29 00:06 | chenq
            用那么多代碼去try catch那個GetXXX,為什么不直接用smart pointer呢  回復  更多評論
              
            # re: C++實用技巧(二) 2010-06-29 09:58 | 陳梓瀚(vczh)
            @chenq
            因為用了smart pointer就不能告訴人們C風格的丑陋  回復  更多評論
              
            久久久久亚洲精品男人的天堂 | 2020最新久久久视精品爱| 久久成人国产精品| 爱做久久久久久| 99久久国产综合精品女同图片| 人妻精品久久无码区| 久久香蕉综合色一综合色88| 久久久久亚洲精品天堂久久久久久| 久久香综合精品久久伊人| 久久99国产精品久久99| 合区精品久久久中文字幕一区| 一本色道久久88精品综合| 欧美日韩中文字幕久久伊人| 成人综合久久精品色婷婷| 久久精品国产精品国产精品污| 一本色道久久综合狠狠躁| 99久久精品免费观看国产| 无码人妻久久一区二区三区免费丨 | 国产女人aaa级久久久级| 热re99久久6国产精品免费| 伊人伊成久久人综合网777| 日本三级久久网| 69久久精品无码一区二区| 久久亚洲国产精品成人AV秋霞| 免费精品久久久久久中文字幕| 四虎国产永久免费久久| 久久香蕉国产线看观看99| 久久久国产精品亚洲一区| 99久久精品免费看国产一区二区三区 | 国产精品无码久久久久| 久久99热国产这有精品| 久久久久免费看成人影片| 精品无码久久久久久尤物| 久久精品国产亚洲AV香蕉| 天天躁日日躁狠狠久久| 欧洲成人午夜精品无码区久久| 精品无码久久久久久午夜| 久久99国产精品久久久 | 区亚洲欧美一级久久精品亚洲精品成人网久久久久 | 精品久久久一二三区| 午夜精品久久久久9999高清|