C++類開發第七篇(詳細說說多態和編譯原理)

来源:https://www.cnblogs.com/ivanlee717/p/18052060
-Advertisement-
Play Games

polymorphism 靜態聯編和動態聯編 多態性(polymorphism)提供介面與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程式具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。 c++ ...


polymorphism

靜態聯編和動態聯編

多態性(polymorphism)提供介面與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程式具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。

c++支持編譯時多態(靜態多態)和運行時多態(動態多態),運算符重載和函數重載就是編譯時多態,而派生類和虛函數實現運行時多態。

靜態多態和動態多態的區別就是函數地址是早綁定(靜態聯編)還是晚綁定(動態聯編)。如果函數的調用,在編譯階段就可以確定函數的調用地址,並產生代碼,就是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數的調用地址不能編譯不能在編譯期間確定,而需要在運行時才能決定,這這就屬於晚綁定(動態多態,運行時多態)。

將源代碼中的函數調用解釋為執行特定的函數代碼塊被稱為函數名聯編。編譯器必須查看函數參數以及函數名才能確定使用哪個函數。

指針和引用類型的相容性以及向上類型轉換

在C++裡面動態聯編與通過指針和引用的調用方法有關。通常c++不允許將一種一類的地址賦給另一種類型的指針,也不允許一種類型的引用指向另一種類型。

double x = 2.5;
int * pi = &x; //類型不對不能這樣定義
long & r1 = x; //問題同上

對象可以作為自己的類或者作為它的基類的對象來使用。還能通過基類的地址來操作它。取一個對象的地址(指針或引用),並將其作為基類的地址來處理,這種稱為向上類型轉換。

父類引用或指針可以指向子類對象,通過父類指針或引用來操作子類對象。

就是指將子類對象的引用賦給父類類型的引用變數的過程。在面向對象編程中,這種類型轉換是安全的,因為子類對象可以被當做父類對象來對待。通過向上類型轉換,可以實現多態性,即一個父類引用變數可以引用不同子類對象,並根據實際對象類型調用相應的方法。

class Base {
public :
	virtual void func() {
		cout << "class Base" << endl;
	}
};
class Derive_1 : public Base {
public:
	void func() {
		cout << "class Derive_1" << endl;
	}
};
class Derive_2 : public Base {
public:
	void func() {
		cout << "class Derive_2" << endl;
	}
};
void test() {
	Derive_1 d1;
	Derive_2 d2;
	//向上類型轉換
	Base* b1 = &d1;
	Base* b2 = &d2;
//通過父類引用變數調用子類方法
	b1->func();
	b2->func();

}

image-20240302105606439

雖然父類調用func函數,但是父類的指針全部指向了子類的引用,並且可以完成隱式類型轉換。再如下麵這個代碼

class Base {
public :
	void func() {
		cout << "class Base" << endl;
	}
};
class Derive_1 : public Base {
public:
	void func() {
		cout << "class Derive_1" << endl;
	}
};

void GetQuestion(Base& b) {
	b.func();
}

void test() {
	Derive_1 d1;
	
	GetQuestion(d1);


}

image-20240302110720280

參數定的是基類的引用,但是傳參傳的是子類,最後調用的依然是基類的方法。這個地方就引出了一個叫做捆綁的概念。把函數體與函數調用相聯繫稱為綁定(捆綁,binding)

當綁定在程式運行之前(由編譯器和連接器)完成時,稱為早綁定(early binding).C語言中只有一種函數調用方式,就是早綁定。上面的問題就是由於早綁定引起的,因為編譯器在只有Base地址時並不知道要調用的正確函數。編譯是根據指向對象的指針或引用的類型來選擇函數調用。

在代碼里,GetQuestion函數的參數類型是Base&,編譯器確定了應該調用的func函數是Base::func(),並不是傳入的d1.解決方法就是遲綁定(遲捆綁,動態綁定,運行時綁定,late binding),意味著綁定要根據對象的實際類型,發生在運行。C++語言要實現這種動態綁定,必須有某種機制來確定運行時對象的類型並調用合適的成員函數。對於一種編譯語言,編譯器並不知道實際的對象類型(編譯器並不知道Animal類型的指針或引用指向的實際的對象類型)。

虛函數

其實在上面的代碼里也能發現區別就是基類的函數是不是虛函數決定了這個綁定發生在什麼時候。如果沒有定義虛的,b.func();將根據引用類型調用函數,編譯時已知類型之後,對於非虛方法就是用的是靜態聯編。

  1. 為創建一個需要動態綁定的虛成員函數,可以簡單在這個函數聲明前面加上virtual關鍵字,定義時候不需要.

  2. 如果一個函數在基類中被聲明為virtual,那麼在所有派生類中它都是virtual的.

  3. 在派生類中virtual函數的重定義稱為重寫(override).

  4. Virtual關鍵字只能修飾成員函數.

  5. 構造函數不能為虛函數

僅需要在基類中聲明一個函數為virtual.調用所有匹配基類聲明行為的派生類函數都將使用虛機制。雖然可以在派生類聲明前使用關鍵字virtual(這也是無害的),但這個樣會使得程式顯得冗餘和雜亂。

