C++多態性和對象替換

来源:https://www.cnblogs.com/qlzstudy/archive/2022/07/25/16516736.html
-Advertisement-
Play Games

派生類繼承了基類除構造函數和析構函數外的所有數據成員和函數成員。派生類和基類存在一種特殊關係:派生類是一種基類,具有基類的所有功能。面向對象的程式設計利用派生類和基類之間的特殊關係,常常將派生類對象當作基類對象使用,或者用基類來代表派生類,其目的是提高代碼可重用性。由於C++對數據類型一致性要求比較 ...


  派生類繼承了基類除構造函數和析構函數外的所有數據成員和函數成員。派生類和基類存在一種特殊關係:派生類是一種基類,具有基類的所有功能。面向對象的程式設計利用派生類和基類之間的特殊關係,常常將派生類對象當作基類對象使用,或者用基類來代表派生類,其目的是提高代碼可重用性。由於C++對數據類型一致性要求比較嚴格,一般不能調用處理A類對象的函數afun(A x)來處理B類對象數據。

一、認識對象的替換和多態

通過一個例子更直觀的理解對象的替換和多態:

class A
{
    public:
        void fun1() //普通函數成員fun1
        { cout<< "A::fun1 called\n"; }
        virtual void fun2() //虛函數成員fun2
        { cout<< "Virtual A::fun2 called\n"; }
};
class B:public A //定義派生類B,公有繼承A
{
     public: 
        void fun1() //新增基類同名函數成員fun1
        { cout<< "B::fun1 called\n"; }
        virtual void fun2() //新增基類同名虛函數成員fun2
        { cout<< "Virtual B::fun2 called\n"; }
};
void exfun(A &x) //類外函數處理類對象
{
    cout<<"exfun call class A\n";
    x.fun1();
}
void exfun1(A &x) //類外函數調用類虛函數,實現對象多態性
{
    cout<<"exfun call class A\n";
    x.fun2();
}

  下麵通過兩個類分別定義對象和引用來觀察它們所調用的函數是那些 

A aobj;B bobj; //分別定義兩個類的對象
A &ra1=aobj;
A &ra2=bobj; //通過基類對象引用兩個對象
ra1.fun1(); //顯示 A::fun1 called
ra2.fun1(); //顯示 A::fun1 called
ra1.fun2(); //顯示 Virtual A::fun2 called
ra2.fun2(); //顯示 Virtual B::fun2 called

  得到以下結論:

    1)通過基類ra2來引用派生類對象bobj,相當於將派生類對象當作基類對象來使用,這被稱作對象替換

    2)通過基類引用調用虛函數成員fun2,基類對象aobj和派生類對象bobj會顯示不同信息。換言之,接收相同指令fun2,基類對象和派生類對象表現出不同行為,呈現多樣化形態,這就是對象的多態性

 二、類型相容語法規則(對象替換)

  為了讓派生類對象可以與基類對象一起共用演算法代碼,C++語言專門指定瞭如下的類型相容語法規則

    1)派生類的對象可以賦值給基類對象。

    2)派生類的對象可以初始化基類引用,或者說基類引用可以引用派生類對象。

    3)派生類對象的地址可以賦值給基類指針,或者說基類的對象指針可指向派生類對象。

  應用類型相容語法規則有一個前提條件和一個使用限制。

  前提條件:派生類必須共有繼承基類。因為公有繼承下,派生類擁有基類的全部功能,派生類對象可以當作基類對象使用。

  使用限制:通過基類的對象、引用或對象指針訪問派生類對象時,只能訪問到基類成員,賦值和訪問時只能接收基類成員。

 示例:

A x1;B x2; A &x3 = x1; //定義類A,類B對象和類A的引用並初始化為類x1
x1 = x2; //派生類對象給基類對象賦值
x3 = x2; //基類引用可以引用派生類對象
A *p; p = &x2; //基類對象指針指向派生類對象
exfun(x2); //輸出exfun call class A[換行] A::fun1 called
//此時通過類A的外部處理函數處理派生類B對象,完成代碼重用

