我們知道,為了能夠正確的調(diào)用對象的析構(gòu)函數(shù),一般要求具有層次結(jié)構(gòu)的頂級類定義其析構(gòu)函數(shù)為虛函數(shù)。因?yàn)樵赿elete一個(gè)抽象類指針時(shí)候,必須要通過虛函數(shù)找到真正的析構(gòu)函數(shù)。

如:


C++:

class CObject{
public:
   CObject()
   {
   }
   virtual ~CObject()
   {
   };
};
class CX: public CObject
{
public:
   CX(){...};
   ~CX(){...};
}
void f()
{
   CObject *px;
   px = new CX;
   delete px;
}

否則delete px只會執(zhí)行CObject的析構(gòu)函數(shù),而不是真正的CX析構(gòu)函數(shù)。
現(xiàn)在的問題是,我們想把CObject做出抽象類,不能直接構(gòu)造對象,需要在其中定義一個(gè)純虛函數(shù)。如果其中沒有其他合適的函數(shù),可以把析構(gòu)函數(shù)定義為純虛的,即將前面的CObject定義改成:


C++:

class CObject{
public:
   CObject()
   {
   }
   virtual ~CObject() = 0;
};


可 是,這段代碼不能通過編譯,通常是鏈接錯(cuò)誤,不能找到~CObject()的引用(gcc的錯(cuò)誤報(bào)告)。這是因?yàn)椋鰳?gòu)函數(shù)、構(gòu)造函數(shù)和其他內(nèi)部函數(shù)不一 樣,在調(diào)用時(shí),編譯器需要產(chǎn)生一個(gè)調(diào)用鏈。也就是,CX的析構(gòu)函數(shù)里面隱含調(diào)用了CObecjt的析構(gòu)函數(shù)。而剛才的代碼中,缺少~CObecjt的函數(shù) 體,當(dāng)然會出現(xiàn)錯(cuò)誤。

這里面有一個(gè)誤區(qū),有人認(rèn)為,virtual f()=0這種純虛函數(shù)語法就是沒有定義體的語義。其實(shí),這是不對的。這種語法只是表明這個(gè)函數(shù)是一個(gè)純虛函數(shù),因此這個(gè)類變成了抽象類,不能產(chǎn)生對象。我們完全可以為純虛函數(shù)指定函數(shù)體。通常的純虛函數(shù)不需要函數(shù)體,是因?yàn)槲覀円话悴粫{(diào)用抽象類的這個(gè)函數(shù),只會調(diào)用派生類的對應(yīng)函數(shù)。這樣,我們就有了一個(gè)純虛析構(gòu)函數(shù)的函數(shù)體,上面的代碼需要改成:


C++:

class CObject{
public:
   CObject()
   {
   }
   virtual ~CObject() = 0;
};

CObject::~CObject()
{
}


從語法角度來說,不可以將上面的析構(gòu)函數(shù)直接寫入定義中(內(nèi)聯(lián)函數(shù)的寫法)。這或許是一個(gè)不正交化的地方。

這個(gè)問題看起來有些學(xué)術(shù)化,因?yàn)橐话阄覀兺耆梢栽贑Object中找到一個(gè)更加適合的函數(shù),通過將其定義為沒有實(shí)現(xiàn)體的純虛函數(shù),而將整個(gè)類定義為抽象類。但這種技術(shù)也有一些應(yīng)用,如這個(gè)例子:


C++:

class CB{
public:
   virtual ~CB(){};
   virtual void Hiberarchy() const = 0;
};

void CB::Hiberarchy() const
{
   std::cout <<"CB::";
}

class CD : public CB
{
public:
   CD()
   {
   };
   virtual void Hiberarchy() const
   {
       CB::Hiberarchy();
       std::cout <<"CD::";
   };
   virtual void f()
   {
   }
};


int main(){
   CB* x=new CD();
   x->Hiberarchy();
   x->CB::Hiberarchy();
   return 0;
}


