說明:
1.基類的析構函數被聲明為虛函數後,派生類的析構函數預設也為虛函數;和一般的虛函數在繼承關系中表現的一樣,隻是名字不再一樣了。
2.編譯器給析構函數中插入額外的代碼規律和構造函數的一樣,隻是順序相反!(不确認,待解決)
3. class A{}; A a; A b=a;注意第三句調用的是copy constructor 而不是copy assignment operator(已确認)。
4.
先看下面一段程式:
#include <iostream>
using namespace std;
class Person
{
public:
virtual ~Person() //加了virtual,将析構函數聲明為虛函數
{
cout << "Person::~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student() // virtual可加可不加
{
cout << "Student::~Student()" << endl;
}
};
int main()
{
Person *pt1 = new Person;
Person *pt2 = new Student; // 用基類的指針指向派生類
// Student *pt3 = new Person; // 不能用派生類指針指向基類,錯誤!
Student *pt4 = new Student;
delete pt1;
cout << "*********" << endl;
delete pt2;
cout << "*********" << endl;
//delete pt3;
//cout << "*********" << endl;
delete pt4;
cout << "*********" << endl;
return 0;
}
運作結果:
Person::~Person()
***********
Student::~Student()
Person::~Person()
**********
Student::~Student()
Person::~Person()
**********
如果在基類中析構函數不加virtual,結果為:
Person::~Person()
***********
Person::~Person()
**********
Student::~Student()
Person::~Person()
**********
可以看出:隻有在用基類的指針指向派生類的時候,才會出現這種情況。因為這個時候虛函數發揮了動态的作用。
析構函數執行時先調用派生類的析構函數,其次才調用基類的析構函數。如果析構函數不是虛函數,而程式執行時又要通過基類的指針去銷毀派生類的動态對象,那麼用delete銷毀對象時,隻調用了基類的析構函數,未調用派生類的析構函數。這樣會造成銷毀對象不完全。
如果在上面的例子中,基類中未定義virtual析構函數,而派生類中定義了virtual的析構函數,此時用基類指針指向派生類,再delete掉,
即:
class Person
{
public:
~Person()
{
cout << "Person::~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "Student::~Student()" << endl;
}
};
Person * pt = new Student;
delete pt;
運作結果會出錯。(注:這個實驗我也做過,但不知道原理,不知道如何解釋)2016.11.7更正
運作結果為:
Person::~Person()
解釋如下:
1. 析構函數跟普通成員沒有什麼不同,隻是編譯器在會在特定的時候自動調用析構函數(離開作用域或者執行delete操作);
2. 對于一個成員函數調用(不論是通過對象obj.func還是通過對象指針obj->func),到底是直接調用還是通過虛函數表調用,在編譯的時候是确定的,取決于這個函數是不是虛函數;
3. 綜上,如果析構不聲明為虛函數,那麼delete pBase,就被編譯器解釋為 Base::~Base,否則被編譯器解釋為 this->virtual_function_table[#析構在虛函數表的偏移]
---------------------------------------------------------------------------------------------------------------------
再例:
class Person
{
public:
~Person()
{
cout << "Person::~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "Student::~Student()" << endl;
}
};
class OneSt : public Student
{
public:
~OneSt()
{
cout << "OneSt::~OneSt()" << endl;
}
};
如果用
Student * pt = OneSt;
delete pt;
運作結果為:(注:這個例子沒有實驗,也不知道該怎麼解釋隻能先這麼記憶了);(現在能解釋了(2016.11.7):結合上面兩個例子即可了解!)
OneSt::~OneSt()
Student::~Student()
Person::~Person()
是可以運作的。
Effective C++ (第7條:要将多态基類的析構函數聲明為虛函數)
注:以上全部是虛析構函數為例進行的實驗,其實驗結構的規律同樣适用于一般的虛函數,一般的虛函數都是同名的,虛的析構函數的名字不同,忽略掉這一條,虛析構函數與一般的虛函數所适用的規律一緻。
以上,除黑色字型外均為引用他人文章,紅色字型為個人思考。
但是,看到此處隻能達到知其然的境界,但是不知其是以然,于是我進一步百度,看到了一篇關于虛函數和虛函數表的文章,很棒,我也将其轉載過來。連結如下:
點選打開連結
看了這篇文章之後就明白,一般的虛函數在使用基類指針指向一個new出來的父類的對象時,其調用虛函數的原理。
例A:
Person *pt2 = new Student;
pt2->GetIdentify();
(注:GetIdentify()函數為Person類和Student類中定義的虛函數)
就會調用Student類的GetIdentify()函數,這樣就實作了多态。
但是,到目前為止還是不能明白虛析構函數的在此種情況下,函數的調用原理是怎麼樣的,百度了多次還是沒有很好的解釋,但據說
《深入探索C++對象模型》(侯傑)一書中有詳細的原理介紹,有待進一步研究……(遺留問題一:尋找合理的原了解釋,待續)
答遺留問題一:以上例子已經解釋清楚,詳細細節還需要進一步研究。
研究完這這篇文章點選打開連結(文章二)似乎可以總結如下:
1.如文章二所示當DoSomething在派生類中重寫時(注意均不是虛函數),如果以
ClxBase *p = new ClxDerived;
p->DoSomething();
形式調用 DoSomething時,其輸出結過證明其調用的是基類的DoSomething函數。
2.基于1的結論,如例A的例子,雖然pt2指向的空間是一個Student類的一個執行個體的空間,但是pt2仍是一個Person類對象類型的指針,當執行delete pt2這樣一個操作時,其仍然是按照Person類來對待,此時如果Person的析構函數不是虛函數的話,就會直接調用他的析構函數,但是如果其析構函數是虛函數時,根據多态的特性,就會調用派生類的析構函數,如果調用了派生類的析構函數,又因為派生類的析構函數是一層層的調用下去的,是以就調用了基類的析構函數。雖然解釋通了,但還需要研究下原理(同遺留問題一)!(2016.11.7更新,按上文“解釋如下”了解)
此時我又有了另外兩個疑問,雖然我已經接受并記住,類的預設構造函數的調用順序是從基類自動到派生類。析構函數的調用順序是從派生類自動到基類,但其原理是什麼?(遺留問題二)
答:沒有什麼原理,編譯器在插入必要的代碼時就是按照逆順序來操作的!至于基類的析構函數和成員變量的析構函數的調用順序還需要确認(遺留問題三)!
2015.4.9