『 C++類與對象 』多態之單繼承與多繼承的虛函數表

文章目錄

    • 🫧 前言
    • 🫧 查看虛表
    • 🫧 單繼承下的虛函數表
    • 🫧 多繼承下的虛函數表


🫧 前言

多態是一種基于繼承關系的語法,既然涉及到繼承,而繼承的方式有多種:

  • 單繼承
  • 多繼承
  • 棱形繼承
  • 棱形虛擬繼承
    不同的繼承方式其虛表的形式也不同;
以下操作均為在CentOS7_x64機器上的操作

🫧 查看虛表

已知虛表為一個void (*)()的函數指針數組,除了以內存的方式查看虛表以外還可以使用函數調用的方式來查看虛表的真實情況;
其思路即為將該指針數組的指針打印并調用;
根據函數調用可以知道哪個指針是哪個函數;

typedef void(*VFPTR)();
void PrintVT( VFPTR vTable[] ,size_t n/*虛函數個數*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}
//函數的參數為函數指針數組(虛表)的首地址;
//由于是自定義類型的前4/8個字節(在該平臺下為8個字節)
//應使用對應的方式取到前8個字節;
//通過該首地址向后進行遍歷;

🫧 單繼承下的虛函數表

存在一個單繼承關系:

class A{//基類public:virtual void Func1(){//虛函數cout<<"A::Func1()"<<endl;}virtual void Func2(){//虛函數cout<<"A::Func2()"<<endl;}int _a = 10;
};class B:public A{//派生類public:virtual void Func1(){//虛函數且完成重寫cout<<"B::Func1()"<<endl;}virtual void Func3(){//虛函數cout<<"B::Func3()"<<endl;}int _b = 20;
};void test1(){//分別實例化出兩個對象A aa;B bb;
}

使用GDB打印出實例化出的aabb的內容;

(gdb) display aa
1: aa = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
(gdb) display bb
2: bb = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}

由于子類對象和父類對象種都存在一張虛表,所以對應的子類對象的虛函數存儲于子類的虛表當中,父類對象的虛函數存儲于父類的虛表當中;

其中該段所出現的結果中的_vptr.A = 0x400ad8_vptr.A = 0x400ab0即為虛表指針,該地址不是兩個對象的地址,而是該對象地址中首地址所存儲的內容;

可以使用&將兩個對象的地址取出并使用x/x進行解析從而驗證;

(gdb) p &aa 
$10 = (A *) 0x7fffffffe430	#aa對象的首地址
(gdb) x/x 0x7fffffffe430 
0x7fffffffe430:	0x00400ad8	#其首地址所存儲的數據(gdb) p &bb
$11 = (B *) 0x7fffffffe420	#bb對象的首地址
(gdb) x/x 0x7fffffffe420
0x7fffffffe420:	0x00400ab0	#其首地址所存儲的數據

其中上面的首地址所存儲的數據即為一個指針,這個指針即為虛表(虛函數表)指針,也就是虛函數表的首地址位置;

在該示例中基類和派生類中各有兩個虛函數,其中派生類的Func1()虛函數重寫了基類的Func1()虛函數,所以在基類和派生類的虛表中都存在該函數,且該函數的地址不同;

  • A類虛表

    #	A類虛表
    (gdb)  p aa
    $12 = {_vptr.A = 0x400ad8 <vtable for A+16>, _a = 10}
    #----------------------------------
    (gdb) x/x 0x400ad8	
    0x400ad8 <_ZTV1A+16>:	0x00400924	#虛表首地址所存儲的數據(A::Func1()函數的地址)
    (gdb) x/x 0x00400924
    0x400924 <A::Func1()>:	0xe5894855	#將地址解析后得到函數
    #----------------------------------
    (gdb) x/x 0x400ae0
    0x400ae0 <_ZTV1A+24>:	0x00400950	#虛表中第二個位置所存儲的數據(由于是64位機器偏移量為8,A::Func2()函數的地址)
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#將地址解析后得到函數
    #----------------------------------
    
  • B類虛表
    B類虛表與之不同的是,B類作為派生類,而派生類的虛表可以看成是基類虛表的拷貝,且若發生重寫的話虛表中的那個被重寫的函數將會被重寫的函數進行覆蓋;

    (gdb) p bb
    #	B類虛表
    $14 = {<A> = {_vptr.A = 0x400ab0 <vtable for B+16>, _a = 10}, _b = 20}
    #----------------------------------
    (gdb) x/x 0x400ab0
    0x400ab0 <_ZTV1B+16>:	0x0040097c	#虛表首地址所存儲的數據(B::Func1()函數的地址[已被重寫所以地址不同])
    (gdb) x/x 0x0040097c
    0x40097c <B::Func1()>:	0xe5894855	#將地址解析后得到函數
    #----------------------------------
    (gdb) x/x 0x400ab8
    0x400ab8 <_ZTV1B+24>:	0x00400950	#虛表中第二個位置所存儲的數據(由于是64位機器偏移量為8,A::Func2()函數的地址[派生類的虛函數表可以看成是基類函數表的拷貝])
    (gdb) x/x 0x00400950
    0x400950 <A::Func2()>:	0xe5894855	#將地址解析后得到函數
    #----------------------------------
    (gdb) x/x 0x400ac0
    0x400ac0 <_ZTV1B+32>:	0x004009a8	#虛表中第三個位置所存儲的數據(由于是64位機器偏移量為8,B::Func3()函數的地址[這里存放的是B類中自身的函數])
    (gdb) x/x 0x004009a8
    0x4009a8 <B::Func3()>:	0xe5894855	#將地址解析后得到函數

在這里插入圖片描述

使用函數查看:

typedef void(*VFPTR)();void PrintVT( VFPTR vTable[] ,size_t n/*虛函數個數*/){cout<<"ptr: "<< vTable <<endl;for(size_t i = 0;i<n;++i){printf(" 第%u地址:0x%x,->",i,vTable[i]);VFPTR f=vTable[i];f();}cout<<endl;
}void test1(){A aa;B bb;PrintVT(*(VFPTR**)&aa,2);PrintVT(*(VFPTR**)&bb,3);
}

