簡(jiǎn)介
虛函數(shù)是C++中用于實(shí)現(xiàn)多態(tài)(polymorphism)的機(jī)制若未。核心理念就是通過基類訪問派生類的函數(shù)弃榨。
例如下面的兩個(gè)類:
class Base {
public:
virtual void func() { std::cout << "Base::func() is called" << std::endl;};
};
class Derive : public Base {
public:
virtual void func() { std::cout << "Derive::func() is called" << std::endl;};
};
使用時(shí):
Base* base = new Derive();
base->func(); //base雖然是指向Base的指針晦炊,但是被調(diào)用的函數(shù)func()是A的函數(shù)
此處輸出的內(nèi)容為:Derive::func() is called
。這只是一個(gè)簡(jiǎn)單的虛函數(shù)的例子涉波,從這里能大概看出虛函數(shù)的一些樣子近顷。此處也需要知道一點(diǎn),創(chuàng)建一個(gè)基類的實(shí)例可以從派生類中new創(chuàng)建出來,但是派生類的實(shí)例不能來源于new
一個(gè)基類该面,這也是面向?qū)ο蟮囊恍┰瓌t。另外一個(gè)方面信卡,虛函數(shù)只能借助于指針或者引用來達(dá)到多態(tài)的效果隔缀,例如下面這樣的代碼,雖然都是虛函數(shù)傍菇,但它卻不是多態(tài):
void f(void) {
Base b;
b.func(); // 此處輸出:"Base::func() is called"
}
多態(tài)
1.多態(tài)有什么用
C++中的虛函數(shù)的作用主要是實(shí)現(xiàn)了多態(tài)的機(jī)制猾瘸。關(guān)于多態(tài),簡(jiǎn)而言之就是用父類型的指針指向其子類的實(shí)例丢习,然后通過父類的指針調(diào)用實(shí)際子類的成員函數(shù)牵触。這種技術(shù)可以讓父類的指針有“多種形態(tài)”,這是一種泛型技術(shù)咐低。所謂泛型技術(shù)揽思,說白了就是試圖使用不變的代碼來實(shí)現(xiàn)可變的算法。比如:模板技術(shù)见擦,RTTI技術(shù)钉汗,虛函數(shù)技術(shù),要么是試圖做到在編譯時(shí)決議鲤屡,要么試圖做到運(yùn)行時(shí)決議损痰。例如:
void f(Base* b)
{
b->func(); // 被調(diào)用的是Derive::func() 還是Base::func()?
}
因?yàn)?code>func()是一個(gè)虛函數(shù),只通過這一段代碼是無法判斷調(diào)用的是哪一個(gè)類的函數(shù)酒来。從面向?qū)ο蟮牡弥矗瘮?shù)傳入的實(shí)例可能是Base
基類,也可能是派生類Derive
役首,所以需要在編譯時(shí)是無法確定此函數(shù)的調(diào)用尝丐,這種同一代碼可以產(chǎn)生不同效果的特點(diǎn)显拜,被稱為“多態(tài)”。
動(dòng)態(tài)聯(lián)編
虛函數(shù)實(shí)際上是如何被編譯器處理的呢爹袁?Lippman在深度探索C++
對(duì)象模型中的不同章節(jié)講到了幾種方式远荠,這里把“標(biāo)準(zhǔn)的”方式簡(jiǎn)單介紹一下。
我所說的“標(biāo)準(zhǔn)”方式失息,也就是所謂的VTABLE(虛函數(shù)表)機(jī)制譬淳。編譯器發(fā)現(xiàn)一個(gè)類中有被聲明為virtual
的函數(shù),就會(huì)為其創(chuàng)建一個(gè)虛函數(shù)表盹兢,也就是 VTABLE邻梆。VTABLE實(shí)際上是一個(gè)函數(shù)指針的數(shù)組,每個(gè)虛函數(shù)的指針占用這個(gè)數(shù)組的一個(gè)slot
绎秒。一個(gè)類只有一個(gè)VTABLE浦妄,不管它有多少個(gè)實(shí)例。派生 類有自己的VTABLE见芹,但是派生類的VTABLE與基類的VTABLE有相同的函數(shù)排列順序剂娄,同名的虛函數(shù)被放在兩個(gè)數(shù)組的相同位置上。后面將會(huì)詳細(xì)講虛函數(shù)表玄呛。
overload和override
- overload(重載)
overload是指一個(gè)與已有函數(shù)同名但是參數(shù)表不同的函數(shù)阅懦。例如一個(gè)函數(shù)即可以接受整型數(shù)作為參數(shù),也可以接受浮點(diǎn)數(shù)作為參數(shù)徘铝,還可以接受string等耳胎,這樣讓編譯器來判斷使用哪一個(gè)函數(shù)來達(dá)到效率最優(yōu)。 - override(覆蓋)
虛函數(shù)總是在派生類中被改寫惕它,這種改寫被稱為“override”怕午,翻譯成覆蓋貌似比較多。override是指派生類重寫基類的虛函數(shù)淹魄,就象我們前面Derive
類中重寫了Base
類中的func()
函數(shù)诗轻。重寫的函數(shù)必須有一致的參數(shù)表和返回值,C++
標(biāo)準(zhǔn)允許 返回值不同的情況揭北,但是很少編譯器支持這個(gè)feature
。目前我也沒有使用過這種特性吏颖,這會(huì)讓我的代碼變的混亂搔体,建議大家最好也不要用到這個(gè)特性。
虛函數(shù)語法
virtual關(guān)鍵字
class Base
{
public:
virtual void func();
};
class Derive: public Base
{
public:
void func(); // 沒有virtual關(guān)鍵字
};
class Derive: public Derive // 從Derive繼承半醉,不是從Base繼承
{
public:
void func(); // 也沒有virtual關(guān)鍵字
};
例如上面的代碼疚俱,Derive::func()
是虛函數(shù),Derive::func()
也同樣是虛函數(shù)缩多〈艮龋基類聲明的虛函數(shù)养晋,在派生類中也是虛函數(shù),不管是否使用virtual
關(guān)鍵字梁钾。但是通常為了代碼簡(jiǎn)潔易懂绳泉,最好在派生類中也添加上virtual
關(guān)鍵字,那么一眼就知道是虛函數(shù)姆泻。
純虛函數(shù)
class Base
{
public:
virtual void func()=0; // =0標(biāo)志一個(gè)虛函數(shù)為純虛函數(shù)
};
一個(gè)函數(shù)聲明為純虛后零酪,純虛函數(shù)的意思是:我是一個(gè)抽象類,不可以把我實(shí)例化。純虛函數(shù)用來規(guī)范派生類的行為拇勃,實(shí)際上就是所謂的“接口”四苇,在java中就是interface
。它告訴使用者方咆,我的派生類都會(huì)有這個(gè)函數(shù)月腋,并且需要實(shí)現(xiàn)它,否則就是一個(gè)指向?yàn)榭盏闹羔槹曷福瑳]有可以執(zhí)行的函數(shù)榆骚。事實(shí)是怎樣的呢,如下:
class Derive : public Base {
public:
};
int main() {
Derive* derive = new Derive();
derive->func();
}
此代碼的在gcc version 6.5.0 (MacPorts gcc6 6.5.0_1)下編譯钩述,事實(shí)結(jié)果如下:
編譯都沒有通過寨躁,所以虛函數(shù)是必須要實(shí)現(xiàn)。
虛析構(gòu)函數(shù)
首先我們看如下的代碼:
class Base {
public:
Base(){};
~Base(){std::cout << "Base::~Base() is called!" << std::endl;}
};
class Derive : public Base {
public:
Derive(){};
~Derive(){std::cout << "Derive::~Derive() is called!" << std::endl;}
};
int main(void) {
Base* base = new Derive;
delete base;
return 0;
}
結(jié)果可能跟你想的不一樣牙勘,只有Base
類的析鉤函數(shù)被調(diào)用了职恳。
所以想要達(dá)到你想要的效果,需要把基類的析構(gòu)函數(shù)加上關(guān)鍵字virtual
方面。結(jié)果下圖:
所以我們?cè)诙x析構(gòu)函數(shù)時(shí)放钦,需要給他加上virtual
關(guān)鍵字,一般情況下不需要定義成純虛函數(shù)恭金。只有在希望將一個(gè)類變成抽象類(不能實(shí)例化的類)操禀,而這個(gè)類又沒有合適的函數(shù)可以被純虛化的時(shí)候,可以使用純虛的析構(gòu)函數(shù)横腿。另外一點(diǎn)如果上面的代碼颓屑,Base
類中虛函數(shù)甚至都沒有定義,main
函數(shù)中delete base
時(shí)耿焊,什么都不會(huì)發(fā)生揪惦,也不會(huì)調(diào)用派生類A的析構(gòu)函數(shù)。
虛函數(shù)表
虛函數(shù)表
使用C++
的人都應(yīng)該知道虛函數(shù)Virtual Function
罗侯,它是通過一張?zhí)摵瘮?shù)表Virtual Table
來實(shí)現(xiàn)器腋,簡(jiǎn)稱為V-Table
,前文所說的標(biāo)準(zhǔn)方式,其他方式不討論纫塌。在這個(gè)表中诊县,主是要一個(gè)類的虛函數(shù)的地址表,這張表解決了繼承措左、覆蓋的問題依痊,保證其容真實(shí)反應(yīng)實(shí)際類中的函數(shù)。這樣媳荒,在有虛函數(shù)的類的實(shí)例中這個(gè)表被分配在了這個(gè)實(shí)例的內(nèi)存中抗悍。所以,當(dāng)我們用父類的指針來操作一個(gè)子類的時(shí)候钳枕,這張?zhí)摵瘮?shù)表就顯得非常重要了缴渊,它就像一個(gè)地圖一樣,指明了實(shí)際所應(yīng)該調(diào)用的函數(shù)鱼炒。
這里我們著重看一下這張?zhí)摵瘮?shù)表衔沼。C++
的編譯器應(yīng)該是保證虛函數(shù)表的指針存在于對(duì)象實(shí)例中最前面的位置(這是為了保證取到虛函數(shù)表的有最高的性能,如果有多層繼承或是多重繼承的情況下)昔瞧。 這意味著我們通過對(duì)象實(shí)例的地址得到這張?zhí)摵瘮?shù)表指蚁,然后就可以遍歷其中函數(shù)指針,并調(diào)用相應(yīng)的函數(shù)自晰。如下代碼通過實(shí)例地址來獲取凝化,編譯環(huán)境(gcc version 6.5.0 (MacPorts gcc6 6.5.0_1)):
class Base {
public:
virtual void func1() { std::cout << "Base::func1" << std::endl; }
virtual void func2() { std::cout << "Base::func2" << std::endl; }
virtual void func3() { std::cout << "Base::func3" << std::endl; }
};
typedef void (*Func)(void);
int main(int argc, char** argv){
Base b;
Func func = nullptr; //c++11的空指針
std::cout << "vtable pointer address:"<< (long*)(&b) << std::endl;
std::cout << "vtable address:"<< *(long*)(&b) << std::endl;
std::cout << "vtable - first function pointer address:"<< (long*)*(long*)(&b) << std::endl;
std::cout << "vtable - first function address:"<< *(long*)*(long*)(&b) << std::endl;
func = (Func)*((long*)*(long*)(&b)); // Invoke the first virtual function
func();
return 0;
}
int main(int argc, char** argv) {
Base b;
Func func1 = nullptr;
Func func2 = nullptr;
Func func3 = nullptr;
func1 = (Func)*((long*)*(long*)(&b) + 0);
func2 = (Func)*((long*)*(long*)(&b) + 1);
func3 = (Func)*((long*)*(long*)(&b) + 2);
func1();
func2();
func3();
}
上面兩段代碼的運(yùn)行結(jié)果如下圖,圖1輸出依次是虛函數(shù)表的指針地址酬荞,對(duì)指針地址取值后是虛函數(shù)表的地址搓劫,然后是第一個(gè)虛函數(shù)的指針地址,取值后為虛函數(shù)的地址混巧。類型轉(zhuǎn)化后枪向,運(yùn)行就知,指向的是第一個(gè)虛函數(shù)咧党。通過后面對(duì)地址的增加可以取到第二個(gè)秘蛔,第三個(gè)虛函數(shù)。虛函數(shù)是按照申明時(shí)的順序排列傍衡,對(duì)地址按順序加深员,就可以按順序得到虛函數(shù),如圖2蛙埂。
內(nèi)存中的結(jié)構(gòu)
如果看了上面還是有些不懂辨液,下面畫出大概的內(nèi)存中的示例圖來表示:
-
一般繼承 - 無虛函數(shù)覆蓋
覆蓋父類的虛函數(shù)是很顯然的事情,不然箱残,虛函數(shù)就變得毫無意義。如下的示意圖:
在這個(gè)繼承關(guān)系中,子類沒有覆蓋或者繼承任何的父類函數(shù)被辑,那么對(duì)于一個(gè)實(shí)例Derive d;
燎悍,虛函數(shù)表示意如下:
我們可以看到下面兩點(diǎn):- 虛函數(shù)按照其聲明順序放于表中。
- 父類的虛函數(shù)在子類的虛函數(shù)前面盼理。
-
一般繼承 - 有虛函數(shù)覆蓋
覆蓋父類的虛函數(shù)是很顯然的事情谈山,不然,虛函數(shù)就變得毫無意義宏怔。子類中有虛函數(shù)重載了父類的虛函數(shù):
我們從表中可以看到下面兩點(diǎn):
- 覆蓋的
f()
函數(shù)被放到了虛表中原來父類虛函數(shù)的位置 - 沒有被覆蓋的函數(shù)依舊
-
多重繼承 - 無虛函數(shù)覆蓋
子類并沒有覆蓋父類的函數(shù):
我們可以看到下面兩點(diǎn):
- 每個(gè)父類都有自己的虛表
- 子類的成員函數(shù)被放到了第一個(gè)父類的表中(第一個(gè)父類是按照聲明順序來判斷)
這樣做就是為了解決不同的父類類型的指針指向同一個(gè)子類實(shí)例奏路,而能夠調(diào)用到實(shí)際的函數(shù)。
-
多重繼承 - 有虛函數(shù)覆蓋
虛函數(shù)覆蓋的情況:
我們可以看見臊诊,兩個(gè)父類虛函數(shù)表中的
f()
的位置被替換成了子類的函數(shù)指針鸽粉。這樣,我們就可以任一靜態(tài)類型的父類來指向子類抓艳,并調(diào)用子類的f()
触机。如下的示例:
int main(int argc, char ** argv) {
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
}
安全性
我們知道,子類沒有重載父類的虛函數(shù)是一件毫無意義的事情玷或。因?yàn)槎鄳B(tài)也是要基于函數(shù)重載儡首。任何妄圖使用父類指針想調(diào)用子類中的未覆蓋父類的成員函數(shù)的行為都會(huì)被編譯器視為非法,所以偏友,這樣的程序根本無法編譯通過蔬胯。但在運(yùn)行時(shí),我們可以通過指針的方式訪問虛函數(shù)表來達(dá)到違反C++
語義的行為位他。如下代碼:
class Base {
public:
virtual void func1() { std::cout << "Base::func1() is called!" << std::endl; }
virtual void func2() { std::cout << "Base::func2() is called!" << std::endl; }
};
class Derive : public Base {
public:
virtual void func1() { std::cout << "Derive::func1() is called!" << std::endl; }
virtual void func3() { std::cout << "Derive::func3() is called!" << std::endl; }
};
Base* b = new Derive();
b->func3(); //編譯失敗
Derive* d = new Derive();
d->func2(); //輸出 Base::func2() is called!
Base* b = new Derive();
b->func1(); //輸出 Derive::func1() is called!
第二種情況是訪問non-public
的虛函數(shù)氛濒。如果父類的虛函數(shù)是private
或protected
,但這些非public
的虛函數(shù)同樣會(huì)存在于虛函數(shù)表中棱诱,所以泼橘,我們同樣可以使用訪問虛函數(shù)表的方式來訪問這些non-public
的虛函數(shù),這很容易做到迈勋。如下:
class Base {
private:
virtual void func1() { std::cout << "Base::func1() is called!" << std::endl; }
};
class Derive : public Base {
};
typedef void (*Func)(void);
int main(int argc, char** argv) {
Derive d;
Func func = (Func)*((long*)*(long*)(&d)+0);
func();
return 0;
}
這種不安全性不僅僅是在虛函數(shù)表會(huì)有炬灭,其他地方也會(huì)有。只要指針能訪問內(nèi)存靡菇,那么就可以做到許多意想不到的事重归,編寫代碼時(shí)需要自己注意這些,不是特別的需要還是不要通過的這樣的方式訪問厦凤。因?yàn)橹苯油ㄟ^訪問內(nèi)存的方式不好理解鼻吮,同時(shí)也容易出錯(cuò)。但是拋開來說訪問內(nèi)存也是
C/C++
語言的魅力所在较鼓!