深入理解C++多態-虛函數

引言

C++多態的實現方式可以分為靜態多態和動態多態,其中靜態多態主要有函數重裝和模板兩種方式,動態多態就是虛函數。
下面我們將通過解答以下幾個問題的方式來深入理解虛函數的原理:

  1. 為什么要引入虛函數?(用來解決什么問題)
  2. 虛函數底層實現原理
  3. 使用虛函數時需要注意什么?

正文

為什么要引入虛函數?

在回答這個問題之前,我們先看一個示例:
假設我們正在開發一個圖形編輯器,其中包含各種類型的圖形元素,比如圓形、矩形、多邊形等。我們要如何管理所有圖形對象呢?

  • 甲同學的方案
class Circle {
public:void draw() const {// 實現繪制圓形的代碼}
};class Rectangle {
public:void draw() const {// 實現繪制矩形的代碼}
};// 管理圖形對象:
std::vector<Circle*> circle_shapes;
std::vector<Rectangle*> rectangle_shapes;
circle_shapes.push_back(new Circle());
rectangle_shapes.push_back(new Rectangle());// 刷新繪制圖形
for (auto shape : circle_shapes) {shape->draw();
}
for (auto shape : rectangle_shapes) {shape->draw();
}

甲同學實現的方法比較直白簡單,有多少種類型的圖形就定義多少種類,維護和繪制都需要根據圖形類型數量來修改。
當我要新增一種圖形類型Polygon時,就需要新增以下代碼:

class Polygon {
public:void draw() const {// 實現繪制矩形的代碼}
};// 管理圖形對象:
std::vector<Polygon*> polygon_shapes;
polygon_shapes.push_back(new Polygon());// 刷新繪制圖形
for (auto shape : polygon_shapes) {shape->draw();
}

這種方式的擴展性、可維護性都是最差的。

  • 乙同學的方案
class Shape {
public:virtual void draw() const = 0; // 純虛函數,使得Shape成為抽象基類
};class Circle : public Shape {
public:void draw() const override {// 實現繪制圓形的代碼}
};class Rectangle : public Shape {
public:void draw() const override {// 實現繪制矩形的代碼}
};// 管理圖形對象:
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());// 刷新繪制圖形
// 通過基類指針調用適當的draw方法
for (auto* shape : shapes) {shape->draw(); // 在運行時決定調用哪個類的draw方法
}

乙同學將圖形抽象出一個基類Shape,然后繼承該類來實現CircleRectangle;同時將通用接口設計成虛函數,派生類重寫虛函數,在運行時根據對象來調用哪個類的函數。
這種方式既簡化了代碼,又提高了可擴展性和可維護性。

具體來說,虛函數解決的主要問題是如何在不完全知道對象類型的情況下,調用正確的函數。在沒有虛函數的情況下,函數的調用在編譯時就已經確定了(這稱為靜態綁定)。但是,如果我們想要在運行時根據對象的實際類型來決定調用哪個函數(動態綁定),就需要使用虛函數。

虛函數底層實現原理

我們先介紹一下虛函數實現原理中最重要的兩個東西:虛函數表(也稱虛表,vtable)和虛指針(也稱虛表指針,vptr)。

虛函數表

每個包含虛函數的類或其派生類都會擁有一個虛函數表。這個表是一個編譯時生成的靜態數組,存儲在每個類的定義中。
虛函數表主要包含以下元素:

  • 虛函數指針:表中的每一個條目都是指向類中每個虛函數的指針。這包括從基類繼承來的虛函數,如果在派生類中被重寫,則指向新的函數地址。
  • 類型信息:在支持運行時類型識別(RTTI)的系統中,虛函數表還可能包含指向類型信息的指針,這有助于typeiddynamic_cast等操作。

虛指針

虛指針是每個對象中的一個隱含成員,如果該對象的類包含虛函數。在對象構造時,編譯器設置這個虛指針指向相應類的虛函數表。

每次通過類的實例調用虛函數時,程序會首先通過虛指針訪問虛函數表,然后通過虛函數表定位到具體的函數地址并調用。這個過程是在運行時完成的,因此允許函數調用根據對象的實際類型動態綁定,而非編譯時決定。

想要了解虛函數的實現原理,就需要先了解類的內存布局,通過內存布局來直觀地學習虛函數的原理。

內存布局

普通類的內存布局
class N {
public:void funA() { std::cout << "funA()" << std::endl; }void funB() { std::cout << "funB()" << std::endl; }int a;int b;
};

class N的內存布局如下:

1>class N	size(8):
1>	+---
1> 0	| a
1> 4	| b
1>	+---

想要看一個類的內存布局,只需要通過添加命令行:/d1 reportSingleClassLayoutXXX(其中XXX就是你想要看的類名)即可。

普通的類只會存儲數據成員。

  • 普通的類中為什么沒有維護成員函數呢?

類的成員函數在編譯后存儲在程序的代碼段中,被程序中所有對象共享。
因為一個類的不同實例對象所執行的成員函數是一樣的,沒有必要在實例對象中再復制維護了。所有同類的實例對象使用相同的函數代碼(通過隱含的this指針來訪問對象的成員變量和成員函數),不僅節省內存,也使得程序更加高效。

這里不再詳細介紹函數調用的原理了,這是最基礎的知識… …

基類的內存布局
class Base {
public:virtual void vFunA() = 0;virtual void vFunB() {}void funA() {}void funB() {}int a;int b;
};

class Base的內存布局如下:

1>class Base	size(12):
1>	+---
1> 0	| {vfptr}
1> 4	| a
1> 8	| b
1>	+---
1>Base::$vftable@:
1>	| &Base_meta
1>	|  0
1> 0	| &Base::vFunA
1> 1	| &Base::vFunB

class Base是一個帶虛函數的類,可以看到它的內存布局和普通類有很大的區別。
class Base中的{vfptr}是一個指向虛函數表(vftable)的指針。
Base::$vftable@就是虛函數表,其中&Base_metaclass Base的元數據(該類的類型信息,用于運行時類型識別)。虛函數表內主要是維護該類的虛函數地址。

派生類A的內存布局
class A : public Base {
public:virtual void vFunA() override {}virtual void vFunB() override {}void funA() {}void funB() {}int c;
};

class A的內存布局如下:

1>class A	size(16):
1>	+---
1> 0	| +--- (base class Base)
1> 0	| | {vfptr}
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| c
1>	+---
1>A::$vftable@:
1>	| &A_meta
1>	|  0
1> 0	| &A::vFunA
1> 1	| &A::vFunB

派生類A的內存布局和基類又不一樣了。
因為class A繼承class Base,所以內存布局就包含了基類的數據,然后才是自己的成員c
這里需要注意的是虛函數表中,虛函數地址發生了變化,原來虛函數表中的虛函數地址分別是&Base::vFunA&Base::vFunB,現在虛函數地址被更新成class A的虛函數地址了。

派生類B的內存布局
class B : public Base {
public:virtual void vFunA() override {}void funA() {}void funB() {}int d;
};

class B的內存布局如下:

1>class B	size(16):
1>	+---
1> 0	| +--- (base class Base)
1> 0	| | {vfptr}
1> 4	| | a
1> 8	| | b
1>	| +---
1>12	| d
1>	+---
1>B::$vftable@:
1>	| &B_meta
1>	|  0
1> 0	| &B::vFunA
1> 1	| &Base::vFunB

派生類B和A的主要區別就是沒有重寫虛函數vFunB,所以在虛函數表中可以看到虛函數vFunB的地址沒有被更新,還是指向基類的虛函數地址。

所以,從上面四個類的內存布局可以看出:

  1. 只要寫了虛函數,就會多生成一個虛函數表,并且還有虛指針指向虛函數表。
  2. 派生類繼承基類,并重寫虛函數后,虛函數表對應的虛函數地址將被更新。

使用虛函數時需要注意什么?

使用虛函數時需要遵循以下規則:

  1. 虛函數不能是靜態的

虛函數的目的是為了實現動態多態,和靜態函數在本質上是沖突的。

  1. 要實現運行時多態性,必須使用基類類型的指針或引用來訪問虛函數

如果調用是通過對象實例(而非指針或引用),則會發生靜態綁定,在編譯時,編譯器確定了要調用的函數版本,這種確定不會延遲到運行時。

  1. 虛函數的原型在派生類和基類中必須保持一致

虛函數的原型指的是虛函數的名稱、返回類型、參數列表、const屬性。
這句話的意思就是說派生類重寫的虛函數需要和基類的虛函數名稱、返回類型、參數列表、const屬性都保持一致。

  1. 類可以有虛析構函數,但不能有虛構造函數
  • 首先我們先分析前半句:類可以有虛析構函數

其實在繼承關系中,析構函數必須是虛函數。因為當析構函數不是虛函數,那么通過基類指針釋放派生類對象時,只能調用基類的析構函數,導致派生類中的部分資源無法釋放。

  • 后半句:但不能有虛構造函數

調用虛函數是通過虛指針定位到虛函數表,然后找到對應的虛函數地址。如果構造函數是虛函數,那么調用構造函數是不是需要先通過虛指針來定位虛函數表了,但虛指針的初始化發生在構造函數階段,所以這里有沖突。

未完待續… …

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

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

相關文章

2024年最新信息安全標準匯總

這些標準是我們在數字化時代保障網絡安全、數據安全的重要基石&#xff0c;對于維護國家安全、企業利益和個人信息安全具有至關重要的作用。 隨著科技的快速發展&#xff0c;網絡空間的安全威脅也在不斷演變&#xff0c;從復雜的網絡攻擊到個人信息泄露&#xff0c;這些安全風…

JAVA面試題大全(十四)

1、Kafka 可以脫離 Zookeeper 單獨使用嗎&#xff1f;為什么&#xff1f; kafka不能脫離zookper單獨使用&#xff0c;因為kafka使用zookper管理和協調kafka的節點服務器。 2、Kafka 有幾種數據保留的策略&#xff1f; Kafka提供了多種數據保留策略&#xff0c;這些策略用于定…

哈希雙指針

文章目錄 一、哈希1.1兩數之和1.2字母異位詞分組1.3最長子序列 二、雙指針2.1[移動零](https://leetcode.cn/problems/move-zeroes/description/?envTypestudy-plan-v2&envIdtop-100-liked)2.2[盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/d…

嵌入式0基礎開始學習 ⅠC語言(7)指針

0.問題引入 int a 5; a 1024; //把1024存放到變量a的地址中去 b a; // 取變量a的值&#xff0c;賦值給b >在c語言中&#xff0c;任何一個變量&#xff0c;都有兩層含義 (1)代表變量的存儲單元的地址&#xff1a;變量的地址…

藍橋樓賽第30期-Python-第三天賽題 統計學習數據題解

樓賽 第30期 Python 模塊大比拼 統計學習數據 介紹 JSON&#xff08;JavaScript Object Notation, /?d?e?s?n/&#xff09;是一種輕量級的數據交換格式&#xff0c;最初是作為 JavaScript 的子集被發明的&#xff0c;但目前已獨立于編程語言之外&#xff0c;成為了通用的…

分享10個國內可以使用的GPT中文網站

在今天的人工智能領域&#xff0c;基于對話的語言模型已成為研究的熱點&#xff0c;尤其是像 ChatGPT 這樣因其出色的語言理解與對話交互能力而廣受關注的模型。本文將介紹10個國內可以直接使用GPT的網站&#xff0c;旨在為大家在選擇和使用這些優秀的AI工具時提供有價值的參考…

使用pyqt繪制一個愛心!

使用pyqt繪制一個愛心&#xff01; 介紹效果代碼 介紹 使用pyqt繪制一個愛心&#xff01; 效果 代碼 import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget from PyQt5.QtGui import QPainter, QPen, QBrush, QColor from PyQt5.QtCore import Qt, Q…

[保姆式教程]使用目標檢測模型YOLO V8 OBB進行旋轉目標的檢測:訓練自己的數據集(基于衛星和無人機的農業大棚數據集)

最近需要做基于衛星和無人機的農業大棚的旋轉目標檢測&#xff0c;基于YOLO V8 OBB的原因是因為嘗試的第二個模型就是YOLO V8&#xff0c;后面會基于YOLO V9模型做農業大棚的旋轉目標檢測。YOLO V9目前還不能進行旋轉目標的檢測&#xff0c;需要修改代碼 PS:歡迎大家分享農業大…

【研發日記】Matlab/Simulink技能解鎖(九)——基于嵌入式處理器仿真

文章目錄 前言 基于嵌入式處理器仿真 使用方式 第一步&#xff0c;硬件連接 第二步&#xff0c;配置硬件資源 第三步&#xff0c;配置XCP協議 第四步&#xff0c;加載Contrl Model 第五步&#xff0c;運行仿真 第六步&#xff0c;仿真報告 分析和應用 總結 參考資料 前言…

無線技術整合到主動噪聲控制(ANC)增強噪聲降低性能

主動噪聲控制&#xff08;ANC&#xff09;已成為一種廣泛使用的降噪技術。基本原理是通過產生與外界噪音相等的反向聲波&#xff0c;將噪音中和&#xff0c;從而達到降噪的效果。ANC系統通常包括以下幾個部分&#xff1a;參考麥克風、處理芯片、揚聲器和誤差麥克風。參考麥克風…

家政保潔服務小程序怎么做?家政公司快速搭建專屬小程序

在數字化時代背景下&#xff0c;家政保潔服務行業也迎來了線上轉型的新機遇。家政保潔服務小程序&#xff0c;作為一種新型的線上服務平臺&#xff0c;不僅能夠提升家政公司的服務效率&#xff0c;還能為顧客提供更加便捷的預約上門服務體驗。那么家政保潔服務小程序怎么做呢&a…

AI與量子計算:科技新時代的雙重飛躍

在科技的浪潮中,每一次技術革新都如同一次深海潛行,探尋著未知的奧秘。近年來,人工智能(AI)和量子計算兩大領域的發展尤為引人注目,它們不僅代表了科技的未來趨勢,更是人類社會進步的強大動力。本文將深入探討這兩項技術的最新進展、潛在影響以及它們之間的潛在聯系。 …

2024年3月小程序類目調整匯總公告

各位小程序開發者&#xff1a; 為進一步加強平臺的規范管理&#xff0c;優化開發者類目選擇體驗&#xff0c;現對以下類目進行調整&#xff0c;請各位開發者知悉。 類目調整 #【文娛-小說】 現資質要求 &#xff08;3選1&#xff09;&#xff1a; 1、提供《互聯網出版許可…

從零開始搭建一個SpringBoot項目

目錄 Spring BootSpring Boot 項目開發環境1、快速創建SpringBoot項目2、pom.xml 添加 Meavn 依賴3、配置application.yml4、驗證數據庫是否連接成功5、配置 Druid 數據源 Spring Boot 整合 MyBatis1、準備依賴2、application-dev.yml 配置3、啟動類添加Mapper接口掃描器4、設置…

BWVS 靶場測試

一、PHP弱類型 is_numeric() 輸入&#xff1a;127.0.0.1/BWVS/bug/php/code.php # 1、源代碼分析 如果num不是數字&#xff0c;那么就輸出num&#xff0c;同時如果num1&#xff0c;就輸出flag。即num要是字符串又要是數字 # 2、函數分析&#xff1a; is_numeric()函數&…

使用Nginx的Mirror模塊的指南

Nginx 是一個廣泛使用的 web 服務器和反向代理服務器&#xff0c;性能出色且易于配置。Nginx 提供了各種模塊來擴展其功能&#xff0c;其中一個有用的模塊是 mirror 模塊。本文將詳細介紹 Nginx 的 mirror 模塊&#xff0c;包括其用途、使用場景、注意事項以及示例代碼。 1. m…

《最新出爐》系列入門篇-Python+Playwright自動化測試-40-錄制生成腳本

宏哥微信粉絲群&#xff1a;https://bbs.csdn.net/topics/618423372 有興趣的可以掃碼加入 1.簡介 各種自動化框架都會有腳本錄制功能&#xff0c; playwright這么牛叉當然也不例外。很早之前的selenium、Jmeter工具&#xff0c;發展到每種瀏覽器都有對應的錄制插件。今天我們…

牛客NC392 參加會議的最大數目【中等 貪心+小頂堆 Java/Go/PHP 力扣1353】

題目 題目鏈接&#xff1a; https://www.nowcoder.com/practice/4d3151698e33454f98bce1284e553651 https://leetcode.cn/problems/maximum-number-of-events-that-can-be-attended/description/ 思路 貪心優先級隊列Java代碼 import java.util.*;public class Solution {/**…

java面試高級篇(JVM、Mysql、Redis、Kafka)

文章目錄 面試專題-java高級篇1. JVM有做過jvm的調優嗎?常用的jvm參數調優有哪些?如果jvm持續一段時間頻繁的發生Young GC (輕GC) 可能原因有哪些? 2. Mysql2.1. 基本功(見為知筆記)2.2. 什么是索引2.3. 索引的優劣勢2.4. MySQL的索引結構2.4.1. B-Tree索引2.4.2. BTree索引…

外賣系統源碼開發全攻略:外賣小程序與后臺管理系統的設計與實現

今天&#xff0c;小編將詳細介紹外賣系統源碼的開發全攻略&#xff0c;從需求分析到設計與實現&#xff0c;為開發者提供全面指導。 一、需求分析 1.用戶需求 用戶是外賣系統的核心&#xff0c;需滿足以下基本需求&#xff1a; -瀏覽菜單并下單 -實時追蹤訂單 -多種支付方…