結果為 (重新編譯過所以導致最終結果不同,但結論相同):

ptr: 0x400c60第0地址:0x400a94,->A::Func1()第1地址:0x400ac0,->A::Func2()ptr: 0x400c38第0地址:0x400aec,->B::Func1()第1地址:0x400ac0,->A::Func2()第2地址:0x400b18,->B::Func3()

🫧 多繼承下的虛函數表

多繼承下的虛函數表較于單繼承來說會更加的復雜;
復雜的原因在于多繼承為多個基類繼承給一個派生類,那么假設兩個基類都有同名虛函數,且派生類重寫了這個虛函數應該如何判斷?

class A{public:virtual void Func1(){cout<<"A::Func1()"<<endl;}virtual void Func2(){cout<<"A::Func2()"<<endl;}
};class B{public:virtual void Func1(){cout<<"B::Func1()"<<endl;}virtual void Func2(){cout<<"B::Func2()"<<endl;}
};class C : public A,public B{public:virtual void Func1(){cout<<"C::Func1()"<<endl;}virtual void Func3(){cout<<"C::Func3()"<<endl;}
};void test2(){C cc;
}

存在以上的繼承關系;

使用GDB調試該程序并打印cc的內容;

p cc
$9 = {<A> = {_vptr.A = 0x400cc0 <vtable for C+16>}, <B> = {_vptr.B = 0x400ce8 <vtable for C+56>}, <No data fields>}

由第一點可以知道,派生類的虛表可以看作是基類虛表的拷貝,那么在該程序中由于存在兩個基類(多繼承),所以應當也有兩個虛表;

那么在這個繼承關系中,派生類自身所增加的虛函數處于哪個虛表?

實際上在多繼承關系中,派生類自身所增加的虛函數都在第一個虛表中,且第一張虛表不僅只存在派生類自身的虛函數,還有一個較為關鍵的數據;

  • 第一張虛表
    #-------64位機器偏移量為8---------
    #	C::Func1()	被重寫
    (gdb) x/x 0x400cc0
    0x400cc0 <_ZTV1C+16>:	0x00400b56
    (gdb) x/x 0x00400b56
    0x400b56 <C::Func1()>:	0xe5894855
    #-------------------------------
    #	A::Func2() 
    (gdb) x/x 0x400cc8
    0x400cc8 <_ZTV1C+24>:	0x00400ad2
    (gdb) x/x 0x00400ad2
    0x400ad2 <A::Func2()>:	0xe5894855
    #-------------------------------
    #	C::Func3()	派生類自身
    (gdb) x/x 0x400cd0 
    0x400cd0 <_ZTV1C+32>:	0x00400b88
    (gdb) x/x 0x00400b88
    0x400b88 <C::Func3()>:	0xe5894855
    #-------------------------------
    (gdb) x/x 0x400cd8
    0x400cd8 <_ZTV1C+40>:	0xfffffff8 #關鍵數據
    #-------------------------------
    

