C++筆記-多態(包含虛函數,純虛函數和虛函數表等)

1.多態的概念

多態(polymorphism)的概念:通俗來說,就是多種形態。多態分為編譯時多態(靜態多態)和運行時多態(動態多態),這里我們重點講運行時多態,編譯時多態(靜態多態)和運行時多態(動態多態)。編譯時多態(靜態多態)主要就是我們前面講的函數重載和函數模板,他們傳不同類型的參數就可以調用不同的函數,通過參數不同達到多種形態,之所以叫編譯時多態,是因為他們實參傳給形參的參數匹配是在編譯時完成的,我們把編譯時一般歸為靜態,運行時歸為動態。
運行時多態,具體點就是去完成某個行為(函數),可以傳不同的對象就會完成不同的行為,就達到多種形態。比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是優惠買票(5折或75折);軍人買票時是優先買票。再比如,同樣是動物叫的一個行為(函數),傳貓對象過去,就是”(>^w^<)喵“,傳狗對象過去,就是"汪汪"。

2.多態的定義及實現

2.1多態的構成條件

多態是一個繼承關系下的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了Person。Person對象買票全價,Student對象優惠買票。

2.1.1實現多態還有兩個必須重要條件

1.必須是基類的指針或者引用調用虛函數

2.被調用的函數必須是虛函數,并且完成了虛函數重寫

說明:要實現多態效果,第一必須是基類的指針或引用,因為只有基類的指針或引用才能既指向基類對象又指向派生類對象;第二派生類必須對基類的虛函數完成重寫/覆蓋,重寫或者覆蓋了,基類和派生類之間才能有不同的函數,多態的不同形態效果才能達到。

這里提到了新名詞:虛函數,我先演示一下多態的基本使用,下面再詳細講虛函數。

這里就是實現了多態,我們可以看到雖然func的參數是Person類型的引用,但是結果卻調用了子類中的虛函數。

里面的原因就如func中所寫的那樣,跟ptr沒關系,和ptr所指向的對象有關。

指針和引用差不多,這里我就不演示了。

注意這兩個條件缺一不可:

這里就是不符合第一個條件,就沒有構成多態,此時就和ptr有關了,調用BuyTicket函數就看的是調用的類型,而ptr類型是Person,故只會調用Person中的函數。

2.1.2虛函數

類成員函數前面加virtual修飾,那么這個成員函數就稱為虛函數。注意非成員函數不能加virtual修飾。

上面例子中的BuyTicket函數就是虛函數。

2.1.3虛函數的重寫/覆蓋

虛函數的重寫/覆蓋:派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數類型完全相同),稱派生類的虛函數重寫了基類的虛函數。
注意:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性),但是該種寫法不是很規范,不建議這樣使用。

上面子類中的BuyTicket就是對父類的重寫,這里注意:重寫/覆蓋的是函數的實現部分,就是括號里面的內容。

接著上面不符合第二個條件:

這里就不符合第二個條件了,此時的BuyTicket函數就不是虛函數,構不成多態,故還是調用父類的函數。

這種也是不構成多態的,virtual只能子類隱藏,父類是不能隱藏的。

講到這我們來看一道題:

問:以下程序輸出結果是什么?

A.A->0? ?B.B->1? ?C.A->1? ?D.B->0? ?E.編譯出錯? ?F.以上都不正確

大家可以思考一下這個問題的答案。

答案選擇B,這里可能很多人都不理解,這里面有倆個坑。

第一個就判斷這里到底是不是多態:我們可以看到,此時創建了一個子類對象,通過子類對象去調用test函數。這里要注意繼承,并不是把父類的函數拷貝到子類,在調用時,先在子類查找,找不到才會去父類去查找。

而這個坑的難點就是test函數中的this指針到底是A*呢,還是B*呢?

遵循上面的原則,我們在子類沒有找到test函數,接著去父類找,找到了,既然要調用父類的test函數,那this指針自然而然就是A*,那既然是基類的指針來調用虛函數,那么就構成多態。

來到第二個坑:這也是為什么這道題選B的原因。

