虛 函 數
說一個人後入為主,通常是說耳根子軟,人家說什麼就把原先的忘掉了,別人說向東走,他就向東走,一會兒有人說向西了,他立馬趕回來。不過好處顯而易見,這個人特別聽話,如果你有時候不知道自己要幹什麼,或者不確定一會兒要幹什麼,那麼就叫他在旁邊等著,到你拿定主意的時候,再告訴他你的要求,讓他照辦。現實中這樣的事情並不少見,秘書需要作記錄、寫報告、安排行程、聯絡客戶等等,而這些工作都是在你的指示下去做的。如果編程的時候也有這麼聽話的代碼就好了,當然這不是做夢,這完全可以實現。
求解方程時有一個未知數的概念,用一個可以為任意數的符號代表一個暫時不知道值的數,然後通過運算規則得到符合要求的值,用編程的角度來看,就是加上了一個間接層,我們暫時不處理數值,而是處理一個與數值有關的符號,如果變化的是數值也就是對應的屬性,我們可以用成員變數來處理;如果變化的是運算規則呢?y=f(x),這個f(x)是可變的,要讓我們的解決方案通用於各種可能的f(x),那就要提供一個間接層來串連不同的函數,在C++中用函數指標就可以辦到。
指標的作用就是可以儲存某種類型的對象在記憶體中的位置,改變指標的值就可以讓他指向不同對象。(指標本身也是一個類型)
編譯器是如何?對虛函數在運行時刻確定被調用函數的代碼呢?也就是說,虛函數實際上是如何被編譯器處理的呢?這裡把“標準的”方式簡單介紹一下。
“標準”方式,也就是所謂的“VTABLE”機制。編譯器發現一個類中有被聲明為virtual的函數,就會為其建立一個虛函數表,也就是VTABLE。VTABLE實際上是一個函數指標的數組,每個虛函數佔用這個數組的一個位置。一個類只有一個VTABLE,不管它有多少個執行個體。衍生類別有自己的VTABLE,但是衍生類別的VTABLE與基類的VTABLE有相同的函數排列順序,同名的虛函數被放在兩個數組的相同位置上。在建立類執行個體(對象)的時候,編譯器還會在每個執行個體的記憶體中增加一個vptr欄位(虛函數表指標),該欄位指向本類的VTABLE。通過這些手段,編譯器在看到一個虛函數調用的時候,就會將這個調用改寫,如下面的例子:
class A
{
public:
virtual void foo() { cout << "A::foo() is called" << endl;}
};
class B: public A
{
public:
virtual void foo() { cout << "B::foo() is called" << endl;}
};
那麼,在使用的時候,我們可以:
A * a = new B();
a->foo(); // 在這裡,a雖然是指向A的指標,但是被調用的函數(foo)卻是B的!
void bar(A * a)
{
a->foo(); // 被調用的是A::foo() 還是B::foo()?
}
因為foo()是個虛函數,所以在bar這個函數中,只根據這段代碼,無法確定這裡被調用的是A::foo()還是B::foo(),但是可以肯定的說:如果a指向的是A類的執行個體,則A::foo()被調用,如果a指向的是B類的執行個體,則B::foo()被調用。
編譯時間bar這個函數會被改寫為:
void bar(A * a)
{
(a->vptr[1])();
}
因為衍生類別和基類的foo()函數具有相同的VTABLE索引,而他們的vptr又指向不同的VTABLE,因此通過這樣的方法可以在運行時刻決定調用哪個foo()函數。
雖然實際情況遠非這麼簡單,但是基本原理大致如此。如類X的物件模型如所示:
虛函數的多態性只能通過對象指標或對象的引用調用來實現,如下的調用:
X obj;X* ptr = &obj; X& ref = obj;ptr->VirtualFunc();ref.VirtualFunc(); |
將被C++編譯器轉換為如下的形式。
( *ptr->vptr[2] )(ptr);( *ptr->vptr[2] )(&ref); |
其中的2表示VirtualFunc在類的虛函數表的第2個槽位。因此,在C++中用函數指標實現虛函數,編譯器在類中隱藏了一個函數指標數組(因為成員函數可能有很多,叫做虛函數表)。顯然這裡增加了一個尋找函數位置的步驟,所以,使用虛函數將降低執行效率,需要在效率和靈活性之間的取捨。虛函數的調用相當於一個C的函數指標調用,其效率也並未降低多少。
我們用C++來看一下虛函數的效果
#include <iostream>
#include <stdlib.h>
class NormalA{
public:
void DoSomething( void ){ cout << "This is class NormalA." << endl; }
};
class NormalB : public NormalA{
public:
void DoSomething( void ){ cout << "This is class NormalB." << endl; }
};
class NormalC : public NormalB{
public:
void DoSomething( void ){ cout << "This is class NormalC." << endl; }
};
class VirtualA{
public:
virtual void DoSomething( void ){ cout << "This is class VirtualA." << endl; }
};
class VirtualB : public VirtualA{
public:
void DoSomething( void ){ cout << "This is class VirtualB." << endl; }
};
class VirtualC : public VirtualB{
public:
void DoSomething( void ){ cout << "This is class VirtualC." << endl; }
};
int main()
{
NormalA na;
NormalB nb;
NormalC nc;
VirtualA va;
VirtualB vb;
VirtualC vc;
NormalA * pna;
VirtualA * pva;
na.DoSomething();
nb.DoSomething();
nc.DoSomething();
va.DoSomething();
vb.DoSomething();
vc.DoSomething();
pna = &na;
pna->DoSomething();
pna = &nb;
pna->DoSomething();
pna = &nc;
pna->DoSomething();
pva = &va;
pva->DoSomething();
pva = &vb;
pva->DoSomething();
pva = &vc;
pva->DoSomething();
cin.get();
return 0;
}
結果是
This is class NormalA. //調用的是NormalA::DoSomething();
This is class NormalB. //調用的是NormalB::DoSomething();
This is class NormalC. //調用的是NormalC::DoSomething();
This is class VirtualA. //調用的是VirtualA::DoSomething();
This is class VirtualB. //調用的是VirtualB::DoSomething();
This is class VirtualC. //調用的是VirtualC::DoSomething();
This is class NormalA. //調用的是NormalA::DoSomething(); 沒有虛函數表,NormalA *指定了類型
This is class NormalA. //調用的是NormalA::DoSomething(); 沒有虛函數表,NormalA *指定了類型
This is class NormalA. //調用的是NormalA::DoSomething(); 沒有虛函數表,NormalA *指定了類型
This is class VirtualA. //調用的是VirtualA::DoSomething(); 虛函數表指標指向的是VirtualA
This is class VirtualB. //調用的是VirtualB::DoSomething(); 虛函數表指標指向的是VirtualB
This is class VirtualC. //調用的是VirtualC::DoSomething(); 虛函數表指標指向的是VirtualC