從該結果可以觀察到,派生類自身的虛函數位于第一張虛表當中;
且在最后一個位置存在一個0xfffffff8的數據;


  • 第二張虛表
    #-------------------------------
    #	所存數據并不為虛函數
    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81
    0x400b81 <_ZThn8_N1C5Func1Ev>:	0x08ef8348
    (gdb) x/x 0x08ef8348
    0x8ef8348:	Cannot access memory at address 0x8ef8348
    #-------------------------------
    #	B類中未重寫的虛函數
    (gdb) x/x 0x400cf0
    0x400cf0 <_ZTV1C+64>:	0x00400b2a
    (gdb) x/x 0x00400b2a
    0x400b2a <B::Func2()>:	0xe5894855
    #-------------------------------
    #	NULL空
    (gdb) x/x 0x400cf8
    0x400cf8 <_ZTV1B>:	0x00000000
    #-------------------------------
    
    從該虛表中能看到第二張虛表的第一個位置所存儲的數據并不是函數指針;
    在這里就可以提到對應的0xfffffff8數據;
    已知0xffffffff的值為-1,對應的0xfffffff8即為-8;
    這里的值其實是一個偏移量,這個偏移量:

    當走到該處時將該處的偏移量-8,即得到該處函數所在的位置;
    根據這個點進行驗證;
    此時已經知道了位置為0x400ce8,且該位置所存儲的數據為0x00400b81;

    (gdb) x/x 0x400ce8
    0x400ce8 <_ZTV1C+56>:	0x00400b81
    (gdb) x/x 0x00400b81-8
    0x400b79 <C::Func1()+35>:	0xfffcb2e8
    
    從這里就已經看出,這里通過了偏移量間接的找到了對應的函數;
    當編譯器在處理這段代碼時,將根據偏移量做出一些處理,使得最終能夠通過該偏移量找到對應的函數;

結論為:若是出現多繼承,其中兩個基類都存在同名的虛函數且在派生類中對該虛函數已經完成了重寫的條件時,其虛表構造為如下圖:

在這里插入圖片描述


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

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

相關文章

ToDesk提示通道限制 - 解決方案

問題 使用ToDesk進行遠程控制時&#xff0c;免費個人賬號最多支持1個設備同時發起遠控&#xff0c;若使用此賬號同時在2個設備發起遠控&#xff0c;則會提示通道限制&#xff0c;如下圖&#xff1a; 解決方案 方案1&#xff1a;斷開其它遠控 出現通道限制彈窗時&#xff0…

數據結構(超詳細講解!!)第二十四節 二叉樹(下)

1.遍歷二叉樹 在二叉樹的一些應用中&#xff0c;常常要求在樹中查找具有某種特征的結點&#xff0c;或者對樹中全部結點逐一進行某種處理。這就引入了遍歷二叉樹的問題&#xff0c;即如何按某條搜索路徑訪問樹中的每一個結點&#xff0c;使得每一個結點僅且僅被訪問一次。 …

python3實現tailf命令

由于windows上面沒有類似linux上面的tailf命令&#xff0c;所以下面的python腳本來代替其能力。 tailf.py import re import timeimport os import argparsedef follow(thefile):thefile.seek(0, os.SEEK_END)while True:_line thefile.readline()if not _line:time.sleep(0…

RabbitMQ 搭建和工作模式

MQ基本概念 1. MQ概述 MQ全稱 Message Queue&#xff08;[kju?]&#xff09;&#xff08;消息隊列&#xff09;&#xff0c;是在消息的傳輸過程中保存消息的容器。多用于分布式系統之間進行通信。 &#xff08;隊列是一種容器&#xff0c;用于存放數據的都是容器&#xff0…

docker部署微服務

目錄 docker操作命令 鏡像操作命令 拉取鏡像 導出鏡像 刪除鏡像 加載鏡像 推送鏡像 部署 pom文件加上 在每個模塊根目錄加上DockerFile文件 項目根目錄加上docker-compose.yml文件 打包&#xff0c;clean&#xff0c;package 服務器上新建文件夾 測試docker-compo…

基于springboot和微信小程序的流浪動物管理系統

基于springboot和微信小程序的流浪動物管理系統 內容簡介 基于微信小程序實現的流浪動物管理系統&#xff0c;該系統針對用戶與管理員兩種角色進行開發。 1、提供流浪動物的信息查詢功能&#xff0c;包括品種、年齡、性別、健康狀況等&#xff0c;提供救助活動報名功能。 2…

5.1 PBR基礎 BRDF介紹

基于物理的渲染&#xff08;Physically Based Rendering&#xff0c;PBR&#xff09;是指使用基于物理原理和微平面理論建模的著色/光照模型&#xff0c;以及使用從現實中測量的表面參數來準確表示真實世界材質的渲染理念。 一、反射率方程 理論基礎放在參考鏈接里。 直接開始…

【uniapp】uniapp開發小程序定制uni-collapse(折疊面板)

