C++封裝、繼承(單繼承)、多態詳細分析。

系列文章目錄


文章目錄

  • 系列文章目錄
  • 摘要
  • 一、基本概念
  • 二、多態的分類
  • 三、多態的實現
    • 3.1 類型兼容與函數重寫
    • 3.2 動態聯編與靜態聯編
    • 3.3 虛函數
    • 3.4 動態多態的實現過程
  • 總結
  • 參考文獻


摘要

多態性特征是 C++中最為重要的一個特征,熟練使用多態是學好 C++的關鍵,而理解多態的實現機制及實現過程則是熟練使用多態的關鍵。文章在分析多態性基本屬性的基礎上,結合具體程序實例重點分析了動態多態的實現機制,并結合虛函數編原理分析了動態多態的實現過程。
關鍵詞: C++; 多態性; 虛函數


一、基本概念

封裝、繼承和多態是面向對象設計的 3 大特點。

  • 封裝就是把客觀事物抽象得到的數據和行為封裝成一個整體,在
    C++中,實現數據和行為封裝的程序單元就叫類。封裝就是將代碼模塊化,實現了類內部對象的隱蔽。
  • 繼承是由已經存在的類創建新類的機制,體現在類的層次關系中,子類擁有父類中的數據和方法,子類繼承父類的同時可以修改和擴充自己的功能。
  • 多態是指父類的方法被子類重寫、可以各自產生自己的功能行為。封裝和繼承的目的是代碼的重用,多態就是實現接口重用,即“一個接口,多種方法”。

相比封裝和繼承,多態因其復雜性、靈活性更難以掌握和理解。

二、多態的分類

C++中利用類繼承的層次關系來實現多態,通常是把具有通用功能的聲明存放在類層次高的地方,而把實現這一個功能的不同方法放在層次較低的類中,C++語言通過子類重定義父類函數來實現多態。
多態通常分為兩種: 通用多態特定多態


三、多態的實現

在 C++中利用類繼承的層次關系來實現多態,通常是把具有通用功能的聲明存放在類層次高的地方,而把實現這一個功能的不同方法放在層次較低的類中,C++語言通過子類重定義父類函數來實現多態

3.1 類型兼容與函數重寫

  • C++中的繼承遵循了類型兼容性原則
    即當子類以 Public方式繼承父類時,將繼承父類的所有屬性和方法,因此,可以變相的理解成子類是一種特殊的父類,可以使用子類對象初始化父類,也可以使用父類的指針或引用來調用子類的對象。
  • C++中的函數重寫
    在程序設計過程中,很多時候會出現這樣一種情況,子類繼承父類的 A 函數,但父類的 A 函數不能滿足子類的需求,此時需要在子類中對 A 函數進行重寫。C++中的函數重寫是指: 函數名、參數、返回類型均相同。

如果程序中類型兼容性原則遇到了函數重寫會怎么樣,調用父類的 A 函數還是子類中重寫的 A函數,類型兼容與函數重寫之間的關系可以用以下程序代碼闡釋
代碼示例:

#include<iostream>
using namespace std;class Animal // 父類
{
public:void Speak(){cout << "動物在說話" << endl;}
};
class Dog : public Animal // 子類
{
public:void Speak(){cout << "小狗在汪汪叫" << endl;}
};
int main()
{// 第一種定義Dog dog;dog.Speak();dog.Animal::Speak();// 第二種定義Animal animal1 = dog;animal1.Speak();// 第三種定義Animal* animal2 = &dog;animal2->Speak();return 0;
}

運行截圖:
在這里插入圖片描述
上述程序中定義了 Animal 和 Dog 兩個類,其中,Dog 類以 Public 方式繼承了 Animal 類,并且重寫了
Speak( ) 方法。

  1. 根據程序運行結果不難看出: main( )函數中定義的 Dog 類對象 dog 的調用方法 dog.Speak( )
    是通過子類對象的 Speak( ) 函數來實現小狗在汪汪叫功能。
  2. dog.Animal: : Speak( ) 是子類對象通過使用操作符作用域調用父類的 Speak( ) 函數來實現:
    動物在說話。定義的 Animal 的對象 animal1 通過調用拷貝構造函數,把 dog 的 數 據 拷 貝 到 animal1
    中,animal1 仍為父類對象,所以animal1.Speak( )執行的結果是動物在說話。
  3. 最終定義了一個指向 Animal 類的指針 animal2,將派生類對象 dog 的地址賦給父類指針 animal2,利用該變量調用animal2 –>speak ( ) 方法。得到的結果是: 動物在說話。

