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

C++內存管理學習筆記(6)

編輯:C++入門知識

上期內容回顧:

C++內存管理學習筆記(5)

     2.5 資源傳遞   2.6 共享所有權  2.7 share_ptr


--------------------------------------------------------------------------------

3 內存洩漏-Memory leak
3.1 C++中動態內存分配引發問題的解決方案
     假設我們要開發一個String類,它可以方便地處理字符串數據。我們可以在類中聲明一個數組,考慮到有時候字符串極長,我們可以把數組大小設為200,但一般的情況下又不需要這麼多的空間,這樣是浪費了內存。很容易想到可以使用new操作符,但在類中就會出現許多意想不到的問題,本小節就以這麼意外的小問題的解決來看內存洩漏這個問題。。現在,我們先來開發一個String類,但它是一個不完善的類。存在很多的問題!如果你能一下子把潛在的全找出來,ok,你是一個技術基礎扎實的讀者,直接看下一小節,或者也可以陪著筆者和那些找不到問題的讀者一起再學習一下吧。

   下面上例子,

   1: /* String.h */   2: #ifndef STRING_H_   3: #define STRING_H_   4:     5: class String   6: {   7: private:   8:     char * str; //存儲數據   9:     int len; //字符串長度  10: public:  11:     String(const char * s); //構造函數  12:     String(); // 默認構造函數  13:     ~String(); // 析構函數  14:     friend ostream & operator<<(ostream & os,const String& st);  15: };  16: #endif  17:    18: /*String.cpp*/  19: #include <iostream>  20: #include <cstring>  21: #include "String.h"  22: using namespace std;  23: String::String(const char * s)  24: {  25:     len = strlen(s);  26:     str = new char[len + 1];  27:     strcpy(str, s);  28: }//拷貝數據  29: String::String()  30: {  31:     len =0;  32:     str = new char[len+1];  33:     str[0]='"0';  34: }  35: String::~String()  36: {  37:     cout<<"這個字符串將被刪除:"<<str<<'"n';//為了方便觀察結果,特留此行代碼。  38:     delete [] str;  39: }  40: ostream & operator<<(ostream & os, const String & st)  41: {  42:     os<<st.str;  43:     return os;  44: }  45:    46: /*test_right.cpp*/  47: #include <iostrea>  48: #include <stdlib.h>  49: #include "String.h"  50: using namespace std;  51: int main()  52: {  53:     String temp("String類的不完整實現,用於後續內容講解");  54:     cout<<temp<<'"n';  55:     system("PAUSE");  56:     return 0;  57: }
    運行結果(運行環境Dev-cpp)如下圖所示,表面看上去程序運行很正確,達到了自己程序運行的目的,但是,不要被表面結果所迷惑!

 


      這時如果你滿足於上面程序的結果,你也就失去了c++中比較意思的一部分知識,請看下面的這個main程序,注意和上面的main加以區別,

   1: #include <iostream>   2: #include <stdlib.h>   3: #include "String.h"   4: using namespace std;   5:     6: void show_right(const String& a)   7: {   8:     cout<<a<<endl;   9: }  10: void show_String(const String a) //注意,參數非引用,而是按值傳遞。  11: {  12:     cout<<a<<endl;  13: }  14:    15: int main()  16: {  17:     String test1("第一個范例。");  18:     String test2("第二個范例。");  19:     String test3("第三個范例。");  20:     String test4("第四個范例。");  21:     cout<<"下面分別輸入三個范例"<<endl;  22:     cout<<test1<<endl;  23:     cout<<test2<<endl;  24:     cout<<test3<<endl;  25:       26:     String* String1=new String(test1);  27:     cout<<*String1<<endl;  28:     delete String1;  29:     cout<<test1<<endl;   30:       31:     cout<<"使用正確的函數:"<<endl;  32:     show_right(test2);  33:     cout<<test2<<endl;  34:     cout<<"使用錯誤的函數:"<<endl;  35:     show_String(test2);  36:     cout<<test2<<endl; //這一段代碼出現嚴重的錯誤!  37:       38:     String String2(test3);  39:     cout<<"String2: "<<String2<<endl;  40:       41:     String String3;  42:     String3=test4;  43:     cout<<"String3: "<<String3<<endl;  44:     cout<<"下面,程序結束,析構函數將被調用。"<<endl;  45:    46:     return 0;  47: }

      運行結果(環境Dev-cpp):程序運行最後崩潰!!!到這裡就看出來上面的String類存在問題了吧。(讀者可以自己運行一下看看,可以換vc或者vs等等試試)

 


     為什麼會崩潰呢,讓我們看一下它的輸出結果,其中有亂碼、有本來被刪除的但是卻正常打印的“第二個范例”,以及最後析構刪除的崩潰等等問題。

通過查看,原來主要是復制構造函數和賦值操作符的問題,讀者可能會有疑問,這兩個函數是什麼,怎會影響程序呢。接下來筆者慢慢結識。

     首先,什麼是復制構造函數和賦值操作符?------>限於篇幅,詳細分析請看《c++中復制控制詳解(copy control)》

Tip:復制構造函數和賦值操作符

