深入理解C++ 虛函數表

目錄

  • 深入理解C++ 虛函數表
    • 虛函數表概述
    • 單繼承下的虛函數表
      • 派生類未覆蓋基類虛函數
      • 派生類覆蓋基類虛函數
    • 多繼承下的虛函數表
      • 無虛函數覆蓋
      • 派生類覆蓋基類虛函數
    • 鉆石型虛繼承
    • 總結
      • 幾個原則
      • 安全性問題

深入理解C++ 虛函數表

? C++中的虛函數的作用主要是實現了多態的機制。關于多態,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

? 這種技術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要么是試圖做到在編譯時決議,要么試圖做到運行時決議。

本文將詳細介紹虛函數表的實現及其內存布局。

虛函數表概述

虛函數表是指在每個包含虛函數的類中都存在著一個函數地址的數組。當我們用父類的指針來操作一個子類的時候,這張虛函數表指明了實際所應該調用的函數。

C++的編譯器保證虛函數表的指針存在于對象實例中最前面的位置,這樣通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,并調用相應的函數。

按照上面的說法,來看一個實際的例子:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};int main()
{Base t;(     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();(     ((void(*)())*((int*)(*((int*)&t)) + 2))   )     ();return 0;
}

經過VS2017,x86測試:

1558792775022

1558844330501

我們成功地通過實例對象的地址,得到了對象所有的類函數。

1558793497748

main定義Base類對象t,把&b轉成int *,取得虛函數表的地址vtptr就是:(int*)(&t),然后再解引用并強轉成int *得到第一個虛函數的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二個虛函數g()的地址就是(int*)(*((int*)&t)) + 1,依次類推。

單繼承下的虛函數表

派生類未覆蓋基類虛函數

下面我們來看下派生類沒有覆蓋基類虛函數的情況,其中Base類延用上一節的定義。從圖中可看出虛函數表中依照聲明順序先放基類的虛函數地址,再放派生類的虛函數地址。

1558796143678

可以看到下面幾點:

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

測試代碼:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Devired :public Base{
public:virtual void x() { cout << "x()" << endl; }
};int main()
{Devired t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

測試效果:

1558798773222

派生類覆蓋基類虛函數

再來看一下派生類覆蓋了基類的虛函數的情形,可見:

  1. 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置 (顯然的,不然虛函數失去意義)
  2. 派生類沒有覆蓋的虛函數延用基類的

測試代碼:

#include <iostream>using namespace std;class Base {
public:virtual void f() { cout << "f()" << endl; }virtual void g() { cout << "g()" << endl; }virtual void h() { cout << "h()" << endl; }
};class Derive :public Base{
public:virtual void x() { cout << "x()" << endl; }virtual void f() { cout << "Derive::f()" << endl; }
};int main()
{Derive t;(((void(*)())   *((int*)(*((int*)&t)))))   ();(((void(*)())*((int*)(*((int*)&t)) + 1)))     ();(((void(*)())*((int*)(*((int*)&t)) + 2)))     ();//(((void(*)())*((int*)(*((int*)&t)) + 3)))     ();return 0;
}

測試效果:

1558844866127

1558844940167

多繼承下的虛函數表

無虛函數覆蓋

如果是多重繼承的話,問題就變得稍微復雜一丟丟,主要有幾點:

  1. 每個基類都有自己的虛函數表
  2. 派生類的虛函數地址存依照聲明順序放在第一個基類的虛表最后(這點和單繼承無虛函數覆蓋相同),具體見下圖所示:

img

測試代碼

#include <iostream>
class Base
{
public:Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }private:int m_iMem1;int m_iMem2;
};class Base2
{
public:Base2(int mem = 3) : m_iBase2Mem(mem) { ; }virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }private:int m_iBase2Mem;
};class Base3
{
public:Base3(int mem = 4) : m_iBase3Mem(mem) { ; }virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }private:int m_iBase3Mem;
};class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }private:int m_iMem1;
};int main()
{// Test_3Devired d;int *dAddress = (int*)&d;typedef void(*FUNC)();/* 1. 獲取對象的內存布局信息 */// 虛表地址一int *vtptr1 = (int*)*(dAddress + 0);int basemem1 = (int)*(dAddress + 1);int basemem2 = (int)*(dAddress + 2);int *vtpttr2 = (int*)*(dAddress + 3);int base2mem = (int)*(dAddress + 4);int *vtptr3 = (int*)*(dAddress + 5);int base3mem = (int)*(dAddress + 6);/* 2. 輸出對象的內存布局信息 */int *pBaseFunc1 = (int *)*(vtptr1 + 0);int *pBaseFunc2 = (int *)*(vtptr1 + 1);int *pBaseFunc3 = (int *)*(vtptr1 + 2);int *pBaseFunc4 = (int *)*(vtptr1 + 3);(FUNC(pBaseFunc1))();(FUNC(pBaseFunc2))();(FUNC(pBaseFunc3))();(FUNC(pBaseFunc4))();// .... 后面省略若干輸出內容,可自行補充return 0;
}

