C++多態

在這里插入圖片描述

文章目錄

  • 🐵1. 什么是多態
  • 🐶2. 構成多態的條件
    • 🐩2.1 虛函數
    • 🐩2.2 虛函數的重寫
    • 🐩2.3 final 和 override關鍵字
    • 🐩2.4 重載、重寫、重定義對比
  • 🐱3. 虛函數表
  • 🐯4. 多態的原理
  • 🐎5. 多繼承的虛表關系
  • 🦬6. 抽象類

🐵1. 什么是多態

當下網絡有個熱門詞匯叫“雙標”,意思就是用不同的標準來衡量人或事,這是一個貶義詞。而在編程世界中,這種“雙標”,我們稱之為多態,當然了這里的多態并不是貶義詞,而是一種技術實現。

比如說某種商城有會員機制,將用戶分為普通用戶、普通會員、尊貴會員等

那買同種東西的時候,不同的用戶等級會有著不同的價格,這就是一種多態行為

🐶2. 構成多態的條件

實現多態性的主要構成條件是使用虛函數繼承

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

🐩2.1 虛函數

只有類的成員函數才能被定義為虛函數,格式如下:

class A
{//函數前面加上virtual 表面該成員函數為虛函數virtual void func() {}
};

🐩2.2 虛函數的重寫

當派生類中有一個和基類完全相同的虛函數時,我們稱這為虛函數的重寫/覆蓋

重寫有三同,即:返回值類型、函數名、參數列表完全相同

class A
{
public://虛函數virtual void func() const{cout << "A->func()" << endl;}
};
class B :public A
{
public://虛函數重寫virtual void func() const{cout << "B->func()" << endl;}
};
//多態調用傳引用過去
void Print(const A& p)
{p.func();
}
int main()
{Print(A());	//A->func()Print(B());	//B->func()return 0;
}

多態調用中,看的是指向的對象;而普通的函數調用,看的是當前的類型

image-20230814125151703

虛函數的重寫,還需注意幾點:

  1. 虛函數父類必須加上virtual修飾,子類虛函數重寫前面可以不加virtual,但在實際中,還是建議加上

    image-20230814125537847

  2. 對于虛函數的重寫,我們規定三同,但是有例外——協變

    即基類與虛函數返回值類型不同,但是返回值類型必須是構成父子關系指針或者引用(同時是指針 或 同時是引用)

    class A
    {
    public://虛函數virtual A* func() const{cout << "A->func()" << endl;return 0;}
    };
    class B :public A
    {
    public://虛函數重寫 B和A是父子關系virtual B* func() const{cout << "B->func()" << endl;return 0;}
    };
    void Print(const A& p)
    {p.func();
    }
    int main()
    {Print(A());Print(B());return 0;
    }
    
  3. 析構函數的重寫,基類和派生類的析構函數名不同

    class A
    {
    public://虛函數virtual ~A(){cout << "~A()" << endl;}
    };
    class B :public A
    {
    public://虛函數重寫virtual ~B(){cout << "~B()" << endl;}
    };
    int main()
    {A* a1 = new A;A* a2 = new B;delete a1;delete a2;return 0;
    }
    

    輸出:image-20230814130832375

    這里的原因是因為編譯器對析構函數的名字做了處理,編譯后名稱統一處理為destructor,那為什么要將析構函數統一處理稱destructor呢?因為這里要讓他們構成重寫。如果不構成重寫,就好出現類似這樣的情況:

    class A
    {
    public:~A(){cout << "~A()" << endl;}
    };
    class B :public A
    {
    public:~B(){delete ptr;cout << "~B()" << endl;}
    protected:int* ptr;
    };
    int main()
    {A* a1 = new A;delete a1;a1 = new B;delete a1;return 0;
    }
    

    輸出發現,我們這里new了一個B對象,但是每次都是調用A的析構函數,這顯然與我們的意愿不符,我們期望的是這個a1->destructor形成的是多態調用,所以這樣統一處理之后,就可以讓他們構成重寫

image-20230814134515099

🐩2.3 final 和 override關鍵字

如果不想讓這個虛函數被重寫,可加上final關鍵字修飾

image-20230814141838376

當然了,final也可以修飾類,讓這個類不被繼承,一般用于最終的類

如果要檢查某個派生類是否重寫了基類的某個虛函數,可用override關鍵字修飾,如果沒有重寫,則編譯報錯

🐩2.4 重載、重寫、重定義對比

image-20230814143143672

🐱3. 虛函數表

class A
{
public:virtual void func(){cout << "func()" << endl;}
protected:int _a;
};
int main()
{cout << sizeof(A) << endl;
}

