【C++ Priemr | 15】虛函數表剖析(二)

一、多重繼承(無虛函數覆蓋)

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類并沒有覆蓋父類的函數。

測試代碼:

class Base1
{
public:  virtual void f() { cout << "Base1::f" << endl; }  //虛函數定義virtual void g() { cout << "Base1::g" << endl; }virtual void h() { cout << "Base1::h" << endl; }
};class Base2
{
public: virtual void f() { cout << "Base2::f" << endl; }  //虛函數定義virtual void g() { cout << "Base2::g" << endl; }virtual void h() { cout << "Base2::h" << endl; }
};class Base3
{
public:virtual void f() { cout << "Base3::f" << endl; }virtual void g() { cout << "Base3::g" << endl; }virtual void h() { cout << "Base3::h" << endl; }
};class Derive :public Base1, public Base2, public Base3 //多繼承的情況——無虛繼承覆蓋
{
public:virtual void f1() { cout << "Derive::f1" << endl; } //虛函數定義virtual void g1() { cout << "Derive::g1" << endl; }
};

對于子類實例中的虛函數表,是下面這個樣子:

我們可以看到:

  • 每個父類都有自己的虛表。
  • 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

?

二、多重繼承(有虛函數覆蓋)

下面我們再來看看,如果發生虛函數覆蓋的情況。

測試代碼:?

class Base1 {
public:  virtual void f() { cout << "Base1::f" << endl; }virtual void g() { cout << "Base1::g" << endl; }virtual void h() { cout << "Base1::h" << endl; }
};class Base2 {
public:  virtual void f() { cout << "Base2::f" << endl; }virtual void g() { cout << "Base2::g" << endl; }virtual void h() { cout << "Base2::h" << endl; }
};class Base3 {
public:  virtual void f() { cout << "Base3::f" << endl; }virtual void g() { cout << "Base3::g" << endl; }virtual void h() { cout << "Base3::h" << endl; }
};class Derive : public Base1, public Base2, public Base3 {
public:virtual void f() { cout << "Derive::f" << endl; }  //唯一一個覆蓋的子類函數virtual void g1() { cout << "Derive::g1" << endl; }
};

下圖中,我們在子類中覆蓋了父類的f()函數。

下面是對于子類實例中的虛函數表的圖:

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,并調用子類的f()了。如:

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()

安全性

每次寫C++的文章,總免不了要批判一下C++。這篇文章也不例外。通過上面的講述,相信我們對虛函數表有一個比較細致的了解了。水可載舟,亦可覆舟。下面,讓我們來看看我們可以用虛函數表來干點什么壞事吧。

1. 通過父類型的指針訪問子類自己的虛函數

我們知道,子類沒有重載父類的虛函數是一件毫無意義的事情。因為多態也是要基于函數重載的。雖然在上面的圖中我們可以看到Base1的虛表中有Derive的虛函數,但我們根本不可能使用下面的語句來調用子類的自有虛函數:

Base1 *b1 = new Derive();
b1->g1();  //編譯出錯

任何妄圖使用父類指針想調用子類中的未覆蓋父類的成員函數的行為都會被編譯器視為非法,所以,這樣的程序根本無法編譯通過。但在運行時,我們可以通過指針的方式訪問虛函數表來達到違反C++語義的行為。(關于這方面的嘗試,通過閱讀后面附錄的代碼,相信你可以做到這一點)

?

2. 訪問non-public的虛函數

另外,如果父類的虛函數是private或是protected的,但這些非public的虛函數同樣會存在于虛函數表中,所以,我們同樣可以使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易做到的。如

#include<iostream>
using namespace std;class Base {
private:virtual void f() { cout << "Base::f" << endl; }
};class Derive : public Base {};typedef void(*Fun)(void);int main() 
{Derive d;Fun  pFun = (Fun)*((int*)*(int*)(&d) + 0);pFun();
}

?輸出結果:

?

三、多重繼承

下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。

注意:子類只overwrite了父類的f()函數,而還有一個是自己的函數(我們這樣做的目的是為了用g1()作為一個標記來標明子類的虛函數表)。而且每個類中都有一個自己的成員變量:

們的類繼承的源代碼如下所示:父類的成員初始為10,20,30,子類的為100

