1. 程式人生 > >C++中基類的解構函式為什麼要用virtual虛解構函式

C++中基類的解構函式為什麼要用virtual虛解構函式

知識背景

         要弄明白這個問題,首先要了解下C++中的動態繫結。 

正題

         直接的講,C++中基類採用virtual虛解構函式是為了防止記憶體洩漏。具體地說,如果派生類中申請了記憶體空間,並在其解構函式中對這些記憶體空間進行釋放。假設基類中採用的是非虛解構函式,當刪除基類指標指向的派生類物件時就不會觸發動態繫結,因而只會呼叫基類的解構函式,而不會呼叫派生類的解構函式。那麼在這種情況下,派生類中申請的空間就得不到釋放從而產生記憶體洩漏。所以,為了防止這種情況的發生,C++中基類的解構函式應採用virtual虛解構函式。

示例程式碼講解

現有Base基類,其解構函式為非虛解構函式。Derived1和Derived2為Base的派生類,這兩個派生類中均有以string* 指向儲存其name的地址空間,name物件是通過new建立在堆上的物件,因此在析構時,需要顯式呼叫delete刪除指標歸還記憶體,否則就會造成記憶體洩漏。

class Base {

 public:

~Base() {

  cout << "~Base()" << endl;

}

};

class Derived1 : public Base {

 public:

  Derived1():name_(new string("NULL")) {}

  Derived1(const string& n):name_(new string(n)) {}

  ~Derived1() {

    delete name_;

    cout << "~Derived1(): name_ has been deleted."

<< endl;

  }

 private:

  string* name_;

};

class Derived2 : public Base {

 public:

  Derived2():name_(new string("NULL")) {}

  Derived2(const string& n):name_(new string(n)) {}

  ~Derived2() {

    delete name_;

    cout << "~Derived2(): name_ has been deleted." << endl;

  }

 private:

  string* name_;

};

我們看下面對其析構情況進行測試:

int main() {

  Derived1* d1 = new Derived1();

  Derived2 d2 = Derived2("Bob");

  delete d1;

  return 0;

}

d1為Derived1類的指標,它指向一個在堆上建立的Derived1的物件;d2為一個在棧上建立的物件。其中d1所指的物件需要我們顯式的用delete呼叫其解構函式;d2物件在其生命週期結束時,系統會自動呼叫其解構函式。看下其執行結果:

剛才我們說,Base基類的解構函式並不是虛解構函式,現在結果顯示,派生類的解構函式被呼叫了,正常的釋放了其申請的記憶體資源。這兩者並不矛盾,因為無論是d1還是d2,兩者都屬於靜態繫結,而且其靜態型別恰好都是派生類,因此,在析構的時候,即使基類的解構函式為非虛解構函式,也會呼叫相應派生類的解構函式。

下面我們來看下,當發生動態繫結時,也就是當用基類指標指向派生類,這時候採用delete顯式刪除指標所指物件時,如果Base基類的解構函式沒有virtual,會發生什麼情況?

int main() {

  Base* base[2] = {

    new Derived1(),

    new Derived2("Bob")     

  };

  for (int i = 0; i != 2; ++i) {

    delete base[i];   

  }

  return 0;

}

從上面結果我們看到,儘管派生類中定義了解構函式來釋放其申請的資源,但是並沒有得到呼叫。原因是基類指標指向了派生類物件,而基類中的解構函式卻是非virtual的,之前講過,虛擬函式是動態繫結的基礎。現在解構函式不是virtual的,因此不會發生動態繫結,而是靜態繫結,指標的靜態型別為基類指標,因此在delete時候只會呼叫基類的解構函式,而不會呼叫派生類的解構函式。這樣,在派生類中申請的資源就不會得到釋放,就會造成記憶體洩漏,這是相當危險的:如果系統中有大量的派生類物件被這樣建立和銷燬,就會有記憶體不斷的洩漏,久而久之,系統就會因為缺少記憶體而崩潰。

        也就是說,在基類的解構函式為非虛解構函式的時候,並不一定會造成記憶體洩漏;當派生類物件的解構函式中有記憶體需要收回,並且在程式設計過程中採用了基類指標指向派生類物件,如為了實現多型,並且通過基類指標將該物件銷燬,這時,就會因為基類的解構函式為非虛解構函式而不觸發動態繫結,從而沒有呼叫派生類的解構函式而導致記憶體洩漏。

        因此,為了防止這種情況下記憶體洩漏的發生,最好將基類的解構函式寫成virtual虛解構函式。

下面把Base基類的解構函式改為虛解構函式:

class Base {

 public:

virtual ~Base() {

  cout << "~Base()" << endl;

}

};

再看下其執行結果:

這樣就會實現動態繫結,派生類的解構函式就會得到呼叫,從而避免了記憶體洩漏。

故: 繼承時,要養成的一個好習慣就是,基類解構函式中,加上virtual。