天天看點

虛函數,虛析構函數,虛函數表

說明:

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