(1)復制構造函數(copy constructor)

         復制構造函數(有時也稱為:拷貝構造函數)是一種特殊的構造函數,具有單個形參,該形參(常用const修飾)是對該類類型的引用.當定義一個新對象並用一個同類型的對象對它進行初始化時,將顯示使用復制構造函數.當將該類型的對象傳遞給函數或者從函數返回該類型的對象時,將隱式使用復制構造函數。

       復制構造函數用在:

對象創建時使用其他相同類型的對象初始化;
   1: Person q("Mickey"); // constructor is used to build q.   2: Person r(p);        // copy constructor is used to build r.   3: Person p = q;       // copy constructor is used to initialize in declaration.   4: p = q;              // Assignment operator, no constructor or copy constructor.
復制對象作為函數的參數進行值傳遞時;

   1: f(p);               // copy constructor initializes formal value parameter.
復制對象以值傳遞的方式從函數返回。

      一般情況下,編譯器會給我們自動產生一個拷貝構造函數,這就是“默認拷貝構造函數”,這個構造函數很簡單,僅僅使用“老對象”的數據成員的值對“新對象”的數據成員一一進行賦值。使用默認的復制構造函數是叫做淺拷貝。

     相對應與淺拷貝,則有必要有深拷貝(deep copy),對於對象中動態成員,就不能僅僅簡單地賦值了,而應該有重新動態分配空間。

     如果對象中沒有指針去動態申請內存,使用默認的復制構造函數就可以了,因為,默認的復制構造、默認的賦值操作和默認的析構函數能夠完成相應的工作,不需要去重寫自己的實現。否則,必須重載復制構造函數,相應的也需要重寫賦值操作以及析構函數。

2.賦值操作符(The Assignment Operator)

      一般而言,如果類需要復制構造函數,則也會需要重載賦值操作符。首先,了解一下重載操作符。重載操作符是一些函數,其名字為operator後跟所定義的操作符符號,因此,可以通過定義名為operator=的函數,進行重載賦值定義。操作符函數有一個返回值和一個形參表。形參表必須具有和該操作數數目相同的形參。賦值是二元運算,所以該操作符有兩個形參:第一個形參對應的左操作數,第二個形參對應右操作數。

    賦值和賦值一般在一起使用,可將這兩個看作一個單元,如果需要其中一個,幾乎也肯定需要另一個。
 

         ok,現在分析上面的程序問題。

   a)程序中有這樣的一段代碼,


   1: String* String1=new String(test1);   2: cout<<*String1<<endl;   3: delete String1;

      假設test1中str指向的地址為2000,而String中str指針同樣指向地址2000,我們刪除了2000處的數據,而test1對象呢?已經被破壞了。大家從運行結果上可以看到,我們使用cout<<test1時,從結果圖上看,顯示的是亂碼類似於“*”,而在test1的析構函數被調用時,顯示是這樣:“這個字符串將被刪除:”,程序崩潰,這裡從結果圖上看,可能沒有執行到這一步,程序已經奔潰了。

   b)另外一段代碼,


   1: cout<<"使用錯誤的函數:"<<endl;   2: show_String(test2);   3: cout<<test2<<endl;//這一段代碼出現嚴重的錯誤!

       show_String函數的參數列表void show_String(const String a)是按值傳遞的,所以,我們相當於執行了這樣的代碼:函數申請一個臨時對象a,然後將a=test2;函數執行完畢後,由於生存周期的緣故,對象a被析構函數刪除,這裡要注意!從輸出結果來看,顯示的是“第二個范例。”,看上去是正確的,但是分析程序發現這裡有漏洞,程序執行的是默認的復制構造函數,類中使用str指針申請內存的,默認的函數不能動態申請空間,只是將臨時對象的str指針指向了test2,即a.str = test2.str,所以這塊不能夠正確執我們的復制目的。因為此時test2也被破壞了!

     這是就需要我們自己重載構造函數了,即定義自己的復制構造函數,

   1: String::String(const String& a)   2: {   3:     len=a.len;   4:     str=new char(len+1);   5:     strcpy(str,a.str);   6: }
      這裡執行的是深拷貝。這個函數的功能是這樣的:假設對象A中的str指針指向地址2000,內容為“I am a C++ Boy!”。我們執行代碼String B=A時,我們先開辟出一塊內存,假設為3000。我們用strcpy函數將地址2000的內容拷貝到地址3000中,再將對象B的str指針指向地址3000。這樣,就互不干擾了。

     c)還有一段代碼

   1: String String3;   2: String3=test4;
      問題和上面的相似,大家應該猜得到,它同樣是執行了淺拷貝,出了同樣的毛病。比如,執行了這段代碼後,析構函數開始執行。由於這些變量是後進先出的,所以最後的String3變量先被刪除:這個字符串將被刪除:String:第四個范例。執行正常。最後,刪除到test4的時候,問題來了:程序崩潰。原因我不用贅述了。

      那怎麼修改這個賦值操作呢,當然是自己定義重載啦,

版本一,

   1: String& String::operator =(const String &a)   2: {   3:     if(this == &a)   4:         return *this;   5:     delete []str;   6:     str = NULL;   7:     len=a.len;   8:     str = new char[len+1];   9:     strcpy(str,a.str);  10:       11:     return *this;  12: } //重載operator=