測試效果:

1558845492296

派生類覆蓋基類虛函數

我們再來看一下派生類覆蓋了基類的虛函數的情形,可見:

  1. 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置
  2. 派生類沒有覆蓋的虛函數延用基類的

代碼如下所示,注意這里只給出了類的定義,main函數的測試代碼與上節一樣:

class Devired : public Base, public Base2, public Base3
{
public:Devired(int mem = 7) : m_iMem1(mem) { ; }virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }private:int m_iMem1;
};

測試效果
1558849805007

鉆石型虛繼承

該繼承還是遵循上述的所有原則,我們直接來測試。

測試代碼

// 測試四:鉆石型虛繼承//虛基指針所指向的虛基表的內容:
//  1. 虛基指針的第一條內容表示的是該虛基指針距離所在的子對象的首地址的偏移
//  2. 虛基指針的第二條內容表示的是該虛基指針距離虛基類子對象的首地址的偏移#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;class B
{
public:B() : _ib(10), _cb('B') {}virtual void f(){cout << "B::f()" << endl;}virtual void Bf(){cout << "B::Bf()" << endl;}private:int _ib;char _cb;
};class B1 : virtual public B
{
public:B1() : _ib1(100), _cb1('1') {}virtual void f(){cout << "B1::f()" << endl;}#if 1virtual void f1(){cout << "B1::f1()" << endl;}virtual void Bf1(){cout << "B1::Bf1()" << endl;}
#endifprivate:int _ib1;char _cb1;
};class B2 : virtual public B
{
public:B2() : _ib2(1000), _cb2('2') {}virtual void f(){cout << "B2::f()" << endl;}
#if 1virtual void f2(){cout << "B2::f2()" << endl;}virtual void Bf2(){cout << "B2::Bf2()" << endl;}
#endif
private:int _ib2;char _cb2;
};class D : public B1, public B2
{
public:D() : _id(10000), _cd('3') {}virtual void f(){cout << "D::f()" << endl;}#if 1virtual void f1(){cout << "D::f1()" << endl;}virtual void f2(){cout << "D::f2()" << endl;}virtual void Df(){cout << "D::Df()" << endl;}
#endif
private:int _id;char _cd;
};int main(void)
{D d;cout << sizeof(d) << endl;return 0;
}

測試效果

1>class D   size(52):
1>  +---
1> 0    | +--- (base class B1)
1> 0    | | {vfptr}
1> 4    | | {vbptr}
1> 8    | | _ib1
1>12    | | _cb1
1>      | | <alignment member> (size=3)
1>  | +---
1>16    | +--- (base class B2)
1>16    | | {vfptr}
1>20    | | {vbptr}
1>24    | | _ib2
1>28    | | _cb2
1>      | | <alignment member> (size=3)
1>  | +---
1>32    | _id
1>36    | _cd
1>      | <alignment member> (size=3)
1>  +---
1>  +--- (virtual base B)
1>40    | {vfptr}
1>44    | _ib
1>48    | _cb
1>      | <alignment member> (size=3)
1>  +---
1>
1>D::$vftable@B1@:
1>  | &D_meta
1>  |  0
1> 0    | &D::f1
1> 1    | &B1::Bf1
1> 2    | &D::Df
1>
1>D::$vftable@B2@:
1>  | -16
1> 0    | &D::f2
1> 1    | &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0    | -4
1> 1    | 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0    | -4
1> 1    | 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1>  | -40
1> 0    | &D::f
1> 1    | &B::Bf
1>

