C++ 多態 | 虛函數、抽象類、虛函數表

文章目錄

  • 多態
  • 虛函數
    • 重寫
    • 重定義(參數不同)
    • 協變(返回值不同)
    • 析構函數重寫(函數名不同)
    • final和override
  • 重載、重寫、重定義
  • 抽象類
  • 多態的原理
    • 虛函數
    • 常見問題解析
    • 虛函數表


多態

一種事物,多種形態。換言之,對于同一個行為,不同的對象去完成就會產生不同的結果。

多態的構成條件

多態是繼承體系中的一個行為,如果要在繼承體系中構成多態,需要滿足兩個條件:

  1. 必須通過基類的指針or引用調用虛函數

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


虛函數

虛函數就是被 virtual 修飾的類成員函數 (這里的 virtual 和虛繼承的 virtual 雖然是同一個關鍵字,但是作用不一樣)。

  • 任何構造函數之外的非靜態函數都可以是虛函數。
  • 關鍵字 virtual 只能出現在類內部的聲明語句之前而不能用于類外部的函數定義
  • 如果基類把一個函數聲明成虛函數,則該函數在派生類中隱式地也是虛函數。

重寫

一般情況下,當派生類中有一個和基類完全相同的虛函數(函數名、返回值、參數完全相同),則說明子類的虛函數重寫了基類的虛函數。

class Human
{
public:virtual void print(){cout << "i am a human" << endl;}
};class Student : public Human
{
public:virtual void print(){cout << "i am a student" << endl;}
};void ShowIdentity(Human &human) // 形參是基類引用,構成多態
{human.print(); // 被調用的函數是虛函數
}int main()
{Human h;Student s;ShowIdentity(h); ShowIdentity(s);
}

在這里插入圖片描述
通常如果不滿足函數名、返回值、參數完全相同,則不構成重寫,即無法實現多態。但也有例外:


重定義(參數不同)

參數不同則會變成重定義

