在Java、C#中有關鍵詞abstract指明抽象函數、抽象類,但是在C++中沒有這個關鍵詞,很顯然,在C++也會需要只需要在基類聲明某函數的情況,而不需要寫具體的實現,那C++中是如何實現這一功能的,答案是純虛函數。 含有純虛函數的類是抽象類,不能生成對象,只能派生。他派生的類的純虛函數沒有被改寫,那麼它的派生類還是個抽象類。定義純虛函數就是爲了讓基類不可實例化化,因爲實例化這樣的抽象數據結構本身並沒有意義,或者給出實現也沒有意義。
一. 純虛函數
在許多情況下,在基類中不能給出有意義的虛函數定義,這時可以把它說明成純虛函數,把它的定義留給派生類來做。定義純虛函數的一般形式爲:
class 類名{
virtual 返回值類型 函數名(參數表)= 0; // 後面的"= 0"是必須的,否則,就成虛函數了
};
純虛函數是一個在基類中說明的虛函數,它在基類中沒有定義,要求任何派生類都定義自己的版本。純虛函數爲各派生類提供一個公共界面。
從基類繼承來的純虛函數,在派生類中仍是虛函數。二. 抽象類
1. 如果一個類中至少有一個純虛函數,那麼這個類被稱爲抽象類(abstract class)。
抽象類中不僅包括純虛函數,也可包括虛函數。抽象類中的純虛函數可能是在抽象類中定義的,也可能是從它的抽象基類中繼承下來且重定義的。2. 抽象類特點,即抽象類必須用作派生其他類的基類,而不能用於直接創建對象實例。
一個抽象類不可以用來創建對象,只能用來爲派生類提供一個接口規範,派生類中必須重載基類中的純虛函數,否則它仍將被看作一個抽象類。
3. 在effective c++上中提到,純虛函數可以被實現(定義)(既然是純虛函數,爲什麼還可以被實現呢?這樣做有什麼好處呢?下文中“巧用純虛析構函數實現接口類”中將說明這一功能的目的。),但是,不能創建對象實例,這也體現了抽象類的概念。
三. 虛析構函數
虛析構函數: 在析構函數前面加上關鍵字virtual進行說明,稱該析構函數爲虛析構函數。雖然構造函數不能被聲明爲虛函數,但析構函數可以被聲明爲虛函數。
一般來說,如果一個類中定義了虛函數, 析構函數也應該定義爲虛析構函數。
例如:
class B
{
virtual ~B(); //虛析構函數
…
};
下面介紹一些實例:
- #include <stdio.h>
- class Animal
- {
- public:
- Animal() //構造函數不能被聲明爲虛函數
- {
- printf(" Animal construct! \n");
- }
- virtual void shout() = 0;
- virtual void impl() = 0;
- virtual ~Animal() {printf(" Animal destory! \n");}; // 虛析構函數
- };
- void Animal::impl() // 純虛函數也可以被實現。
- {
- printf(" Animal: I can be implement! \n");
- }
- class Dog: public Animal
- {
- public:
- Dog()
- {
- printf(" Dog construct! \n");
- }
- virtual void shout() // 必須要被實現,即使函數體是空的
- {
- printf(" Dog: wang! wang! wang! \n");
- }
- virtual void impl()
- {
- printf(" Dog: implement of Dog! \n");
- }
- virtual ~Dog() {printf(" Dog destory! \n");}; // 虛析構函數
- };
- class Cat: public Animal
- {
- public:
- Cat()
- {
- printf(" Cat construct! \n");
- }
- virtual void shout() // 必須要被實現,即使函數體是空的
- {
- printf(" Cat: miao! miao! miao! \n");
- }
- virtual void impl()
- {
- printf(" Cat: implement of Cat! \n");
- }
- virtual ~Cat() {printf(" Cat destory! \n");}; // 虛析構函數
- };
- /*
- Animal f() // error, 抽象類不能作爲返回類型
- {
- }
- void display( Animal a) //error, 抽象類不能作爲參數類型
- {
- }
- */
- //ok,可以聲明抽象類的引用
- Animal &display(Animal &a)
- {
- Dog d;
- Animal &p = d;
- return p;
- }
- void test_func()
- {
- //Animal a; // error: 抽象類不能建立對象
- Dog dog; //ok,可以聲明抽象類的指針
- Cat cat; //ok,可以聲明抽象類的指針
- printf("\n");
- Animal *animal = &dog;
- animal->shout();
- animal->impl();
- printf("\n");
- animal = &cat;
- animal->shout();
- animal->impl();
- printf("\n");
- }
- int main()
- {
- test_func();
- while(1);
- }
- //result:
- /*
- Animal construct!
- Dog construct!
- Animal construct!
- Cat construct!
- Dog: wang! wang! wang!
- Dog: implement of Dog!
- Cat: miao! miao! miao!
- Cat: implement of Cat!
- Cat destory!
- Animal destory!
- Dog destory!
- Animal destory!
- */
- (YC:代碼已調試無誤)
四. 巧用純虛析構函數實現接口類
c++不像java一樣有純接口類的語法,但我們可以通過一些手段實現相同的功能。
(1)能不能用“protected”實現接口類?
看如下代碼:
- #include <stdio.h>
- class A
- {
- protected:
- virtual ~A()
- {
- printf(" A: 析構函數 \n");
- }
- };
- class B : public A
- {
- public:
- virtual ~B()
- {
- printf(" B: 析構函數 \n");
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- //A* p1 = new A; //error:[1]有問題
- //delete p1;
- B* p2 = new B; //ok:[2]沒問題,輸出結果爲:
- delete p2; /* B: 析構函數
- A: 析構函數*/(注意此處還是會調用A的析構函數的,不過編譯沒問題)
- //A* p3 = new B;
- //delete p3; //error:[3] 有問題
- return 0;
- }
通過在類中,將類的構造函數或者析構函數申明成protected ,可以有效防止類被實例話,要說實用的話,構造函數是protected更有用,肯定能保證類不會被實例化,而如果析構函數是protected的話,構造函數不是protected的話,還可能存在編譯通過的漏洞,如下:
Case1:
- class A
- {
- protected:
- A()
- {
- printf(" A: A() \n");
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p1 = new A; //編譯不通過,無法訪問protected構造函數
- delete p1;
- return 0;
- }
Case2:
- class A
- {
- protected:
- ~A()
- {
- printf(" A: ~A() \n");
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p1 = new A; //編譯通過,此時因爲僅僅是用到了A的構造函數,還不需要它的析構函數
- return 0;
- }
- (附:如果將main中改爲:
- int _tmain(int argc, _TCHAR* argv[])
- {
- A a;
- return 0;
- }
- 則編譯出錯,提示無法訪問protected成員A::~A().兩種情況出現差異的原因是什麼?
- )
Case3:
- class A
- {
- protected:
- ~A()
- {
- printf(" A: ~A() \n");
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p1 = new A;
- delete p1; //編譯失敗,因爲編譯器發現A的析構函數是protected
- return 0;
- }
所以,一種可行的辦法貌似是這樣的:
- class A
- {
- protected:
- virtual ~A()
- {
- printf(" A: ~A() \n");
- }
- };
- class B : public A
- {
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- B* p =new B; //ok:這種情況下確實是可行的(YC:仔細看會發現這種情況同“(1)看如下代碼”下面的代碼中ok的情況相同)
- delete p;
- return 0;
- }
由於B public繼承自A,所以其可以完全訪問A的構造或析構函數,但是:
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p =new B;
- delete p; //error:由於p變成指向A的指針,字面上編譯器需要知道A的析構函數,然後A的析構函數又是protected
- return 0;
- }
即便像這樣B顯示重載了A的析構函數:
- class A
- {
- protected:
- virtual ~A()
- {
- printf(" A: ~A() \n");
- }
- };
- class B : public A
- {
- public:
- virtual ~B()
- {
- printf(" B: ~B() \n");
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- A* p =new B;
- delete p; //error:也還是不行,因爲重載是運行時的事情,在編譯時編譯器就認定了A的析構函數,結果無法訪問
- return 0
- }
小結:
貌似用protected這樣的方法並不是很恰當,雖然在遵守一定規則的情況下確實有他的實用價值,但並不是很通用(2)應該怎樣實現接口類?
其實上面protected的思路是對的,無非是讓父類無法實例化,那麼爲了讓父類無法實例化,其實還有一個方法,使用純虛函數。
- class A
- {
- public: //這裏就不用protected了
- virtual ~A() = 0;
- };
- class B : public A
- {
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- B* p =new B;
- delete p; //編譯ok,鏈接error
- return 0;
- }
這樣寫貌似不錯,以往大家都把類中的一般成員函數寫成純虛的,這次將析構函數寫成純虛的,更加增加通用性,編譯也通過了,但就是在鏈接的時候出問題,報錯說找不到A的析構函數的實現,很顯然嘛,因爲A的析構是純虛的嘛。
那麼如何修改上述代碼可以達到既可以去除上述error,又可以讓基類不能被實例化呢?如下所示:
- class A
- {
- public: //這裏就不用protected了
- virtual ~A() = 0 //它雖然是個純虛函數,但是也可以被實現
- { //這個語法很好很強大(完全是爲了實現其接口類而弄的語法吧)
- printf(" A: ~A() \n");
- }
- };
- class B : public A
- {
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- B* p =new B;
- delete p;
- A* p2 =new B;
- delete p2; //不用擔心編譯器報錯了,因爲此時A的析構函數是public
- return 0;
- }
- //result:
- /*
- A: ~A()
- A: ~A()
- */
如此終於大功告成了,注意,不能將構造函數替代上面的析構函數的用法,因爲構造函數是不允許作爲虛函數的。
補充:以上那個語法就真的只是爲了這種情況而存在的,因爲一般我們在虛類中申明的接口:
virtual foo()= 0;
virtual foo()= 0 {}
這兩種寫法是完全沒有區別的,純虛函數的默認實現,僅僅在它是析構函數中才有意義!!!
所以可以說,老外是完全爲了這一個目的而發明了這種語法...
最終的接口類
- classInterface
- {
- public:
- virtual ~Interface() = 0 {}
- };
應該挺完美的了吧
[備註:內容多收集於