既然上面構成多態了,那么指針指向的對象是子類對象,就該調用子類里面的func函數,正常來說應該是B->0,但是我們上面寫了,虛函數的重載/覆蓋只是針對函數實現部分,所以只是把實現部分的func給重寫了,那么既然只針對實現部分,那么參數部分的val就不會發生變化,就還是默認的缺省值1.

這里很多人出錯就是被這個缺省值給誤導了,所以我們要牢記虛函數重載/覆蓋只針對函數實現部分。

有人會有疑惑:那缺省值不是不一樣嗎,怎么會構成虛函數重寫呢?
這個問題我們要看上面虛函數重寫的概念,是函數名,返回值類型和參數類型皆相同,里面是不包含缺省值的,缺省值不同不影響。

2.1.4虛函數重寫的一些其他問題

1.協變

派生類重寫基類虛函數是,與積累虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變。

以上面為例,將返回類型改成對應的指針或者引用即為協變,當然斜邊也不只一種方式:

也可以是其他類的指針或者引用做返回值,但要求是父類和子類的指針或引用。

協變的實際意義不大,這里了解一下即可。

2.析構函數的重寫

基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同看起來不符合重寫的規則,實際上編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor,所以基類的析構函數加了 vialtual修飾,派生類的析構函數就構成重寫。

由上面的代碼我們可以看到,如果~A(),不加virtual,那么delete p2時只調用的A的析構函數,沒有調用 B的析構函數,就會導致內存泄漏問題,因為~B()中在釋放資源。

原因就如上面所言,在繼承關系中析構函數的名稱會被統一處理,不加virtual就構不成多態,就只能根據類型去調用析構函數,所以盡量在析構函數前面加上virtual構成多態,避免內存泄漏。

2.1.5override和final關鍵字

從上面可以看出,C++對虛函數重寫的要求比較嚴格,但是有些情況下由于疏忽,比如上面由于函數名寫錯導致無法構成重寫,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此C++11提供了override,可以幫助用戶檢測是否重寫。如果我們不想讓派生類重寫這個虛函數,那么可以用final去修飾。

2.1.6重載/重寫/隱藏的對比

我們學到這里這三個概念會有人搞混了,重載和其他兩個可以很好區分開來,重載是在同一作用域下,而重寫和隱藏都是在不同作用域下。

而隱藏和重寫,這兩個而言,隱藏范圍會更大一些,畢竟同名成員變量也會構成隱藏,重寫只針對成員函數,并且要求三同(函數名,返回值類型和參數類型),隱藏只要函數名相同即可。

3.純虛函數和抽象類

在虛函數的后面寫上=0,則這個函數為純虛函數,純虛函數不需要定義實現(實現沒啥意義因為要被派生類重寫,但是語法上可以實現),只要聲明即可。包含純虛函數的類叫做抽象類,抽象類不能實例化出對象,如果派生類繼承后不重寫純虛函數,那么派生類也是抽象類。純虛函數某種程度上強制了派生類重寫虛函數,因為不重寫實例化不出對象。

以上面為例,這就是虛函數的基本使用。可以看出,此時Car中的Drive函數就是純虛函數,而含有純虛函數的類無法實例化出對象。

而如果子類沒有重寫純虛函數,也會變成抽象類:

同樣無法實例化出對象。

無法實例化處對象就意味著很多功能就無法實現,所以如果父類中有純虛函數,子類就要重寫純虛函數。

4.多態的原理

4.1虛函數表指針

大家可以思考一下在32位下b是多大。

可能有人覺得是8,因為根據對其原則先是int,接著是char的話確實是8,但其實答案是12。

為什么是12呢?

這就與虛函數表指針有關:

通過調試可以發現,再b中還含有一個叫_vfptr的變量,里面存儲的是一個地址,這個地址指向虛函數表。

而指針我們都知道,再32位下是4個字節,64位下是8個字節,我在測試的時候是在32位環境下,所以_vfptr,int和char三個加起來,根據對其原則,最后得出是12。

用圖來表示就如上圖所示,虛函數表這里先簡單提一下,下面會詳細講。

虛函數表又叫虛函數指針數組或者虛表,里面存的就是虛函數的指針。

4.2.1多態是如何實現的

依舊以上面的例子來說明,我們上面講了虛函數表指針,現在就可以來探究多態到底是如何實現的。

