一個簡單的「貓吃老鼠」例子,5分鐘徹底弄懂C++中的虛函數

2020-12-07 IT劉小虎

初學者剛接觸C++語言中的 virtual 函數(虛函數)時,常常會感覺到迷惑,比如,書上說虛函數定義在基類中,其他繼承此基類的派生類都可以重寫該虛函數,因此虛函數是C++語言多態特性中非常重要的概念。但是派生類也可以重寫基類中的其他的常規函數(非虛函數)呀,那為什麼還要引入虛函數這樣看起來很複雜的概念呢?

為什麼要引入虛函數?

「貓吃老鼠」

本文不打算從理論上探討C++語言引入虛函數的原因,那樣太枯燥乏味了,我們先來看一個例子,直觀上感覺下常規(非虛)函數在面向對象編程中的局限性,請看:

定義了Animal(動物)和Cat(貓)兩個

上面這段C++語言代碼定義了Animal(動物)和Cat(貓)兩個類,其中Cat繼承了Animal(貓顯然是動物),我們可以在 main() 函數中使用這兩個類:

Animal *animal = new Animal;

Cat *cat = new Cat;animal->eat();

// 輸出: "I'm eating generic food."

cat->eat();

// 輸出: "I'm eating a rat."

到這裡一切都挺好的,動物吃食物,貓吃老鼠,即使不使用virtual關鍵字定義虛函數也完全沒有問題。現在稍稍修改下這段C++語言代碼,引入另外一個函數func()(暫時不考慮實用性,僅做示例),它的代碼如下,請看:

void func(Animal *xyz){

xyz->eat();

}

在 main() 函數中調用 func() 函數,相關的C++語言代碼示例如下,請看:

Animal *animal = new Animal;

Cat *cat = new Cat;func(animal);

// 輸出: "I'm eating generic food."

func(cat);

// 輸出: "I'm eating generic food."

出問題了~請注意第二個 func() 函數調用,我們傳遞了一個Cat對象指針給它,但是輸出的卻不是「I'm eating a rat.」!仔細觀察一下,發現 func() 函數的參數類型是Animal *xyz,那麼為了讓 func() 函數也能輸出「I'm eating a rat.」,只能重載 func() 函數了:

void func(Animal *xyz){

xyz->eat();

}

void func(Cat *xyz){

xyz->eat();

}

現在 func() 函數能夠根據輸入參數類型的不同,輸出不同的內容了。可是問題來了,Animal(動物)是一個基類,它能派生出的動物遠遠不止Cat(貓)一種,若是派生出許多派生類,每個派生類都重載一遍 func() 函數,是不是太麻煩了?

太麻煩了

神奇的虛函數

在C++語言中,重寫基類中的常規(非虛)函數當然是可以的,但是從上面的例子可以看出,重寫常規函數實現多態有時會帶來非常麻煩的問題,要避免這樣的問題可以使用 virtual function(虛函數)。現在,我們在 Animal 基類的 eat() 成員函數前加上virtual關鍵字:

加上virtual關鍵字

此時無需重載 func() 函數,僅保留一份func() 函數:

void func(Animal *xyz){

xyz->eat();

}

再執行下面的C++語言代碼,輸出就不同了:

func(animal);

// 輸出: "I'm eating generic food."

func(cat);

// 輸出: "I'm eating a rat."

看來,C++語言中的虛函數的確有些過人之處,有必要好好了解一下它。

事實上,每一個C++程式設計師都不可能避開虛函數的,就像每一個C語言程式設計師都不可能避開指針一樣。

再總結下「什麼是虛函數」

理論是少不了的,它可以加深和儘可能全面的幫助我們理解概念。基類中的虛函數允許派生類重寫功能,編譯器會保證派生類對象使用的是自己重寫的功能,即使對象是通過基類指針訪問的,例如前文中的 func(Animal *xyz) 函數,func(cat) 輸出的實際上是 Cat 類重寫的功能。這是一個非常有用的特性,調用者甚至都不需要知道 Cat 等派生類的實現,因為只需使用基類 Animal 指針就能夠輕易的調用所有派生類的重寫功能。

基類的虛函數可以完全被重寫,也可以部分的被重寫,所謂的「部分被重寫」,其實就是派生類在重寫基類虛函數時,也可以調用基類虛函數的功能。

虛函數和常規函數被調用時有什麼不同?

常規的非虛函數是靜態解析的,即在編譯時即可根據指針指向的對象確定是否被調用,例如文章開頭的例子,如果 eat() 函數是非虛函數:

