C++虛函數食用筆記

虛函數定義與作用:

virtual關鍵字聲明虛函數,虛函數可被派生類override(保證返回類型與參數列表,名字均相同),從而通過基類指針調用時,實現多態的功能

virtual關鍵字:

將函數聲明為虛函數

override關鍵字:

告訴編譯器該函數為重寫的虛函數,若重寫失敗,報錯,防止出現疏忽導致虛函數未重寫的情況

final關鍵字:

聲明給虛函數時,表明該虛函數不可再被派生類重寫

聲明給類時,表明該類不可再被繼承

class Base{
public:virtual void print() const{cout << "Base" << endl;}
};class Derived final : public Base{//注意此處final的位置
public:void print() const override final{ //此處const override final的順序不可調換cout << "Derived" << endl;}
};

指針的動態類型與靜態類型:

指針/引用的定義類型為其靜態類型

指針/引用指向的對象類型為其動態類型

當調用非虛函數時,函數的匹配取決于指針/引用的靜態類型

當調用虛函數時,函數的匹配取決于指針/引用的動態類型

例子:

class Base{
public:virtual void print(){cout << "Base" << endl;}
};class Derived:public Base{
public:void print()override{cout << "Derived" << endl;}
};int main(){Base *p_base{new Derived};p_base->print(); //調用Derived::print()Base obj_base = *p_base;obj_base.print();//調用Base::print()return 0;
}

輸出:

Derived
Base

特殊的虛函數重寫:協變返回類型

當虛函數的返回類型為一系列的基類/派生類的指針/引用時,重寫的虛函數返回類型可以不一樣

例子:

class A{
public:void print(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}
};class Base{
public:virtual A& get(){cout << "return A" << endl;return *(new A{});}
};class Derived:public Base{
public:B& get() override{cout << "return B" << endl;return *(new B{});}
};int main(){Base *p_base{new Derived};p_base->get();Base obj_base = *p_base;obj_base.get();return 0;
}

輸出:

return B
return A

綜合的虛函數調用例子:

#include <iostream>
using namespace std;class A{
public:void print(){cout << "A" << endl;}virtual void vprint(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}void vprint()override{cout << "B" << endl;}
};class C{
private:A m_a{};
public:virtual A& get(){return m_a;}
};class D:public C{
private:B m_b{};
public:B& get() override{return m_b;}
};int main(){C *p_C{new D{}};p_C->get().print(); //靜態調用print(),所以調用的是p_C的靜態類型對應的get(),返回A&p_C->get().vprint(); //動態調用vprint(), 所以調用的是p_C的動態類型對應的get(),返回B&return 0;
}

輸出:

A
B

虛析構函數:

在類的析構函數前加上virtual關鍵字,可將其變為虛析構函數,此后的派生類寫自己的析構函數時,相當于重寫基類的虛函數,派生類的析構函數默認成為虛函數

如果一個類會被繼承的話,那么應當將其析構函數寫成虛析構函數,以避免內存泄漏

不使用虛析構函數的繼承:

class Base{
public:~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived(){cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}

輸出:

~Base()

可以看到delete只調用了Base的析構函數,從而導致Derived部分分配的內存未被清空,發生內存泄漏

使用虛析構函數的繼承:

class Base{
public:virtual ~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived() override{cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}

輸出:

~Derived()
~Base()

指針正確調用了Derived的虛析構函數,而該析構函數又調用了~Base(),從而正確的清空了分配的內存

純虛函數:

在虛函數聲明后面加上=0,使其成為純虛函數

class A{
public:virtual void func() = 0;void func2(){}
};

純虛函數也可以有定義,如寫在類外面的定義:

void A::func(){cout << "I'm a pure virtual function" << endl;
}

抽象類:

只要包含純虛函數的類就稱為抽象類(如上述的A類),抽象類不可被實例化

繼承自抽象類的派生類需要重寫其所有純虛函數,否則該派生類也是抽象類

接口類:

不包含任何屬性和成員函數,只包含純虛函數的類稱為接口類

利用虛函數修改派生類的operator<<

為了使用ostream,我們通常將operator<<寫成友元函數,但友元函數不能是虛函數,因此無法被派生類重寫

但我們又不想每定義一個派生類就新添加一個友元operator<<

因此,我們可以定義一個輔助print()虛函數,然后用operator<<來調用這個虛函數,從而達到多態的目的

class Base{
public:virtual ostream& print(ostream& out){out << "Base" << endl;}friend ostream& operator<<(ostream& out,Base& obj){return obj.print(out);}
};class Derived:public Base{
public:ostream& print(ostream& out) override{out << "Derived" << endl;}
};int main(){Derived d{};cout << d << endl;return 0;
}

輸出:

Derived

在cout<<d的時候,由于沒有與Derived匹配的<<運算符,因此編譯器將d隱式轉換為Base,然后傳入operator<<(ostream& out,Base& obj)里,從而通過Base&調用Derived對應的虛函數print(),實現多態的目的

虛函數的實現原理:

結構

當一個類內包含虛函數,那么編譯器就會為這個類分配一個數組,數組里存了若干個指針(虛函數指針,vfptr),每個指針指向對應的虛函數的地址,我們把這個數組稱作虛函數表(__vtable)。
當該類實例化為對象時,編譯器會在該對象頭部插入一個指針,該指針指向虛函數表,我們把這個指針稱作虛函數表指針(__vptr),虛函數表指針的初始化在構造函數之前。

結構如圖所示:

運行:

當我們通過類指針調用虛函數的時候,編譯器并不會在編譯期就根據函數簽名來確定調用的函數的地址(靜態綁定),而是在運行期讓類指針通過__vptr找到vtable,并通過調用的函數簽名,確定在vtable的偏移量,從而找到對應的vfptr,通過vfptr找到需要調用的函數的地址,進而調用該函數,此謂動態綁定

繼承規則:

當我們發生繼承、虛函數重寫、添加虛函數時,

那么vtable以及__vptr的分配規則如下:

每個含有虛函數的基類的子對象的首地址都會有對應的__vptr

重寫虛函數:修改對應__vptr所指向的vtable的對應位置(下標)內的vfptr,

新添虛函數: 在首個繼承的基類的vtable后添加vfptr

例子:

當我們有如下代碼的繼承關系時:

class Base1{
public:virtual void func1(){}virtual void print1(){}void test1(){}
};class Base2{
public:virtual void func2(){}virtual void print2(){}void test2(){}
};class Derived:public Base1,public Base2{
public:void func1() override{}void print2() override{}virtual void f_derived(){}
};

Base1,Base2各有兩個虛函數和一個非虛函數,Derived各重寫了基類的一個虛函數,以及新添加了一個自己的虛函數

結構如圖所示:

可以看到

在Derived obj內,

虛函數表內vfptr_func1_b和vfptr_print2_b對應的位置被替換成了vfptr_func1_d和vfptr_print2_d

新添加的f_derived對應的vfptr_f_d被添加到了__vptr1指向的vtable的末尾

值得注意的是

Derived的vtable和Base1,Base2的vtable是獨立開來的,因此這里總共有三個虛函數表

如果Derived沒有重寫任何虛函數,其仍會生成一個獨立的vtable

多態實現原理:

當我們用基類指針指向不同的派生類,調用其相同的虛函數時,若其虛函數被重寫過,則會產生不同的效果,此謂多態

那么多態是如何實現的呢:

用上述的Base1,Base2,Derived舉例

調用重寫的虛函數:

比如我們現在有

Base2* p{new Derived{}};
p->print2();

由于我們使用了Base2指針指向Derived對象,那么該指針會指向Derived中的Base2子對象的首地址,也就是__vptr2所在處,當我們調用print2()時,指針會通過__vptr2找到vtable,然后由print2()的函數簽名,編譯器會讓指針偏移一個指針偏移量,從而找到vfptr_print2_d,進而調用Derived::print2(),這也就是為什么要修改對應位置的vfptr的原因,因為vtable就是根據位置來找對應的虛函數的

調用新添的虛函數:

當我們使用Base1指針指向Derived對象時,也是同樣的道理:

Base1* p2{new Derived{}};
p2->f_derived();

當我們調用f_derived()時,編譯器是用__vptr1去找該函數的,這也就是為什么vfptr_f_d會加到__vptr1的末尾的原因,也就是說Derived與其Base1的子對象是共用一個vtable的,也因此,p2無法調用在Base2定義的虛函數

指向不同對象時的調用:

當我們用Base1指針指向Base1對象時,其__vptr指向的是Base1的vtable(圖右上角),因此調用時只會調用Base1定義的虛函數

總結:

多態的根本原理是不同類的__vtpr指向的vtable不同,從而在運行時索引的時候找到不同的函數并運行

this指針調整:

編譯器在運行時動態調整this指針的功能,具體實現原理這里不再贅述

例子1:

在上述例子中,我們再加入一個派生類:

class Derived2:public Derived{
public:
};int main(){Derived* p{new Derived2{}};p->print2();return 0;
}

上述代碼肯定是可運行的,但按上述運行原理,Derived*所用的__vptr是不可能找到print2()的,也就是說其使用了__vptr2,那么編譯器是怎么實現的呢?

在這里,編譯器用到了一種叫做this指針調整的功能,在調用虛函數表指針之前,編譯器先讓p向下偏移到了Base2子對象首地址的位置,進而使用__vptr2正確調用print2()

例子2:

若Derived有虛析構函數,那么該虛析構函數只會放在__vptr1所指向的vtable內,此時,若用Base2指針指向Derived對象,在調用虛析構函數時,編譯器會先將this指針偏移到Base1子對象的首地址處,進而使用__vptr1正確地調用虛析構函數

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

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

相關文章

運算放大器相關的電路

1運算放大器介紹 解釋&#xff1a;運算放大器本質就是一個放大倍數很大的元件&#xff0c;就如上圖公式所示 Vp和Vn相差很小但是放大后輸出還是會很大。 運算放大器不止上面的三個引腳&#xff0c;他需要獨立供電&#xff1b; 如圖比較器&#xff1a; 解釋&#xff1a;Vp&…

華為OD機試真題——通信系統策略調度(用戶調度問題)(2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳實現

2025 B卷 100分 題型 本專欄內全部題目均提供Java、python、JavaScript、C、C++、GO六種語言的最佳實現方式; 并且每種語言均涵蓋詳細的問題分析、解題思路、代碼實現、代碼詳解、3個測試用例以及綜合分析; 本文收錄于專欄:《2025華為OD真題目錄+全流程解析+備考攻略+經驗分…

Ubuntu 系統默認已安裝 python,此處只需添加一個超鏈接即可

步驟 1&#xff1a;確認 Python 3 的安裝路徑 查看當前 Python 3 的路徑&#xff1a; which python3 輸出類似&#xff1a; /usr/bin/python3 步驟 2&#xff1a;創建符號鏈接 使用 ln -s 創建符號鏈接&#xff0c;將 python 指向 python3&#xff1a; sudo ln -s /usr/b…

深度學習-分布式訓練機制

1、分布式訓練時&#xff0c;包括train.py的全部的代碼都會在每個gpu上運行嗎&#xff1f; 在分布式訓練&#xff08;如使用 PyTorch 的 DistributedDataParallel&#xff0c;DDP&#xff09;時&#xff0c;每個 GPU 上運行的進程會執行 train.py 的全部代碼&#xff0c;但通過…

yarn的介紹

### Yarn 的基本概念 Yarn 是 Hadoop 生態系統中的一個重要組成部分&#xff0c;它是一種分布式資源管理框架&#xff0c;旨在為大規模數據處理提供高效的資源管理和調度能力。以下是關于 Yarn 的一些核心概念&#xff1a; #### 1. **Yarn 的定義** Yarn 是一個資源調度平臺&a…

Spring-messaging-MessageHandler接口實現類ServiceActivatingHandler

ServiceActivatingHandler實現了MessageHandler接口&#xff0c;所以它是一個MessageHandler&#xff0c;在spring-integration中&#xff0c;它也叫做服務激活器&#xff08;Service Activitor&#xff09;&#xff0c;因為這個類是依賴spring容器BeanFactory的&#xff0c;所…

快速入門深度學習系列(2)----損失函數、邏輯回歸、向量化

針對深度學習入門新手目標不明確 知識體系雜亂的問題 擬開啟快速入門深度學習系列文章的創作 旨在幫助大家快速的入門深度學習 寫在前面&#xff1a; 本系列按照吳恩達系列課程順序發布(說明一下為什么不直接看原筆記 因為內容太多 沒有大量時間去閱讀 所有作者需要一次梳理…

KingBase問題篇

安裝環境 操作系統&#xff1a;CentOS7 CPU&#xff1a;X86_64架構 數據庫&#xff1a;KingbaseES_V008R006C009B0014_Lin64_install.iso 項目中遇到的問題 Q1. 執行sql中有字符串常量&#xff0c;且用雙引號包裹&#xff0c;執行報錯 A1. 默認KingBase不認雙引號&#xff0…

瀕危仙草的重生敘事:九仙尊米斛花節如何以雅集重構中醫藥文化IP

五月的霍山深處,層巒疊翠之間,中華仙草霍山米斛迎來一年一度的花期。九仙尊以“斛韻雅集,春野茶會”為主題,舉辦為期半月的米斛花文化節,融合中醫藥文化、東方美學與自然體驗,打造一場跨越古今的沉浸式文化盛宴。活動涵蓋古琴雅集、書法創作、茶道冥想、詩歌吟誦、民族歌舞等多…

LeetCode100.1 兩數之和

今天晚上看了許多關于未來計算機就業的視頻&#xff0c;有種正被販賣焦慮的感覺&#xff0c;翻來覆去下決定先做一遍leetcode100給自己降降溫&#xff0c;打算每周做四題&#xff0c;盡量嘗試不同的方法與不同的語言。 一開始想到的是暴力解法&#xff0c;兩層循環。數據量為1e…

python制造一個報錯

以下是用Python制造常見錯誤的示例及解析&#xff0c;涵蓋不同錯誤類型&#xff0c;便于理解調試原理&#xff1a; 一、語法錯誤 (SyntaxError) # 錯誤1&#xff1a;缺少冒號 if Trueprint("這行不會執行")# 錯誤2&#xff1a;縮進錯誤 def func(): print("未對…

idea整合maven環境配置

idea整合maven 提示&#xff1a;幫幫志會陸續更新非常多的IT技術知識&#xff0c;希望分享的內容對您有用。本章分享的是springboot的使用。前后每一小節的內容是存在的有&#xff1a;學習and理解的關聯性。【幫幫志系列文章】&#xff1a;每個知識點&#xff0c;都是寫出代碼…

Node.js中那些常用的進程通信方式

文章目錄 1 什么是子進程?2 核心方法詳解2.1 `child_process.spawn(command, [args], [options])`2.2 `child_process.exec(command, [options], callback)`2.3 `child_process.execFile(file, [args], [options], callback)`2.4 `child_process.fork(modulePath, [args], [op…

Vue3吸頂導航的實現

吸頂導航實現 【實現目標】&#xff1a; 在Layout頁面中&#xff0c;瀏覽器上下滾動時&#xff0c;距離頂部距離大于80px吸頂導航顯示&#xff0c;小于則隱藏。 【實現過程】&#xff1a; 通過layout接口獲取分類列表內容并使用categorystore進行狀態管理&#xff0c;獲取到…

雙向長短期記憶網絡-BiLSTM

5月14日復盤 二、BiLSTM 1. 概述 雙向長短期記憶網絡&#xff08;Bi-directional Long Short-Term Memory&#xff0c;BiLSTM&#xff09;是一種擴展自長短期記憶網絡&#xff08;LSTM&#xff09;的結構&#xff0c;旨在解決傳統 LSTM 模型只能考慮到過去信息的問題。BiLST…

2025年Flutter項目管理技能要求

在2025年&#xff0c;隨著Flutter技術的廣泛應用和項目復雜度的提升&#xff0c;項目管理的重要性愈發凸顯。Flutter項目管理不僅需要技術能力&#xff0c;還需要良好的溝通、協調、規劃和執行能力。本文將詳細探討2025年Flutter項目管理應具備的技能要求&#xff0c;幫助項目管…

OpenCV CUDA模塊中逐元素操作------數學函數

操作系統&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 編程語言&#xff1a;C11 算法描述 在OpenCV的CUDA模塊中&#xff0c;確實存在一系列用于執行逐元素數學運算的函數&#xff0c;包括指數、對數、平方根等。這些函數對于高級圖像處…

PhpStudy | PhpStudy 工具安裝 —— Kali Linux 系統安裝 PhpStudy

&#x1f31f;想了解這個工具的其它相關筆記&#xff1f;看看這個&#xff1a;[網安工具] 服務器環境配置工具 —— PhpStudy 使用手冊 筆者備注&#xff1a;演示雖然是 Kali Linux&#xff0c;但其實 Linux 系列都可以參考此流程完成安裝。 在前面的章節中&#xff0c;筆者簡…

第6講、全面拆解Encoder、Decoder內部模塊

全面拆解 Transformer 架構&#xff1a;Encoder、Decoder 內部模塊解析&#xff08;附流程圖小測驗&#xff09; 關鍵詞&#xff1a;Transformer、Encoder、Decoder、Self-Attention、Masked Attention、位置編碼、殘差連接、多頭注意力機制 Transformer 自 2017 年誕生以來&am…

游戲引擎學習第283天:“讓‘Standing-on’成為一個更嚴謹的概念

如果同時使用多個OpenGL上下文&#xff0c;并且它們都有工作負載&#xff0c;GPU或GPU驅動程序如何決定調度這些工作&#xff1f;我注意到Windows似乎優先處理活動窗口的OpenGL上下文&#xff08;即活動窗口表現更好&#xff09;&#xff0c;挺有意思的…… 當多個OpenGL上下文…