編譯器眼中的虛函數

先看一段代碼:

#include <iostream.h>
class A
{
public:
 virtual void Display(){cout<<"class A::Display()"<<endl;}
};

class B:public A
{ public:  
virtual void Test(){cout<<"class B::Test()"<<endl;}  
void Display(){cout<<"class B::Display()"<<endl;}
};

void main()
{
 A *a;  
B b;  
a=&b;
 a->Test();//出錯
 a->Display();
}

這段代碼在編譯時就出錯了,提示: 'Test': is not a member of 'A'。a是指向B類對象的一個指針啊,按理說可以調用其虛函數Test()。我們要注意,這個程序在編譯時就出錯了!讓我們站在編譯器的角度來看這個問題:

對於編譯器而言,指針a與A類對象只有一個區別:調用成員的方式不同,前者是通過“->”,後者是通過“.”

當編譯到a->Test();時,編譯器發現:唉,不對啊,A類沒有這個函數啊。因此就報錯了。下面我們對程序進行小小的修改:

#include <iostream.h>
class A
{
public:  
void Test(){cout<<"class A::Test()"<<endl;}
 virtual void Display(){cout<<"class A::Display()"<<endl;}
};

class B:public A
{
public:
 virtual void Test(){cout<<"class B::Test()"<<endl;}  
void Display(){cout<<"class B::Display()"<<endl;}
};

void main()
{  
A *a;  
B b;  
a=&b;  
a->Test();  
a->Display();
}

編譯通過,運行結果:
class A::Test()
class B::Display()

我們再站在編譯器的角度分析一下:
1.當編譯到a->Test();時,編譯器發現:恩,A類有這個函數,並且不是虛函數,將此處和class A::Test()靜態綁定之;
2.當編譯到a->Display();時,編譯器發現:恩,A類有這個函數,等等!好小子,你竟然是個虛函數,那我得通過存儲在對象中的指向虛函數表的的指針vptr調用你。所以編譯器把a->Display();翻譯爲:a->vptr->Display();在編譯階段,編譯器不知道vptr指針指向的地址,因爲此時程序還沒運行,對象還沒有構造出來;只有在程序運行時,b對象被構造出來了,b中才含有vptr。這就是傳說中的神祕的動態綁定!!!

再來看看,虛函數最常見的一種用法,這種用法也是在MFC中經常出現的,如果你有《深入淺出MFC》(第二版)這本書,請翻到147頁,Command Routing將虛函數的作用發揮到了極致!如果沒有,請看下面的代碼:
#include <iostream.h>
class A
{
public:  
void Func(){Display();}
 virtual void Display(){cout<<"class A::Display()"<<endl;}
};

class B:public A
{
public: 
 void Display(){cout<<"class B::Display()"<<endl;}
};

void main()
{  
B b;   
b.Func();
}
編譯通過,運行結果:
class B::Display()

看看編譯器又變了什麼魔術,當編譯器編譯到void Func(){Display();}時,發現Display()是虛函數,所以翻譯爲:void Func(){this->vptr->Display();},又是動態綁定!因爲在編譯階段,編譯器並不知道this指向誰,只有在運行時才知道原來指向的是b對象。

對於虛函數表的說明:虛函數表的內容是依據類中的虛函數聲明次序,一一填入函數指針。派生類會繼承基類的虛函數表,當我們在派生類中改寫虛函數時,虛函數表就受到了影響:表中元素所指的函數地址將不再是基類的函數地址,而是派生類的函數地址。

發佈了20 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章