// eat() 函數不是虛函數

Animal *animal = new Animal;

Cat *cat = new Cat;

animal->eat();

// 輸出: "I'm eating generic food."

cat->eat();

// 輸出: "I'm eating a rat."

此時編譯器在編譯時就能確定 animal->eat() 調用的是 Animal::eat() 函數,cat->eat() 調用的是 Cat::eat() 函數。在 func(Animal *xyz) 函數中,因為其形參是 Animal *指針類型,所以即使傳入的是 cat 對象指針,在 func() 函數內部也會被強制轉換為Animal *指針,因此 func(cat) 調用的仍然是 Animal::eat() 函數。

而虛函數就不同了,它是動態解析的,也即在程序被編譯後,運行時才根據對象的類型,而不是指向對象的指針類型決定其是否被調用,這就是說為的「動態綁定」。

關於「動態綁定」,我們在下一節中再做詳細討論,這裡先留個印象。

在C++語言中,如果某個類有虛函數,那麼大多數編譯器都會自動的為其對象維護一個隱藏的「虛指針(virtul-pointer)」,虛指針指向一個全局「虛表(virtual-table)」,虛表中存放若干函數指針,這些函數指針指向類中的虛函數。請看下面這段C++語言代碼:

class A {

public:

virtual void vfoo1();

virtual void vfoo2();

void foo1();

void foo2();

private:

int prv_i1, prv_i2;

};

顯然,類 A 有兩個常規函數以及兩個 int 型的成員變量,此外,它還有兩個虛函數,因此編譯器會創建一個虛表,虛表中存放的是函數指針,它們分別指向類 A 的虛函數,如下圖所示:

類A的虛表和虛函數

這裡應注意,虛表是屬於類的,而不是對象的,也就是說,即使有成千上萬個 A 對象,虛表也僅有一個,這些對象共用一個類虛表。編譯器會自動的為每個對象創建一個隱藏的「虛指針」__vptr,它指向類 A 的虛表,如下圖所示:

隱藏的「虛指針」__vptr

C++語言這樣實現虛函數機制的空間開銷是微乎其微的,事實上,每一個對象只需要一個額外的「虛指針」__vptr就能夠調用類的虛函數。同樣的,時間開銷也很小:相比於常規函數的調用,虛函數的調用只不過多出了額外的兩個步驟:

獲取虛表指針,得到虛表從虛表中取出虛函數的地址讀者應注意,這裡的討論為了突出主題,理想化了一些情況。

為什麼類成員函數默認都不是虛函數?

既然虛函數這麼好用,那為什麼C++語言不把類的成員函數默認定義為虛函數呢?

其實仔細考慮一下,虛函數的「好用」主要體現在定義在基類中實現多態上,但並不是所有的類都需要被設計為基類,一昧的使用虛函數可能會造成語義上的歧義,隱藏程式設計師的設計。僅將需要被繼承的基類中需要被重寫的函數定義為虛函數,要比將所有函數定義為虛函數清晰多了。

此外,經過前面的討論,我們也知道虛函數的效率實際上是沒有常規函數高的,同樣的功能中,僅從被調用過程來看,它的時間開銷和空間開銷都比常規函數高,每個對象還需要額外的虛指針索引虛表。

小結

點個關注吧

歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。

未經許可,禁止轉載。