總結

幾個原則

單繼承

  1. 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置
  2. 派生類沒有覆蓋的虛函數就延用基類的。同時,虛函數按照其聲明順序放于表中,父類的虛函數在子類的虛函數前面。

多繼承

  1. 每個基類都有自己的虛函數表
  2. 派生類的虛函數地址存依照聲明順序放在第一個基類的虛表最后

安全性問題

當我們直接通過父類指針調用子類中的未覆蓋父類的成員函數,編譯器會報錯,但通過實驗,我們可以用對象的地址訪問到各個子類的成員函數,就違背了C++語義,操作會有一定的隱患,當我們使用時要注意這些危險的東西!

參考:

https://coolshell.cn/articles/12165.html

https://jocent.me/2017/08/07/virtual-table.html

https://blog.csdn.net/lihao21/article/details/50688337

轉載于:https://www.cnblogs.com/Mered1th/p/10924545.html

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

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

相關文章

react-native-baidu-map使用及注意問題

使用組件&#xff1a; react-native-baidu-map 獲取百度地圖API_KEY 地址&#xff1a;lbsyun.baidu.com&#xff0c;在控制臺成功創建應用后&#xff0c;就可以看到應用的api key了 安裝 yarn add react-native-baidu-map 復制代碼原生部分 Android配置 react-native link reac…

簡單掃清身體垃圾

“我們的身體在被‘設計’之初&#xff0c;就擁有了自主掃除體內垃圾的功能。只不過&#xff0c;這需要我們按照正確的方法去激發它 。”美國暢銷書作者喬斯卡曼和朱莉佩萊斯&#xff0c;在她們去年合著的《自我清潔》一書中強調了養成良好生活習慣可為身體排毒的重要性。 近日…

linux (阿里云 CentOS7) 中安裝配置 RocketMQ

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 JDK1.8的安裝&#xff1a; 1.檢查系統的JDK版本 根目錄下操作&#xff1a;cd java -version 2.檢測JDK安裝包 rpm -qa | grep ja…

Bootstrap簡介

1.使用準備 1.1 Bootstrap的下載 http://www.bootcss.com&#xff0c;下載用于生產環境的Bootstrap即可。 1.2 Bootstrap包含的內容 ● 全局CSS&#xff1a;基本的 HTML 元素均可以通過 class 設置樣式并得到增強效果&#xff1b;還有先進的柵格系統。 ● 組件&#xff1a;無數…

用TortoiseGit時的實用git命令

生成并獲取 sshkey&#xff1a; ssh-keygen -t rsa -C "xxxxxxxxxx.com" cat ~/.ssh/id_rsa.pub 克隆倉庫&#xff1a; git clone xxxxxx/xxx.git 重命名文件&#xff1a; mv file_name new_file_name git目錄區分大小寫&#xff1a; git config core.ignorecase fal…

有一種失敗叫瞎忙

很多時候&#xff0c;我們都在不知不覺的瞎忙&#xff0c;為了避免這樣的瞎忙&#xff0c;特為大家分享一個小的故事。 在一個山谷的禪房里有一位老禪師&#xff0c;他發現自己有一個徒弟非常勤奮&#xff0c;不管是去化緣&#xff0c;還是去廚房洗菜&#xff0c;這個徒弟從…

解決:org.apache.rocketmq.client.exception.MQClientException: No route info of this topic, TopicTest

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 原因1&#xff1a;啟動 broker 方式不對。 我完全是按照官方文檔操作的&#xff0c;在網上看到說這一步是錯誤的啟動 broker 方式&#…

tomcat需要設置環境變量嗎

tomcat是一款輕量級web應用服務器&#xff0c;安裝的時候我們都是直接解壓zip包&#xff0c;然后在bin目錄下雙擊startup.bat就可以啟動了&#xff08;當然&#xff0c;前提是本地要安裝jdk并配置JAVA_HOME環境變量&#xff09; 所以我一直認為tomcat是不用配置環境變量的 但是…

Intro OpenCL Tutorial