image-20240302111643528

用《C++Primers》裡面的一個圖解釋一下虛函數的工作原理。通常編譯器處理虛函數的方法是:給每個對象添加一個隱藏成員,這個隱藏成員中保存了一個指向函數地址數組的指針(圖裡的vptr),而這種地址數組就叫做虛函數表(vtbl)。虛函數表裡存儲了為類對象進行聲明的虛函數的地址。比如基類對象Base包含一個指針,該指針指向基類中所有虛函數的地址表,派生類對象將包含一個指向獨立地址表的指針。如果派生類提供了虛函數的新定義,該虛函數表將保存新的地址;相反,該虛函數表將保存原始版本的地址。

調用虛函數時,程式將查看存儲在對象里的vtbl地址,然後轉向相應的函數地址表,如果使用類聲明中的第一個虛函數,則程式將使用數組中的第一個函數地址,並執行具有該地址的函數。總之使用虛函數時,在記憶體和執行速度方面有一定成本:

  1. 每個對象都會被增大其存儲空間(和虛基類一樣
  2. 每個類編譯器都會有一個虛函數地址表
  3. 每個函數的調用都需要執行一項額外的操作就是到表裡查找。

實現動態綁定細節過程

當子類無重寫基類虛函數時:

image-20240302114920328

image-20240302114947419

子類完全繼承基類的函數,他們擁有各自的虛函數表image-20240302115023473

當程式執行到這裡,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,此時由於子類並沒有重寫也就是覆蓋基類的func1函數,所以調用func1時,仍然調用的是基類的func1.

當子類重寫基類虛函數時

image-20240302115324609

子類重寫了基類的func1,但是沒有寫func2,所以對應的地址表應該是image-20240302115400573

當程式執行到這裡,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,由於子類重寫基類的func1函數,所以調用func1時,調用的是子類的func1.

抽象基類和純虛函數

在設計時,常常希望基類僅僅作為其派生類的一個介面。這就是說,僅想對基類進行向上類型轉換,使用它的介面,而不希望用戶實際的創建一個基類的對象。同時創建一個純虛函數允許介面中放置成員原函數,而不一定要提供一段可能對這個函數毫無意義的代碼。

做到這點,可以在基類中加入至少一個純虛函數(pure virtual function),使得基類稱為抽象類(abstract class).

  1. 純虛函數使用關鍵字virtual,併在其後面加上=0。如果試圖去實例化一個抽象類,編譯器則會阻止這種操作。

  2. 當繼承一個抽象類的時候,必須實現所有的純虛函數,否則由抽象類派生的類也是一個抽象類。

  3. Virtual void fun() = 0;告訴編譯器在vtable中為函數保留一個位置,但在這個特定位置不放地址。

建立公共介面目的是為了將子類公共的操作抽象出來,可以通過一個公共介面來操縱一組類,且這個公共介面不需要事先(或者不需要完全實現)。可以創建一個公共類.

class AbstractClass {
public:
	virtual void sleep() = 0;
	virtual void dolove() = 0;
	virtual void cook() = 0;
	void func() {
		cook();
		dolove();
		sleep();
	}

};
class Regina : public AbstractClass {
public:
	virtual void sleep() {
		cout << "Regina::sleep()" << endl;
	}
	virtual void dolove() {
		cout << "Regina::dolove()" << endl;
	}
	virtual void cook() {
		cout << "Regina::cook()" << endl;
	}
	
};
class Ivanlee : public AbstractClass {
public:
	virtual void sleep() {
		cout << "Ivanlee::sleep()" << endl;
	}
	virtual void dolove() {
		cout << "Ivanlee::dolove()" << endl;
	}
	virtual void cook() {
		cout << "Ivanlee::cook()" << endl;
	}
};
void home(AbstractClass* a) {
	a->func();
	delete a;
}
void home(AbstractClass& a) {
	a.func();
}
void test() {
	home(new Regina);
	Ivanlee ivan;
	home(ivan);
}

image-20240304155721828

純虛函數和虛函數是 C++ 中的重要概念,它們都與多態性(polymorphism)和繼承相關。它們之間的主要區別在於以下幾點:

  1. 虛函數:
    • 虛函數是在基類中聲明為虛函數的成員函數,它可以在派生類中被重寫(覆蓋)。
    • 虛函數可以有預設的實現,如果派生類沒有重寫虛函數,則會調用基類的實現。
    • 虛函數通過基類指針或引用調用時,可以根據指針或引用所指向的對象的實際類型來動態地決定調用哪個版本的函數(動態聯編)。
  2. 純虛函數:
    • 純虛函數是在基類中聲明並且沒有給出實現的虛函數,它只是一個介面,要求任何派生類都必須提供實現。
    • 在 C++ 中,通過在虛函數聲明後面加上 = 0 來將其聲明為純虛函數,例如:virtual void myFunction() = 0;
    • 含有純虛函數的類稱為抽象類,無法實例化對象,只能作為基類來派生出其他類。派生類必須提供純虛函數的實現,否則它們也會變成抽象類。

虛析構函數

虛析構函數是為瞭解決基類指針指向派生類對象,並用基類的指針刪除派生類對象。當通過基類指針刪除指向派生類對象的實例時,如果析構函數不是虛函數,那麼只會調用基類的析構函數,而不會調用派生類的析構函數,這可能導致派生類資源得不到正確釋放,從而產生記憶體泄漏或未定義的行為。

class Base {
public:
    virtual ~Base() {
        // 虛析構函數
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 派生類的析構函數
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 通過基類指針刪除派生類對象
    return 0;
}

重寫 重載 重定義

class Shape {
public:
    virtual double calculateArea() {
        return 0.0;
    }
};
  1. 重寫(Override):

    • 重寫是指派生類重新定義(覆蓋)基類中已經存在的虛函數的行為。

    • 當派生類定義一個與基類中的虛函數具有相同名稱和簽名的函數時,它就會覆蓋(重寫)基類中的虛函數。

    • 通過使用重寫,可以在派生類中改變虛函數的行為,實現多態性,即在運行時根據對象的實際類型來確定調用哪個版本的函數。

      class Rectangle : public Shape {
      public:
          double calculateArea() override {
              // 重寫基類的虛函數
              return width * height;
          }
      private:
          double width, height;
      };
      
  2. 重載(Overload):

    • 重載是指在同一個作用域內允許存在多個同名函數,但它們的參數列表不同(參數類型、參數個數或參數順序不同)。

    • 重載函數可以具有相同的名稱,但是由於參數列表不同,編譯器可以根據調用時提供的參數類型來確定應該調用哪個版本的函數。

      class Shape {
      public:
          virtual double calculateArea() {
              return 0.0;
          }
      
          double calculateArea(int a, int b) {
              // 重載的函數
              return a * b;
          }
      };
      
  3. 重新定義(Redefine):

    • 重新定義通常用於描述對於非虛函數的重新定義。在基類和派生類中,如果存在同名但參數列表不同的函數,這種情況稱為函數的重新定義。

    • 在重新定義中,基類和派生類中的函數並不構成多態性,調用哪個版本的函數取決於編譯器能夠靜態確定的最匹配的函數。

      class Circle : public Shape {
      public:
          void draw(int radius) {
              // 派生類中重新定義的函數
              cout << "Drawing a circle with radius " << radius << endl;
          }
      };
      

本文來自博客園,作者:ivanlee717,轉載請註明原文鏈接:https://www.cnblogs.com/ivanlee717/p/18052060


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

-Advertisement-
Play Games
更多相關文章
  • 1.創建 2.配置tomcat 3.創建webapp step01,war包 step02 創建web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xml ...
  • 3. Java程式流程式控制制(重點) 程式的三種控制結構 3.1 分支結構 if, switch 3.1.1 if if 分支 根據條件(真或假)來決定執行某段代碼。 if分支應用場景 if 第一種形式 執行流程: 首先判斷條件表達式的結果,如果為true執行語句體,為false就不執行語句體。 if ...
  • C-18.MySQL8其他新特性 1.MySQL8新特性概述 MySQL從5.7版本直接跳躍發佈了8.0版本,可見是一個令人興奮的里程碑的版本。MySQL 8版本在功能上,做了顯著的改進與增強,開發者對MySQL的源代碼進行了重構,最突出的一點是對MySQL Optimizer優化器進行了改進。不僅 ...
  • C++ MySQL資料庫連接池 新手學了C++多線程,看了些資料練手寫了C++資料庫連接池小項目,自己的源碼地址 關鍵技術點 MySQL資料庫編程、單例模式、queue隊列容器、C++11多線程編程、線程互斥、線程同步通信和 unique_lock、基於CAS的原子整形、智能指針shared_ptr ...
  • 數據過濾在數據分析過程中具有極其重要的地位,因為在真實世界的數據集中,往往存在重覆、缺失或異常的數據。pandas提供的數據過濾功能可以幫助我們輕鬆地識別和處理這些問題數據,從而確保數據的質量和準確性。 今天介紹的query函數,為我們提供了強大靈活的數據過濾方式,有助於從複雜的數據集中提取有價值的 ...
  • 目錄數組(Array)一、數組概念二、如何聲明一個數組三、如何為數組初始化1、數組本身初始化:2、數組的元素初始化2.1 一維數組2.2多維數組四、如何表示數組的各個概念五、數組記憶體和分配空間六、數組相關演算法七、十大內部排序演算法八、數組的工具類:Arrays九、數組的異常 數組(Array) 一、數 ...
  • 前言 在學習C++時,const關鍵字的知識點分散在書的各個章節。當我們嘗試在編程時使用const時,總會感覺有一些細節被遺忘,因而不能得心應手地使用const關鍵字。因此,本篇文章嘗試著對const關鍵字的做一些總結。參考書籍《C++ Primer Plus》 const總結 這裡是我做的關於co ...
  • 在Spring中,實例化Bean對象涉及構造方法的調用。通過分析源碼,我們瞭解到實例化的步驟和推斷構造方法的過程。當一個類只有一個構造方法時,Spring會根據具體情況決定是否使用該構造方法。如果一個類存在多個構造方法,就需要根據具體情況具體分析。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...