c++中的多態

文章目錄

  • 1.多態的概念
    • 1.1概念
  • 2.多態的定義及實現
    • 2.1多態的構成條件
    • 2.2虛函數
    • 2.3虛函數的重寫
    • 2.4 C++11 override 和 final
    • 2.5 重載、覆蓋(重寫)、隱藏(重定義)的對比
  • 3. 抽象類
    • 3.1概念
    • 3.2接口繼承和實現繼承
  • 4.多態的原理
    • 4.1虛函數表
    • 4.2多態原理分析
    • 4.3 動態綁定與靜態綁定
  • 5.單繼承和多繼承關系的虛函數表
    • 5.1 單繼承中的虛函數表
    • 5.2 多繼承中的虛函數表
      • 5.2.1 對象模型
      • 5.2.2 原理分析
    • 5.3菱形繼承、菱形虛擬繼承

1.多態的概念

1.1概念

多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態
舉個例子:比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票。

2.多態的定義及實現

2.1多態的構成條件

多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了
Person。Person對象買票全價,Student對象買票半價。
那么在繼承中要構成多態還有兩個條件

  1. 必須通過基類的指針或者引用調用虛函數
  2. 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫

2.2虛函數

虛函數:即被virtual修飾的類成員函數稱為虛函數。

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};

在這里插入圖片描述

2.3虛函數的重寫

虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。

class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }//注意:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,//雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了在派生//類依舊保持虛函數屬性),但是該種寫法不是很規范,不建議這樣使用
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

虛函數重寫的兩個例外:

  1. 協變(基類與派生類虛函數返回值類型不同)
    派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
  1. 析構函數的重寫(基類與派生類析構函數的名字不同)
    如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生類Student的析構函數重寫了Person的析構函數,
//下面的delete對象調用析構函數,才能構成多態,才能保證p1和p2指向
//的對象正確的調用析構函數。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}

2.4 C++11 override 和 final

從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫

  1. final:修飾虛函數,表示該虛函數不能再被重寫
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒適" << endl;}
};
  1. override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒適" << endl;}
};

2.5 重載、覆蓋(重寫)、隱藏(重定義)的對比

在這里插入圖片描述

3. 抽象類

3.1概念

在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。

class Person {
public:virtual void BuyTicket() = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
class soldier :public Person
{virtual void BuyTicket() { cout << "買票-優先" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{soldier ps;Student st;Func(ps);Func(st);return 0;
}

3.2接口繼承和實現繼承

普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。

4.多態的原理

4.1虛函數表

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{Base b;cout << sizeof(b);return 0;
}

在這里插入圖片描述
我們可以得知結果為8
通過觀察測試我們發現b對象是8bytes,除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表,。那么派生類中這個表放了些什么呢?我們接著往下分析

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

在這里插入圖片描述
監視窗口我們得到
1.虛表里存的是虛函數的地址,被重寫的虛函數在相應的虛表下地址會改變
2.派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員
3.虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。
4.總結一下派生類的虛表生成:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后
5.虛函數和普通函數一樣是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針,我們看虛表的地址和虛函數的地址是比較相像的,所以虛表也是存在代碼段
在這里插入圖片描述

4.2多態原理分析

class Person 
{
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{
//第一張圖測試Person Mike;Func(Mike);Student Johnson;Func(Johnson);
//第二張圖測試Person Mike;Student Johnson;Func(Mike);Johnson.BuyTicket();return 0;
}

在這里插入圖片描述

  1. 觀察上圖的淺藍色箭頭我們看到,p是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數是Person::BuyTicket。
  2. 觀察上圖的深藍色箭頭我們看到,p是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數是Student::BuyTicket。
  3. 這樣就實現出了不同對象去完成同一行為時,展現出不同的形態
    在這里插入圖片描述
    4.再通過上面的匯編代碼分析,看出滿足多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的

4.3 動態綁定與靜態綁定

  1. 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,
    比如:函數重載
  2. 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體
    行為,調用具體的函數,也稱為動態多態。

5.單繼承和多繼承關系的虛函數表

需要注意的是在單繼承和多繼承關系中,下面我們去關注的是派生類對象的虛表模型,因為基類的虛表模型前面我們已經看過了,沒什么需要特別研究的

5.1 單繼承中的虛函數表

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;PrintVTable((VFPTR*)(*(int*)&b));// 思路:取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr
// 1.先取b的地址,強轉成一個int*的指針
// 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針
// 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
// 4.虛表指針傳遞給PrintVTable進行打印虛表
// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再
編譯就好了。PrintVTable((VFPTR*)(*(int*)&d));
}

在這里插入圖片描述
觀察下圖中的監視窗口中我們發現看不見func3和func4。這里是編譯器的監視窗口故意隱藏了這兩個函數,也可以認為是他的一個小bug。那么我們如何查看d的虛表呢?下面我們使用代碼打印出虛表中的函數。
在這里插入圖片描述

5.2 多繼承中的虛函數表

5.2.1 對象模型

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1=1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2=2;int bb=2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1=3;
};// 用程序打印虛表
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main()
{Derive d;PrintVFTable((VF_PTR*)(*(int*)&d));//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));//不理解就看下面的對象模型Base2* ptr2 = &d;PrintVFTable((VF_PTR*)(*(int*)(ptr2)));return 0;
}

觀察下圖可以看出:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中,但是我們同樣發現個問題,重寫的func1()地址不一樣,這是為什么呢?接下來看原理分析
在這里插入圖片描述
多繼承對象模型

5.2.2 原理分析

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1=1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2=2;int bb=2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1=3;
};// 用程序打印虛表
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main()
{Derive d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();return 0;
}

在這里插入圖片描述

至于為什么要偏移呢?
假設我們這時候有個指針p,
Derive *p=&d
p調用func1()是合理的無可厚非,ptr1調用func1()就也是合理的,因為ptr1和p指向的是同一個位置,那么問題解決掉了,ptr2就是要偏移到p指針的位置才可以調用func1()

5.3菱形繼承、菱形虛擬繼承

class A
{
public:virtual void func1(){}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){}virtual void func2(){}
public:int _b;
};class C : virtual public A
{
public:virtual void func1(){}virtual void func3(){}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){}
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在這里插入圖片描述
簡單了解下模型

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

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

