C++ 多態詳解:從概念到實現原理----《Hello C++ Wrold!》(14)--(C/C++)

文章目錄

  • 前言
  • 多態的概念
  • 多態的定義和實現
    • 虛函數
    • 虛函數的重寫(覆蓋)
  • 多態的構成條件
  • override 和 final(C++11提出)
    • final
    • override
  • 重載、覆蓋(重寫)、隱藏(重定義)的對比
  • 抽象類
    • 接口繼承和實現繼承
  • 多態的原理
    • 虛函數表(也叫做虛表)
      • 引申:虛表的打印
    • 多態的原理
    • 靜態多態和動態多態
  • 多繼承中的虛函數表
  • 作業部分

前言

多態是面向對象編程的三大核心特性(封裝、繼承、多態)之一,它使得同一接口可以呈現出不同的行為,極大地提升了代碼的靈活性和可擴展性。在 C++ 中,多態的實現與虛函數、虛表等機制緊密相關,其底層邏輯涉及編譯期與運行期的不同處理方式。
本文將系統梳理 C++ 多態的概念、實現條件、核心機制(虛函數與虛表),并深入解析多態在繼承場景下的表現,同時結合典型問題與示例代碼,幫助讀者全面理解多態的本質與應用。無論是基礎的虛函數重寫,還是復雜的多繼承虛表結構,本文都將逐一剖析,為開發者在實際編程中合理運用多態提供清晰指引。

多態的概念

通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。

為了更方便和靈活的實現多種形態的調用

多態的定義和實現

虛函數

概念:被virtual修飾的類成員函數稱為虛函數(和前面的虛繼承區分)

eg:class Person {
public:virtual void text() {};

虛函數的重寫(覆蓋)

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

省流:虛函數+三同

虛構函數重寫的兩個例外情況:

1.協變:

此時基類與派生類虛函數返回值類型可以不同,但是返回值必須是父子關系的指針和引用

一個虛函數返回值是指針,一個是引用這樣也不行
但是一個返回的是父的,一個返回的是子的沒關系

2.派生類重寫虛函數可以不加virtual(但是建議加上)

總問題: 析構函數可以是虛函數嗎?為什么需要是虛函數?

析構函數加virtual,是不是虛函數重寫?
是,因為類析構函數都被處理成destructor這個統一的名字

為什么要這么處理呢?

因為要讓他們構成重寫

那為什么要讓他們構成重寫呢?

因為下面的場景

(Person是基類,Student是派生類)
Person* p = new Person;p->text();delete p;p = new Student;//注意:這里的p還是Person類的p->text();delete p; // p->destructor() + operator delete(p)// 這里我們期望p->destructor()是一個多態調用,而不是普通調用

多態的構成條件

1.必須通過基類的指針或者引用調用虛函數(注意是基類!!!)

2.被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫

注意:多態調用看的是指向的對象,普通的調用看的是當前的類型
eg: class Person{
public:
virtual void text(){}
};class Student : public Person{
public:virtual void text(){}//--a
};void func(const Person& p)
{
p.text();
}main函數里面func(Student());是調用的a的

問題:

1.為什么必須要是父類的指針或引用,而不是父類對象或者子類的指針或引用

(編譯器把這幾種行為ban了的原因)

原因:

1.不能是父類對象的原因:

不會拷貝子類的虛表和其他特有的,所以這個父類對象根本不知道子類的存在(指針和引用就可以避開這一點)

編譯器選擇不拷貝子類的虛表指針的原因:

害怕別人不知道父類對象虛表中是父類的還是子類的

2.不能是子類指針或引用:

怕去訪問到父類中沒有的成員

引申:

1.子類虛表的構建:

子類繼承父類時,會先復制一份父類的虛表。如果子類沒有重寫父類的虛函數,那么虛表中對應函數指針就指向父類虛函數實現;若子類重寫了某個虛函數,就會用子類自己的虛函數地址覆蓋虛表中從父類繼承來的對應函數指針。

2.子類賦值給父類對象切片,不會拷貝虛表,父類還是會要自己的虛表

override 和 final(C++11提出)

final

作用:1.修飾虛函數,表示該虛函數不能再被重寫

2.使用final關鍵字修飾類,直接禁止任何類繼承它

eg: class Person final{};
用法:eg:virtual void text() final {}(前有無virtual不重要哈)
引申:一個有final一個無final也能構成重載和隱藏

override

作用:檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯

class Person{
public:virtual void text(){}
};class Student :public Person {
public:virtual void text() override {}
};

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

在這里插入圖片描述

抽象類

概念:

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

作用:強制要求派生類重寫虛函數,另外抽象類體現出了接口繼承的關系

比如:class Car
{
public:
virtual void Drive() = 0;
};

接口繼承和實現繼承

普通函數的繼承是一種實現繼承,繼承的是函數的實現。

虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。

多態的原理

虛函數表(也叫做虛表)

包含虛函數的類會有虛函數表指針,虛函數表指針指向的是虛函數表的地址

虛函數表里面存了虛函數的指針

引申:函數不符合多態,編譯時就確定地址了
符合多態,運行時到指向對象的虛函數表中找調用函數的地址

注意:同一個類的所有實例對象共享同一個虛函數表

比較特殊的是:VS編譯器的虛表指向的地址后面會有0作為結束(可以用內存窗口看)

比如:在這里插入圖片描述

但是在進行增量編譯之后,可能這個0就沒了,這時候需要清理一下解決方案或者重新生成解決方案才行

引申:虛表的打印

虛表本質上是函數指針數組

typedef void(*FUNC_PTR) ();
//這里就是將   一個void(*)()的函數指針類型取別名為FUNC_PTR// 打印函數指針數組的方法
void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);FUNC_PTR f = table[i];f();}printf("\n");
}int main()
{Student st;int vft2 = *((int*)&st);
//這個強轉之后就一次++指++(int)個字節的東西了;而且這個32位上正好一個指針4個字節,正好讀完
//注意:Linux是64位的!!!PrintVFT((FUNC_PTR*)vft2);
//發現隱式類型轉換會報錯,就改成強轉了return 0;
}

