C++之解構函式與虛擬函式
一個例子
class Base {
public:
Base() { }
~Base() { cout << "Base 析構" << endl; }
};
class Derived :public Base {
public:
int a;
Derived() { };
~Derived() { cout << "Derived析構" << endl; };
};
Base *p;
p = new Derived;
cout << "*p的大小為" << sizeof(*p) << endl;
cout << "Derived 的大小為" << sizeof(Derived) << endl;
delete p;
輸出結果是:
*p的大小為1
Derived的大小為4
Base 析構
class Base {
public:
Base() { }
virtual ~Base() { cout << "Base 析構" << endl; }
};
class Derived :public Base {
public:
int a;
Derived() { };
~Derived() { cout << "Derived析構" << endl; };
};
Base *p;
p = new Derived;
cout << "*p 的大小為" << sizeof(*p) << endl;
cout << "Derived 的大小為" << sizeof(Derived) << endl;
delete p;
輸出結果是:
*p的大小為4
Derived的大小為8
Derived析構
Base 析構
所以: 為什麼基類的解構函式要宣告成虛擬函式
第一種情況會發生銷燬不完全的情況,因為delete p呼叫的是宣告型別(即基類)的解構函式,所以只能銷燬基類物件而無法銷燬派生類物件。
當基類的解構函式宣告為虛擬函式,那麼派生類的解構函式也是虛擬函式,此時呼叫delete p時發生動態繫結,執行時會根據實際型別呼叫該物件的虛擬函式。
當然,並不是要把所有類的解構函式都寫成虛擬函式。只有當一個類是基類(即希望被繼承)的時候才需要宣告成虛擬函式,因為虛擬函式的作用是實現多型,而多型是建立在繼承的基礎上。單一類不能把解構函式寫成虛擬函式,因為會產生額外的開銷,比如虛表的建立和虛指標的定義。
關於虛擬函式和虛表的思考
多型:
C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。
先看一段程式:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虛擬函式表地址:" << (int*)(&b) << endl;
cout << "虛擬函式表 — 第一個函式地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
pFun = (Fun)*((int*)*(int*)(&b) + 1);// Base::g()
pFun();
pFun = (Fun)*((int*)*(int*)(&b) + 2); // Base::h()
pFun();
執行結果:
虛擬函式表地址:0018FDCC
虛擬函式表 — 第一個函式地址:00B38B34
Base::f
Base::g
Base::h
通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛擬函式表的地址,然後,再次取址就可以得到第一個虛擬函式的地址了,也就是Base::f(),這在上面的程式中得到了驗證(把int* 強制轉成了函式指標)。通過這個示例,我們就可以知道如果要呼叫Base::g()和Base::h(),其程式碼如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
一般繼承(無虛擬函式覆蓋)
再看一個例子:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derived:public Base
{
public:
virtual void f1() { cout << "Base::f1" << endl; }
virtual void g1() { cout << "Base::g1" << endl; }
virtual void h1() { cout << "Base::h1" << endl; }
};
Derived d;
cout << sizeof(d) << endl;//4
pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
pFun = (Fun)*((int*)*(int*)(&d)+1);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 2);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 3);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 4);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 5);
pFun();
結果:
Base::f
Base::g
Base::h
Base::f1//(注意這裡呼叫的是Derived函式,f1)
Base::g1
Base::h1
具體解釋:http://blog.csdn.net/haoel/article/details/1948051/
請注意,在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:
我們可以看到下面幾點:
1)虛擬函式按照其宣告順序放於表中。
2)父類的虛擬函式在子類的虛擬函式前面。
一般繼承(有虛擬函式覆蓋)
覆蓋父類的虛擬函式是很顯然的事情,不然,虛擬函式就變得毫無意義。下面,我們來看一下,如果子類中有虛擬函式過載了父類的虛擬函式,會是一個什麼樣子?假設,我們有下面這樣的一個繼承關係。
為了讓大家看到被繼承過後的效果,在這個類的設計中,我只覆蓋了父類的一個函式:f()。那麼,對於派生類的例項,其虛擬函式表會是下面的一個樣子:
我們從表中可以看到下面幾點,
1)覆蓋的f()函式被放到了虛表中原來父類虛擬函式的位置。
2)沒有被覆蓋的函式依舊。
這樣,我們就可以看到對於下面這樣的程式,
Base *b = new Derive();
b->f();
由b所指的記憶體中的虛擬函式表的f()的位置已經被Derive::f()函式地址所取代,於是在實際呼叫發生時,是Derive::f()被呼叫了。這就實現了多型。
測試程式:
Derived d;
cout << sizeof(d) << endl;//4
pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
pFun = (Fun)*((int*)*(int*)(&d)+1);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 2);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 3);
pFun();
pFun = (Fun)*((int*)*(int*)(&d) + 4);
pFun();
/*pFun = (Fun)*((int*)*(int*)(&d) + 5);
pFun();*/
Base *c = new Derived();
c->f();
Derived::f
Base::g
Base::h
Derived::g1
Derived::h1
Derived::f
和上面分析的結果是一致的!!!
多重繼承(無虛擬函式覆蓋)
下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關係。注意:子類並沒有覆蓋父類的函式。
對於子類例項中的虛擬函式表,是下面這個樣子:
我們可以看到:
1) 每個父類都有自己的虛表。
2) 子類的成員函式被放到了第一個父類的表中。(所謂的第一個父類是按照宣告順序來判斷的)
這樣做就是為了解決不同的父類型別的指標指向同一個子類例項,而能夠呼叫到實際的函式。
測試:
class Base1 {
public:
virtual void f1() { cout << "Base1::f" << endl; }
virtual void g1() { cout << "Base1::g" << endl; }
virtual void h1() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f2() { cout << "Base2::f" << endl; }
virtual void g2() { cout << "Base2::g" << endl; }
virtual void h2() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f3() { cout << "Base3::f" << endl; }
virtual void g3() { cout << "Base3::g" << endl; }
virtual void h3() { cout << "Base3::h" << endl; }
};
class Derived1 :public Base1, public Base2, public Base3
{
public:
virtual void f() { cout << "Derived1::f" << endl; }
virtual void g() { cout << "Derived1::g" << endl; }
virtual void h() { cout << "Derived1::h" << endl; }
};
cout << endl << endl << endl;
Derived1 d1;
pFun = (Fun)*((int*)*(int*)(&d1) + 0);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 1);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 2);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) +3);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 4);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 5);
pFun();
Base1::f
Base1::g
Base1::h
Derived1::f
Derived1::g
Derived1::h
多重繼承(有虛擬函式覆蓋)
下面我們再來看看,如果發生虛擬函式覆蓋的情況。
下圖中,我們在子類中覆蓋了父類的f()函式。
下面是對於子類例項中的虛擬函式表的圖:
我們可以看見,三個父類虛擬函式表中的f()的位置被替換成了子類的函式指標。這樣,我們就可以任一靜態型別的父類來指向子類,並呼叫子類的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
```
安全性
每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛擬函式表有一個比較細緻的瞭解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛擬函式表來乾點什麼壞事吧。
一、通過父型別的指標訪問子類自己的虛擬函式
我們知道,子類沒有過載父類的虛擬函式是一件毫無意義的事情。因為多型也是要基於函式過載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛擬函式,但我們根本不可能使用下面的語句來呼叫子類的自有虛擬函式:
Base1 *b1 = new Derive();
b1->f1(); //編譯出錯
任何妄圖使用父類指標想呼叫子類中的未覆蓋父類的成員函式的行為都會被編譯器視為非法,所以,這樣的程式根本無法編譯通過。但在執行時,我們可以通過指標的方式訪問虛擬函式表來達到違反C++語義的行為。(關於這方面的嘗試,通過閱讀後面附錄的程式碼,相信你可以做到這一點)
二、訪問non-public的虛擬函式
另外,如果父類的虛擬函式是private或是protected的,但這些非public的虛擬函式同樣會存在於虛擬函式表中,所以,我們同樣可以使用訪問虛擬函式表的方式來訪問這些non-public的虛擬函式,這是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
額外的測試
class Base1 {
public:
virtual void f1() { cout << "Base1::f" << endl; }
virtual void g1() { cout << "Base1::g" << endl; }
virtual void h1() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f2() { cout << "Base2::f" << endl; }
virtual void g2() { cout << "Base2::g" << endl; }
virtual void h2() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f3() { cout << "Base3::f" << endl; }
virtual void g3() { cout << "Base3::g" << endl; }
virtual void h3() { cout << "Base3::h" << endl; }
};
class Derived1 :public Base1, public Base2, public Base3//Derived1中沒有虛擬函式
{
public:
void f() { cout << "Derived1::f" << endl; }
void g() { cout << "Derived1::g" << endl; }
void h() { cout << "Derived1::h" << endl; }
};
cout << endl << endl << endl;
Derived1 d1;
cout << sizeof(d1) << endl;
pFun = (Fun)*((int*)*(int*)(&d1) + 0);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 1);
pFun();
pFun = (Fun)*((int*)*(int*)(&d1) + 2);
pFun();
結果:
12
Base1::f
Base1::g
Base1::h
虛繼承
虛繼承和虛基類的定義是非常的簡單的,同時也是非常容易判斷一個繼承是否是虛繼承的,雖然這兩個概念的定義是非常的簡單明確的,但是在C++語言中虛繼承作為一個比較生僻的但是又是絕對必要的組成部份而存在著,並且其行為和模型均表現出和一般的繼承體系之間的巨大的差異(包括訪問效能上的差異),現在我們就來徹底的從語言、模型、效能和應用等多個方面對虛繼承和虛基類進行研究。
首先還是先給出虛繼承和虛基類的定義。
虛繼承:在繼承定義中包含了virtual關鍵字的繼承關係;
虛基類:在虛繼承體系中的通過virtual繼承而來的基類,需要注意的是:
struct CSubClass : public virtual CBase {}; 其中CBase稱之為CSubClass
的虛基類,而不是說CBase就是個虛基類,因為CBase還可以作為不是虛繼承體系中的基類。
有了上面的定義後,就可以開始虛繼承和虛基類的本質研究了,下面按照語法、語義、模型、效能和應用五個方面進行全面的描述。
三種訪問許可權和三種繼承
一個非常好的總結:
詳細如下:
- 如果子類從父類繼承時使用的繼承限定符是public,那麼
(1)父類的public成員成為子類的public成員,允許類以外的程式碼訪問這些成員;
(2)父類的private成員仍舊是父類的private成員,子類成員不可以訪問這些成員;
(3)父類的protected成員成為子類的protected成員,只允許子類成員訪問;(外部程式碼不可訪問)
2.如果子類從父類繼承時使用的繼承限定符是private,那麼(所有型別外部不可以訪問)
(1)父類的public成員成為子類的private成員,只允許子類成員訪問;
(2)父類的private成員仍舊是父類的private成員,子類成員不可以訪問這些成員;
(3)父類的protected成員成為子類的private成員,只允許子類成員訪問;
3.如果子類從父類繼承時使用的繼承限定符是protected,那麼(所有型別外部不可以訪問)
(1)父類的public成員成為子類的protected成員,只允許子類成員訪問;
(2)父類的private成員仍舊是父類的private成員,子類成員不可以訪問這些成員;
(3)父類的protected成員成為子類的protected成員,只允許子類成員訪問;
特別要注意的有2點1. C++ 基類私有成員被子類繼承,但不能被子類被訪問,派生類會從其基類接收所有成員,包括私有成員,這些私有成員只能在基類內被直接使用,不能在派生類或者類外直接使用。所以說私有,不是獨自擁有,而是說基類私有直接使用權。2. Protected成員可以被派生類物件訪問但是不能被該型別的普通使用者訪問。派生類只能通過派生類物件訪問其基類的protected成員,派生類對其基類型別物件的protected成員沒有特殊訪問許可權