原因
a) C++編譯器進行了類型轉換,允許父類和子類之間進行類型轉換,即父類指針可以直接指向子類對象。根據賦值兼容,編譯器認為父類指針指向的是父類對象,因此,編譯結果只可能是調用父類中定義的同名函數。
b) 在此時,C++認為變量animal2中保存的就是 Animal 對象的地址,即編譯器不知道指針 animal2指向的是一個什么對象,編譯器認為最安全的方法就是調用父類對象的函數,因為父類和子類肯定都有相同的 Speak( )函數。因此,在 main() 函數中執行 animal2 –>Speak( ) 時,調用的是 Animal 對象的 Speak( ) 函數。

3.2 動態聯編與靜態聯編

  • 以上程序出現這種情況的原因涉及 C++在具體編譯過程中函數調用的問題,這種確定調用同名函數的哪個函數的過程就叫做聯編( 又稱綁定) 。在C++中聯編就是指函數調用與執行代碼之間關聯的過程,即確定某個標識符對應的存儲地址的過程,在C++程序中,程序的每一個函數在內存中會被分配一段存儲空間,而被分配的存儲空間的起始地址則為函數的入口地址。
  • 按照程序聯編所進行的階段,聯編可分為兩種:靜態聯編和動態聯編。靜態聯編就是在程序的編譯與連接階段就已經確定函數調用和執行該調用的函數之間的關聯。在生成可執行文件中,函數的調用所關聯執行的代碼是確定好的,因此,靜態聯編也稱為早綁定動態聯編是在程序的運行時根據具體情況才能確定函數調用所關聯的執行代碼,因此,動態聯編也稱為晚綁定
  • 當類型兼容原則與函數重寫發生沖突時,程序員希望根據程序設計的子類對象類型來調用子類對象的函數,而不是編譯器認為的調用父類的對象函數。也就是說,如果父類指針(引用) 指向( 引用) 父類的對象時,程序就應該調用父類的函數,如果父類指針( 引用) 指向( 引用)子類的對象時,程序就應該調用子類的函數。這一功能可以通過動態聯編實現。與靜態聯編相比,動態聯編是在程序運行階段,根據成員函數基于對象的類型不同,編譯的結果就不同,這就是動態多態。動態多態的基礎是虛函數。虛函數是用來表現父類和子類成員函數的一種關系。

3.3 虛函數

虛函數的定義方法是用關鍵字 virtual 修飾類的成員函數,虛函數的定義格式:

 virtual〈返回值類型〉〈函數名〉( 〈形式參數表〉) { <函數體>} 

在類的層次結構中,成員函數一旦被聲明為虛函數,那么,該類之后所有派生出來的新類中其都是虛函數。父類的虛函數在派生類中可以不重新定義,若在子類中沒有重新改寫父類的虛函數,則調用父類的虛函數。對兼容性與函數重寫程序,進行適當的修改,將父 類 Animal 中 的 Speak ( ) 函數使用關鍵子Virtual 將其定義為虛函數,代碼如下所示。

#include<iostream>
using namespace std;
class Animal // 父類
{
public:virtual void Speak() //用virtual 關鍵子定義 Speak() 為虛函數{cout << "動物在說話" << endl;}
};
class Dog : public Animal // 子類 Dog以public 方式繼承了 Animal
{
public:void Speak() //重寫了 Speak() 函數{cout << "小狗在汪汪叫" << endl;}
};
int main()
{Dog dog;dog.Speak();dog.Animal::Speak();Animal animal1 = dog;animal1.Speak();Animal* animal2 = &dog;animal2->Speak();return 0;
}

運行截圖:
在這里插入圖片描述