版本二,

   1: String& String::operator =(const String& a)   2: {   3:     if(this != &a)   4:     {   5:         String strTemp(a);   6:            7:         len = a.len;   8:         char* pTemp = strTemp.str;   9:         strTemp.str = str;  10:         str = pTemp;  11:     }  12:     return *this;      13: }

 

 

 


    這個重載函數實現時要考慮填補很多的陷阱!限於篇幅,大概說下,返回值須是String類型的引用,形參為const 修飾的Sting引用類型,程序中要首先判斷是否為a=a的情形,最後要返回對*this的引用,至於為什麼需要利用一個臨時strTemp,是考慮到內存不足是會出現new異常的,將改變Srting對象的有效狀態,違背C++異常安全性原則,當然這裡可以先new,然後在刪除原來對象的指針方式來替換使用臨時對象賦值。

    我們根據上面的要求重新修改程序後,執行程序,結果顯示為,從圖的右側可以到,這次執行正確了。

 


3.2 如何對付內存洩漏
        寫出那些不會導致任何內存洩漏的代碼。很明顯,當你的代碼中到處充滿了new 操作、delete操作和指針運算的話,你將會在某個地方搞暈了頭,導致內存洩漏,指針引用錯誤,以及諸如此類的問題。這和你如何小心地對待內存分配工作其實完全沒有關系:代碼的復雜性最終總是會超過你能夠付出的時間和努力。於是隨後產生了一些成功的技巧,它們依賴於將內存分配(allocations)與重新分配(deallocation)工作隱藏在易於管理的類型之後。標准容器(standard containers)是一個優秀的例子。它們不是通過你而是自己為元素管理內存,從而避免了產生糟糕的結果。

    如果不考慮vector和Sting使用來寫下面的程序,你大腦很會費勁的…..

   1: #include <vector>   2: #include <string>   3: #include <iostream>   4: #include <algorithm>   5:     6: using namespace std;   7:     8: int main() // small program messing around with strings   9: {  10:     cout<<"enter some whitespace-seperated words:"<<endl;  11:     vector<string> v;  12:     string s;  13:     while (cin>>s)  14:         v.push_back(s);  15:     sort(v.begin(),v.end());  16:     string cat;  17:     typedef vector<string>::const_iterator Iter;  18:     for (Iter p = v.begin(); p!=v.end(); ++p)   19:     {   20:         cat += *p+"+";  21:         std::cout<<cat<<'n';  22:     }  23:     return 0;  24: }

    運行結果:這個程序利用標准庫的string和vector來申請和管理內存,方便簡單,若是設想使用new和delete來重新寫程序,會頭疼的。

 


      注 意,程序中沒有出現顯式的內存管理,宏,溢出檢查,顯式的長度限制,以及指針。通過使用函數對象和標准算法(standard algorithm),我可以避免使用指針——例如使用迭代子(iterator),不過對於一個這麼小的程序來說有點小題大作了。

  這些技巧並不完美,要系統化地使用它們也並不總是那麼容易。但是,應用它們產生了驚人的差異,而且通過減少顯式的內存分配與重新分配的次數,你甚至可以使余下的例子更加容易被跟蹤。  如果你的程序還沒有包含將顯式內存管理減少到最小限度的庫,那麼要讓你程序完成和正確運行的話,最快的途徑也許就是先建立一個這樣的庫。模板和標准庫實現了容器、資源句柄等等

  如果你實在不能將內存分配/重新分配的操作隱藏到你需要的對象中時,你可以使用資源句柄(resource handle),以將內存洩漏的可能性降至最低。

      這裡有個例子:需要通過一個函數,在空閒內存中建立一個對象並返回它。這時候可能忘記釋放這個對象。畢竟,我們不能說,僅僅關注當這個指針要被釋放的時候,誰將負責去做。使用資源句柄,這裡用了標准庫中的auto_ptr,使需要為之負責的地方變得明確了。

   1: #include<memory>   2: #include<iostream>   3: using namespace std;   4:     5: struct S {   6:     S() { cout << "make an S"<<endl; }   7:     ~S() { cout << "destroy an S"<<endl; }   8:     S(const S&) { cout << "copy initialize an S"<<endl; }   9:     S& operator=(const S&) { cout << "copy assign an S"<<endl; }  10: };  11:    12: S* f()  13: {  14:     return new S; // 誰該負責釋放這個S?  15: };  16:    17: auto_ptr<S> g()  18: {  19:     return auto_ptr<S>(new S); // 顯式傳遞負責釋放這個S  20: }  21:    22: void test()  23: {  24:     cout << "start main"<<endl;  25:     S* p = f();  26:     cout << "after f() before g()"<<endl;  27:     // S* q = g(); // 將被編譯器捕捉  28:     auto_ptr<S> q = g();  29:     cout << "exit main"<<endl;  30:     // *p產生了內存洩漏  31:     // *q被自動釋放      32: }  33: int main()  34: {  35:     test();  36:     system("PAUSE");  37:     return 0;  38: }

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