Benedict R. Gaster, AMD Architect, OpenCL? OpenCL? is a young technology, and, while a specification has been published (www.khronos.org/registry/cl/), there are currently few documents that provide a basic introduction with examples. This article helps…

雷林鵬分享:codeigniter框架文件上傳處理

CodeIgniter 框架input表單的重新填充&#xff0c;主要是針對text、radio、checkbox、select等input表單&#xff0c;那么對于文件上傳表單file該如何處理呢? 自己的處理方式&#xff1a; //設置文件上傳屬性 $webroot $_SERVER[DOCUMENT_ROOT]; $time time(); $year date(…

jQuery基本使用

一.what 1&#xff09;.一個優秀的JS函數庫&#xff1b; 2&#xff09;.中大型WEB項目開發的首選&#xff1b; 3&#xff09;.使用了jQuery的網站超過90%&#xff1b; 4&#xff09;.http://jquery.com/; 二.why(即jq的好處) html元素選取&#xff08;選擇器&#xff09;&#…

解決:-bash: telnet: command not found

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 報錯如題 -bash: telnet: command not found只是因為沒有安裝這個命令&#xff0c;不識別。 安裝命令&#xff1a; yum install telne…

錢荒下銀行理財收益率角逐:郵儲銀行墊底

21世紀資管研究員松壑 由于銀行理財的收益定價機制為設定預期收益率的“先行定價”&#xff0c;而銀行對產品本金收益又保有或明或暗的兌付要求&#xff0c;其業績往往在理財產品發行前就已決定。 因此&#xff0c;本次榜單根據已披露最高預期收益率&#xff08;下稱收益率&a…

數據結構7.3_圖的遍歷

我們希望從圖中某一頂點出發訪遍圖中其余頂點&#xff0c;且使每一個頂點僅被訪問一次。 這一過程就叫做圖的遍歷。 圖的遍歷算法是求解圖的連通性問題、拓撲排序和求關鍵路徑等算法的基礎。 然而&#xff0c;圖的遍歷要比樹的遍歷復雜得多。 因為圖的任一頂點都可能和其余的頂…

CentOS7 使用 firewalld 打開關閉防火墻與端口

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1、firewalld的基本使用 啟動&#xff1a; systemctl start firewalld關閉&#xff1a; systemctl stop firewalld查看狀態&#xff1a…

HCL實驗四

PC端配置&#xff1a;配置ip地址 配置網關 交換機配置&#xff1a;①創建VLAN system-view vlan 10 vlan 20 ②配置PC端接口 interface vlan-interface 10 ip add 192.168.10.254 24 interface vlan-interface 20 ip add 192.168.20.254 24 轉載于:https://www.cnblogs.com/zy5…

程序員/設計師能用上的 75 份速查表

本文由 伯樂在線 - 黃利民 翻譯自 designzum。歡迎加入 技術翻譯小組。轉載請參見文章末尾處的要求。75 份速查表&#xff0c;由 vikas 收集整理&#xff0c;包括&#xff1a;jQuery、HTML、HTML5、CSS、CSS3、JavaScript、Photoshop 、git、Linux、Java、Perl、PHP、Python、…

移動端真機測試怎么做

準備工作&#xff1a; 1、必須安裝了node 環境和npm&#xff1b; 2、手機和電腦在同一個熱點或者wifi下&#xff1b; 3、知道你的IP地址&#xff1b; 步驟一、 啟動cmd&#xff0c;進入項目根目錄&#xff0c;使用指令&#xff1a;npm i -g live-server 進行全局安裝 步驟二、 …

Linux 下清空或刪除大文件內容的 5 種方法

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 下面的這些方法都是從命令行中達到清空文件的目的。 使用名為 access.log 的文件作為示例樣本。 1. 通過重定向到 Null 來清空文件內容…

管理飛揚跋扈的技術部

摘要&#xff1a;有的管理人員認為最頭疼的就是技術部的管理。因為技術工作看起來棘手&#xff0c;管理人員不能輕易了解技術工作的內涵&#xff0c;技術人員也覺得很難和管理人員溝通。要管理好技術人員&#xff0c;就一定要懂技術&#xff0c;這是其他管理方法都無法替代的。…