注意:成員變量的變化會導致虛表的打印出錯–因為可能會影響到內存布局

虛表和虛基表都是在編譯階段生成的

對象實例化之后,才會與虛表有聯系(通過虛表指針)

多態的原理

核心的實現機制就是虛函數表和虛指針

滿足多態的話,子類的虛指針指向的虛表中的虛函數就會覆蓋父類的虛函數的地址,然后調用的就是子類的虛函數了

靜態多態和動態多態

靜態多態,又叫靜態綁定,前期綁定(早綁定),在程序編譯期間就確定了程序的行為

比如:函數重載

動態多態又稱為動態綁定,后期綁定(晚綁定),是在程序運行期間才確定調用什么函數的

也就是繼承+虛函數重寫實現的多態

在默認情況下,多態一般指的是動態多態

多繼承中的虛函數表

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;cout << sizeof(d) << endl;
//X86環境下,這個占20個字節,組成:兩個基類(都是一個虛表指針加一個成員變量)加一個成員變量Base1* ptr1 = &d;ptr1->func1();Base2* ptr2 = &d;ptr2->func1();//通過修正this指針,來讓this指針指向派生類的頭return 0;
}
問題:為什么重寫func1,Base1和Base2的虛表中func1的地址不一樣?
Base2中func1的地址不一樣是為了jmp去修正this指針的位置

注意:

1.多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中(其實是末尾)

作業部分

設計不想被繼承類,如何設計?方法1:基類構造函數私有   (C++98)方法2:基類加一個final  (C++11)方法1:eg:
class A
{
public:static A CreateObj()//這個static不能去掉,不然就不能通過域名去調用了{return A();}
private:A(){}
};//當然,用析構函數這么搞也行哈
int main()
{A::CreateObj();return 0;
}方法2:
class A final
{}
 這里常考一道筆試題:sizeof(Base)是多少?(X86環境下的話)
答案:8個字節//不是一個字節,也不是四個字節
要注意的是:類里面還有一個虛函數表指針(_vfptr)
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:char _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}
class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func();}
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }};
//三同里面的形參相同只用形參的類型相同就行,缺省參數和名字可以不同(但要有名)int main(int argc, char* argv[])//相當于int main()
{B* p = new B;p->test();return 0;
}
結果:輸出B->1引申:如果把test()放在了B里面的話,就應該輸出B->0了
因為此時this->func()的this不是父類指針,不構成多態

派生類那里不用加virtual的原因:

本質上只重寫了實現

面試常考題:

1.什么是多態?–靜態多態和動態多態都要答

2.inline函數可以是虛函數嗎?答:可以,不過編譯器就忽略inline屬性,這個函數就不再是

inline,因為虛函數要放到虛表中去。

3.靜態成員可以是虛函數嗎?答:不能,因為靜態成員函數沒有this指針,使用類型::成員函數

的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。(語法也會強制檢查這個,會報錯)

4.構造函數可以是虛函數嗎?答:不能,因為對象中的虛函數表指針是在構造函數初始化列表

階段才初始化的。