Animal * animal2 = &dog,animal2.Speak( ) 時,由于在父類 Animal 的 Speak( ) 函數前加關鍵字 Virtual,
使得 Speak( ) 函數變成虛函數,編譯器在編譯的時候,發現 animal 類中有虛函數,此時,編譯器會為每個包含虛函數的類創建一個虛函數表,該表是一個一維數組,在這個數組中存放每個虛函數的地址,這樣就實現了動態聯編,也就是晚綁定。也就實現了前面說的當調用父類指針( 引用) 指向( 引用) 子類對象函數時,調用的是子類對象的函數,實現了動態多態。通過分析發現,要想實現動態多態
要滿足以下 3個條件:

  1. 必須存在繼承關系,程序中的 Dog 類以public 的方式繼承了 Animal 類。
  2. 繼承關系中必須要有同名的虛函數。在兩個類中 Speak( ) 函數為同名虛函數,子類重寫父類的虛函數。
  3. 存在父類的指針或引用調用子類該虛函數。

了解多態是如何實現的之前,先要了解虛函數的調用原理,虛函數的調用原理和普通函數不一樣,編譯器在程序編譯的時候,發現類中有關鍵字 virtual 的虛函數時,編譯器會自動為每個包含虛函數的類創建一個虛函數表用來存放類對象中虛函數的地址,并同時創建一個虛函數表指針指向該虛函數表[6]。每個類使用一個虛函數表,每個類對象用一個指向虛表地址的虛表指針。父類對象包含一個指針指向父類所有虛函數的地址,子類對象也包含一個指向獨立地址的指針。
如果子類沒有重新定義虛函數,該虛函數表將保存函數原始版本的地址,如果子類提供了虛函數的新定義,該虛函數表將保存新函數的地址。示例程序中定義了兩個類 A 和 B,類 B 繼承自類 A,父類 A
中定義了兩個虛函數,子類 B 中重寫了其中一個虛函數,代碼如下所示:

class A
{
public:virtual void fun1(){cout << " fun1 是類 A 虛函數";}virtual void fun2(){cout << " fun2 是虛類 A 函數";}
};
class B : public A
{
public:virtual void fun1(){cout << " fun1 是類 B 的虛函數";}
};

分析上述程序,對于父類 A 中的兩個虛函數 fun1( ) 和 fun2( ) ,由于子類 B 重寫了類 A 中的 fun1( ) 函
數,就導致子類 B 的虛函數表的第一個指針指向的是類 B 的 fun1( ) 的函數而不是父類 A 的 fun1( ) 函數,
具體如下表所示:

類 A 的虛函數表類 B 的虛函數表
0: 指向類 A 的 fun1 的指針0: 指向類 B 的 fun1 的指針
1: 指向類 A 的 fun2 的指針1: 指向類 A 的 fun2 的指針

3.4 動態多態的實現過程

編譯器進行編譯程序時發現有 virtual 聲明的函數,就會在這個類中產生一個虛函數表。即使子類中沒有用 virtual 定義虛函數,由于父類中的定義,子類通過繼承后仍為虛函數。程序中 Animal 類和 Dog 類都包含一個虛函數 Speak( ) ,因此,編譯器會為這兩個類都建立一個虛函數表,將虛函數地址存放到該表中。
在這里插入圖片描述

編譯器在為每個類創建虛函數表的同時,還為每個類的對象提供了一個虛函數表指針( vfptr) ,虛函數表指針指向了對象所屬類的虛表。根據程序運行的對象類型去初始化虛函數表指針。虛函數表指針在沒有初始化的情況下,程序是無法調用虛函數的。虛函數表的創建和虛函數表指針的始化是在構造函數中實現的,在構造子類對象時,先調用父類的構造函數,并初始化父類的虛函數指針,指向父類的虛函數表,當子類對象執行構造函數時,子類對象的虛函數表指針也被初始化,指向子類的虛函數表。實現了在調用虛函數時,就能夠找到正確的函數,如下圖所示。
在這里插入圖片描述

總結

多態性作為面向對象程序設計語言的 3 大要素之一,因其靈活性、伸縮性和復雜性而難以掌握。本文著重分析多態的分類、特征及動態多態的實現機制和原理,但本文對于動態多態的分析僅僅局限于單繼承的情況,對于多繼承的情況原理基本相同,本文未作過多說明。

參考文獻