這段代碼如果不加上virtual,則輸出的是4;但是加上virtual之后,輸出的是16(64位下,指針是8字節,然后內存對齊)image-20230814144214403

這是因為有了虛函數,這個類里面會多一個虛函數表的指針,這些表里面存的是虛函數的地址

image-20230814144727594

但如果將這個虛函數沒有被重寫,那么派生類的虛函數表還是指向基類的虛函數;如果重寫了,則指向重寫的虛函數。

image-20230814145808846

所以多態調用的時候,不管我們傳的是基類和派生類,在內存里看到的都是父類;普通調用是在編譯的時候就確定了地址,而多態調用時,運行時會到指向對象的虛表找函數的地址

動態綁定與靜態綁定:

  • 靜態綁定:在編譯時確定調用哪個函數或方法。這是在編譯器根據變量的靜態類型(聲明類型)來決定調用哪個函數
  • 動態綁定:在運行時根據對象的實際類型來確定調用哪個函數或方法。這是通過虛函數(在基類中聲明為虛函數,子類進行重寫)實現的。動態綁定適用于通過基類指針或引用調用虛函數的情況,確保調用正確的派生類函數

image-20230814154255881

在這里虛表的地址,是存儲在哪里的呢?我們通過這段代碼來驗證

class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{ 
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A aa;B bb;int a = 0;printf("棧:%p\n", &a);static int b = 0;printf("靜態區:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "hello";printf("常量區:%p\n", str);//前四個字節,一定是虛表的地址printf("虛表a:%p\n", *((int*)&aa));printf("虛表b:%p\n", *((int*)&bb));
}

輸出發現虛表的地址和常量區的地址隔的較近,所以我們可以得出結論:虛表的地址存儲在常量區

image-20230814223647957

另外,我們在Vs的監視窗口只能查看3個虛函數的地址,但這不代表這,內存里面只有三個虛函數的地址,我們可通過這段代碼進行驗證:

class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}virtual void func3(){cout << "A->func3()" << endl;}
};
class B :public A
{virtual void func3(){cout << "B->func3()" << endl;}virtual void func4(){cout << "B->func4()" << endl;}
};
//函數指針命名
typedef 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()
{A a;B b;int vft1 = *((int*)&a);PrintVFT((Func_Ptr*)vft1);int vft2 = *((int*)&b);PrintVFT((Func_Ptr*)vft2);return 0;
}

🐯4. 多態的原理

有了虛表的概念,這我們就能理解,為什么構成多必須是通過基類的指針或引用調用虛函數。因為只有父類的虛表才能既能指向父類,又能指向子類。

那這里還有一個問題就是,為什么必須是指針或引用呢?

class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A a;a._a = 1;B b;b._a = 10;a = b;A* pa = &b;A& ref = b;
}

這段代碼調試發現,子類賦值給父類,父類會進行切片,這里值會拷貝過去,但是虛表并不會拷貝;因為如果拷貝了虛表的話,這樣父類對象中的虛表指向的是父類還是子類就混淆了

🐎5. 多繼承的虛表關系

上面講的內容,包括舉得例子都是單繼承的,所以就不再贅述。這里我們看一下多繼承里面的虛表是怎樣的

class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}
protected:int _a;
};
class B
{
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func2(){cout << "B->func2()" << endl;}
protected:int _b;
};
class C :public A, public B
{
public:virtual void func1(){cout << "C->func1()" << endl;}virtual void funcC(){cout << "C->funcC()" << endl;}
protected:int _c;
};
typedef 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()
{C c;cout<<sizeof(c)<<endl;int vft1 = *((int*)&c);//int vft2 = *((int*)(char*)&c + sizeof(A));B* ptr = &c;int vft2 = *((int*)ptr);PrintVFT((Func_Ptr*)vft1);PrintVFT((Func_Ptr*)vft2);
}

通過驗證,我們可以發現,C類里面有兩張虛表,一張是A的,一張是B的。而C里面的虛函數funcC()的虛表,是存放在第一張虛表里面

image-20230815003928251

但是,我們這里發現,重寫的func1()函數,明明是一樣的,但是地址卻不一樣,我們這段代碼轉到匯編代碼查看

int main()
{C c;A* ptr1 = &c;B* ptr2 = &c;ptr1->func1();ptr2->func1();return 0;
}

我們發現,ptr1是直接調用找個func1(),而ptr2最終調用的地址和ptr1是一樣的,但是在jump的,寄存器減了一個8,這個減8正好是c的地址。ptr1不用修改是因為正好指向了c的起始地址,內存不看類型,只看地址