5.對象訪問普通函數快還是虛函數更快?答:首先如果是普通對象,是一樣快的。如果是指針

對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函

數表中去查找。

6.虛函數表是在什么階段生成的,存在哪的?答:虛函數表是在編譯階段就生成的,一般情況

下存在代碼段(常量區)的。

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

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

相關文章

Node.js + Express的數據庫AB View切換方案設計

方案總覽數據導入過程&#xff1a; - 根據控制表判斷當前活躍組&#xff08;假設當前活躍的是a&#xff0c;那么接下來要導入到b&#xff09;。 - 清空非活躍表&#xff08;即b表&#xff09;的數據&#xff0c;然后將新數據導入到b表。 - 切換控制表&#xff0c;將活…

C++_編程提升_temaplate模板_案例

類模板案例案例描述: 實現一個通用的數組類&#xff0c;要求如下&#xff1a;可以對內置數據類型以及自定義數據類型的數據進行存儲將數組中的數據存儲到堆區構造函數中可以傳入數組的容量提供對應的拷貝構造函數以及operator防止淺拷貝問題提供尾插法和尾刪法對數組中的數據進…

Win11系統安裝Anaconda環境極簡教程

Win11系統安裝Anaconda環境極簡教程 &#x1f4e5; 第一步&#xff1a;下載 Anaconda 安裝包 打開瀏覽器&#xff0c;訪問 Anaconda 官網&#xff0c;選擇View All Installers 選擇所需版本&#xff08;此文以2024.02-1為例&#xff09;&#xff0c;點擊進行下載&#xff08;…

Datawhale AI夏令營-基于帶貨視頻評論的用戶洞察挑戰賽

一.賽事目標基于星火大模型Spark 4.0 Ultra&#xff0c;對視頻和評論的數據進行商品識別&#xff0c;情感分析&#xff0c;歸類分析&#xff0c;最終為帶貨效果進行評價。并通過優化模型來提高評價準確度二.賽事環境1.基礎平臺&#xff1a;星火大模型Spark 4.0 Ultra2.賽事數據…

如何基于FFMPEG 實現視頻推拉流

文章目錄 前言環境準備為什么選擇 FFmpeg什么是nginx 1.7.11.3 GryphonNginx的conf配置啟動nginx推流命令接收視頻流Untiy播放視頻流最后前言 我們經常會有在電腦上實現推拉流的需求,Unity 和Unreal 都提供了基于WebRTC 的視頻流方案,效果還不錯,但是當我們需要推拉整個電腦…

飛算JavaAI:從情緒價值到代碼革命,智能合并項目與定制化開發新范式

目錄一、飛算 JavaAI 是什么&#xff1f;二、飛算JavaAI&#xff1a;安裝登錄2.1 IDEA插件市場安裝&#xff08;推薦&#xff09;2.2 離線安裝包三、飛算JavaAI核心功能&#xff1a;一鍵生成完整工程代碼功能背景3.1 理解需求3.2 設計接口3.3 表結構自動設計3.4 處理邏輯&#…

Python 基礎語法與數據類型(十一) - 類 (class) 與對象 (實例)

文章目錄1. 什么是類 (Class)&#xff1f;1.1 定義一個類2. 什么是對象 (Object) 或實例 (Instance)&#xff1f;2.1 創建對象&#xff08;實例化&#xff09;3. 訪問屬性和調用方法4. 類屬性 vs 實例屬性5. self 的重要性總結練習題練習題答案前幾篇文章我們學習了變量、數據類…

精準數據檢索+數據飛輪自驅優化,彩訊AI知識庫助力企業知識賦能和效率創新

近兩年&#xff0c;人工智能技術的精細化發展&#xff0c;讓知識庫概念重新成為“熱門詞匯”&#xff0c;騰訊ima等智能工作臺產品為個人用戶打造專屬知識庫&#xff0c;而面向B端市場&#xff0c;企業AI知識庫也逐步成為企業集中存儲與管理核心文檔、數據、經驗和流程的知識中…

打破空間邊界!Nas-Cab用模塊化設計重構個人存儲邏輯

文章目錄前言1. Windows安裝Nas-Cab2. 本地局域網連接Nas-Cab3. 安裝Cpolar內網穿透4. 固定Nas-Cab 公網地址"數據管理不該受制于硬件形態或地理邊界。這個開源方案證明&#xff1a;當功能模塊化且可擴展時&#xff0c;私有云可以像水一樣滲透進所有設備——現在就去Git倉…

Sigma-Aldrich細胞培養基礎知識:細胞培養的安全注意事項