#include<iostream>
using namespace std;class Base1 {
public:int ibase1;Base1() :ibase1(10) {}virtual void f() { cout << "Base1::f()" << endl; }virtual void g() { cout << "Base1::g()" << endl; }virtual void h() { cout << "Base1::h()" << endl; }};class Base2 {
public:int ibase2;Base2() :ibase2(20) {}virtual void f() { cout << "Base2::f()" << endl; }virtual void g() { cout << "Base2::g()" << endl; }virtual void h() { cout << "Base2::h()" << endl; }
};class Base3 {
public:int ibase3;Base3() :ibase3(30) {}virtual void f() { cout << "Base3::f()" << endl; }virtual void g() { cout << "Base3::g()" << endl; }virtual void h() { cout << "Base3::h()" << endl; }
};class Derive : public Base1, public Base2, public Base3 {
public:int iderive;Derive() :iderive(100) {}virtual void f() { cout << "Derive::f()" << endl; }virtual void g1() { cout << "Derive::g1()" << endl; }
};int main()
{typedef void(*Fun)(void);Derive d;int** pVtab = (int**)&d;cout << "[0] Base1::_vptr->" << endl;Fun pFun = (Fun)pVtab[0][0];cout << "     [0] ";pFun();pFun = (Fun)pVtab[0][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[0][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[0][3];cout << "     [3] "; pFun();pFun = (Fun)pVtab[0][4];cout << "     [4] "; cout << pFun << endl;cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl;int s = sizeof(Base1) / 4;cout << "[" << s << "] Base2::_vptr->" << endl;pFun = (Fun)pVtab[s][0];cout << "     [0] "; pFun();pFun = (Fun)pVtab[s][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];cout << "     [3] ";cout << pFun << endl;cout << "[" << s + 1 << "] Base2.ibase2 = " << (int)pVtab[s + 1] << endl;s = s + sizeof(Base2) / 4;cout << "[" << s << "] Base3::_vptr->" << endl;pFun = (Fun)pVtab[s][0];cout << "     [0] "; pFun();pFun = (Fun)pVtab[s][1];cout << "     [1] "; pFun();pFun = (Fun)pVtab[s][2];cout << "     [2] "; pFun();pFun = (Fun)pVtab[s][3];cout << "     [3] ";cout << pFun << endl;s++;cout << "[" << s << "] Base3.ibase3 = " << (int)pVtab[s] << endl;s++;cout << "[" << s << "] Derive.iderive = " << (int)pVtab[s] << endl;
}

輸出結果:

使用圖片表示是下面這個樣子:

我們可以看到:

  • 每個父類都有自己的虛表。
  • 子類的成員函數被放到了第一個父類的表中。
  • 內存布局中,其父類布局依次按聲明順序排列。
  • 每個父類的虛表中的f()函數都被overwrite成了子類的f()。這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

?

四、重復繼承

面我們再來看看,發生重復繼承的情況。所謂重復繼承,也就是某個基類被間接地重復繼承了多次。

下圖是一個繼承圖,我們重載了父類的f()函數。其類繼承的源代碼如下所示。其中,每個類都有兩個變量,一個是整形(4字節),一個是字符(1字節),而且還有自己的虛函數,自己overwrite父類的虛函數。如子類D中,f()覆蓋了超類的函數, f1() 和f2() 覆蓋了其父類的虛函數,Df()為自己的虛函數。

測試代碼:?

#include<iostream>
using namespace std;class B {
public:int ib;char cb;
public:B() : ib(0), cb('B') {}virtual void f() { cout << "B::f()" << endl; }virtual void Bf() { cout << "B::Bf()" << endl; }
};class B1 : public B {
public:int ib1;char cb1;
public:B1() : ib1(11), cb1('1') {}virtual void f() { cout << "B1::f()" << endl; }virtual void f1() { cout << "B1::f1()" << endl; }virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};class B2 : public B {
public:int ib2;char cb2;
public:B2() :ib2(12), cb2('2') {}virtual void f() { cout << "B2::f()" << endl; }virtual void f2() { cout << "B2::f2()" << endl; }virtual void Bf2() { cout << "B2::Bf2()" << endl; }};class D : public B1, public B2 {
public:int id;char cd;
public:D() : id(100), cd('D') {}virtual void f() { cout << "D::f()" << endl; }virtual void f1() { cout << "D::f1()" << endl; }virtual void f2() { cout << "D::f2()" << endl; }virtual void Df() { cout << "D::Df()" << endl; }};int main()
{typedef void(*Fun)(void);int** pVtab = NULL;Fun pFun = NULL;D d;pVtab = (int**)&d;cout << "[0] D::B1::_vptr->" << endl;pFun = (Fun)pVtab[0][0];cout << "     [0] ";    pFun();pFun = (Fun)pVtab[0][1];cout << "     [1] ";    pFun();pFun = (Fun)pVtab[0][2];cout << "     [2] ";    pFun();pFun = (Fun)pVtab[0][3];cout << "     [3] ";    pFun();pFun = (Fun)pVtab[0][4];cout << "     [4] ";    pFun();pFun = (Fun)pVtab[0][5];cout << "     [5] 0x" << pFun << endl;cout << "[1] B::ib = " << (int)pVtab[1] << endl;cout << "[2] B::cb = " << (char)pVtab[2] << endl;cout << "[3] B1::ib1 = " << (int)pVtab[3] << endl;cout << "[4] B1::cb1 = " << (char)pVtab[4] << endl;cout << "[5] D::B2::_vptr->" << endl;pFun = (Fun)pVtab[5][0];cout << "     [0] ";    pFun();pFun = (Fun)pVtab[5][1];cout << "     [1] ";    pFun();pFun = (Fun)pVtab[5][2];cout << "     [2] ";    pFun();pFun = (Fun)pVtab[5][3];cout << "     [3] ";    pFun();pFun = (Fun)pVtab[5][4];cout << "     [4] 0x" << pFun << endl;cout << "[6] B::ib = " << (int)pVtab[6] << endl;cout << "[7] B::cb = " << (char)pVtab[7] << endl;cout << "[8] B2::ib2 = " << (int)pVtab[8] << endl;cout << "[9] B2::cb2 = " << (char)pVtab[9] << endl;cout << "[10] D::id = " << (int)pVtab[10] << endl;cout << "[11] D::cd = " << (char)pVtab[11] << endl;
}

輸出結果:

下面是對于子類實例中的虛函數表的圖:(第一份圖為原作者的圖,第二幅圖為修改的圖)

?

我們可以看見,最頂端的父類B其成員變量存在于B1和B2中,并被D給繼承下去了。而在D中,其有B1和B2的實例,于是B的成員在D的實例中存在兩份,一份是B1繼承而來的,另一份是B2繼承而來的。所以,如果我們使用以下語句,則會產生二義性編譯錯誤:

1

2

3

4

D d;

d.ib = 0;?//二義性錯誤

d.B1::ib = 1;?//正確

d.B2::ib = 2;?//正確

注意,上面例程中的最后兩條語句存取的是兩個變量。雖然我們消除了二義性的編譯錯誤,但B類在D中還是有兩個實例,這種繼承造成了數據的重復,我們叫這種繼承為重復繼承。重復的基類數據成員可能并不是我們想要的。所以,C++引入了虛基類的概念。

?

參考資料:

  • C++ 虛函數表解析 陳皓著
  • C++ 對象的內存布局?陳皓著

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

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

相關文章

1074. Reversing Linked List (25)

Given a constant K and a singly linked list L, you are supposed to reverse the links of every K elements on L. For example, given L being 1→2→3→4→5→6, if K 3, then you must output 3→2→1→6→5→4; if K 4, you must output 4→3→2→1→5→6. Input Spe…

【Leetcode | 47】 222. 完全二叉樹的節點個數

給出一個完全二叉樹&#xff0c;求出該樹的節點個數。 說明&#xff1a; 完全二叉樹的定義如下&#xff1a;在完全二叉樹中&#xff0c;除了最底層節點可能沒填滿外&#xff0c;其余每層節點數都達到最大值&#xff0c;并且最下面一層的節點都集中在該層最左邊的若干位置。若最…

makefile中的兩個函數(wildcard和patsubst)

(1) wildcard函數 作用是查找指定目錄下指定類型的文件&#xff0c;并最終返回一個環境變量&#xff0c;需要用$取值賦值給另一個環境變量&#xff01;該函數只有一個參數&#xff0c;如取出當前目錄下的所有.c文件&#xff0c;并賦值給allc普通變量&#xff1a; allc$(wildc…

231. 2的冪

給定一個整數&#xff0c;編寫一個函數來判斷它是否是 2 的冪次方。 示例 1: 輸入: 1 輸出: true 解釋: 20 1 示例 2: 輸入: 16 輸出: true 解釋: 24 16 示例 3: 輸入: 218 輸出: false 解法一&#xff1a; class Solution { public:bool isPowerOfTwo(int n) {return(n >…

C庫函數

Linux的系統I/O函數&#xff08;read、write、open、close和 lseek等&#xff09;與C語言的C庫函數&#xff08;libc.so庫文件中&#xff09;都是相對應的&#xff0c;它們都是動態庫函數。如下圖所示&#xff0c;C庫函數有fopen、fclose、fwrite、fread和fseek等。這些C庫函數…

【Leetcode | 48】226. 翻轉二叉樹

翻轉一棵二叉樹。 示例&#xff1a; 輸入&#xff1a; 4 / \ 2 7 / \ / \ 1 3 6 9 輸出&#xff1a; 4 / \ 7 2 / \ / \ 9 6 3 1 備注: 這個問題是受到 Max Howell 的 原問題 啟發的 &#xff1a; 谷歌&#xff1a;我們90&#xff05;的…

C庫函數與Linux系統函數之間的關系

由上小節知道&#xff0c;C庫函數是借助FILE類型的結構體來對文件進行操作的&#xff0c;其本身只是在用戶空間&#xff08;I/O緩沖區&#xff09;進行讀寫操作&#xff0c;而數據在內核與用戶空間之間的傳遞、以及將內核與I/O設備之間的數據傳遞都是該C庫函數進行一系列的系統…

【第十六章】模板實參推斷

二、模板顯式推斷 在C中&#xff0c;若函數模板返回類型需要用戶指定&#xff0c;那么在定義函數模板時&#xff0c;模板參數的順序是很重要的&#xff0c;如下代碼&#xff1a; template <typename T1, typename T2, typename T3> //模板一 T1 sum(T2 a, T3 b) {retu…

open函數和errno全局變量

&#xff08;1&#xff09;open函數 man man 查看man文檔的首頁 其中DESCRIPTION部分描述了man文檔的每一章的章節內容 第2章System calls為系統調用&#xff0c;即Liunx系統函數。 man 2 open 查看第二章的open函數的詳細幫助文件。 open函數用于打開一個已經的文件或者創…

open函數和close函數的使用

學習幾個常用的Linux系統I/O函數&#xff1a;open、close、write、read和lseek。注意&#xff0c;系統調用函數必須都考慮返回值。 &#xff08;1&#xff09;open函數的使用 首先&#xff0c;需要包含三個頭文件&#xff1a;<sys/types.h> <sys/stat.h> <…

【Leetcode | 9】217. 存在重復元素

解題代碼&#xff1a; bool containsDuplicate(vector<int>& nums) {return nums.size() > set<int>(nums.begin(), nums.end()).size(); }

全緩沖、行緩沖和無緩沖

這里的緩沖是指的是用戶空間的I/O緩沖區&#xff0c;不是內核緩沖。 無緩沖&#xff1a;用戶層不提供緩沖&#xff0c;數據流直接到內核緩沖&#xff0c;再到磁盤等外設上。標準錯誤輸出&#xff08;2&#xff09;通常是無緩存的&#xff0c;因為它必須盡快輸出&#xff0c;且…

【Leetcode】1. 兩數之和

給定一個整數數組 nums 和一個目標值 target&#xff0c;請你在該數組中找出和為目標值的那 兩個 整數&#xff0c;并返回他們的數組下標。 你可以假設每種輸入只會對應一個答案。但是&#xff0c;你不能重復利用這個數組中同樣的元素。 示例: 給定 nums [2, 7, 11, 15], targ…

read和write函數的使用

都需要包含頭文件&#xff1a; <unistd.h> read系統函數從打開的設備或文件中讀取數據&#xff0c;即將數據從外設上經過內核讀到用戶空間&#xff1b;write系統函數相反&#xff0c;向打開的設備或文件中寫入數據&#xff0c;即將數據從用戶空間&#xff08;I/O緩沖&am…

1091. Acute Stroke (30)

One important factor to identify acute stroke (急性腦卒中) is the volume of the stroke core. Given the results of image analysis in which the core regions are identified in each MRI slice, your job is to calculate the volume of the stroke core. Input Speci…

lseek函數的使用

需要包含頭文件&#xff1a;<sys/types.h> <unistd.h> off_t lseek(int fd, off_t offset, int whence)&#xff1b; 函數原型 函數功能&#xff1a;移動文件讀寫指針&#xff1b;獲取文件長度&#xff1b;拓展文件空間。 在使用該函數之前需要將文件打開&…

19. 刪除鏈表的倒數第N個節點

給定一個鏈表&#xff0c;刪除鏈表的倒數第 n 個節點&#xff0c;并且返回鏈表的頭結點。 示例&#xff1a; 給定一個鏈表: 1->2->3->4->5, 和 n 2. 當刪除了倒數第二個節點后&#xff0c;鏈表變為 1->2->3->5. 說明&#xff1a; 給定的 n 保證是有效的。…

文件操作相關的系統函數

重點學習&#xff1a;stat&#xff08;fstat、lstat 獲取文件屬性&#xff09;、access&#xff08;測試指定文件是否擁有某種權限&#xff09;、chmod&#xff08;改變文件的權限&#xff09;、chown&#xff08;改變文件的所屬主和所屬組&#xff09;、truncate&#xff08;截…

stat函數(stat、fstat、lstat)

#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> //需包含頭文件 有如下三個函數的函數原型&#xff1a; int stat(const char *path, struct stat *buf); 第一個形參&#xff1a;指出文件&#xff08;文件路徑&#xff09;&…

1062. Talent and Virtue (25)

About 900 years ago, a Chinese philosopher Sima Guang wrote a history book in which he talked about peoples talent and virtue. According to his theory, a man being outstanding in both talent and virtue must be a "sage&#xff08;圣人&#xff09;"…