class Base{
public:virtual void Show(int n = 10)const{ // 提供缺省參數值std::cout << "Base:" << n << std::endl;}
};class Base1 : public Base{
public:virtual void Show(int n = 20)const{ // 重新定義繼承而來的缺省參數值std::cout << "Base1:" << n << std::endl;}
};int main(){Base* p1 = new Base1;        p1->Show();           return 0;
}

在這里插入圖片描述

如果子類重寫了缺省值,此時的子類的缺省值是無效的,使用的還是父類的缺省值。

因為虛函數是動態綁定,而缺省值是靜態綁定。

  • 對于 p1,他的靜態類型即指針的類型——Base,所以這里的缺省值是 Base 的缺省值。
  • 而動態類型也就是指向的對象是 Base1,所以這里調用的虛函數則是 Base1 中的虛函數。
  • 調用了 Base1 中的虛函數,Base 中的缺省值,因此輸出 Base1:10

或者可以更簡單的一句話描述,虛函數的重寫只重寫函數實現,不重寫缺省值。

動態綁定和靜態綁定

  • 對象的靜態類型:對象在聲明時采用的類型。是在編譯期確定的。(比如上面的 p1Base 是靜態類型,指向的對象的類型 Base1 是動態類型)
  • 對象的動態類型:目前所指對象的類型。是在運行期決定的。

對象的動態類型可以更改,但是靜態類型無法更改。

  • 靜態綁定:綁定的是對象的靜態類型,發生在編譯期。
  • 動態綁定:綁定的是對象的動態類型,發生在運行期。

協變(返回值不同)

當基類和派生類的返回值類型不同時,如果基類對象返回基類的 引用or指針,派生類對象返回的是派生類的 引用or指針能實現多態。這樣實現多態的方式叫協變

class Human
{
public:virtual Human& print(){cout << "i am a human" << endl;return *this;}
};class Student : public Human
{
public:virtual Student& print(){cout << "i am a student" << endl;return *this;}
};

在這里插入圖片描述
但如果返回類型不是對應類的 指針or引用,則不足以構成協變:
在這里插入圖片描述


析構函數重寫(函數名不同)

在特定條件下,函數名不同也能實現多態,最好的例子是析構函數,編譯器為了讓析構函數實現多態,會將它的名字處理成destructor ,也就是說,實際上析構函數的函數名也是“相同的”,其多態實現遵循重寫的規定。

class Human
{
public:~Human(){cout << "~Human()" << endl;}
};class Student : public Human
{
public:~Student(){cout << "~Student()" << endl;}
};

在這里插入圖片描述
可以看到,如果不構成多態,那么指針是什么類型的,就會調用什么類型的析構函數。那么,如果派生類的析構函數中有資源釋放的操作,而這里卻沒有釋放掉那些資源,就會導致內存泄漏的問題。

所以為了防止這種情況,必須要將析構函數定義為虛函數。這也就是編譯器將析構函數重命名為 destructor 的原因:
在這里插入圖片描述


final和override

finaloverrideC++11 中提供給用戶用來檢測是否進行重寫的兩個關鍵字。

final

使用 final 修飾的基類虛函數不能被重寫。

如果虛函數不想被派生類重寫,就可以用 final 來修飾這個虛函數:
在這里插入圖片描述

override

override 關鍵字是用來檢測派生類虛函數是否構成重寫的關鍵字。C++11 允許派生類顯式地注明它覆蓋了繼承基類的虛函數。

在我們寫代碼的時候難免會出現些小錯誤,如 基類虛函數沒有 virtual 或者 派生類虛函數名拼錯 等問題,這些問題不會被編譯器檢查出來,發生錯誤時也很難一下子鎖定,所以 C++ 增添了 override 這一層保險,當修飾的虛函數不構成重寫時就會編譯錯誤。

具體做法是在:

  1. 形參列表后面
  2. 或者 const 成員函數的 const 關鍵字后面
  3. 或者 引用成員函數的引用限定符后面

加一個關鍵字 override

下例中,基類虛函數沒有 virtual 因此會報錯:
在這里插入圖片描述


重載、重寫、重定義

重載:

  1. 在同一作用域
  2. 函數名相同,參數的類型、順序、數量不同。
  3. 重載不一定要求返回值相同:參數相同、返回值不同不構成重載;參數、返回值都不同則構成重載。(會發現返回值不同是否構成重載還是看參數相同與否……)

重寫(覆蓋):

  1. 作用域不同,一個在基類一個在派生類。
  2. 函數名,參數,返回值必須相同(協變和析構函數除外)
  3. 基類和派生類必須都是虛函數(派生類可以不加 virtual,基類的虛函數屬性可以繼承,但是最好要加上 virtual

考慮這樣一個問題,下面有幾個虛函數:
在這里插入圖片描述

正確答案是 3 個,A 中的 fun1,B 中的 fun1fun2。原因就如第三點所說,基類的虛函數屬性可以繼承 ,但是如果有 C類 繼承了 B類 ,且也有一個 沒有virtual關鍵字的 void fun1(); 函數 ,該函數并不是虛函數,因為 B類fun1 并沒有顯式聲明 virtual 屬性。

而形如 fun2 這樣的,子類是虛函數而父類沒有 virtual 屬性的,父類的 fun2 不是虛函數,虛函數不具備對稱性。

重定義(隱藏):

  1. 作用域不同,一個在基類一個在派生類
  2. 函數名相同
  3. 派生類和基類同名函數如果不構成重寫那就是重定義

重定義無法覆蓋虛函數,只能覆蓋普通函數,但是父類被覆蓋的普通函數可以通過作用域運算符調用:

class A
{
public:virtual void f2(){cout << "A.f2()" << endl;}
};
class B :public A {
public:void f2(int){cout << "B.f2(int)" << endl;}
};
class C:public B{
public:// C類中的兩個f2函數互相構成重載,但又分別構成重定義和重寫void f2() { // 重寫了A類中的虛函數f2()cout << "C.f2()" << endl;}void f2(int) { // 重定義了B類中的普通函數f2(int)cout << "C.f2(int)" << endl;}
};

請添加圖片描述


抽象類

如果在虛函數的后面加上 =0,并且不進行實現,這樣的虛函數就叫做純虛函數。而包含純虛函數的類,也叫做抽象(基)類或者接口類。抽象類不能實例化出對象,因為他所具有的信息不足以描述一個對象,派生類繼承后也只有在重寫純虛函數后才能實例化出對象。

我們也可以對純虛函數提供定義,不過函數體必須在類的外部。

抽象類就像是一個藍圖,為派生類描述好一個大概的架構,派生類必須實現完這些架構,至于要在這些架構上面做些什么,增加什么,就屬于派生類自己的問題。

class Human
{
public:virtual void print() = 0;
};class Student : public Human
{
public:virtual void print(){cout << "i am a student" << endl;}
};class Teacher : public Human
{
public:virtual void print(){cout << "i am a teacher" << endl;}
};void ShowIdentity(Human& human)
{human.print();
}

在這里插入圖片描述

  • 普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。
  • 虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,所以如果不實現多態,不要把函數定義成虛函數。

多態的原理

虛函數

class Human
{
public:virtual void print(){cout << "i am a human" << endl;}virtual void test1(){cout << "1test1" << endl;}void test2(){cout << "1test1" << endl;}int _age;
};class Student : public Human
{
public:virtual void print() {cout << "i am a student" << endl;}void test2(){cout << "2test2" << endl;}int _stuNum;
};

在這里插入圖片描述

我們創建一個 Human 類對象 h,觀察它的大小,按理來說應該輸出 4,因為它只有一個 int類型 的數據成員,但是結果卻是 8

在這里插入圖片描述
可以看到奇怪的是除了 _age 之外,還有個 void**(void*類型的指針,注意不是數組) 類型的 _vfptr ,這個 _vfptr 也被稱為虛函數表指針,其指向了一個函數指針數組,這個函數指針數組也就是虛函數表,其中的每一個成員指針指向的都是虛函數,而不是虛函數的 test2 則沒有被放入表中。

此時再創建一個 Student 類的對象 s,進一步觀察:

在這里插入圖片描述
我們可以看到,如果派生類實現了某個虛函數的重寫,那么在派生類的虛函數表中,重寫的虛函數就會覆蓋掉原有的函數,如Student::print。而沒有完成重寫的 test1 則依舊保留著從基類繼承下來的虛函數 Human::test1

總結

  • 派生類會繼承基類的虛函數表,如果派生類完成了重寫,則會將重寫的虛函數覆蓋掉原有的函數。
  • 指針或引用指向哪一個對象,就調用對象中虛函數表中對應位置的虛函數,來實現多態。

繼續分析構成多態的另一個條件,為什么必須要指針或者引用才能構成多態?
在這里插入圖片描述

  • 如果將派生類對象賦值給基類對象,會因為對象切割,導致他的內存布局整個被修改,完全轉換為基類對象的類型,虛函數表也與基類相同,所以不能實現多態。
  • 如果用基類指針或者引用指向派生類對象,他們的內存布局是兼容的,不會像賦值一樣改變派生類對象的內存結構,所以派生類對象的虛函數表得到了保留,所以他可以通過訪問派生類對象的虛函數表來實現多態。

總結一下派生類虛函數表的生成過程:

  1. 首先派生類會將基類的虛函數表拷貝過來
  2. 如果派生類完成了對虛函數的重寫,則用重寫后的虛函數覆蓋掉虛函數表中繼承下來的基類虛函數
  3. 如果派生類自己又新增了虛函數,則添加在虛函數表的最后面

常見問題解析

內聯函數可以是虛函數嗎?

不可以,內聯函數沒有地址,無法放進虛函數表中。

靜態成員函數可以是虛函數嗎?

不可以,靜態成員函數沒有 this指針,無法訪問虛函數表。

構造函數可以是虛函數嗎?

不可以,虛函數表指針也是對象的成員之一,是在構造函數初始化列表初始化時才生成的

析構函數可以是虛函數嗎?

可以,上面有寫,最好把基類析構函數聲明為虛函數,防止使用基類指針或者引用指向派生類對象時,派生類的析構函數沒有調用,可能導致內存泄漏。

對象訪問虛函數快還是普通函數快?

  • 如果不構成多態,虛函數和普通函數的訪問是一樣快的,都是直接在編譯時符號表中找到函數的地址后調用。
  • 如果構成多態,調用虛函數就得在運行期虛函數表中查找,就會導致速度變慢,所以普通函數更快一些。

虛函數表

從上面的觀察可以看出來,虛函數存于虛函數表中,那么虛函數表又存儲在哪里呢?

虛函數表在編譯階段生成,存儲于代碼段。

詳情可以看這篇博客。

注意:

  • 同一個類的不同實例(對象)共用同一份虛函數表。
  • 子類 特有的虛函數 會加在父類 虛函數表 中的 父類虛函數的后面
  • 如果子類繼承多個父類、且這些父類都有虛函數表,子類特有的虛函數 加在 第一個父類的虛函數表 中。
  • 如果子類繼承多個父類、但只有部分父類有虛函數表,子類特有的虛函數 加在 第一個有虛函數表的父類虛函數表 中。
  • 如果子類繼承多個父類、且這些父類都沒有虛函數表,子類會自己創建一個虛函數表來存儲特有的虛函數。
    在這里插入圖片描述

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

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

相關文章

C++ 運算符重載(一) | 輸入/輸出,相等/不等,復合賦值,下標,自增/自減,成員訪問運算符

文章目錄輸出運算符<<輸入運算符>>相等/不等運算符復合賦值運算符下標運算符自增/自減運算符成員訪問運算符輸出運算符<< 通常情況下&#xff0c;輸出運算符的第一個形參是一個 非常量ostream對象的引用 。之所以 ostream 是非常量是因為向流寫入內容會改變…

C++ 重載函數調用運算符 | 再探lambda,函數對象,可調用對象

文章目錄重載函數調用運算符lambdalambda等價于函數對象lambda等價于類標準庫函數對象可調用對象與function可調用對象function函數重載與function重載函數調用運算符 函數調用運算符必須是成員函數。 一個類可以定義多個不同版本的調用運算符&#xff0c;互相之間應該在參數數…

C++ 運算符重載(二) | 類型轉換運算符,二義性問題

文章目錄類型轉換運算符概念避免過度使用類型轉換函數解決上述問題的方法轉換為 bool顯式的類型轉換運算符類型轉換二義性重載函數與類型轉換結合導致的二義性重載運算符與類型轉換結合導致的二義性類型轉換運算符 概念 類型轉換運算符&#xff08;conversion operator&#…

Tomcat中JVM內存溢出及合理配置

Tomcat本身不能直接在計算機上運行&#xff0c;需要依賴于硬件基礎之上的操作系統和一個Java虛擬機。Tomcat的內存溢出本質就是JVM內存溢出&#xff0c;所以在本文開始時&#xff0c;應該先對Java JVM有關內存方面的知識進行詳細介紹。 一、Java JVM內存介紹 JVM管理兩種類型的…

俄羅斯農民乘法 | 快速乘

文章目錄概念概念 俄羅斯農民乘法經常被用于兩數相乘取模的場景&#xff0c;如果兩數相乘已經超過數據范圍&#xff0c;但取模后不會超過&#xff0c;我們就可以利用這個方法來拆位取模計算貢獻&#xff0c;保證每次運算都在數據范圍內。 A 和 B 兩數相乘的時候我們如何利用加…

Linux網絡編程 | socket選項設定 及 網絡信息API

文章目錄讀取和設置 socket 選項SO_REUSEADDRSO_RCVBUF 和 SO_SNDBUFSO_RCVLOWAT 和 SO_SNDLOWATSO_LINGER 選項網絡信息APIgethostbyname 和 gethostbyaddrgetservbyname 和 getservbyportgetaddrinfogetnameinfo讀取和設置 socket 選項 正如 fcntl 系統調用是控制文件描述符…

Linux | 高級I/O函數

文章目錄創建文件描述符的函數pipe函數dup函數、dup2函數讀取或寫入數據readv函數、writev函數零拷貝sendfile函數splice函數tee函數進程間通信——共享內存mmap函數 和 munmap函數控制文件描述符fcntl函數創建文件描述符的函數 pipe函數 不再贅述&#xff0c;詳情見我的另一…

分布式理論:CAP、BASE | 分布式存儲與一致性哈希

文章目錄分布式理論CAP定理BASE理論分布式存儲與一致性哈希簡單哈希一致性哈希虛擬節點分布式理論 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系統中的所有數據副本&#xff0c;在同一時刻是否一致&#xff08;所有節點訪問同一份最新的數據副…

Tomcat服務器性能優化

一、概述 本文檔主要介紹了Tomcat的性能調優的原理和方法。可作為公司技術人員為客戶Tomcat系統調優的技術指南&#xff0c;也可以提供給客戶的技術人員作為他們性能調優的指導手冊。 二、調優分類 由于Tomcat的運行依賴于JVM&#xff0c;從虛擬機的角度我們把Tomcat的調整分為…

分布式系統概念 | 分布式事務:2PC、3PC、本地消息表

文章目錄分布式事務2PC&#xff08;二階段提交協議&#xff09;執行流程優缺點3PC&#xff08;三階段提交協議&#xff09;執行流程優缺點本地消息表&#xff08;異步確保&#xff09;分布式事務 分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分…

數據結構算法 | 單調棧

文章目錄算法概述題目下一個更大的元素 I思路代碼下一個更大元素 II思路代碼132 模式思路代碼接雨水思路算法概述 當題目出現 「找到最近一個比其大的元素」 的字眼時&#xff0c;自然會想到 「單調棧」 。——三葉姐 單調棧以嚴格遞增or遞減的規則將無序的數列進行選擇性排序…

最長下降子序列

文章目錄題目解法DP暴搜思路代碼實現貪心二分思路代碼實現題目 給出一組數據 nums&#xff0c;求出其最長下降子序列&#xff08;子序列允許不連續&#xff09;的長度。&#xff08;類似于lc的最長遞增子序列&#xff09; 示例&#xff1a; 輸入&#xff1a; 6 // 數組元素個…

Linux 服務器程序規范、服務器日志、用戶、進程間的關系

文章目錄服務器程序規范日志rsyslogd 守護進程syslog函數openlog函數setlogmask函數closelog函數用戶進程間的關系進程組會話系統資源限制改變工作目錄和根目錄服務器程序后臺化服務器程序規范 Linux 服務器程序一般以后臺進程&#xff08;守護進程[daemon]&#xff09;形式運…

IO模型 :阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO

文章目錄IO模型阻塞IO非阻塞IO信號驅動IO多路復用IO異步IOIO模型 根據各自的特性不同&#xff0c;IO模型被分為阻塞IO、非阻塞IO、信號驅動IO、異步IO、多路復用IO五類。 最主要的兩個區別就是阻塞與非阻塞&#xff0c;同步與異步。 阻塞與非阻塞 阻塞與非阻塞最主要的區別就…

Tomcat服務器集群與負載均衡實現

一、前言 在單一的服務器上執行WEB應用程序有一些重大的問題&#xff0c;當網站成功建成并開始接受大量請求時&#xff0c;單一服務器終究無法滿足需要處理的負荷量&#xff0c;所以就有點顯得有點力不從心了。另外一個常見的問題是會產生單點故障&#xff0c;如果該服務器壞掉…

Linux服務器 | 事件處理模式:Reactor模式、Proactor模式

文章目錄Reactor模式Proactor模式同步I/O模型模擬Proactor模式兩者的優缺點ReactorProactor同步I/O模型通常用于實現 Reactor 模式&#xff0c;異步I/O模型通常用于實現 Proactor 模式。&#xff08;不是絕對的&#xff0c;同步I/O也可模擬出 Proactor 模式&#xff09; React…

Linux服務器 | 服務器模型與三個模塊、兩種并發模式:半同步/半異步、領導者/追隨者

文章目錄兩種服務器模型及三個模塊C/S模型P2P模型I/O處理單元、邏輯單元、存儲單元并發同步與異步半同步/半異步模式變體&#xff1a;半同步/半反應堆模式改進&#xff1a;高效的半同步/半異步模式領導者/追隨者模式組件 &#xff1a;句柄集、線程集、事件處理器工作流程兩種服…

香農信息熵之可憐的小豬

文章目錄題目解析香農熵公式樣例具體分析代碼題目 有 n 桶液體&#xff0c;其其中 正好 有一桶含有毒藥&#xff0c;其裝的都是水。它們從外觀看起來都一樣。為了弄清楚哪只水桶含有毒藥&#xff0c;你可以喂一些豬喝&#xff0c;通過觀察豬是否會死進行判斷&#xff0c;實驗對…

字符串匹配之KMP(KnuthMorrisPratt)算法(圖解)

文章目錄最長相等前后綴next數組概念代碼實現圖解GetNext中的回溯改進代碼實現代碼復雜度分析最長相等前后綴 給出一個字符串 ababa 前綴集合&#xff1a;{a, ab, aba, abab} 后綴集合&#xff1a;{a, ba, aba, baba} 相等前后綴 即上面用同樣顏色標識出來的集合元素&#…

linux下tomcat6.0與jdk安裝詳細步驟

安裝Tomcat6.0和JDK1.6 在linux系統上安裝tomcat和jdk應該說是我學習linux知識的第一課了&#xff0c;之前只 是聽說過&#xff0c;從沒接觸過&#xff0c;但我們公司項目都是部署在linux系統上的&#xff0c;那天上司突 然給我發了幾個文檔&#xff0c;讓我看一下&#xff…