三、對象的多態性

  通過上面示例思考,如何通過基類引用或指針訪問派生類中與基類中同名的函數?我們將調用對象的某個函數成員稱為向對象發送一條消息。將執行函數成員完成某種程式功能稱為對象響應該消息。不同對象接收相同消息,但會表現除不同的行為,這就是對象的多態性。對象多態性就是:調用不同對象的同名函數成員,所執行的函數不同,完成的程式功能也不同。導致對象多態性的同名函數有以下三種不同形式:

    1) 不同類之間的同名函數。類成員有作用域,不同類之間的函數成員可以同名互不幹擾。

    2)類中的重載函數。通過一類中的函數成員可以重名,只要他們的形參個數不同或類型不同。重載函數成員導致的多態本質上屬於重載函數多態。

    3)派生類中的同名覆蓋。派生類中新增的函數成員可以與從基類繼承來函數成員同名,但他們不是重載函數。

  引入對象多態性的目的是為了讓另外一些處理基類的代碼也能夠被派生類對象重用。“另外一些”代碼的作用也就是通過基類引用或對象指針訪問派生類代碼時可以根據實際引用或指向的對象類型,自動調用該類同名函數中新增成員。這是需要將這些同名函數聲明成虛函數。C++語言以虛函數的語法形式來實現對象多態性,例如示例中的exfun1(A &x)函數。

 四、虛函數

  應用虛函數實現對象多態性的過程分為兩步:先聲明虛函數,在調用虛函數

1、聲明虛函數

  在定義基類時使用關鍵字virtual將函數聲明成虛函數,然後通過公有繼承定義派生類,並重寫虛函數成員,也就是新增一個與虛函數同名的函數成員。此後使用基類或派生類定義對象,其函數成員中只有虛函數成員才會在調用時呈現出多態性。

  為了更好的說明虛函數的聲明和使用,我們編寫一個簡單示例代碼如下:

class A //類聲明
{
    public:
        virtual void fun1(); //聲明虛函數成員fun1
        void fun2(); //聲明非虛函數成員fun2
};
//類實現
void A::fun1() { cout<<"Base class A:virtual fun1() called"<<endl; }
void A::fun2() { cout<<"Base class A:non-virtual fun2() called"<<endl; }
class B:public A //定義派生類B,公有繼承A
{
     public: 
       virtual void fun1(); //重寫虛函數成員fun1
       void fun2(); //重寫非虛函數成員fun2
};
void B::fun1() { cout<<"Derived class B:virtual fun1() called"<<endl; }
void B::fun2() { cout<<"Derived class B:non-virtual fun2() called"<<endl; }

  聲明虛函數的語法細則:

    1)只能在類聲明部分聲明虛函數。在類實現部分定義函數成員時不能在使用關鍵字virtual

    2)基類中聲明虛函數成員被繼承到派生類後,自動成為派生類的虛函數成員。

    3)派生類可以重寫虛函數成員。如果重寫後的函數原型與基類虛函數成員完全一致,則該函數自動成為派生類的虛函數成員,無論聲明時加不加virtual

    4)類函數成員中的靜態函數、構造函數不能是虛函數。析構函數可以是虛函數。

2、調用虛函數

  下麵我們通過派生類對象、基類引用和基類對象指針分別調用派生類虛函數和非虛函數,得到的結果如下:

//通過對象名調用函數成員
A aObj; B bObj;
bObj.fun1(); //調用結果:調用派生類bObj的新增虛函數成員fun1
bObj.fun2(); //調用結果:調用派生類bObj的新增非虛函數成員fun2(同名覆蓋)
//通過基類引用調用函數成員
A &raObj = aObj; //定義基類引用,引用基類對象
raObj.fun1(); //調用結果:調用基類對象aObj的虛函數成員fun1
raObj.fun2(); //調用結果:調用基類對象aObj的非虛函數成員fun2
A &rbObj = bObj; //定義基類引用,引用基類對象
rbObj.fun1(); //調用結果:調用派生類對象bObj的新增虛函數成員fun1
rbObj.fun2(); //調用結果:調用派生類對象bObj的基類非虛函數成員fun2(類型相容規則)
//通過基類對象指針調用函數成員
A *paObj = &aObj;//定義基類對象指針paObj,指向基類對象aObj
paObj->fun1(); //調用結果:調用基類對象aObj的虛函數成員fun1
paObj->fun2(); //調用結果:調用基類對象aObj的非虛函數成員fun2
A *pbObj = &bObj;//定義基類對象指針paObj,指向基類對象aObj
pbObj->fun1(); //調用結果:調用派生類對象bObj的新增虛函數成員fun1
pbObj->fun2(); //調用結果:調用派生類對象bObj的基類非虛函數成員fun2(類型相容規則)

總結:通過基類的引用或對象指針訪問類族中對象的虛函數成員(例如:fun1),基類對象和派生類對象將分別調用各自的虛函數成員,呈現出多態性。如果訪問的是非虛函數成員(例如:fun2),則訪問的都是基類成員,不會呈現多態性。

  實現基類對象與派生類對象之間的多態性要滿足以下三個條件:

    1)在基類中聲明虛函數成員。

    2)派生類需公有繼承基類,並重寫虛函數成員(屬於新增成員)。

    3)通過基類的引用或對象指針調用虛函數成員。

  只有滿足這三個條件,基類對象和派生類對象才會分別調用各自的虛函數,呈現出多態性。

  將源程式中具有多態性的虛函數名轉換成某個具體的函數存儲地址,這種函數名到存儲地址的轉換被稱為是對函數的綁定。通過基類的引用或對象指針調用虛函數成員,到底是調用基類成員還是新增成員,這在編譯時還不能確定。其綁定過程需要在程式執行時才能完成。對象多態是一種執行時多態

  總結以下通過對象的多態性讓類族共用演算法代碼需按以下步驟進行編程:

  1)聲明虛函數。定義基類時需要確定將那些函數成員聲明成虛函數。一般將可能被派生類修改或擴充的函數成員聲明成虛函數。

  2)重寫虛函數。定義派生類時公用繼承基類,並重寫那些從基類繼承來的虛函數成員。主要是為了修改和擴充基類功能。

  3)通過基類引用和對象指針訪問對象。訪問派生類對象,調用其中的虛函數成員將自動調用重寫的虛函數,否則自動調用從基類繼承來的函數成員(即類型相容語法規則)。

3、虛析構函數

  構造函數不能聲明成虛函數。析構函數可以聲明成虛函數,析構函數無形參,無函數類型。其聲明語法形式如:virtual ~類名();

  示例:

A *p1 = new A; //動態分配一個基類對象
A *p2 = new B; //動態分配一個派生類對象,使用基類的對象指針保存其地址
//使用對象略
delete p1;  //自動調用基類析構函數
delete p2;  //自動調用派生類析構函數

  可以註意到,p1 和p2都是基類的對象指針,使用delete運算符刪除對象時,將根據所指向對象的類型自動調用不同的析構函數,呈現出多態性。刪除派生類對象時,先執行派生類析構函數來析構新增成員,再執行基類析構函數來析構基類成員。如果不採用虛析構函數,那麼刪除派生類對象時將只會調用基類析構函數。

五、抽象類和純虛函數

1、純虛函數

  類定義中,“只聲明,未定義”的函數成員被稱為純虛函數。純虛函數的聲明語法形式為: virtual 函數類型 函數名(形參列表)=0;純虛函數是一種虛函數,具有虛函數額特性,其中最重要的一條就是虛函數成員再調用時具有多態性。函數有純虛函數的類就是抽象類

  抽象類具有如下特性:

  1)抽象類不能實例化

    不能使用抽象類定義對象(即不能實例化),因為抽象類中含有未定義的純虛函數,其類型定義不完整。但可以定義抽象類的引用或對象指針,所定義引用、對象指針可以引用其派生類的實例化對象。

  2)抽象類可以作為基類定義派生類

    抽象類可以作為基類定義派生類。此時的派生類也會繼承抽象類的純虛函數,由於抽象類只是聲明瞭純虛函數的函數原型,沒有定義函數體代碼,因此其派生類只是繼承了其函數原型。派生類需要為純虛函數成員編寫函數體代碼,這稱為實現純虛函數成員。派生類如果實現了所有的純虛函數成員,那麼它就變成了一個普通的類,可以實例化。只要派生類還有一個為實現的純虛函數,那麼它就還是一個抽象類,不能實例化,這是它還是只能作為基類繼續往下派生,直到實現所有純虛函數成員後才能實現化。