image-20230815010438247

菱形繼承這里就不講了,很混亂~

🦬6. 抽象類

虛函數后面加上=0,則這個函數為純虛函數,包含了純虛函數的類,叫做抽象類

抽象類不能實例化出對象,之后繼承的派生類也不能實例化對象,只能重寫虛函數,派生類才能實例化出對象。這里規定了派生類必須重新虛函數,所以抽象類也叫接口類

class A
{
public:virtual void func() = 0;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
class C :public A
{
public:virtual void func(){cout << "C->func()" << endl;}
};
void Func(A*a)
{a->func();
}
int main()
{Func(new B);Func(new C);return 0;
}

那么本期的分享就到這里咯,我們下期再見,如果還有下期的話。

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

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

相關文章

神經網絡基礎-神經網絡補充概念-17-計算神經網絡的輸出

計算神經網絡的輸出通常涉及前向傳播&#xff08;Forward Propagation&#xff09;的過程&#xff0c;其中輸入數據通過網絡的層級結構&#xff0c;逐步被傳遞并變換&#xff0c;最終生成預測結果。下面我將為你展示一個簡單的神經網絡前向傳播的示例。 假設我們有一個具有以下…

【變形金剛01】attention和transformer所有信息

圖1.來源&#xff1a;Arseny Togulev在Unsplash上的照片 一、說明 這是一篇 長文 &#xff0c;幾乎討論了人們需要了解的有關注意力機制的所有信息&#xff0c;包括自我注意、查詢、鍵、值、多頭注意力、屏蔽多頭注意力和轉換器&#xff0c;包括有關 BERT 和 GPT 的一些細節。因…

OpenCV圖像處理——輪廓檢測

目錄 圖像的輪廓查找輪廓繪制輪廓 輪廓的特征輪廓面積輪廓周長輪廓近似凸包邊界矩形最小外接圓橢圓擬合直線擬合 圖像的矩特征矩的概念圖像中的矩特征 圖像的輪廓 查找輪廓 binary,contours,hierarchycv.findContours(img,mode,method)繪制輪廓 cv.drawContours(img,coutours…

WSL2安裝Ubuntu,配置機器學習環境

文章目錄 1.WSL2安裝Ubuntu&#xff0c;更改安裝位置&#xff0c;作為開發環境供vscode和pycharm使用&#xff1a;2.更換國內源&#xff1a;3.安裝圖形界面&#xff1a;4.安裝cudacudnntorch5.安裝opencv6.調用攝像頭7.使用yolov8測試 WSL全稱Windows Subsystem for Linux&…

印度貨代專線【我國到印度專線有哪些方式】

隨著全球貿易的不斷發展&#xff0c;我國與印度之間的貿易往來也日益頻繁。作為兩個人口最多的國家之一&#xff0c;中國和印度之間的貨物運輸需求不斷增長。為了滿足這一需求&#xff0c;印度貨代專線應運而生&#xff0c;為進出口商提供高效、可靠的貨物運輸服務。本文將探索…

939. 最小面積矩形;2166. 設計位集;2400. 恰好移動 k 步到達某一位置的方法數目

939. 最小面積矩形 核心思想&#xff1a;枚舉矩形的右邊那條邊的兩個點&#xff0c;并用一個哈希表存儲相同縱坐標的最近出現的列的列數,不斷更新最近出現的左邊那條邊。 2166. 設計位集 核心思想&#xff1a;這題主要是時間復雜度的優化&#xff0c;用一個flag來標記當前翻轉…

CSS自學框架之表單

首先我們看一下表單樣式&#xff0c;下面共有5張截圖 一、CSS代碼 /*表單*/fieldset{border: none;margin-bottom: 2em;}fieldset > *{ margin-bottom: 1em }fieldset:last-child{ margin-bottom: 0 }fieldset legend{ margin: 0 0 1em }/* legend標簽是CSS中用于定義…

IOS開發-XCode14介紹與入門

IOS開發-XCode14介紹與入門 1. XCODE14的小吐槽2. XCODE的功能bar一覽3. XCODE項目配置一覽4. XCODE更改DEBUG/RELEASE模式5. XCODE單元測試 1. XCODE14的小吐槽 iOS開發工具一直有個毛病&#xff0c;就是新版本的開發工具的總會有一些奇奇怪怪的bug。比如在我的Mac-Pro&#…

Springboot 實踐(3)配置DataSource及創建數據庫

前文講述了利用MyEclipse2019開發工具&#xff0c;創建maven工程、加載springboot、swagger-ui功能。本文講述創建數據庫&#xff0c;為項目配置數據源&#xff0c;實現數據的增刪改查服務&#xff0c;并通過swagger-ui界面舉例調試服務控制器 創建數據庫 項目使用MySQL 8.0.…

vue基礎知識四:Vue實例掛載的過程

一、思考 我們都聽過知其然知其所以然這句話 那么不知道大家是否思考過new Vue()這個過程中究竟做了些什么&#xff1f; 過程中是如何完成數據的綁定&#xff0c;又是如何將數據渲染到視圖的等等 一、分析 首先找到vue的構造函數 源碼位置&#xff1a;src\core\instance\…

一生一芯4——使用星火應用商店在ubuntu下載QQ、微信、百度網盤

星火應用商店可以非常方便的完成一些應用的下載&#xff0c;下面是官方網址 http://spark-app.store/download 我使用的是intel處理器&#xff0c;無需下載依賴項&#xff0c;直接點擊軟件本體 我這里下載amd64,根據自己的處理器下載對應版本 sudo apt install ./spark-stor…

做視頻_Style

Video 1> 風格2> 技巧3> 借鑒 &#x1f517; B站視頻 1> 風格 記錄分享生活&#xff0c;工作&#xff0c;學習方面的總結&#xff1b; 4個段位&#xff1a; 實用 -> 簡潔 -> 清晰流暢 -> 生動有趣 2> 技巧 1> 大視頻分段錄制&#xff0c;最后合并…

pytorch入門-神經網絡

神經網絡的基本骨架 import torch from torch import nn #nn模塊是PyTorch中用于構建神經網絡模型的核心模塊。它提供了各種類和函數&#xff0c;可以幫助你定義和訓練神經網絡。class Tudui(nn.Module):def __init__(self):super(Tudui, self).__init__() #調用 super(Tudui,…

數據結構入門指南:二叉樹

目錄 文章目錄 前言 1. 樹的概念及結構 1.1 樹的概念 1.2 樹的基礎概念 1.3 樹的表示 1.4 樹的應用 2. 二叉樹 2.1 二叉樹的概念 2.2 二叉樹的遍歷 前言 在計算機科學中&#xff0c;數據結構是解決問題的關鍵。而二叉樹作為最基本、最常用的數據結構之一&#xff0c;不僅在算法…

java對大文件分片上傳

這里記錄一下&#xff0c;Java對大文件的切分&#xff0c;和后端接口分片上傳的實現邏輯 正常&#xff0c;前后端分離的項目其實是前端去切分文件&#xff0c;后端接口接收到切分后的分片文件去合并&#xff0c;這里都用java來記錄一下。特別說明&#xff1a;我這里用的是zip包…

vue+java實現在線播放mp4視頻

java: 讀取本地視頻文件的流然后給response的輸出流 File file new File("/Users/zhangqingtian/Documents/水庫/Floodforecast/static/" videoName);BufferedInputStream inputStream new BufferedInputStream(new FileInputStream(file));response.setContentT…

ReactDOM模塊react-dom/client沒有默認導出報錯解決辦法

import ReactDOM 模塊“"E:/Dpandata/Shbank/rt-pro/node_modules/.pnpm/registry.npmmirror.comtypesreact-dom18.2.7/node_modules/types/react-dom/client"”沒有默認導出。 解決辦法 只需要在tsconfig.json里面添加配置 "esModuleInterop": true 即…

【C++】queue容器

1.queue容器基本概念 2.queue常用接口 #include <iostream> using namespace std;//隊列queue #include<queue>//創建Person類 class Person { public:Person(string name, int age){this->m_Name name;this->m_Age age;}string m_Name; //姓名int m_Age; …

mysql創建新用戶并授權

目錄 前言登錄到mysql創建用戶用戶授權更改用戶密碼參考 前言 略 登錄到mysql shell> mysql -h127.0.0.1 -P3306 -uroot -p******創建用戶 mysql> CREATE USER abc% IDENTIFIED BY 123456;用戶授權 mysql> GRANT all privileges ON ruoyi.* TO abc%;用戶ruoyi擁有…

優維低代碼實踐:自定義模板

優維低代碼技術專欄&#xff0c;是一個全新的、技術為主的專欄&#xff0c;由優維技術委員會成員執筆&#xff0c;基于優維7年低代碼技術研發及運維成果&#xff0c;主要介紹低代碼相關的技術原理及架構邏輯&#xff0c;目的是給廣大運維人提供一個技術交流與學習的平臺。 優維…