虛函數(shù)

簡(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é)果如下:


1563933633906.jpg

編譯都沒有通過寨躁,所以虛函數(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)用了职恳。

1564019256607.jpg

所以想要達(dá)到你想要的效果,需要把基類的析構(gòu)函數(shù)加上關(guān)鍵字virtual方面。結(jié)果下圖:

image.png

所以我們?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蛙埂。


1.png
2.png
內(nèi)存中的結(jié)構(gòu)

如果看了上面還是有些不懂辨液,下面畫出大概的內(nèi)存中的示例圖來表示:

  • 一般繼承 - 無虛函數(shù)覆蓋
    覆蓋父類的虛函數(shù)是很顯然的事情,不然箱残,虛函數(shù)就變得毫無意義。如下的示意圖:

    image.png

    在這個(gè)繼承關(guān)系中,子類沒有覆蓋或者繼承任何的父類函數(shù)被辑,那么對(duì)于一個(gè)實(shí)例Derive d;燎悍,虛函數(shù)表示意如下:
    image.png

    我們可以看到下面兩點(diǎn):

    1. 虛函數(shù)按照其聲明順序放于表中。
    2. 父類的虛函數(shù)在子類的虛函數(shù)前面盼理。

  • 一般繼承 - 有虛函數(shù)覆蓋
    覆蓋父類的虛函數(shù)是很顯然的事情谈山,不然,虛函數(shù)就變得毫無意義宏怔。子類中有虛函數(shù)重載了父類的虛函數(shù):


    image.png
image.png

我們從表中可以看到下面兩點(diǎn):

  1. 覆蓋的f()函數(shù)被放到了虛表中原來父類虛函數(shù)的位置
  2. 沒有被覆蓋的函數(shù)依舊

  • 多重繼承 - 無虛函數(shù)覆蓋
    子類并沒有覆蓋父類的函數(shù):


    image.png
image.png

我們可以看到下面兩點(diǎn):

  1. 每個(gè)父類都有自己的虛表
  2. 子類的成員函數(shù)被放到了第一個(gè)父類的表中(第一個(gè)父類是按照聲明順序來判斷)

這樣做就是為了解決不同的父類類型的指針指向同一個(gè)子類實(shí)例奏路,而能夠調(diào)用到實(shí)際的函數(shù)。

  • 多重繼承 - 有虛函數(shù)覆蓋
    虛函數(shù)覆蓋的情況:


    image.png

image.png

我們可以看見臊诊,兩個(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ù)是privateprotected,但這些非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;
}

image.png

這種不安全性不僅僅是在虛函數(shù)表會(huì)有炬灭,其他地方也會(huì)有。只要指針能訪問內(nèi)存靡菇,那么就可以做到許多意想不到的事重归,編寫代碼時(shí)需要自己注意這些,不是特別的需要還是不要通過的這樣的方式訪問厦凤。因?yàn)橹苯油ㄟ^訪問內(nèi)存的方式不好理解鼻吮,同時(shí)也容易出錯(cuò)。但是拋開來說訪問內(nèi)存也是C/C++語言的魅力所在较鼓!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末椎木,一起剝皮案震驚了整個(gè)濱河市违柏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌香椎,老刑警劉巖漱竖,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異畜伐,居然都是意外死亡馍惹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門玛界,熙熙樓的掌柜王于貴愁眉苦臉地迎上來万矾,“玉大人,你說我怎么就攤上這事慎框×急罚” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵鲤脏,是天一觀的道長(zhǎng)们颜。 經(jīng)常有香客問我,道長(zhǎng)猎醇,這世上最難降的妖魔是什么窥突? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮硫嘶,結(jié)果婚禮上阻问,老公的妹妹穿的比我還像新娘。我一直安慰自己沦疾,他們只是感情好称近,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哮塞,像睡著了一般刨秆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忆畅,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天衡未,我揣著相機(jī)與錄音,去河邊找鬼家凯。 笑死缓醋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的绊诲。 我是一名探鬼主播送粱,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼掂之!你這毒婦竟也來了抗俄?” 一聲冷哼從身側(cè)響起脆丁,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橄镜,沒想到半個(gè)月后偎快,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洽胶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裆馒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姊氓。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖喷好,靈堂內(nèi)的尸體忽然破棺而出翔横,到底是詐尸還是另有隱情,我是刑警寧澤梗搅,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布禾唁,位于F島的核電站,受9級(jí)特大地震影響无切,放射性物質(zhì)發(fā)生泄漏荡短。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一哆键、第九天 我趴在偏房一處隱蔽的房頂上張望掘托。 院中可真熱鬧,春花似錦籍嘹、人聲如沸闪盔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泪掀。三九已至,卻和暖如春颂碘,著一層夾襖步出監(jiān)牢的瞬間异赫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工凭涂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祝辣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓切油,卻偏偏與公主長(zhǎng)得像蝙斜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子澎胡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容