1. 程式人生 > >C++ 虛函數 、純虛函數、接口的實用方法和意義

C++ 虛函數 、純虛函數、接口的實用方法和意義

函數聲明 函數 order ack 創建 無需 art %d 最終

也許之前我很少寫代碼,更很少寫面向對象的代碼,即使有寫多半也很容易寫回到面向過程的老路上去。在寫面向過程的代碼的時候,根本不管什麽函數重載和覆蓋,想到要什麽功能就變得法子的換個函數名字,心裏想想:反正函數重載本質也就是入棧了兩個不同的函數。 回過頭來講,讓我了解標題這三個概念的實際用處,還是在於我這第四次重寫畢業論文的代碼,將它改寫成面向對象的時候,才理解的。在面向對象設計的過程中, 類是從抽象逐漸具體起來的,父類可以是非常非常抽象的東西,而最終實例化的子類就非常具體了。在這個繼承的過程中,不斷的對父類進行填充豐富,最終得到的 子類就是有血有肉的 - 我的理解。 純虛函數,就是虛函數了以後,末尾還要加=0的那一類函數。我一直沒想通的是,既然這個函數完全沒有實現方法,那麽定義這個函數有個蛋用啊?我也曾經試著 在網上搜索過純虛函數的意義和作用,回答大多千篇一律照本宣科。於是我漸漸的也就無視這個純虛函數了。直到現在我開始寫一個PSO算法的時候,才發現天哪 這居然是一個完全不可或缺的東西!如果說虛函數還可以用重命名作為另外一種解決方法,那麽純虛函數則是沒有第二種可以替代的方法。我可以拿一個非常簡單的 代碼說明一下: class test{ public: virtual void print(); virtual void order()=0; int array[20]; };

void test::print(){ order(); printf("打印結果: "); for(int i=0; i<20; i++) printf("%d ", array[i]); } 至於接口,這是一個只有JAVA中才用到的概念,C++中不存在接口,與接口相似的是:抽象類。因為JAVA不允許多重繼承類,但可以繼承多個接口。關於 接口,在我編寫JAVA SERVLET的時候,碰到過一個httpservlet,用戶需要為doget和dopost等函數編寫實現方法。而這些函數就可以看成是純虛函數,它 在HTTPservlet也類似於上述代碼的order函數,有著在局部函數中的作用。
總結 :1、虛函數 通過虛函數,在調用不同的衍生類的時候,可以擁有不同的功能。同時,我們可以通過將這麽做完全可以,只要你自己能熟記或者找到這個重命名函數是幹嘛用的;但是在大一點的項目中,由於類中的函數成百上千,恐怕你就會為此瘋狂。

本文較為深入的分析了C++中虛函數與純虛函數的用法,對於學習和掌握面向對象程序設計來說是至關重要的。具體內容如下:

首先,面向對象程序設計(object-oriented programming)的核心思想是數據抽象、繼承、動態綁定通過數據抽象,可以使類的接口與實現分離,使用繼承,可以更容易地定義與其他類相似但不完全相同的新類,使用動態綁定,可以在一定程度上忽略相似類的區別,而以統一的方式使用它們的對象。

虛函數的作用是實現多態性(Polymorphism),多態性是將接口與實現進行分離,采用共同的方法,但因個體差異而采用不同的策略。純虛函數則是一種特殊的虛函數。虛函數聯系到多態,多態聯系到繼承。所以本文中都是在繼承層次上做文章。沒了繼承,什麽都沒得談。

一、虛函數

1 . 定義

在C++中,基類必須將它的兩種成員函數區分開來:一種是基類希望其派生類進行覆蓋的函數;另一種是基類希望派生類直接繼承而不要改變的函數。對於前者,基類通過在函數之前加上virtual關鍵字將其定義為虛函數(virtual)。