2、抽象類的應用

  1)統一類族介面

    派生類繼承基類是為了重用基類的代碼。如果基類時抽象類,純虛函數成員只聲明函數原型,這樣類族中的所有派生類都具有相同的對外介面。統一介面可以方便類族的使用。

  2)類族共用演算法代碼

    抽象類中定義的純虛函數具有虛函數特性,不同派生類中的虛函數實現和作用功能不同,調用時呈現多態性。類外函數可以通過抽象類(基類)引用和對象指針調用調用不同派生類對象,使得不同派生類對象公用該類外函數(演算法)。

六、多繼承、重覆繼承、虛基類(挖坑)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • python3.9安裝pyqt,並設置pycharm 前言 這個學期的暑假實訓需要使用pyqt開發gui界面,然後我的python不知道發生了什麼,安裝總是爆出各種奇怪的錯誤,安裝完後,開發和運行的過程中也發生了很多的離奇的問題,我浪費了很多的時間在解決各種報錯上,項目進度被耽誤了許多,幸運的是後來 ...
  • Java面向對象(八) 二十四、abstract 關鍵字 abstract 可以用來修飾的結構:類、方法。 abstract 修飾類:抽象類。 此類不能實例化。 抽象類中一定有構造器,便於子類實例化時調用。(涉及:子類對象實例化的全過程) 開發中,都會提供抽象類的子類,讓子類對象實例化,完成相關的操 ...
  • 上一講【RocketMQ】消息的拉取 消息消費 當RocketMQ進行消息消費的時候,是通過ConsumeMessageConcurrentlyService的submitConsumeRequest方法,將消息提交到線程池中進行消費,具體的處理邏輯如下: 如果本次消息的個數小於等於批量消費的大小c ...
  • 遇到問題和需求 我的電腦環境:先安裝py2再安裝py3,平時我工作中是使用python2,如何保證兩個版本共存且讓代碼來選擇要使用的版本。 遇到問題 在cmd中輸入python,進入的是py2的環境,但是通過pip install模塊是安裝到了python3目錄下 需求 工作中使用pytho2,在學 ...
  • 一、Http 1.什麼事Http Http(超文本傳輸協議)是一個簡單的請求-響應協議,它通常運行在TCP之上 文本:html,字元串,~.. 超文本:圖片,音樂,視屏,定位,地圖... 埠為80 Https:安全的,埠號443 2.兩個時代 http1.0 HTTP/1.0:客戶端可以與web ...
  • Java流程式控制制 1.用戶交互Scanner java.util.Scanner是Java5的新特征,我們可以通過Scannner類來獲取用戶的輸入。 基本語法: Scanner s = new Scanner(System.in); 通過Scanner類的next()與nextLine()方法獲取 ...
  • 一: 背景 最近在看 C++ 的右值引用和移動構造函數,感覺這東西一時半會還挺難理解的,可能是沒踩過這方面的坑,所以沒有那麼大的深有體會,不管怎麼說,這一篇我試著聊一下。 二: 右值引用 1. 它到底解決了什麼問題? 在其他編程語言中,很少聽到 右值引用 這個詞,我個人感覺還是 C++ 這個 值類型 ...
  • 引言 今天來談談設計模式中的單例模式,溫故知新,以免生疏。 軟體設計領域的四位世界級大師Gang Of Four (GoF):Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides四人合著了《Design Patterns - Elements o ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...