1. 程式人生 > >深入探索C++物件模型(九) 解構函式 (以及顯式定義的解構函式問題、解構函式Rules of Three)

深入探索C++物件模型(九) 解構函式 (以及顯式定義的解構函式問題、解構函式Rules of Three)

  如果類沒有定義解構函式,那麼只有類中含有成員物件(或者本類的基類)擁有解構函式的情況下,編譯器才會合成一個出來,否則解構函式被視為不要,也就不需要合成。例如,如下類,雖然Point類擁有虛擬函式:

  1. class Point {  
  2. piblic:   
  3.     Point(float x = 0.0, float y = 0.0);  
  4.     Point (const Point&);  
  5.     virtualfloat z();  
  6. private:  
  7.     flaot _x, _y;  
  8. };  
同樣的道理,如下類也不需要合成解構函式:
  1. class Line {  
  2. public:  
  3.     Line(const
     Point&, const Point&);  
  4.     virtual draw();  
  5. protected:  
  6.     Point _begin, _end;  
  7. };  
當由Point類派生出(包括虛繼承)Point3d類時,如果沒有宣告虛擬函式,那麼編譯器就不會合成一個。

之所以有上述機制,當此語句發生時:

  1. Point *p = new Point3d;  

必須呼叫建構函式初始化類物件,沒有建構函式,抽象化的使用就會有錯誤的傾向。當我們delete掉一個類物件時:

  1. delete p;  

不需要在delete之前做類似的操作:

  1. p->x(0);//x(float)和y(float)是Point類的成員函式 
  2. p->y(0);  
結束p宣告之前,沒有任何類使用者層面的程式操作是程式設計師所必需的,因此,也就不一定需要一個解構函式。

2. 對於多重繼承和虛擬繼承而言,情況要有所不同,當我們從Point3d和Vertex(虛擬繼承自Point)派生出Vertex3d時,如果我們不宣告Vertex3d的解構函式,但我們仍然希望Vertex3d物件結束時,呼叫Vertex的解構函式,那麼編譯器此時必須合成一個Vertex3d的解構函式,該函式唯一的任務就是呼叫Vertex的解構函式,如果我們提供一個Vertex3d的解構函式,編譯器會擴充套件之,在我們的程式碼之後呼叫Vertex的解構函式,該擴充套件與建構函式的擴充套件類似,但是順序相反:

    a. 解構函式本身被執行。

    b. 呼叫成員類物件的解構函式(如果有的話),呼叫順序使他們宣告的相反順序。

    c. 重置虛表指標。

    d. 呼叫非虛基類的建構函式(如果有的話),呼叫順序使他們宣告的相反順序。

    e. 呼叫虛基類的建構函式(如果有的話),如果當前類是最尾端(most-derived)的類,那麼虛基類用原來構造順序的相反順序析構。

一個類物件生命結束於其解構函式呼叫開始執行之時,由於每一個基類解構函式都會被呼叫,所以子類實際上變成了一個完整的物件,例如:對於類d繼承自c,c繼承自b,b繼承自a,當d物件析構時,會依次變成一個c物件,b物件,a物件,當我們在解構函式中呼叫成員函式時,物件的蛻變會因為虛表指標的重新設定(解構函式中,程式設計師所供應的程式碼執行之前)而受到影響,這個會在第六章的內容中整理。

 C++的類中有兩種函式非常特別,一種是建構函式(constructor),另一種是解構函式(deconstructor)。在上篇文章中已經講述了建構函式,本文將討論解構函式。

        當我們定義了類的一個物件時,就會隱式的呼叫建構函式,建構函式執行完成後,物件就有了資源。當我們不需要該物件時,即程式執行到物件作用域之外時,會隱式的呼叫解構函式,解構函式執行完成後,物件的資源就被釋放。

一、解構函式(deconstructor)

        建構函式的英文名為constructor,它是起到構造物件的作用的函式。解構函式的英文名為deconstructor,解構函式的作用與建構函式的作用相反,它是用來銷燬物件的。(至於它為什麼叫解構函式,而不叫銷燬函式這樣的函式名,我也不知道,自己想吧)

        解構函式定義方式為: ~類名(){...}