相關文章

學習筆記整理-面向對象-03-構造函數

一、構造函數 1. 用new調用函數的四步走 new 函數();JS規定&#xff0c;使用new操作符調用函數會進行"四步走"&#xff1a; 函數體內會自動創建出一個空白對象函數的上下文(this)會指向這個對象函數體內的語句會執行函數會自動返回上下文對象&#xff0c;即使函數沒…

HDMI接口的PCB布局布線要求

高清多媒體接口&#xff08;High Definition Multimedia Interface&#xff09;&#xff0c;簡稱&#xff1a;HDMI&#xff0c;是一種全數字化視頻和聲音發送接口&#xff0c;可以發送未壓縮的音頻及視頻信號。隨著技術的不斷提升&#xff0c;HDMI的傳輸速率也不斷的提升&#…

使用GEWE框架進行微信群組管理(三)

友情鏈接&#xff1a;GEWE框架官網 geweapi.com 點擊訪問即可。 邀請或添加聯系人進群 小提示&#xff1a; 不管是添加40人以內還是以上都用此接口cause填寫邀請進群的理由 請求URL&#xff1a; http://域名地址/api/group/invite 請求方式&#xff1a; POST 請求頭&…

brew+nginx配置靜態文件服務器

背景 一下子閑下來了&#xff0c;了解的我的人都知道我閑不下來。于是&#xff0c;我在思考COS之后&#xff0c;決定自己整一個本地的OSS&#xff0c;實現靜態文件的訪問。那么&#xff0c;首屈一指的就是我很熟的nginx。也算是個小復習吧&#xff0c;復習一下nginx代理靜態文…

解決生成式AI落地之困,亞馬遜云科技提供完整解決方案

生成式AI技術無疑是當前最大的時代想象力之一。 資本、創業者、普通人都在涌入生成式AI里去一探究竟&#xff1a;“百模大戰”連夜打響&#xff0c;融資規模連創新高&#xff0c;各種消費類產品概念不斷涌現……根據Bloomberg Intelligence 的報告&#xff0c;2022年生成式AI 市…

文件操作/IO

文件 文件是一種在硬盤上存儲數據的方式&#xff0c;操作系統幫我們把硬盤的一些細節都封裝起來了&#xff0c;程序員只需要了解文件相關的接口即可&#xff0c;相當于操作文件就是間接的操作硬盤了 硬盤用來存儲數據&#xff0c;和內存相比硬盤的存儲空間更大&#xff0c;訪問…

使用FTP文件傳輸協議的潛在風險

數據&#xff08;事實&#xff0c;數字&#xff0c;價值&#xff09;是當今業務運行的核心要素。但是&#xff0c;如果數據沒有得到有效的存儲和傳輸&#xff0c;它們就會成為阻礙業務發展的障礙。如果企業不能及時地把數據送到合適的地方&#xff0c;就會造成嚴重的經濟損失。…

【skynet】skynet 入門代碼

寫在前面 本文將從零開始&#xff0c;寫第一個 skynet 程序 HelloWorld 。通過 HelloWorld 可以熟悉 skynet 的運作方式&#xff0c;和了解其 api 。 文章目錄 寫在前面準備工作編寫代碼運行結果 準備工作 首先要有一個編譯好&#xff0c;而且工作正常的 skynet 。 編寫代碼…

【Linux】Shell腳本之流程控制語句 if判斷、for循環、while循環、case循環判斷 + 實戰詳解[?建議收藏!!?]

&#x1f468;?&#x1f393;博主簡介 &#x1f3c5;云計算領域優質創作者 ??&#x1f3c5;華為云開發者社區專家博主 ??&#x1f3c5;阿里云開發者社區專家博主 &#x1f48a;交流社區&#xff1a;運維交流社區 歡迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…

.bit域名調研

.bit域名研究 問題&#xff1a; .bit域名和ENS域名的相同點&#xff1f;不同點&#xff1f;有什么關系&#xff1f; .bit的定義 .bit 是基于區塊鏈的&#xff0c;開源的&#xff0c;跨鏈去中心化賬戶系統.bit 提供了以 .bit 為后綴的全局唯一的命名體系&#xff0c;可用于加密…

安全第二次

一&#xff0c;iframe <iframe>標簽用于在網頁里面嵌入其他網頁。 1&#xff0c;sandbox屬性 如果嵌入的網頁是其他網站的頁面&#xff0c;因不了解對方會執行什么操作&#xff0c;因此就存在安全風險。為了限制<iframe>的風險&#xff0c;HTML 提供了sandb…

分布式應用:Zabbix監控MariaDB

目錄 一、理論 1.Zabbix監控MariaDB 二、實驗 1.Zabbix監控MariaDB 一、理論 1.Zabbix監控MariaDB &#xff08;1&#xff09;環境 zabbix服務端&#xff1a;192.168.204.214 zabbix客戶端&#xff1a;192.168.204.215 &#xff08;2&#xff09;MareaDB安裝 安裝 za…

做海外游戲推廣有哪些條件?

做海外游戲推廣需要充分準備和一系列條件的支持。以下是一些關鍵條件&#xff1a; 市場調研和策略制定&#xff1a;了解目標市場的文化、玩家偏好、競爭格局等是必要的。根據調研結果制定適合的推廣策略。 本地化&#xff1a;將游戲內容、界面、語言、貨幣等進行本地化&#…

使用ip2region獲取客戶端地區

目錄 從gitee拉取ip2region.xdb資源文件 寫測試類 注意要寫對資源路徑 本地測試結果 ?編輯 遠端測試結果 從gitee拉取ip2region.xdb資源文件 git clone https://gitee.com/lionsoul/ip2region.git 將xdb放入resources資源文件夾 引入依賴 <dependency><groupId&…

由淺入深C系列五:使用libcurl進行基于http get/post模式的C語言交互應用開發

使用libcurl進行基于http get/post模式的C語言交互應用開發 簡介環境準備在線資源示例代碼測試調用運行結果 簡介 大多數在linux下的開發者&#xff0c;都會用到curl這個命令行工具。對于進行restful api的測試等&#xff0c;非常方便。其實&#xff0c;這個工具還提供了一個C…

Python中單引號、雙引號和三引號的區別

① 單引號和雙引號主要用來表示字符串 # 單引號 astr = Python print(type(astr)) # <class str># 雙引號"" bstr = "Python" print(type(bstr)) # <class str> str1 = I\m a big fan of Python. print(str1) # Im a big fan of Python.s…

[HDLBits] Exams/m2014 q4d

Implement the following circuit: module top_module (input clk,input in, output out);always(posedge clk) beginout<out^in;end endmodule直接寫out^in就行

Vue 使用 vite 創建項目

vite 是新一代前端構建工具&#xff0c;和 webpack 類似。 vite 的啟動速度更快。在開發環境中&#xff0c;不需要打包就可以直接運行。 vite 的更新速度更快。當修改內容后&#xff0c;不需要刷新瀏覽器&#xff0c;頁面就會實時更新。 vite 完全是按需編譯。它只會編譯需要…

【考研數學】高等數學第三模塊——積分學 | Part II 定積分(反常積分及定積分應用)

文章目錄 前言三、廣義積分3.1 斂散性概念&#xff08;一&#xff09;積分區間為無限的廣義積分&#xff08;二&#xff09;積分區間有限但存在無窮間斷點 3.2 斂散性判別法 四、定積分應用寫在最后 前言 承接前文&#xff0c;梳理完定積分的定義及性質后&#xff0c;我們進入…

企業網盤 vs 傳統存儲設備:為何云存儲成為首選?

企業網盤的出現為企業提供了新的存儲方式&#xff0c;相較于傳統的存儲設備&#xff0c;為何越來越多的企業選擇了云存儲呢&#xff1f; 一、存儲成本 在企業數據存儲方面&#xff0c;成本是企業重要的考量因素。企業網盤是基于云存儲技術的存儲工具&#xff0c;因此它比傳統的…