?
1 2 3 4 5 6 7 8 9 class Base{ // 基類 public: virtual int func(int n) const; }; class Derive_Class : public Base{ public: int func(int n) const; // 默認也為虛函數 };

當我們在派生類中覆蓋某個函數時,可以在函數前加virtual關鍵字。然而這不是必須的,因為一旦某個函數被聲明成虛函數,則所有派生類中它都是 虛函數。任何構造函數之外的非靜態函數都可以是虛函數。派生類經常(但不總是)覆蓋它繼承的虛函數,如果派生類沒有覆蓋其基類中某個虛函數,則該虛函數的 行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本。

2 . 動態綁定

當我們使用基類的引用(或指針)調用一個虛函數時將發生動態綁定(dynamic binding)。因為我們直到運行時才能知道到底調用了哪個版本的虛函數,可能是基類中的版本也可能是派生類中的版本,判斷的依據是引用(或指針)所綁 定的對象的真實類型。與非虛函數在編譯時綁定不同,虛函數是在運行時選擇函數的版本,所以動態綁定也叫運行時綁定(run-time binding)。

3 . 靜態類型與動態類型

靜態類型指的是變量聲明時的類型或表達式生成的類型,它在編譯時總是已知的;動態類型指的是變量或表達式表示的內存中的對象的類型,它直到運行時才 可知。當且僅當通過基類的指針或引用調用虛函數時,才會在運行時解析該調用,也只有在這種情況下對象的動態類型才有可能與靜態類型不同。如果表達式既不是 引用也不是指針,則它的動態類型永遠與靜態類型一致。

4 . final和override

派生類中如果定義了一個函數與基類中虛函數同名但形參列表不同,編譯器會認為這是派生類新定義的函數。如果我們的意圖本是覆蓋虛函數,則這種錯誤很 難發現。通過在派生類中的虛函數最後加override關鍵字使得意圖更加清晰。如果我們使用override標記了某個函數,但該函數並沒有覆蓋已存在 的虛函數,編譯器將報錯。

?
1 2 3 4 5 6 7 8 9 class Base{ // 基類 public: virtual int func(int a, int b) const; }; class Derive_Class : public Base{ public: int func(int a) const override; // 報錯,沒有覆蓋虛函數 };

如果我們定義一個類,並不希望它被繼承。或者希望某個函數不被覆蓋,則可以把類或者函數指定為final,則之後任何嘗試繼承該類或覆蓋該函數的操作將引發錯誤。

?
1 2 3 4 class Base final { /* */ }; // 基類不能被繼承 class Derive_Class : public Base { /* */ }; // 報錯 void func(int) const final; // 不允許後續的其他類覆蓋func(int)

5 . 回避虛函數的機制

在某些情況下,我們希望對虛函數的調用不要進行動態綁定,而是強迫其執行虛函數的某個特定版本。可以使用作用域運算符實現這一目的。

?
1 2 // 強行調用基類中定義的函數版本而不管baseP的動態類型是什麽 int a = baseP->Base::func(42);

如果一個派生類虛函數需要調用它的基類版本,但是沒有使用作用域運算符,則在運行時該調用將被解析為對派生類版本自身的調用,從而導致無限遞歸。

二、純虛函數

1 . 定義

為了方便使用多態特性,我們常常需要在基類中定義虛函數。在許多情況下,在基類中不能對虛函數給出有意義的實現。為了讓虛函數在基類什麽也不做,引 進了“純虛函數”的概念,使函數無須定義。我們通過在函數體的位置(即在聲明語句的分號之前)書寫=0就可以將一個虛函數說明為純虛函數(pure virtual)。其中,=0只能出現在類內部的虛函數聲明語句處:

?
1 2 3 4 class Base{ // 抽象基類 public: virtual int func(int n) const =0; };

需要註意的是,我們也可以為純虛函數提供定義,不過函數體必須定義在類的外部。

2 . 抽象基類

含有(或者未經覆蓋直接繼承)純虛函數的類叫抽象基類(abstract base class)。抽象基類負責定義接口,而後續的其他類可以覆蓋該接口。如果派生類中沒有重新定義純虛函數,而只是繼承基類的純虛函數,則這個派生類仍然還 是一個抽象基類。因為抽象基類含有純虛函數(沒有定義),所以我們不能創建一個抽象基類的對象,但可以聲明指向抽象基類的指針或引用。

?
1 Base base; // 錯誤,不能實例化抽象基類

總結:

①.虛函數必須實現,不實現編譯器會報錯。

②.父類和子類都有各自的虛函數版本。由多態方式在運行時動態綁定。

③.通過作用域運算符可以強行調用指定的虛函數版本。

④.純虛函數聲明如下:virtual void funtion()=0; 純虛函數無需定義。包含純虛函數的類是抽象基類,抽象基類不能創建對象,但可以聲明指向抽象基類的指針或引用。

⑤.派生類實現了純虛函數以後,該純虛函數在派生類中就變成了虛函數,其子類可以再對該函數進行覆蓋。

⑥.析構函數通常應該是虛函數,這樣就能確保在析構時調用正確的析構函數版本。

C++ 虛函數 、純虛函數、接口的實用方法和意義