先看一段代碼:
#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對象。
對於虛函數表的說明:虛函數表的內容是依據類中的虛函數聲明次序,一一填入函數指針。派生類會繼承基類的虛函數表,當我們在派生類中改寫虛函數時,虛函數表就受到了影響:表中元素所指的函數地址將不再是基類的函數地址,而是派生類的函數地址。