[1]李家宏,孫慶英.C++多態性的實現過程[J].無線互聯科技,2023,19(02):131-134.

網址鏈接

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

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

相關文章

Kotlin關鍵字二——constructor和init

在關鍵字一——var和val中最后提到了構造函數&#xff0c;這里就學習下構造函數相關的關鍵字: constructor和init。 主要構造(primary constructor) kotlin和java一樣&#xff0c;在定義類時就自動生成了無參構造 // 會生成默認的無參構造函數 class Person{ }與java不同的是…

configure腳本的常用參數

下面是一些常用的configure選項參數及其解釋&#xff1a; --prefix<directory>&#xff1a;指定安裝目錄--with-<package>&#xff1a;指定依賴的外部庫或軟件包--enable-<feature>&#xff1a;啟用某個特性--disable-<feature>&#xff1a;禁用某個特…

原創 | 數據的確權、流通、入表與監管研究(一):數據與確權

作者&#xff1a;張建軍&#xff0c;中國電科首席專家&#xff0c;神州網信技術總監 本文約7100字&#xff0c;建議閱讀10分鐘 本文主要介紹數據與數據分類、數據確權規則、數據的所有權與其他權利等方面內容&#xff0c;并進行案例分析。 2022年12月發布的《關于構建數據基礎制…

Linux 和 macOS 的主要區別在哪幾個方面呢?

(??? )&#xff0c;Hello我是祐言QAQ我的博客主頁&#xff1a;C/C語言&#xff0c;數據結構&#xff0c;Linux基礎&#xff0c;ARM開發板&#xff0c;網絡編程等領域UP&#x1f30d;快上&#x1f698;&#xff0c;一起學習&#xff0c;讓我們成為一個強大的攻城獅&#xff0…

uniapp實戰 —— 彈出層 uni-popup (含vue3子組件調父組件的方法)

效果預覽 彈出的內容 src\pages\goods\components\ServicePanel.vue <script setup lang"ts"> // 子組件調父組件的方法 const emit defineEmits<{(event: close): void }>() </script><template><view class"service-panel"…

ALSA Compress-Offload API

概述 從 ALSA API 的早期開始&#xff0c;它就被定義為支持 PCM&#xff0c;或考慮到了 IEC61937 等固定比特率的載荷。參數和返回值以幀計算是常態&#xff0c;這使得擴展已有的 API 以支持壓縮數據流充滿挑戰。 最近這些年&#xff0c;音頻數字信號處理器 (DSP) 常常被集成…

git如何配置多個遠程倉庫,并且進行切換

一、配置多個遠程倉庫并進行切換&#xff0c;請按照以下步驟進行操作&#xff1a; 打開命令行終端&#xff0c;并進入您的 Git 倉庫所在的目錄。添加第一個遠程倉庫&#xff0c;使用以下命令&#xff1a;git remote add origin <第一個遠程倉庫的 URL>這里將遠程倉庫命名…

C# .NET平臺提取PDF表格數據,并轉換為txt、CSV和Excel表格文件

處理PDF文件中的內容是比較麻煩的事情&#xff0c;特別是以表格形式呈現的各種數據。為了充分利用這些寶貴的數據資源&#xff0c;我們可以通過程序提取PDF文件中的表格&#xff0c;并將其保存為更易于處理和分析的格式&#xff0c;如txt、csv、xlsx&#xff0c;從而更方便地對…

leetcode面試經典150題——35 螺旋矩陣

題目&#xff1a; 螺旋矩陣 描述&#xff1a; 給你一個 m 行 n 列的矩陣 matrix &#xff0c;請按照 順時針螺旋順序 &#xff0c;返回矩陣中的所有元素。 示例&#xff1a; 輸入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 輸出&#xff1a;[1,2,3,6,9,8,7,4,5] 提示&…

Redis Geo操作地理位置

Redis Geo 使用場景API列表名詞API列表Springboot使用mavenyamlTest 注意事項 Redis Geo 是Redis在3.2版本中新增的功能&#xff0c;用于存儲和操作地理位置信息 使用場景 滴滴打車&#xff1a;這是一個對地理位置精度要求較高的場景。通過使用Redis的GEO功能&#xff0c;滴滴…

12月8日作業