細胞培養實驗室風險評估風險評估的主要目的是防止人員受傷&#xff0c;保護財產&#xff0c;并避免對個人和環境的傷害。在許多國家&#xff0c;法律要求進行風險評估。例如&#xff0c;英國的《英國職業健康與安全法案&#xff08;1974年&#xff09;》就是一個例子。歐洲共同…

Imx6ull用網線與電腦連接

理解工作方式沒有路由器時&#xff0c;可以使用&#xff0c;只要保持虛擬機的兩個網卡一個與電腦在同一網,一個與板子在同一網段(保持通信)就可以從虛擬機往板子下載第一步&#xff1a;查看電腦連接的網絡這一步是在找到主機ip地址這兩步在其他同類教程里一樣的第二步:設置以太…

力扣454.四數相加Ⅱ

給你四個整數數組 nums1、nums2、nums3 和 nums4 &#xff0c;數組長度都是 n &#xff0c;請你計算有多少個元組 (i, j, k, l) 能滿足&#xff1a;0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0示例 1&#xff1a;輸入&#xff1a;nums1 [1,2], nums2 …

Joplin:一款免費開源、功能強大且注重隱私的筆記軟件

Joplin 是一款免費開源、功能強大且注重隱私的筆記和待辦事項應用程序&#xff0c;它的設計目標是成為 Evernote 等流行筆記應用的強大替代品&#xff0c;尤其適合重視數據所有權和隱私的用戶。 功能特性 Joplin 的核心定位與優勢如下&#xff1a; 完全開源&#xff1a;代碼公…

滲透前四天總結

目錄 一.DNS DNS 基本概述 DNS解析過程 二.HTTPS TLS握手過程 RSA加密 對稱加密&#xff1a; 非對稱加密&#xff1a; RSA加密過程 三.使用xdebug調試php 四.信息收集 一.DNS DNS 基本概述 DNS&#xff1a;域名系統(DomainNameSystem)因特網的一項核心服務&#xf…

Python----NLP自然語言處理(中文分詞器--jieba分詞器)

一、介紹文本語料在輸送給NLP模型前&#xff0c;需要一系列的預處理工作&#xff0c;才能符合模型輸入的要求。對于NLP來說&#xff0c;他學習一篇人類書寫的文章不是整體的來學習&#xff0c;而是一個詞一個詞的來學習。所以文本預處理的第一個步驟就是對文本進行分詞處理。&a…

深入了解linux系統—— 進程信號的保存

信號 信號&#xff0c;什么是信號&#xff1f; 在現實生活中&#xff0c;鬧鐘&#xff0c;紅綠燈&#xff0c;電話鈴聲等等&#xff1b;這些都是現實生活中的信號&#xff0c;當鬧鐘想起時&#xff0c;我就要起床&#xff1b;當電話鈴聲想起時&#xff0c;我就知道有人給我打電…

Redis 事務錯誤處理機制與開發應對策略

&#x1f4d8; Redis 事務錯誤處理機制與開發應對策略一、Redis 事務基礎回顧 Redis 中的事務由以下三組命令構成&#xff1a;命令作用說明MULTI開始一個事務&#xff0c;進入命令入隊模式命令集所有后續命令不會立即執行&#xff0c;而是入隊等待提交EXEC提交事務&#xff0c;…

信息學奧賽一本通 1549:最大數 | 洛谷 P1198 [JSOI2008] 最大數

【題目鏈接】 ybt 1549&#xff1a;最大數 洛谷 P1198 [JSOI2008] 最大數 【題目考點】 1. 線段樹&#xff1a;單點修改 區間查詢 知識點講解見&#xff1a;洛谷 P3374 【模板】樹狀數組 1&#xff08;線段樹解法&#xff09; 【解題思路】 本題為設線段樹維護區間最值&a…

【STM32】什么在使能寄存器或外設之前必須先打開時鐘?

這篇文章解釋一個非常基礎但是重要的問題&#xff1a; 為什么在使能寄存器或外設之前必須先打開時鐘&#xff1f; 我們會發現&#xff0c;如果不開時鐘就訪問寄存器 ? 會“寫不進去”或“讀取錯誤”。 因此&#xff0c;我們在寫代碼時&#xff0c;總是需要 先開時鐘&#xff0…

Go·并發處理http請求實現

一、Goroutine介紹 基本原理 goroutine 是 Go 運行時(Runtime)管理的??用戶態線程。與線程相比,其初始棧空間僅約 2KB,創建和切換的開銷更低,能夠同時運行大量并發任務。 創建goroutine的方法非常簡單,在將要調用的函數前加入go關鍵字即可。 func hello() {fmt.Pri…