需求 最近在做小程序&#xff0c;有一個類似折疊面板的ui控件&#xff0c;效果大概是這樣 代碼 因為項目使用的是uniapp&#xff0c;所以打算去找uniapp的擴展組件&#xff0c;果然給我找到了這個叫uni-collapse的組件&#xff08;鏈接&#xff1a;uni-collapse&#xff09…

超詳細的接口測試

本文主要分為兩個部分&#xff1a; 第一部分&#xff1a;主要從問題出發&#xff0c;引入接口測試的相關內容并與前端測試進行簡單對比&#xff0c;總結兩者之前的區別與聯系。但該部分只交代了怎么做和如何做&#xff1f;并沒有解釋為什么要做&#xff1f; 第二部分&#xf…

ShellCode漏洞

ShellCode漏洞 可以查看如下網址&#xff1a; https://www.cnblogs.com/kakadewo/p/12996878.html 定義&#xff1a; shellcode是一段用于利用軟件漏洞而執行的代碼&#xff0c;shellcode為16進制之機械碼&#xff0c;以其經常讓攻擊者獲得shell而得名。shellcode常常使用機…

老鳥總結,軟件測試工程師職業發展規劃路線,入門到沖擊大廠...

目錄&#xff1a;導讀 前言一、Python編程入門到精通二、接口自動化項目實戰三、Web自動化項目實戰四、App自動化項目實戰五、一線大廠簡歷六、測試開發DevOps體系七、常用自動化測試工具八、JMeter性能測試九、總結&#xff08;尾部小驚喜&#xff09; 前言 1、測試工程師發展…

YOCTO 下載repo工具失敗解決辦法

curl https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o repocp repo ~/binchmod ax ~/bin/repo如果使用時報錯&#xff0c; 切換ubuntu 到 python3 版本。gedit repo 修改repo默認鏈接地址&#xff1a;REPO_URL "https://gerrit.googlesource.com/git-repo"…

Spring AOP-面向切面編程概念

Spring AOP-面向切面編程概念 AOP&#xff08;面向切面編程&#xff09;是編程范式的一種&#xff0c;它允許程序員將橫切關注點&#xff08;cross-cutting concerns&#xff09;模塊化。在面向切面編程中&#xff0c;這些橫切關注點通常體現為在多個點重復出現的代碼&#xf…

Android設計模式--適配器模式

至誠之道&#xff0c;可以前知 一&#xff0c;定義 適配器模式把一個類的接口變換成客戶端所期待的另一種接口&#xff0c;從而使原本因接口不匹配而無法在一起工作的兩個類能夠在一起工作。 適配器模式在我們的開發中使用率極高&#xff0c;ListView&#xff0c;GridView&am…

面試cast:reinterpret_cast/const_cast/static_cast/dynamic_cast

目錄 1. cast 2. reinterpret_cast 3. const_cast 3.1 加上const的情況 3.2 去掉const的情況 4. static_cast 4.1 基本類型之間的轉換 4.2 void指針轉換為任意基本類型的指針 4.3 子類和父類之間的轉換 5. dynamic_cast 5.1 RTTI(Run-time Type Identification) 1.…

Selenium實現多頁面切換

當使用 Selenium 進行自動化測試或爬取數據時&#xff0c;有時需要處理多個頁面之間的切換。以下是一些可能需要多頁面切換的情況&#xff1a; 1、打開新窗口/頁面&#xff1a; 在當前頁面上點擊鏈接、按鈕或執行某些操作時&#xff0c;可能會打開一個新的窗口或頁面。此時&a…

【element優化經驗】怎么讓element-ui中表單多語言切換排版不亂

目錄 前言&#xff1a; 痛點&#xff1a; 1.左對齊&#xff0c;右對齊在中文和外語情況下字數不同&#xff0c;固定寬度會使名稱換行&#xff0c;不在整行對齊&#xff0c;影響美觀。 2.如果名稱和輸入框不在一行&#xff0c;會使頁面越來越長 3.label-width值給變量&#…

隨筆記錄-springmvc_ResourceHandlerRegistry+ResourceHttpRequestHandler

環境&#xff1a;springboot-2.7.5 配置文件配置靜態資源映射 springboot配置靜態資源映射方式是通過 WebMvcAutoConfiguration 實現的 spring: # resources: # # 自springboot 2.5.5之后&#xff0c;該屬性已經被廢棄&#xff0c;使用spring.web.resources.static-locat…

爬蟲逆向你應該懂得Javascript知識

背景 大家在學習爬蟲逆向的時候&#xff0c;一般都會涉及到對js源文件進行代碼扣去&#xff0c;但是有的時候&#xff0c;你最好有js基礎&#xff0c;能發現加密或者解密在那個位置&#xff0c;或者是能用python改寫js代碼&#xff0c;這就對個人的Javascript的能力有一定要求…