程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++實用技巧(二)

C++實用技巧(二)

編輯:C++入門知識

  復雜的東西寫多了,如今寫點簡單的好了。由於功能上的需要,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, 0, sizeof(vczh));
3 strcpy(vczh.name, "vczh");
4 strcpy(vczh.id, "VCZHS 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, 0, sizeof(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)
&nbs

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved