進階了解C++(4)——多態

? ? ? ?在上篇文章中,簡單的介紹了多態中的概念以及其相關原理。本文將針對多態中其他的概念進一步進行介紹,并且更加深入的介紹關于多態的相關原理。

目錄

1. 抽象類:

2. 再談虛表:

3. 多繼承中的虛函數表:


1. 抽象類:

? ? ? ?在上篇文章中提到了,如果使用關鍵字virtual修飾一個成員函數,則這個成員函數被稱為虛函數。此處,針對虛函數進行擴展,如果在虛函數的聲明后面加上=0,則這個函數被稱為純虛函數。包含純虛函數的類又叫抽象類,其特點是不能初始化出對象。即使是子類繼承這個類,同樣也不能初始化出對象。只有認為對純虛函數進行重寫,才能初始化出一個對象。

? ? 給定一個抽象類及其子類如下:

//抽象類
class Person
{
public:virtual void func() = 0{cout << "Person-func()";}
};class Teacher : public Person
{
public:};class Student : public Person
{
public:};

如果向初始化出這三個類的對象,即:
?

int main()
{Person p;Student s;Teacher t;
}

此時編譯器報錯如下:

如果對子類中繼承父類中的純虛函數進行重寫,即:

class Teacher : public Person
{
public:virtual void func(){cout << "Teacher-func()" << endl;}
};class Student : public Person
{
public:virtual void func(){cout << "Student-func()" << endl;}
};

此時再去分別初始化兩個子類的對象,即:

int main()
{Student s;s.func();Teacher t;t.func();
}

代碼可以正常運行,且運行結果如下:

2. 再談虛表:

在之前C++基礎的文章中提到了,在構造函數中,存在初始化列表,初始化列表初始化成員變量的順序并不是根據初始化列表的順序,而是根據成員變量聲明的順序。對于虛函數,其也符合這個特性。具體可以用下面的代碼進行證明:

class Base
{
public:virtual void Func1(){cout << "Base::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;
}

通過監視窗口,查看對象b中虛表:

? ? ? ?可以看到,虛函數在虛表中存放的順序,正是虛函數在類中聲明的順序。對于這一點,也同樣可以在內存窗口中進行查看。

? ? ? ?

?從圖中不難發現,對象b中的第一個地址,恰好對應了虛表指針的地址。此時再查看虛表中的內容,即:

不難看出,再內存窗口中,第二,第三條地址分別對應了虛表中兩個虛函數的地址。

而對于子類,其生成的對象d中的內容如下:

對于子類對象的內容,可以分為兩個部分,一是從父類中繼承的內容,二是子類中自己的成員變量以及函數。在監視窗口中,可以看到子類繼承了父類的虛表,并且對其中進行重寫的虛函數的地址進行了覆蓋。但是需要注意,在子類中,并不存在自己的虛表?。對于子類虛表中的函數指針如下:
在上面給出的圖片中可以看出,藍線連接的兩個地址分別是父類、子類中的虛函數Func1(),但是因為這個函數在子類中發生了重寫,因此,父類,子類中這兩個虛函數的地址并不相同。

而對于紫線連接的兩個虛函數,由于虛函數并未在子類中發生虛函數的重寫,因此,父類,子類中倆個虛函數的地址相同。

如果對于子類,再添加一個虛函數,例如:

class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func(){;}
private:int _d = 2;
};

?此時,在監視窗口中進行查看,子類對象的虛表中并沒有出現新的虛函數的函數指針,但是在內存窗口中,卻出現了一條新的地址,對于這個新的地址,一般認為就是子類中新加入的虛函數。至于具體的驗證,將在文章后面給出。?

?

(注:為了方便演示,下面的代碼在x86,即32位環境下運行)

在之前C++基礎關于內存管理的文章中(C++(9)——內存管理-CSDN博客?)提到了系統根據不同的需求,將內存劃分為不同的部分,具體如下:

1.棧:用于存儲非全局、非靜態的局部變量,函數參數,返回值等等

2.堆:用于程序運行時的內存的動態開辟

3.數據段(靜態區):用于存儲全局變量和靜態變量

4.代碼段(常量區):可執行代碼\只讀常量

在給出了上述概念后,文章將探討一個 問題,即:虛表指針是存儲在什么地方的。

為了方便測試,首先給出上面四個類型變量的地址,即:

int i = 1;//棧int* p = new int;//堆static int j = 0;//數據段(靜態區)const char* p2 = "xxxxxxx";//代碼段(常量區)printf("棧=%p\n", &i);printf("堆=%p\n", p);printf("靜態區=%p\n", &j);printf("常量區=%p\n",p2);

打印結果如下:

對于如何獲取虛表指針,本文提供一種方法:由于虛表指針存儲在一個類的前四個字節,因此,只需要初始化出一個該類的對象,首先獲取這個對象的指針,在將這個指針強轉成int*類型,即可獲取虛表指針,具體代碼如下:

Base* B = &b;Derive* D = &d;printf("B=%p\n", *(int*)B);printf("D=%p\n", *(int*)D);

打印結果如下:

從上述區段以及兩個虛表指針的指針對比來看,虛表指針應該存儲在常量區,也就是代碼段。

上面給出了如何獲取虛表指針的存儲地址,下面給出虛表中,如何獲取虛表中存儲各個虛函數的指針,具體方法如下:

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);}
}
PrintVF((VF_PTR*) * (int*)&d);

打印結果如下:

如果在獲取了上述指針后,直接調用這些函數指針,便可知道上述 獲取的地址是否是類中的虛函數,即:
?

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);VF_PTR f = vf[i];f();}
}

打印結果如下:

通過這個例子可以看出,雖然在上面添加新的虛函數Func3()時,在子類的虛表中并沒有看到這個函數的地址,但是在次數,照樣可以通過函數指針調用這個函數,這也間接證明了Func3()其實添加到了子類中,只是在監視窗口不可見。

3. 多繼承中的虛函數表:

給定代碼如下:

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;return 0;
}

在上面給出的代碼中,存在三個類,其中Base1,Base2被集成到了Derive中,由于Base1最先被繼承到子類中,因此,可以認為,父類成員在子類的空間中的位置是最靠前的。對于&d表示取對象d的首地址,由于父類成員在空間中位置是最靠前的,因此,理論上p1==&d。而對于p2,由于其在Base1后繼承,因此p2相對于p1是靠后的,因此,在子類中,存在著兩張虛表,這兩個虛表分別有著自己獨立的地址。在監視窗口中,同樣可以證明這一點:

而對于Derive中的虛函數func3(),為了驗證func3()是存儲在哪個虛表中的,可以用下面的代碼進行檢驗:

PrintVF((VF_PTR*)*(int*)p1);

對于Base1中虛表中存儲的函數指針打印結果如下:

下面打印Base2中虛表中的函數指針:

由此證明,子類中的虛函數func3()是存儲在子類繼承并且進行覆蓋的Base1中的虛表。

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

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

相關文章

MySQL 用戶賬號遷移

文章目錄 前言1. 工具安裝1.1 下載安裝包1.2 編譯安裝 2. 用戶遷移后記 前言 有一個典型的使用場景&#xff0c;就是 RDS 下云大多數都是通過 DTS 進行數據傳輸的&#xff0c;用戶是不會同步到自建數據庫的。需要運維人員在自建數據庫重新創建用戶&#xff0c;如果用戶數量很多…

基于springboot+vue的在線考試與學習交流平臺

博主主頁&#xff1a;貓頭鷹源碼 博主簡介&#xff1a;Java領域優質創作者、CSDN博客專家、阿里云專家博主、公司架構師、全網粉絲5萬、專注Java技術領域和畢業設計項目實戰&#xff0c;歡迎高校老師\講師\同行交流合作 ?主要內容&#xff1a;畢業設計(Javaweb項目|小程序|Pyt…

中小型水庫安全監測運營解決方案,筑牢水庫安全防線

我國水庫大壩具有“六多”的特點。第一&#xff0c;總量多。我國現有水庫9.8萬座&#xff0c;是世界上水庫大壩最多的國家。第二&#xff0c;小水庫多。我國現有水庫中95%的水庫是小型水庫。第三&#xff0c;病險水庫多。 目前&#xff0c;在我國水庫管理中&#xff0c;部分地方…

供應鏈|NUS覃含章MS論文解讀:數據驅動下聯合定價和庫存控制的近似方法 (二)

編者按 本次解讀的文章發表于 Management Science&#xff0c;原文信息&#xff1a;Hanzhang Qin, David Simchi-Levi, Li Wang (2022) Data-Driven Approximation Schemes for Joint Pricing and Inventory Control Models. https://doi.org/10.1287/mnsc.2021.4212 文章在數…

深度神經網絡聯結主義的本質

一、介紹 在新興的人工智能 (AI) 領域&#xff0c;深度神經網絡 (DNN) 是一項里程碑式的成就&#xff0c;突破了機器學習、模式識別和認知模擬的界限。這一技術奇跡的核心是一個與認知科學本身一樣古老的思想&#xff1a;聯結主義。本文深入探討了聯結主義的基本原理&#xff0…

c# this關鍵字

c#this關鍵字 1. 代表當前類的對象 class Father {public int Age { get; set; }public string Name { get; set; }public Father(int age, string name){this.Age age;this.Name name;}public void Test(){Console.WriteLine($"name:{this.Name },age:{this.Age }&qu…

實例:NX二次開發抽取平面以及標準柱面中心線

一、概述 最近體驗許多外掛&#xff0c;包括胡波外掛、星空外掛及模圣等都有抽取面的中心線&#xff0c;由于剛剛學習&#xff0c;我嘗試看看能不能做出來&#xff0c;本博客代碼沒有封裝函數&#xff0c;代碼有待改進&#xff0c;但基本可以實現相應的功能。 二、案例實現的功…

【web APIs】3、(學習筆記)有案例!

提示&#xff1a;文章寫完后&#xff0c;目錄可以自動生成&#xff0c;如何生成可參考右邊的幫助文檔 文章目錄 前言一、概念其他事件頁面加載事件元素滾動事件頁面尺寸事件 元素尺寸與位置 二、案例舉例電梯導航 前言 掌握阻止事件冒泡的方法理解事件委托的實現原理 一、概念…

SpringCloud Alibaba(保姆級入門及操作)

第一章 微服務概念 1.0 科普一些術語 科普一下項目開發過程中常出現的術語,方便后續內容的理解。 **服務器:**分軟件與硬件,軟件:類型tomcat這種跑項目的程序, 硬件:用來部署項目的電腦(一般性能比個人電腦好) **服務:**操作系統上術語:一個程序,開發中術語:一個…

數學建模【分類模型】

一、分類模型簡介 本篇將介紹分類模型。對于二分類模型&#xff0c;我們將介紹邏輯回歸&#xff08;logistic regression&#xff09;和Fisher線性判別分析兩種分類算法&#xff1b;對于多分類模型&#xff0c;我們將簡單介紹SPSS中的多分類線性判別分析和多分類邏輯回歸。 分…

Java面試題之并發

并發 1.并發編程的優缺點&#xff1f;2.并發編程三要素&#xff1f;3.什么叫指令重排&#xff1f;4.如何避免指令重排&#xff1f;5.并發&#xff1f;并行&#xff1f;串行&#xff1f;6.線程和進程的概念和區別&#xff1f;7.什么是上下文切換&#xff1f;8.守護線程和用戶線程…

<網絡安全>《60 概念講解<第七課 網絡模型OSI對應協議>》

1 OSI模型 OSI模型&#xff08;Open Systems Interconnection Model&#xff09;是一個由國際標準化組織&#xff08;ISO&#xff09;提出的概念模型&#xff0c;用于描述和標準化電信或計算系統的通信功能&#xff0c;以實現不同通信系統之間的互操作性。該模型將通信系統劃分…

【k8s管理--Helm包管理器】

1、Helm的概念 Kubernetes包管器 Helm是查找、分享和使用軟件構件Kubernetes的最優方式。 Helm管理名為chart的Kubernetes包的工具。Helm可以做以下的事情&#xff1a; 從頭開始創建新的chat將chart打包成歸檔tgz)文件與存儲chat的倉庫進行交互在現有的Kubernetes集群中安裝和…

【Android】View 的滑動

View 的滑動是 Android 實現自定義控件的基礎&#xff0c;同時在開發中我們也難免會遇到 View 的滑動處理。其實不管是哪種滑動方式&#xff0c;其基本思想都是類似的&#xff1a;當點擊事件傳到 View 時&#xff0c;系統記下觸摸點的坐標&#xff0c;手指移動時系統記下移動后…

【AI+應用】怎么快速制作一個類chatGPT套殼網站

最近有人問我&#xff0c; 看了我之前寫的一篇文章 [人工智能] AI浪潮下Sora對于普通人的機會 &#xff0c; 怎么做一個類chatGPT的套殼網站&#xff0c;是從0開始做么。 對于普通人來說&#xff0c;萬事不懂先AI&#xff0c; AI找不到答案搜索google或百度。對于程序員來說…

C# 獲取類型 Type.GetType()

背景 C#是強類型語言&#xff0c;任何對象都有Type&#xff0c;有時候需要使用Type來進行反射、序列化、篩選等&#xff0c;獲取Type有Type.GetType, typeof()&#xff0c;object.GetType() 等方法&#xff0c;本文重點介紹Type.GetType()。 系統類型/本程序集內的類型 對于系…

有哪些視頻媒體?邀請視頻媒體報道活動的好處

傳媒如春雨&#xff0c;潤物細無聲&#xff0c;大家好&#xff0c;我是51媒體網胡老師。 視頻媒體在當今的媒體生態中占據了重要的地位。以下是一些主要的視頻媒體類型&#xff1a; 電視臺&#xff1a;如中央電視臺、各省級衛視臺、地方電視臺等&#xff0c;他們擁有專業的視…

學習linux從0到初級工程師-3

一、LNMP 1.1 搭建LNMP LNMP&#xff1a;LinuxNginxMysqlPHP LNMP優勢&#xff1a; 1.web服務器一種&#xff0c;Nginx處理靜態文件、索引文件&#xff0c;自動索引的效率非常高&#xff1b; 2.作為代理服務器,Nginx可以實現無緩存的反向代理加速&#xff0c;提高網站運行…

探索Redis 6.0的新特性

Redis&#xff08;Remote Dictionary Server&#xff09;是一個開源的內存中數據結構存儲系統&#xff0c;通常被用作緩存、消息隊列和實時數據處理等場景。它的簡單性、高性能以及豐富的數據結構支持使其成為了眾多開發者和企業的首選。在Redis 6.0版本中&#xff0c;引入了一…

Vue3報錯Promise executor functions should not be async.

解決方法 加注釋。。。// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor new Promise<boolean>(async (resolve, reject) > {... }),