使用手動連接&#xff0c;將登錄框中的取消按鈕使用qt4版本的連接到自定義的槽函數中&#xff0c;在自定義的槽函數中調用關閉函數&#xff1b;將登錄按鈕使用qt5版本的連接到自定義的槽函數中&#xff0c;在槽函數中判斷u界面上輸入的賬號是否為"admin"&#xff0c;…

kafka學習筆記--安裝部署、簡單操作

本文內容來自尚硅谷B站公開教學視頻&#xff0c;僅做個人總結、學習、復習使用&#xff0c;任何對此文章的引用&#xff0c;應當說明源出處為尚硅谷&#xff0c;不得用于商業用途。 如有侵權、聯系速刪 視頻教程鏈接&#xff1a;【尚硅谷】Kafka3.x教程&#xff08;從入門到調優…

Day54力扣打卡

打卡記錄 出租車的最大盈利&#xff08;動態規劃&#xff09; 鏈接 class Solution:def maxTaxiEarnings(self, n: int, rides: List[List[int]]) -> int:d defaultdict(list)for start, end, w in rides:d[end].append((start, end - start w))f [0] * (n 1)for i in…

文章解讀與仿真程序復現思路——電力自動化設備EI\CSCD\北大核心《考慮源網荷效益的峰谷電價與峰谷時段雙層優化模型》

這個標題涉及到電力定價和能源效益的優化模型。讓我來分解一下&#xff1a; 峰谷電價&#xff1a;這是一種電力定價策略&#xff0c;即在一天內不同時間段設定不同的電價。通常&#xff0c;高峰時段&#xff08;需求高&#xff09;的電價相對較高&#xff0c;而低谷時段&#x…

人工智能學習9(LightGBM)

編譯工具&#xff1a;PyCharm 文章目錄 編譯工具&#xff1a;PyCharm lightGBM原理lightGBM的基礎使用案例1&#xff1a;鳶尾花案例2&#xff1a;絕對求生玩家排名預測一、數據處理部分1.數據獲取及分析2.缺失數據處理3.數據規范化4.規范化輸出部分數據5.異常數據處理5.1刪除開…

利用私域運營的四大策略實現企業營銷目標

私域運營指的是企業利用各種網絡技術和工具&#xff0c;以自己的平臺為基礎&#xff0c;建立、維護、更新和升級與用戶的私人關系。這種運營模式讓企業能更準確地了解客戶需求和喜好&#xff0c;通過定制化服務、優惠政策、個性化體驗等方式&#xff0c;獲取更多的客戶價值。相…

Child Mind Institute - Detect Sleep States(2023年第一次Kaggle拿到了銀牌總結)

感謝 感謝艾兄&#xff08;大佬帶隊&#xff09;、rich師弟&#xff08;師弟通過這次比賽機械轉碼成功、耐心學習&#xff09;、張同學&#xff08;也很有耐心的在學習&#xff09;&#xff0c;感謝開源方案&#xff08;開源就是銀牌&#xff09;&#xff0c;在此基礎上一個月…

基于Lucene的全文檢索系統的實現與應用

文章目錄 一、概念二、引入案例1、數據庫搜索2、數據分類3、非結構化數據查詢方法1&#xff09; 順序掃描法(Serial Scanning)2&#xff09;全文檢索(Full-text Search) 4、如何實現全文檢索 三、Lucene實現全文檢索的流程1、索引和搜索流程圖2、創建索引1&#xff09;獲取原始…

模板與泛型編程

函數模板 顯示實例化 區別定義與聲明 T是模板形參 int是模板實參 inpunt是函數形參 3是函數實參 顯示實例化 模板必須實例化可見 翻譯單元一處定義原則 與內聯函數異同 引入原因&#xff1a;函數模板是為了編譯器兩個階段的處理 內聯函數是為了能在編譯期展開 模板實參的類…

Android Kotlin語言下的文件存儲

目錄 將數據存儲到文件中 創建文件和保存數據 讀取文件 SharedPreferences存儲 存儲數據到SharedPreferences中 Context類中的getSharedPreferences()方法 Activity類中的getPreferences()方法 從SharedPreferences中讀取數據 SQLite數據庫存儲 創建數據庫 調用數據…