通過重載可以發現,三個變量中的_vfptr所包涵的地址都不一樣,這是因為重寫導致的,重寫過后,不同類型的變量中的_vfptr就指向不同的虛函數表,不同的虛函數表中指向的也是不同的虛函數。

而多態的原理就是如此,上面的例子中通過ptr來調用相應對象中_vfptr存的虛函數表的地址,再通過虛函數表中找到相應的虛函數,調用相應的虛函數,完成多態的操作。

注意,這個_vfptr是不能直接訪問的:

會直接顯示沒有這個成員。

并且虛函數表存的是當前類中的所有虛函數,不只有一個:

可以看出里面不僅存了BuyTicket函數,還存了func1函數。

4.2.2動態綁定與靜態綁定

對不滿足多態條件(指針或者引用+調用虛函數)的函數調用是在編譯時綁定,也就是編譯時確定調用函數的地址,叫做靜態綁定。
滿足多態條件的函數調用是在運行時綁定,也就是在運行時到指向對象的虛函數表中找到調用函數的地址,也就做動態綁定。

動態綁定就如上圖所示,滿足多態條件,運行時到虛函數表中找到對應虛函數進行調用。

靜態綁定就如上圖所示,不滿足多態條件,編譯時通過調用者的類型,確定函數地址進行調用。

4.2.3虛函數表

1.基類對象的虛函數表中存放基類所有虛函數的地址。同類型的對象共用同一張虛表,不同類型的對象各自有獨立的虛表,所以基類和派生類有各自獨立的虛表。
2.派生類由兩部分構成,繼承下來的基類和自己的成員,一般情況下,繼承下來的基類中有虛函數表指針,自己就不會再生成虛函數表指針。但是要注意的這里繼承下來的基類部分虛函數表指針和基類對象的虛函數表指針不是同一個,就像基類對象的成員和派生類對象中的基類對象成員也獨立的。
3.派生類中重寫的基類的虛函數,派生類的虛函數表中對應的虛函數就會被覆蓋成派生類重寫的虛函數地址。
4.派生類的虛函數表中包含,(1)基類的虛函數地址,(2)派生類重寫的虛函數地址完成覆蓋,派生類自己的虛函數地址三個部分。
5.虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個0x00000000標記。(這個C++并沒有進行規定,各個編譯器自行定義的,vs系列編譯器會再后面放個0x00000000標記,g++系列編譯不會放)
6.虛函數存在哪的?虛函數和普通函數一樣的,編譯好后是一段指令,都是存在代碼段的,只是虛函數的地址又存到了虛表中。

7.虛函數表在哪兒呢?這個問題并沒有標準答案,c++并沒有規定。

有的上面已經涉及到,這里就不過多贅述。

第二條我們上面展示的調試中就演示了,子類本身是沒有_vfptr的,只是繼承了父類的。

這里主要就是講一下如何找到虛函數表在哪兒:

再找之前呢我們先得到幾個常見的區域的地址,好拿來比較。

而找虛函數表的難點就在于_vfptr我們拿不出來,就無法拿到里面所保存的虛函數表的地址。

但是我們可以利用其他的方法,比如:再32位下,指針是四個字節,那我們只要拿到相應對象的前四個字節,在解引用,就可以拿到虛函數表的地址。

而我們如何拿到前四個字節呢?

這里可以用強轉來實現,把自定義類型的指針強轉成int*指針,在解引用即可。

因為int取4個字節,我們對int*解引用就可以拿到前四個字節。

這里就拿到了虛函數表的地址,我么通過觀察可以看出和常量區的地址最為接近,所以在vs下,虛函數表就存在常量區。

注意:這里不能直接強轉成int類型,因為強轉只能是相近類型才可以,比如int和double,int*和double*以及上面的Student*和int*,這種情況下才可以強轉。

以上就是多態的內容。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/79315.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/79315.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/79315.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Unity】MVP框架的使用例子

在提到MVP之前&#xff0c;可以先看看這篇MVC的帖子&#xff1a; 【Unity】MVC的簡單分享以及一個在UI中使用的例子 MVC的不足之處&#xff1a; 在MVC的使用中&#xff0c;會發現View層直接調用了Model層的引用&#xff0c;即這兩個層之間存在著一定的耦合性&#xff0c;而MV…

