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

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

(轉自:https://blog.csdn.net/iicy266/article/details/11906457

知識背景

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

         關於動態繫結的講解,請參閱:  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;
}
};


再看下其執行結果:

 

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