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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...