在 這個(gè)例子中,我們試圖打印出類的繼承關(guān)系。在根基類中定義了虛函數(shù)Hiberarchy,然后在每個(gè)派生類中都重載此函數(shù)。我們再一次看到,由于想把CB 做成個(gè)抽象類,而這個(gè)類中沒有其他合適的方法成員可以定義為純虛的,我們還是只好將Hiberarchy定義為純虛的。(當(dāng)然,完全可以定義~CB函數(shù), 這就和上面的討論一樣了。^_^)

另外,可以看到,在main中有兩種調(diào)用方法,第一種是普通的方式,進(jìn)行動態(tài)鏈接,執(zhí)行虛函數(shù),得到結(jié)果"CB::CD::";第二種是指定類的方式,就不再執(zhí)行虛函數(shù)的動態(tài)鏈接過程了,結(jié)果是"CB::"。

通過上面的分析可以看出,定義純虛函數(shù)的真正目的是為了定義抽象類,而并不是函數(shù)本身。與之對比,在java中,定義抽象類的語法是 abstract class,也就是在類的一級作指定(當(dāng)然虛函數(shù)還是也要加上abstract關(guān)鍵字)。是不是這種方式更好一些呢?在Stroustrup的《C++語言的設(shè)計(jì)與演化》中我找到這樣一段話:

“我選擇的是將個(gè)別的函數(shù)描述為純虛的方式,沒有采用將完整的類聲明定義為抽象的形式,這是因?yàn)?layer id="google-toolbar-hilite-16" style="background-color: Fuchsia; color: black;">純虛函數(shù)的概念更加靈活一些。我很看重能夠分階段定義類的能力;也就是說,我發(fā)現(xiàn)預(yù)先定義一些純虛函數(shù),并把另外一些留給進(jìn)一步的派生類去定義也是很有用的”。

我 還沒有完全理解后一句話,我想從另外一個(gè)角度來闡述這個(gè)概念。那就是,在一個(gè)多層復(fù)雜類結(jié)構(gòu)中,中間層次的類應(yīng)該具體化一些抽象函數(shù),但很可能并不是所有 的。中間類沒必要知道是否具體化了所有的虛函數(shù),以及其祖先已經(jīng)具體化了哪些函數(shù),只要關(guān)注自己的職責(zé)就可以了。也就是說,中間類沒必要知道自己是否是一 個(gè)真正的抽象類,設(shè)計(jì)者也就不用考慮是否需要在這個(gè)中間類的類級別上加上類似abstract的說明了。

當(dāng)然,一個(gè)語言的設(shè)計(jì)有多種因素,好壞都是各個(gè)方面的。這只是一個(gè)解釋而已。

最后,總結(jié)一下關(guān)于虛函數(shù)的一些常見問題:

虛函數(shù)是動態(tài)綁定的,也就是說,使用虛函數(shù)的指針和引用能夠正確找到實(shí)際類的對應(yīng)函數(shù),而不是執(zhí)行定義類的函數(shù)。這是虛函數(shù)的基本功能,就不再解釋了。
構(gòu)造函數(shù)不能是虛函數(shù)。而且,在構(gòu)造函數(shù)中調(diào)用虛函數(shù),實(shí)際執(zhí)行的是父類的對應(yīng)函數(shù),因?yàn)樽约哼€沒有構(gòu)造好。
析構(gòu)函數(shù)可以是虛函數(shù),而且,在一個(gè)復(fù)雜類結(jié)構(gòu)中,這往往是必須的。
將一個(gè)函數(shù)定義為純虛函數(shù),實(shí)際上是將這個(gè)類定義為抽象類,不能實(shí)例化對象。
純虛函數(shù)通常沒有定義體,但也完全可以擁有。純虛析構(gòu)函數(shù)必須有定義體,因?yàn)槲鰳?gòu)函數(shù)的調(diào)用是在子類中隱含的。
非純的虛函數(shù)必須有定義體,不然是一個(gè)錯(cuò)誤。
派 生類的重載虛函數(shù)定義必須和父類完全一致。除了一個(gè)特例,如果父類中返回值是一個(gè)指針或引用,子類重載時(shí)可以返回這個(gè)指針(或引用)的派生。例如,在上面 的例子中,在CB中定義了 virtual CB* clone(); 在CD中可以定義為 virtual CD* clone()。可以看到,這種放松對于Clone模式是非常有用的。
其他,有待補(bǔ)充。