二、解構函式的特點

        解構函式有很多特點,有的和建構函式一樣,有的不同,本文將會做一些對比。

        (1)解構函式的定義方式。

         A、解構函式的函式名和建構函式幾乎相同,只是在類名前加了一個波浪號(tilde ~),以示區別。

         B、解構函式沒有引數。這點和建構函式不同,因為沒有引數,所以解構函式也不能像建構函式一樣過載。因此一個類中不可能像建構函式那樣,有多種解構函式。我們只可為類提供一個解構函式

         C、可以顯式的定義解構函式,也可以不定義。這一點和建構函式相同。如果我們顯式的定義了解構函式,我們可以在函式體中書寫一些語句,用於顯示物件在釋放前的值等。如果我們沒有定義解構函式,編譯器會為我們生成一個解構函式。

         (2)解構函式的呼叫。

         當類的物件離開其作用域時,解構函式被呼叫,用於釋放物件資源並銷燬資源。

         值得注意的是,解構函式只會刪除真實物件的真實資源。(這句話是我自己想出來的,下面是這句話的註釋)

         A、只有真實存在的物件離開其作用域時才會呼叫解構函式,物件的引用,指向物件的指標離開其作用域時,不會呼叫解構函式。這是為了安全起見,因為很多時候可能物件的引用,指向物件的指標離開作用域時,物件還在其作用域。為了減少程式的bug,建議當物件離開其作用域後,我們讓物件的引用,指向物件的指標失效,或者乾脆就不再使用它。

         B、使用new運算子建立的物件的資源,只有使用delete運算子刪除指向它的指標時,才會呼叫它的解構函式,釋放它的資源。這點要特別注意,當我們在類中顯式定義解構函式時,函式體中通常就包含delete語句。

         C、類中的靜態成員屬於類,不屬於類的物件,它們的資源不會被解構函式釋放。

         解構函式的呼叫與建構函式的呼叫有明顯不同:解構函式可以被顯式呼叫,而建構函式不能。顯式呼叫解構函式和呼叫類的其它成員函式沒什麼不同。當解構函式被顯式呼叫時,只執行它的函式體,而不刪除物件的資源。也就是說,當解構函式被顯式呼叫時,它就是一個普通的成員函式,沒有析構功能。

三、解構函式的Rules of Three

        通常情況下,我們不需要顯式定義解構函式,除非我們需要它完成一些工作。(這一點在下部分講述)

Rules of Three:如果一個類需要手動定義一個解構函式,那麼通常情況下,這個類也需要手動定義複製建構函式和賦值運算子過載函式。

        我們在上一篇文章中講過,複製建構函式用於物件的複製,賦值運算子過載函式的功能和複製建構函式幾乎一樣。通常,我們將複製建構函式和賦值運算子過載函式繫結,定義了一個,另一個也必須出現。

        解構函式、複製建構函式和賦值運算子過載函式,這三個函式是C++類的複製控制(copy-control)成員。複製控制,就是控制類的物件的複製。其中複製建構函式和賦值運算子過載函式是用來複制物件,解構函式是用來刪除物件。

        通常,使用複製建構函式或者賦值運算子過載函式建立一個物件時,會獲得資源,有時必須顯式定義解構函式才能釋放這樣的物件的資源。

四、編譯器生成的解構函式

        解構函式最特別的一點是,編譯器總是為我們生成一個解構函式,不管我們是否顯式的定義一個解構函式。這一點和建構函式非常不同。當我們顯式的定義了解構函式以後,編譯器仍然為我們生成一個解構函式。程式執行過程中,先呼叫使用者顯式定義的解構函式,再呼叫編譯器生成的解構函式。

五、顯示定義的解構函式不析構

        根據上文,可以提出這樣兩個問題:

        1、顯式定義的解構函式為什麼可以顯式呼叫,而建構函式不能

        2、編譯器為什麼總是為我們生成一個解構函式?既然這樣,使用者自定義的解構函式有什麼用?

        也許很多人都有這樣的疑問,我想,弄懂了這兩個問題,就算真正明白了解構函式。

        第一個問題,只有顯示定義了解構函式,我們才能顯式呼叫解構函式。當顯式呼叫解構函式的時候,執行的是解構函式體內的語句,沒有執行析構功能。我們可以寫如下程式來測試:

  1. class Test  
  2. {  
  3. public:  
  4.     Test():x(10),y(10){}  
  5.     ~Test()  
  6.     {  
  7.           cout<<"deconstructor"<<endl;  
  8.           cout<<x<<" "<<y<<endl;  
  9.      }  
  10. private:  
  11.    int x;  
  12.    int y;  
  13. };  
  14. int main()  
  15. {  
  16.      Test app;  
  17.      app.~Test();  
  18.      return 0;  
  19. }  

          上述程式中,我們在類中顯式定義了解構函式,在主函式中,我們顯式的呼叫了一次解構函式,之後物件被銷燬,又呼叫了解構函式。可以看到結果出現了兩次,而且均相同。

        這個結果說明了什麼呢?

        第一次顯式呼叫解構函式,函式和普通成員函式一樣被執行,沒有析構。第二次隱式呼叫解構函式,輸出的結果與第一次一樣。之後物件釋放資源,物件被銷燬。那麼,我們思考一下,物件是在什麼時候釋放的資源,是在什麼時候被銷燬的?

        首先,物件肯定不會在解構函式執行之初和執行之時便釋放資源,因為之後我們仍然可以輸出物件的成員變數的值。當然,也可以認為執行解構函式時建立了一個臨時物件,將物件複製後儲存在臨時物件中,物件此時已被刪除。但解構函式執行完成後,臨時物件被刪除。但是這樣一來,無疑加重了記憶體的負擔,假設物件非常之大,臨時物件也講會很大,而且複製的過程也會很長,語言應該不會設計成這樣。

        其次,物件可能在解構函式體內的語句執行完畢後開始釋放資源。這樣需要維持一個值,用來表示解構函式是顯式呼叫還是隱式呼叫,如果是顯式呼叫,那麼不能釋放資源,如果是隱式呼叫,必須釋放資源。