前端js學算法-實踐

1、兩數之和 const twoSum (nums, target) > {const obj {}for (let m 0; m < nums.length; m) {const cur nums[m]const diff target - curif(obj.hasOwnProperty(diff)){ // 查詢對象中是否存在目標值-當前值鍵值對console.log([obj[diff], m]) // 存在則直接獲取…

《MATLAB實戰訓練營:從入門到工業級應用》趣味入門篇-用聲音合成玩音樂:MATLAB電子琴制作(超級趣味實踐版)

《MATLAB實戰訓練營&#xff1a;從入門到工業級應用》趣味入門篇-用聲音合成玩音樂&#xff1a;MATLAB電子琴制作&#xff08;超級趣味實踐版&#xff09; 開篇&#xff1a;當MATLAB遇見音樂 - 一場數字與藝術的浪漫邂逅 想象一下&#xff0c;你正坐在一臺古老的鋼琴前&#x…

實戰探討:為什么 Redis Zset 選擇跳表?

在了解了跳表的原理和實現后&#xff0c;一個常見的問題&#xff08;尤其是在面試中&#xff09;隨之而來&#xff1a;為什么像 Redis 的有序集合 (Zset) 這樣的高性能組件會選擇使用跳表&#xff0c;而不是大家熟知的平衡樹&#xff08;如紅黑樹&#xff09;呢&#xff1f; 對…

數據結構-線性結構(鏈表、棧、隊列)實現

公共頭文件common.h #define TRUE 1 #define FALSE 0// 定義節點數據類型 #define DATA_TYPE int單鏈表C語言實現 SingleList.h #pragma once#include "common.h"typedef struct Node {DATA_TYPE data;struct Node *next; } Node;Node *initList();void headInser…

高中數學聯賽模擬試題精選學數學系列第3套幾何題

△ A B C \triangle ABC △ABC 的內切圓 ⊙ I \odot I ⊙I 分別與邊 B C BC BC, C A CA CA, A B AB AB 相切于點 D D D, E E E, F F F, D D ′ DD DD′ 為 ⊙ I \odot I ⊙I 的直徑, 過圓心 I I I 作直線 A D ′ AD AD′ 的垂線 l l l, 直線 l l l 分別與 D E DE…

使用 ossutil 上傳文件到阿里云 OSS

在處理文件存儲和傳輸時&#xff0c;阿里云的對象存儲服務&#xff08;OSS&#xff09;是一個非常方便的選擇。特別是在需要批量上傳文件或通過命令行工具進行文件管理時&#xff0c;ossutil提供了強大的功能。本文將詳細說明如何使用 ossutil 上傳文件到阿里云 OSS&#xff0c…

DeepSeek與MySQL:開啟數據智能新時代

目錄 一、引言&#xff1a;技術融合的力量二、DeepSeek 與 MySQL&#xff1a;技術基石2.1 DeepSeek 技術探秘2.2 MySQL 數據庫深度解析 三、DeepSeek 與 MySQL 集成&#xff1a;從理論到實踐3.1 集成原理剖析3.2 集成步驟詳解 四、應用案例&#xff1a;實戰中的價值體現4.1 電商…

WebAPI項目從Newtonsoft.Json遷移到System.Text.Json踩坑備忘

1.控制器層方法返回類型不能為元組 控制器層方法返回類型為元組時&#xff0c;序列化結果為空。 因為元組沒有屬性只有field&#xff0c;除非使用IncludeFields參數專門指定&#xff0c;否則使用System.Text.Json進行序列化時不會序列化field var options new JsonSerializ…

202553-sql

目錄 一、196. 刪除重復的電子郵箱 - 力扣&#xff08;LeetCode&#xff09; 二、602. 好友申請 II &#xff1a;誰有最多的好友 - 力扣&#xff08;LeetCode&#xff09; 三、176. 第二高的薪水 - 力扣&#xff08;LeetCode&#xff09; 一、196. 刪除重復的電子郵箱 - 力扣…

Spring Boot的GraalVM支持:構建低資源消耗微服務

文章目錄 引言一、GraalVM原生鏡像技術概述二、Spring Boot 3.x的GraalVM支持三、適配GraalVM的關鍵技術點四、構建原生鏡像微服務實例五、性能優化與最佳實踐總結 引言 微服務架構已成為企業應用開發的主流模式&#xff0c;但隨著微服務數量的增加&#xff0c;資源消耗問題日…

pip 常用命令及配置

一、python -m pip install 和 pip install 的區別 在講解 pip 的命令之前&#xff0c;我們有必要了解一下 python -m pip install 和 pip install 的區別&#xff0c;以便于我們在不同的場景使用不同的方式。 python -m pip install 命令使用 python 可執行文件將 pip 模塊作…

Vue高級特性實戰:自定義指令、插槽與路由全解析

一、自定義指令 1.如何自定義指令 ⑴.全局注冊語法 通過 Vue.directive 方法注冊&#xff0c;語法格式為&#xff1a; Vue.directive(指令名, {// 鉤子函數&#xff0c;元素插入父節點時觸發&#xff08;僅保證父節點存在&#xff0c;不一定已插入文檔&#xff09;inserted(…

本地大模型編程實戰(32)用websocket顯示大模型的流式輸出

在與 LLM(大語言模型) 對話時&#xff0c;如果每次都等 LLM 處理完畢再返回給客戶端&#xff0c;會顯得比較卡頓&#xff0c;不友好。如何能夠像主流的AI平臺那樣&#xff1a;可以一點一點吐出字符呢&#xff1f; 本文將模仿后端流式輸出文字&#xff0c;前端一塊一塊的顯示文字…

人工智能-深度學習之卷積神經網絡

深度學習 mlp弊端卷積神經網絡圖像卷積運算卷積神經網絡的核心池化層實現維度縮減卷積神經網絡卷積神經網絡兩大特點卷積運算導致的兩個問題&#xff1a;圖像填充&#xff08;padding&#xff09;結構組合問題經典CNN模型LeNet-5模型AlexNet模型VGG-16模型 經典的CNN模型用于新…

藍橋杯電子賽_繼電器和蜂鳴器

目錄 一 前言 二 繼電器和蜂鳴器實物 三 分析部分 &#xff08;1&#xff09;bsp_init.c &#xff08;2&#xff09;蜂鳴器和繼電器原理圖 &#xff08;3&#xff09;ULN2003 &#xff08;4&#xff09;他們倆所連接的鎖存器 四 代碼 在這里要特別說一點&#xff01;&…

仿騰訊會議——主界面設計創建房間加入房間客戶端實現

1、實現騰訊會議主界面 2、添加Qt類WeChatDialog 3、定義創建會議和加入會議的函數 4、實現顯示名字、頭像的函數 調用函數 5、在中間者類中綁定函數 6、實現創建房間的槽函數 7、實現加入房間的槽函數 8、設置界面標題 9、服務器定義創建和進入房間函數 10、服務器實現創建房間…

網絡編程初識

注&#xff1a;此博文為本人學習過程中的筆記 1.socket api 這是操作系統提供的一組api&#xff0c;由傳輸層向應用層提供。 2.傳輸層的兩個核心協議 傳輸層的兩個核心協議分別是TCP協議和UDP協議&#xff0c;它們的差別非常大&#xff0c;編寫代碼的風格也不同&#xff0c…

【質量管理】現代TRIZ問題識別中的功能分析——功能模型

功能模型的定義 功能模型是對工程系統進行功能分析的一個階段&#xff0c;目的是建立工程系統的功能模型。功能模型描述了工程系統和超系統組件的功能&#xff0c;包括有用功能、性能水平和成本等。 在文章【質量管理】現代TRIZ中問題識別中的功能分析——相互接觸分析-CSDN博客…

廣告事件聚合系統設計

需求背景 廣告事件需要進行統計&#xff0c;計費&#xff0c;分析等。所以我們需要由數據接入&#xff0c;數據處理&#xff0c;數據存儲&#xff0c;數據查詢等多個服務模塊去支持我們的廣告系統 規模上 10000 0000個點擊&#xff08;10000 00000 / 100k 1wQPS&#xff09; …