相關焦點

  • c++ 內存,虛函數,運算函數,三角函數
    :就是java中的抽象,純虛函數只有聲明沒有具體實現就是空方法,在子類中必須重新寫,虛函數就是在基類中寫了有實現。他們都得用關鍵字 virtual(虛擬的) 聲明的函數1. 虛函數和純虛函數可以定義在同一個類(class)中,含有純虛函數的類被稱為抽象類(abstract class),而只含有虛函數的類(class)不能被稱為抽象類(abstract class)。2.
  • C++中的虛函數(virtual function)
    正在閱讀:C++中的虛函數(virtual function)C++中的虛函數(virtual function)2005-07-15 10:36出處:作者:unknow  這個例子是虛函數的一個典型應用,通過這個例子,也許你就對虛函數有了一些概念。它虛就虛在所謂「推遲聯編」或者「動態聯編」上,一個類函數的調用並不是在編譯時刻被確定的,而是在運行時刻被確定的。由於編寫代碼的時候並不能確定被調用的是基類的函數還是哪個派生類的函數,所以被成為「虛」函數。
  • C++中函數重載的例子
    函數重載是指在同一作用域內,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱為重載函數。重載函數通常用來命名一組功能相似的函數,這樣做減少了函數名的數量,避免了名字空間的汙染,對於程序的可讀性有很大的好處。
  • C++的轉換手段並與explicit關鍵詞配合使用
    static_cast:任何具有明確定義的類型轉化,只要不包含底層const,都可以使用static_cast,舉一個例子。4.使用dynamic_cast進行轉換的,基類中一定要有虛函數,否則編譯不通過。可以從父類轉基類,但是可能為空5.在類的轉換時,在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的。在進行下行轉換時,dynamic_cast具有類型檢查的功能,比static_cast更安全。
  • 《貓和老鼠》遊戲中貓為什麼不吃奶酪?一張圖給出答案!
    在遊戲裡,老鼠尋找奶酪,並把它們推進洞中,而貓這是奶酪守護者,但是防守的再嚴密,依舊會有奶酪被老鼠推進洞裡。那麼問題來了,為什麼貓不吃奶酪?其實在遊戲剛出來的時候,那時候貓是可以吃奶酪掉在洞口的殘渣的,至於說為什麼現在的貓不吃奶酪,玩家用一張圖給出了答案。
  • 函數與反函數
    (這樣能將現實中的很多事情數學化)什麼是映射?映射是兩個集合之間的對應規則。具體的,已知集合X和Y. 集合X到Y的一個映射f,是將X中的任何一個元素,都對應Y中的一個元素。抽象的說,映射f是一些偶對的集合{ (a,b): f(a)=b }例如大帥,你今天更新的文章太難懂了吧。
  • 吃貓的大老鼠,重30斤,吃了2000多隻貓後與貓王同歸於盡
    我說一件事大家自己想想,這個老鼠沒事就吃愛吃個貓當小菜。衍聖公府為了對付他找了無數的貓,然後它吃了無數的貓,足有幾千隻,差點撐死,衍聖公簡直要氣死了。 後來有一個西洋商人來到衍聖公府,帶來一隻異種大貓,號稱貓王,說保證可以收拾這隻巨鼠,但是要價50兩黃金
  • 【趣味百科】貓為什麼喜歡吃老鼠?
    ——貓和老鼠的天敵關係,地球人都知道貓和老鼠是天敵,自古以來,貓就是普通百姓家裡豢養的抓鼠能手。動畫片《貓和老鼠》中的貓雖然總是被老鼠捉弄,可是現實中老鼠最害怕的就是貓,哪還有膽調戲貓呢?貓以老鼠為食,充滿了站在食物鏈上端的優越感。可是,你知道貓為什麼愛吃老鼠嗎?
  • 十二生肖的來歷,貓為什麼吃老鼠?
    關於他們的來歷,故事中這樣說:人族方興,萬物初安,天幹地支剛定時,玉皇大帝頒布法定普召天下動物,要按子、醜、寅、卯、辰、巳、午、未、申、酉、戌、亥十二地支選出十二個屬相。選定吉日報名,按到達天庭的先後順序排名。
  • 《貓和老鼠》是真科學神作:貓才沒那麼愛抓老鼠呢!
    簡單的說,在液體的定義中有一個基本理念,那就是需要材料必須能夠改變其形狀,以適應容器。而這個改變的動作也必須有一個特殊的持續時間,它就被稱作「弛豫時間」。要確定某物是否是液體,則取決於它是否能在一個比弛豫時間更短或更長的時間內觀察到的,簡單說,弛豫時間除去實驗時間可以得到一個底波拉數,當這個數字大於1,則材料相對固體;相反如果結果小於1,則材料相對液體。
  • 老鼠搶了貓的生肖OR補充所需的牛磺酸 貓為什麼要吃老鼠你造嗎?
    老鼠搶了貓的生肖OR補充所需的牛磺酸 貓為什麼要吃老鼠你造嗎?時間:2020-06-15 09:18   來源:99遊戲    責任編輯:沫朵 川北在線核心提示:原標題:老鼠搶了貓的生肖OR補充所需的牛磺酸 貓為什麼要吃老鼠你造嗎? 小雞寶寶考考你,貓為什麼要吃老鼠?
  • 主人把兔子和貓放在一起,10分鐘後悲劇發生:不可以吃兔兔!
    對於貓咪,相信大家都是有所了解的,貓咪是一種好奇心很重的動物,不管是看到什麼東西,都想去看一看,抓一抓,或者是咬一咬,非要把那東西弄懂了不行。對於它們來說,自己就會死最厲害的,什麼都不怕,什麼都敢做。有一句話叫做」好奇心害死貓「,這也足以說明貓咪的好奇心有多大了,什麼東西都逃不過它的嘴和爪子。貓咪之前會抓老鼠吃,抓鳥吃,抓魚吃,非常的厲害。雖然說,現在的很多貓什麼都不用做,但是別忘了,它還是一個肉食動物。主人把兔子跟貓放在一起,一轉身悲劇發生:不,怎麼可以吃兔兔!
  • C++ 函數重載例子
    //函數重載代碼示例#include int qq(int a,int b);int qq(int
  • 小貓被老鼠叼走,男子笑著拍視頻,老鼠不是吃貓鼠,人卻是冷心人
    還有老鼠不怕貓?不是說好的「血脈」壓制嗎?從小到大,無論是看過的動畫片,還是現實生活中的經歷,都在告訴咱們一個事實——貓抓老鼠。《黑貓警長》裡的吃貓鼠咱記得小時候特別愛看的《黑貓警長》,雖然只有短短五集,但第一集裡面的「一隻耳」那滑頭的形象真是令咱記憶尤深。
  • 5首:貓和老鼠的古典古風音樂,滿滿的回憶!我有一個大膽的想法
    5首:貓和老鼠的古典古風音樂,滿滿的回憶!我有一個大膽的想法有多少次我們聽到世界名曲總是感覺似曾相識,又有多少次我們聽到交響樂民謠的時候腦子裡就是他們兩個的身影?感謝貓和老鼠,很小的時候的記憶?說實話這裡面是早期的古風音樂。1.
  • 每過一分鐘,就有一個物種在地球上徹底消失
    作者:不正經的大白編輯:不正經的大白審核:李柯縈自從生命起源以來地球上先後出現了多達5億種生物絕大部分都已經徹底消失於漫長的生命長河物種的滅絕只是再正常不過的事情諸如早已經沉睡在2.5億年前地質層的三葉蟲
  • 老鼠真的怕貓嗎?淺談老鼠的天敵
    老鼠,俗稱「耗子」,是哺乳動物中繁殖最快、生存能力很強的動物。無論室內、野外都可以看到它們的足跡。在適宜的環境下,從理論上說,老鼠的數量可以按幾何級數倍增。貓行動敏捷,善於跳躍,平衡能力極強;同時,貓的趾底有脂肪質肉墊,因而行走無聲,捕鼠時不會驚跑鼠,可謂是老鼠的剋星。很多人不知道,貓之所以喜愛吃魚和老鼠,是因為貓是夜行動物,為了在夜間能看清事物,需要大量的牛磺酸,而老鼠和魚的體內就含牛磺酸,所以貓不僅僅是因為喜歡吃魚和老鼠,也是因為自己的需要才吃。天生克制,典型的爸爸打兒子,吃了還有好處,老鼠能不怕嗎?
  • 為什麼老鼠會怕貓?
    如果貓在附近,有的老鼠即使還沒看到貓的身影,也會拔腿就跑。這是為什麼呢,難道老鼠能聞出貓的氣味?老鼠有兩個嗅覺器官可以聞到東西,一個就是我們所熟知的鼻子,另一個則是犁鼻器(Vomeronasal organ,縮寫VNO)。
  • 貓和老鼠:萊特寧的二武來啦!掌握方法快速拿到尚方寶劍——鹹魚
    在貓和老鼠手遊裡,每個角色都有它們特殊的武器,其中有些角色還有2個武器可以選擇。這次5月14號的更新,萊特寧的第二武器終於來啦!掌握方法,快速拿到尚方寶劍——鹹魚!PS:武器鐵塊和藍圖是通用的,舉個例子,我們可以完成傑瑞的成就任務拿到鐵塊和藍圖,然後去解鎖萊特寧的第二武器。快速拿到80積分的辦法當然是皮膚了,擁有4個皮膚就能拿到75積分,之後隨便做一個任務就搞定了。但是,大部分玩家只有1~2個皮膚(土豪當我沒說過),最多只能拿到15積分,其他積分的只能靠任務了。
  • 一個簡單的例子學明白用Python插值
    這篇文章嘗試通過一個簡單的例子來為讀者講明白怎樣使用Python實現數據插值。總共分3部分來介紹:為什麼需要做插值這種事?通過拉格朗日插值法來看看插值這個事的理論要怎麼理解?Python實現拉格朗日插值的一個例子。為什麼需要做插值這種事?