我們不能排除這種情況,但是於理不可。如果是這樣,大可讓解構函式和建構函式一樣,不能被顯式呼叫,這樣多方便。而且,現在我們也沒看出來顯式呼叫解構函式有什麼用。

        最後,物件可能在解構函式執行完成後釋放資源。和上種情況一樣,我們仍然需要維持一個值,來表示它是顯式還是隱式呼叫。

        通過以上分析,我們可以得出這樣一個結論:顯式定義的解構函式可能根本就不執行析構功能。乍一看,很神奇,顯式定義的解構函式不析構,那由誰析構?

        我們來看第二個問題。為什麼顯式定義了解構函式,編譯器還要為我們生成一個解構函式?編譯器定義的解構函式有什麼用?在C++ Primer第四版中,作者提到:當類的物件被銷燬時,顯式定義的解構函式先執行,當它執行完成後,編譯器生成的解構函式開始執行。編譯器生成的解構函式會銷燬物件的成員變數,對於類型別的成員,如string類的成員,會呼叫它所屬類的解構函式來釋放該成員所佔用的記憶體,對於內建型別成員,編譯器生成的解構函式什麼也不做而銷燬它(does nothing to destroy them)。銷燬一個內建型別的成員沒有任何影響,特別是,編譯器生成的解構函式不會刪除指標成員指向的物件。

        從書中那段話可以看出,顯式定義的解構函式可能與物件的銷燬無關,析構的過程可能由編譯器生成的解構函式完成的。這也就解釋了為什麼顯式定義的解構函式可以顯式呼叫,因為顯式定義的解構函式只是虛有其表,名不符實,它和普通的成員函式沒什麼大的區別,唯一的不同就是在物件的生存週期中,它一定會被呼叫至少一次(這一次就是在物件銷燬時)。

        我無法考證這段話的正確性,幾乎沒有書講到這一點,也很難用C++程式證明這一點。這個結論是根據書本推論出來的,沒有嚴格的證明,只能算是推論。

六、顯式定義解構函式

        上述部分告訴我們:如非必要,不要定義解構函式,以免引起不必要的錯誤。

        顯式定義解構函式多用於以下兩種情況:

        1、用於檢視物件在銷燬的前一刻儲存的內容。有時候為了測試程式,會用到。

        2、在類中用new運算子動態分配了記憶體,可以在解構函式中使用delete運算子釋放記憶體。這種情況是最常用的。因為編譯器生成的解構函式是不會銷燬new出來的動態物件,這一點是因為new出來的物件儲存在記憶體中的堆(heap)區,而編譯器生成的解構函式只會釋放記憶體中的棧(stack)區。舉個例子:

  1. class Test  
  2. {  
  3. public:  
  4.     Test()  
  5.     {  
  6.          p=newint[10];  
  7.      }  
  8.     ~Test()  
  9.      {  
  10.           delete []p;p=null;  
  11.       }  
  12. private:  
  13.     int *p;  
  14. };  

        當我們定義的類中含有指標成員,並在成員函式中使用new運算子動態分配了記憶體,我們一定要記得使用delete運算子刪除之。使用了new運算子,就一定要使用delete運算子,這是一個好的程式設計習慣。

        當然,我們可以在其它的函式中使用delete運算子,甚至我們可以單獨定義一個函式如safe_del(){delete []p;p=null;},然後顯式的呼叫它。但是如果我們把它寫在解構函式中,那麼即使我們忘了刪除new出來的物件,程式執行時也不會忘。雖說顯式定義的解構函式名不符實,但是我們還是儘量讓它實現析構功能吧。

總結:本文講了解構函式的一些特性,前面的部分很多書都會涉及到,最後的一部分是筆者的學習心得,才是本文的重點。為了文章的完整性,才闡述了前面部分的內容。

            1、顯式定義的解構函式可以被顯式呼叫,但是顯式呼叫它沒有什麼意義。

            2、顯式定義的解構函式的作用不像顯式定義的建構函式那麼有用,顯示定義的解構函式完全可以用別的函式代替,但是,為了使用方便,為了其它程式設計人員的使用,在需要顯示定義解構函式的情況下,還是定義它比較好,這樣符合通用程式設計風格。

            3、Rules of Three:如果需要定義解構函式,那麼通常也需要定義複製建構函式和賦值運算子過載函式。