存儲

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

?

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

?

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()了。如:

 1 Derive d;2 Base1 *b1 = &d;3 Base2 *b2 = &d;4 Base3 *b3 = &d;5 b1->f(); //Derive::f()6 b2->f(); //Derive::f()7 b3->f(); //Derive::f()8  9 b1->g(); //Base1::g()
10 b2->g(); //Base2::g()
11 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++引入了虛基類的概念。

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

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

相關文章

【對象程序設計面向】虛繼承

B C虛繼承A&#xff0c;D public繼承 B C &#xff0c;有A *a new D&#xff0c;a->fun(),fun是虛函數&#xff0c;并且B C都重寫了&#xff0c;怎么保證a調用的是B重寫的虛函數。 #include <iostream> using namespace std;class A { public:virtual void fun() { …

BTree和B+Tree詳解

B 樹是為了磁盤或其它存儲設備而設計的一種多叉&#xff08;下面你會看到&#xff0c;相對于二叉&#xff0c;B樹每個內結點有多個分支&#xff0c;即多叉&#xff09;平衡查找樹。 B 樹又叫平衡多路查找樹。一棵m階的B 樹 (m叉樹)的特性如下&#xff1a; 樹中每個結點最多含…

【1】MySQL的四種事務隔離級別

二、事務的并發問題 1、臟讀&#xff1a;事務A讀取了事務B更新的數據&#xff0c;然后B回滾操作&#xff0c;那么A讀取到的數據是臟數據 2、不可重復讀&#xff1a;事務 A 多次讀取同一數據&#xff0c;事務 B 在事務A多次讀取的過程中&#xff0c;對數據作了更新并提交&#x…

MySQL的四種事務隔離級別

1. MySQL的四種事務隔離級別

__thread

__thread是GCC內置的線程局部存儲設施&#xff0c;存取效率可以和全局變量相比。__thread變量每一個線程有一份獨立實體&#xff0c;各個線程的值互不干擾。可以用來修飾那些帶有全局性且值可能變&#xff0c;但是又不值得用全局變量保護的變量。 __thread使用規則&#xff1a…

eventfd(三)

1. 測試代碼&#xff1a; //https://www.jianshu.com/p/d7ebac8dc9f8 #include <stdio.h> #include <unistd.h> #include <stdint.h> #include <pthread.h> #include <sys/eventfd.h> #include <sys/epoll.h>int event_fd -1;void *rea…

04-樹4 是否同一棵二叉搜索樹 (25 分)

給定一個插入序列就可以唯一確定一棵二叉搜索樹。然而&#xff0c;一棵給定的二叉搜索樹卻可以由多種不同的插入序列得到。例如分別按照序列{2, 1, 3}和{2, 3, 1}插入初始為空的二叉搜索樹&#xff0c;都得到一樣的結果。于是對于輸入的各種插入序列&#xff0c;你需要判斷它們…

strtol,strtoll,strtoul, strtoull函數的使用

#include<stdlib.h> // 這個是C標準庫&#xff0c;與linux無關。這套函數是通用 long int strtol(const char *nptr, char **endptr, int base); long long int strtoll(const char *nptr, char **endptr, int base); unsigned long int strtoul(const char *nptr, char …

eventfd(一)

函數原型&#xff1a; 創建的時候可以傳入一個計數器的初始值initval。 第二個參數flags在linux 2.6.26之前的版本是沒有使用的&#xff0c;必須初始化為0&#xff0c;在2.6.27之后的版本flag才被使用。 #include <sys/eventfd.h> int eventfd(unsigned int initval, in…

gettimeofday

作用&#xff1a;需要打印代碼執行到某處的時間&#xff0c;或者需要計算程序執行的時間差&#xff08;精確到微妙級&#xff09;。這時會用到gettimeofday函數&#xff0c;它可以返回自1970-01-01 00:00:00到現在經歷的秒數。 #include <sys/time.h> int gettimeofday(…

02-線性結構2 一元多項式的乘法與加法運算 (20 分

設計函數分別求兩個一元多項式的乘積與和。 輸入格式: 輸入分2行&#xff0c;每行分別先給出多項式非零項的個數&#xff0c;再以指數遞降方式輸入一個多項式非零項系數和指數&#xff08;絕對值均為不超過1000的整數&#xff09;。數字間以空格分隔。 輸出格式: 輸出分2行&…

1066 圖像過濾 (15 分)

圖像過濾是把圖像中不重要的像素都染成背景色&#xff0c;使得重要部分被凸顯出來。現給定一幅黑白圖像&#xff0c;要求你將灰度值位于某指定區間內的所有像素顏色都用一種指定的顏色替換。 輸入格式&#xff1a; 輸入在第一行給出一幅圖像的分辨率&#xff0c;即兩個正整數 M…

從零實現一個http服務器

如果GET請求帶參數&#xff0c;那么一般是附加在請求的url后面&#xff0c;參數與參數之間使用&分割&#xff0c;例如請求http://www.hootina.org/index_2013.php?param1value1m2value2m3value3&#xff0c;我們看下這個請求組裝的的http協議包格式&#xff1a; GET /ind…

1068 萬綠叢中一點紅 (20 分)

對于計算機而言&#xff0c;顏色不過是像素點對應的一個 24 位的數值。現給定一幅分辨率為 MN 的畫&#xff0c;要求你找出萬綠叢中的一點紅&#xff0c;即有獨一無二顏色的那個像素點&#xff0c;并且該點的顏色與其周圍 8 個相鄰像素的顏色差充分大。 輸入格式&#xff1a; 輸…

《個人項目學習指引》

1. 從零實現一個http服務器

1069 微博轉發抽獎 (20 分)

小明 PAT 考了滿分&#xff0c;高興之余決定發起微博轉發抽獎活動&#xff0c;從轉發的網友中按順序每隔 N 個人就發出一個紅包。請你編寫程序幫助他確定中獎名單。 輸入格式&#xff1a; 輸入第一行給出三個正整數 M&#xff08;≤ 1000&#xff09;、N 和 S&#xff0c;分別是…

【1】TCP三次握手的第三次的 ack包丟失會怎樣?

面試題&#xff1a; 在 TCP 建立連接的三次握手連接階段&#xff0c;如果客戶端發送的第三個ACK包丟了&#xff0c;那么客戶端和服務端分別進行什么處理呢&#xff1f; 相信了解 tcp 協議的人&#xff0c;三次握手的過程肯定很了解了。第三次的 ack 包丟失就是說在 client 端…

1070 結繩 (25 分

給定一段一段的繩子&#xff0c;你需要把它們串成一條繩。每次串連的時候&#xff0c;是把兩段繩子對折&#xff0c;再如下圖所示套接在一起。這樣得到的繩子又被當成是另一段繩子&#xff0c;可以再次對折去跟另一段繩子串連。每次串連后&#xff0c;原來兩段繩子的長度就會減…

動態規劃目錄

序號題目1 70. 爬樓梯

1071 小賭怡情 (15 分)

常言道“小賭怡情”。這是一個很簡單的小游戲&#xff1a;首先由計算機給出第一個整數&#xff1b;然后玩家下注賭第二個整數將會比第一個數大還是小&#xff1b;玩家下注 t 個籌碼后&#xff0c;計算機給出第二個數。若玩家猜對了&#xff0c;則系統獎勵玩家 t 個籌碼&#xf…