中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當前位置: 首頁 > news >正文

成都專業(yè)網(wǎng)站推廣公司網(wǎng)絡營銷的特點有幾個

成都專業(yè)網(wǎng)站推廣公司,網(wǎng)絡營銷的特點有幾個,一個網(wǎng)站域名ip,交通信用網(wǎng)站建設文章目錄 一、day11. 什么是面向?qū)ο?. 面向?qū)ο蟮娜?amp;#xff1a;繼承、封裝和多態(tài)2.1 封裝**2.1.1 封裝的概念****2.1.2 如何實現(xiàn)封裝****2.1.3 封裝的底層實現(xiàn)**2.1.4 為什么使用封裝?(好處)**2.1.5 封裝只有類能做嗎?結構體…

文章目錄

  • 一、day1
  • 1. 什么是面向?qū)ο?/li>
  • 2. 面向?qū)ο蟮娜?#xff1a;繼承、封裝和多態(tài)
    • 2.1 封裝
      • **2.1.1 封裝的概念**
      • **2.1.2 如何實現(xiàn)封裝**
      • **2.1.3 封裝的底層實現(xiàn)**
      • 2.1.4 為什么使用封裝?(好處)
      • **2.1.5 封裝只有類能做嗎?結構體如何封裝?命名空間能實現(xiàn)封裝嗎?**
    • 2.2 繼承
      • **2.2.1 繼承的概念**
      • **2.2.2 繼承的主要作用**
      • **2.2.3 如何實現(xiàn)繼承**
      • **2.2.4 構造函數(shù)和析構函數(shù)總結**
      • **2.2.5 派生類和基類之間的特殊關系**
      • **2.2.6 繼承的底層實現(xiàn)**
      • 2.2.7 **繼承的類型**
      • 2.2.8 **繼承的優(yōu)缺點**
    • 2.3 多態(tài)
      • **2.3.1 多態(tài)的概念**
      • **2.3.2 多態(tài)的類型**
      • **2.3.3 存在類繼承的情況下,為何需要虛析構函數(shù)**
      • **2.3.4 多態(tài)的底層實現(xiàn)**(虛函數(shù)表的實現(xiàn))
      • 2.3.5 使用虛方法時需注意的一些點
      • 2.3.6 純虛函數(shù)
      • 2.3.7 動態(tài)聯(lián)編和靜態(tài)聯(lián)編

一、day1

本節(jié)學習設計模式的前置知識,面向?qū)ο缶幊毯兔嫦蜻^程編程的區(qū)別,以及面向?qū)ο缶幊痰娜筇卣?#xff1a;封裝、繼承和多態(tài)。

參考:

設計模式 | 愛編程的大丙

封裝、繼承與多態(tài)究極詳解(面試必問) - Further_Step - 博客園

C++ 動態(tài)聯(lián)編和靜態(tài)聯(lián)編 - scyq - 博客園

1. 什么是面向?qū)ο?/h2>

要學習設計模式,首先需要了解什么是面向?qū)ο?#xff0c;并掌握其三大要素:封裝、繼承和多態(tài)。我們可以通過一個簡單的例子來說明:

假設我們想要把一頭大象放進冰箱,這個過程可以分為三個步驟:1)打開冰箱門;2)把大象放進去;3)關上冰箱門。在面向過程的編程中,這三個步驟通常被抽象為三個函數(shù),并在調(diào)用時按需提供參數(shù)。而在面向?qū)ο蟮木幊讨?#xff0c;需要圍繞具體的對象進行設計。這里有兩個關鍵對象:冰箱和大象。冰箱需要具備開門和關門的功能;大象則需要具備進入冰箱和離開冰箱的功能。

對象是類的實例。以大象為例,它的耳朵、鼻子、嘴巴等是屬性,而“進入冰箱”“走出冰箱”或“跳起來”是行為。通過設計冰箱類和大象類,使它們具備相應的功能,就可以實現(xiàn)讓大象進入冰箱的目標。從面向?qū)ο蟮慕嵌葋砜?#xff0c;這個過程需要先調(diào)用冰箱對象的開門功能,再調(diào)用大象對象的進入功能,最后調(diào)用冰箱對象的關門功能。

在這里插入圖片描述

圖片來源:https://subingwen.cn/design-patterns/

B站up愛編程的大丙舉了一個很形象的例子說明了面向過程和面向?qū)ο蟮膮^(qū)別:

假設現(xiàn)在有三個人:織女、牛郎和紅娘。紅娘想撮合牛郎和織女,她可以采用兩種編程思路:面向過程和面向?qū)ο蟆?/p>

面向過程編程:

  1. 紅娘把牛郎的牛牽到河邊。
  2. 紅娘把織女的紡車放到牛郎的牛車上。
  3. 紅娘告訴牛郎去找牛。
  4. 紅娘告訴織女去找紡車。
  5. 牛郎和織女在河邊相遇,一見鐘情。
  6. 兩人過上了幸福的生活。

在這個場景中,前四步是由紅娘主導完成的,后兩步則是牛郎和織女的互動。如果用代碼實現(xiàn),每一步都會對應一個函數(shù),函數(shù)需要傳入必要的參數(shù)。例如,在第一個函數(shù)中,我們忽略了紅娘這個主語,僅僅實現(xiàn)了“將牛牽到河邊”的功能。

面向?qū)ο缶幊?#xff1a;

  1. 紅娘:牛郎,能借你的牛用一下嗎?
    牛郎:好的,我去牽牛。
  2. 紅娘:織女,能借你的紡車用一下嗎?
    織女:沒問題,我去搬紡車。

隨后發(fā)生了意外:

  1. 牛郎:呀!牛丟了,我得趕緊去找牛。
  2. 織女:呀!紡車丟了,我得趕緊去找紡車。

最終,牛郎和織女相遇并交流:

  1. 牛郎:織女,我知道108種牛肉做法,要不要嘗嘗?
  2. 織女:我會做很多漂亮的衣服,你想不想試試?
  3. 牛郎:那我們結婚吧!
  4. 織女:好的!

在面向?qū)ο蟮乃悸分?#xff0c;我們會將場景中的對象抽象出來。例如:

  • 牛和牛車是牛郎的屬性,牽牛、找牛、說話、結婚是牛郎的行為。
  • 紡車是織女的屬性,搬紡車、找紡車、說話、結婚是織女的行為。
  • 紅娘負責協(xié)調(diào)和推動整個事件的發(fā)生,這是她的行為。

面向?qū)ο缶幊痰谋举|(zhì)

面向?qū)ο缶幊痰暮诵氖?strong>將屬性和行為解耦,明確屬性和行為分別屬于哪個對象。基于這些屬性和行為,定義相應的類,例如牛郎類和織女類。類是模板,實例化類就會生成具體的對象(如具體的牛郎和織女)。通過對象,我們可以調(diào)用類中定義的屬性和行為。

相比之下,面向過程編程沒有定義牛郎、織女和紅娘的類,所有的步驟都通過函數(shù)一步步實現(xiàn)。雖然這種方式簡單直觀,但隨著功能復雜度的增加,函數(shù)體會變得冗長且難以維護,增加了出錯的可能性。而在面向?qū)ο笾?#xff1a;

  • 織女類只處理與自己相關的行為,例如搬紡車、找紡車、說話和結婚。
  • 牛郎類同樣專注于自己的行為,例如牽牛、找牛、說話和結婚。

這種分工明確的設計,讓代碼更加模塊化、可維護,也更貼近真實場景的邏輯。

總結

  • 面向過程編程(POP):是一種依賴于函數(shù)調(diào)用和過程的編程范式。在POP中,程序通過執(zhí)行一系列步驟(函數(shù)調(diào)用)來達到目標。數(shù)據(jù)和操作這些數(shù)據(jù)的功能是分開的。程序的核心是通過操作全局數(shù)據(jù)來進行的。
  • 面向?qū)ο缶幊?#xff08;OOP):將數(shù)據(jù)和操作這些數(shù)據(jù)的功能封裝在一起,構成一個“對象”。面向?qū)ο蟮某绦蚴怯蓪ο蠼M成的,這些對象通過消息(方法調(diào)用)與其他對象交互。

2. 面向?qū)ο蟮娜?#xff1a;繼承、封裝和多態(tài)

面向?qū)ο缶幊?/strong>有三大特征:封裝、繼承和多態(tài)。

  • 封裝(Encapsulation):封裝確保對象中的數(shù)據(jù)安全,通過將數(shù)據(jù)和操作數(shù)據(jù)的方法封裝在一個對象中,避免外部直接訪問對象的數(shù)據(jù)。
  • 繼承(Inheritance):繼承保證了對象的可擴展性,子類可以繼承父類的屬性和方法,并且可以在此基礎上進行擴展。
  • 多態(tài)(Polymorphism):多態(tài)保證了程序的靈活性,允許不同類型的對象對于相同的消息作出不同的響應。

封裝是類的一個天然特性,就像一個盒子天生可以用來裝東西。類通過封裝,將數(shù)據(jù)和方法保護起來,對外只提供必要的接口,從而提高了代碼的安全性和可維護性。

繼承是類之間的一種重要關系。盡管類之間還可以有其他關系,例如關聯(lián)、依賴、實現(xiàn)、聚合和組合,但我們常強調(diào)繼承。這是因為繼承不僅是一種特殊的關系,還為類之間的代碼復用提供了基礎。事實上,實現(xiàn)可以看作是繼承的一種特例,而其他關系更像是根據(jù)需求將類放在不同位置靈活組合。需要注意的是,這些關系在 C 的結構體中也可以實現(xiàn),結構體并不是 C++ 的獨創(chuàng)。但繼承不同,它是一種全新的機制,需要在設計時明確約定規(guī)則。

繼承的一個重要作用是引入多態(tài)性。通過繼承,不同的子類可以在運行時根據(jù)相同的消息動態(tài)決定使用哪個方法,這使得資源分配更加靈活。這種多態(tài)性是繼承的延伸,是面向?qū)ο缶幊痰囊淮蠛诵奶攸c。

總結來說,封裝是類的內(nèi)在特性,繼承是類之間的一種新型關系,而多態(tài)則是繼承帶來的資源分配新規(guī)則。這三者正是 C++ 相較于 C 的主要創(chuàng)新點,也為從面向過程編程轉(zhuǎn)向面向?qū)ο缶幊烫峁┝藦娪辛Φ闹С帧?/strong>

2.1 封裝

2.1.1 封裝的概念

在面向?qū)ο缶幊讨?#xff0c;封裝是將數(shù)據(jù)和方法綁定到一個對象中,并通過控制數(shù)據(jù)的訪問來保證對象內(nèi)部的一致性和安全性。

封裝的基本思想是隱藏內(nèi)部實現(xiàn)細節(jié)暴露必要的接口。封裝有兩個主要方面:

  • 數(shù)據(jù)隱藏:只允許通過公開的接口(方法)訪問和修改數(shù)據(jù)。這樣可以避免外部代碼直接修改對象的內(nèi)部狀態(tài),減少錯誤的發(fā)生。
  • 接口與實現(xiàn)分離:對象暴露的是一組操作數(shù)據(jù)的接口,而不是數(shù)據(jù)本身。外部只關心如何使用這個對象提供的功能,而不需要了解它的內(nèi)部實現(xiàn)。

2.1.2 如何實現(xiàn)封裝

在C++中,封裝是通過和訪問修飾符(如publicprivate、protected)來實現(xiàn)的。

  • public:類的公共部分,外部可以訪問和修改。
  • private:類的私有部分,外部無法直接訪問,只能通過類提供的公有方法來間接訪問。
  • protected:類似于private,但允許派生類(子類)訪問。

2.1.3 封裝的底層實現(xiàn)

從底層的角度看,封裝的實現(xiàn)通常依賴于內(nèi)存布局和訪問控制機制。在C++中,類的成員變量通常會在對象實例化時分配內(nèi)存。通過訪問控制(private、public)和get、set方法,編譯器幫助開發(fā)者實現(xiàn)了對數(shù)據(jù)訪問的精細控制。

  • 內(nèi)存分配:每個對象都有獨立的內(nèi)存區(qū)域來存儲成員變量。當對象被創(chuàng)建時,內(nèi)存會分配給它的所有成員變量。privatepublic 只是影響這些成員在外部代碼中的訪問方式,實際的內(nèi)存布局不會變化。
  • 訪問控制private、publicprotected 是由編譯器支持的訪問權限控制機制,確保類的私有數(shù)據(jù)只能通過特定的公有方法來修改。編譯器會在編譯時檢查是否有非法訪問的代碼,防止程序出現(xiàn)不可預期的行為。

2.1.4 為什么使用封裝?(好處)

  1. 數(shù)據(jù)保護:封裝隱藏了數(shù)據(jù)的實現(xiàn),外部無法直接改變對象的內(nèi)部狀態(tài),防止了誤操作或非法操作。
  2. 提高代碼可維護性:通過暴露清晰的接口和隱藏復雜的內(nèi)部實現(xiàn),程序更加模塊化。如果需要改變實現(xiàn)細節(jié),只需要修改類的內(nèi)部代碼,不會影響到其他依賴這個類的代碼。
  3. 提高安全性:封裝可以確保對象的一致性和有效性。比如,withdraw方法中檢查提款金額是否合理,確保余額不被非法提取。

2.1.5 封裝只有類能做嗎?結構體如何封裝?命名空間能實現(xiàn)封裝嗎?

除了類之外,結構體和命名空間也可以實現(xiàn)一定程度的封裝:

  • 在類中,編譯器通過訪問修飾符(如public、private、protected)來實現(xiàn)封裝。

  • structclass本質(zhì)上是相似的,唯一區(qū)別是:

    • class 的成員默認是 private
    • struct的成員默認是public
  • 命名空間(namespace)主要用于邏輯上的分組和避免名字沖突,但它不能像類一樣提供訪問控制。通過命名空間,也可以實現(xiàn)一種“偽封裝”,但沒有訪問權限控制。

    namespace MyNamespace {namespace Detail { // 內(nèi)部命名空間,相當于隱藏的實現(xiàn)int hiddenFunction(int x) {return x * x;}}int publicFunction(int x) {return Detail::hiddenFunction(x) + 10;}
    }
    

    雖然Detail::hiddenFunction仍然可以被訪問,但在設計上約定為只在MyNamespace內(nèi)部使用

2.2 繼承

2.2.1 繼承的概念

繼承是面向?qū)ο缶幊讨械囊环N機制,它允許我們創(chuàng)建一個新的類,該類可以繼承自一個或多個已存在的類。被繼承的類稱為父類(或基類),新創(chuàng)建的類稱為子類(或派生類)。子類繼承了父類的屬性和方法,并可以在此基礎上進行擴展和修改。

派生類和基類的關系是一種 is-a 關系(公有繼承),即派生類對象也是一個基類對象,可以對基類對象執(zhí)行的任何操作,也可以對派生類對象執(zhí)行。但不是 has-a、is-like-auses-ais-implemented-as-a關系。

2.2.2 繼承的主要作用

  • 代碼復用:子類無需重新定義父類已經(jīng)實現(xiàn)的方法和屬性,可以直接使用它們。
  • 擴展性:子類可以在繼承的基礎上擴展功能,添加特有的行為。
  • 層次化設計:繼承允許程序員通過類層次結構來組織和簡化代碼。例如,DogCat都可以繼承自Animal,然后你可以根據(jù)需要為DogCat添加各自的特殊行為。

2.2.3 如何實現(xiàn)繼承

在C++中,繼承通過classpublic、protectedprivate修飾符來實現(xiàn),不同的修飾符會影響父類成員在子類中的訪問權限。

1.Public 繼承

  • 子類會繼承父類的 公有成員保護成員。
  • 在子類中,父類的 公有成員 仍然是 公有的,可以直接訪問。
  • 父類的 保護成員 在子類中仍然是 保護的。
  • 私有成員 雖然不能直接被子類訪問,但仍然是子類的一部分,可以通過父類的 公有或保護方法 進行間接訪問。

2.Protected 繼承

  • 子類會繼承父類的 公有成員保護成員。
  • 在子類中,父類的 公有成員 會變成 保護的
  • 父類的 保護成員 保持不變,仍然是 保護的。
  • 私有成員 和 Public 繼承一樣,不能直接訪問,但仍然可以通過父類的相關方法間接訪問。

3.Private 繼承

  • 子類會繼承父類的 公有成員保護成員。
  • 在子類中,父類的 公有成員保護成員 都變成了 私有的,只能在子類的內(nèi)部訪問。
  • 私有成員 和前兩種繼承方式一樣,不能直接訪問,但仍然是子類的一部分,可以通過父類的方法間接訪問。

因為派生類不能直接訪問基類的私有成員,而必須通過基類的公有方法進行訪問,因此基類的構造函數(shù)不能直接設置繼承的私有成員,所以派生類構造函數(shù)必須使用基類構造函數(shù)。派生類構造函數(shù)的流程如下:

  1. 首先創(chuàng)建基類對象
  2. 派生類構造函數(shù)應通過成員初始化列表將基類信息傳遞給基類構造函數(shù)
  3. 派生類構造函數(shù)應初始化派生類新增的數(shù)據(jù)成員

2.2.4 構造函數(shù)和析構函數(shù)總結

  • 創(chuàng)建派生類對象時,程序首先調(diào)用基類構造函數(shù),然后再調(diào)用派生類構造函數(shù),派生類的構造函數(shù)總是調(diào)用一個基類構造函數(shù)。
  • 派生類對象過期時,程序?qū)⑹紫日{(diào)用派生類析構函數(shù),然后再調(diào)用基類析構函數(shù)。

2.2.5 派生類和基類之間的特殊關系

  • 派生類對象可以使用基類的方法,條件是方法不是私有的(只能是公有或保護的)。
  • 基類指針可以在不進行顯示類型轉(zhuǎn)換的情況下指向派生類對象。
  • 基類引用可以在不進行顯示類型轉(zhuǎn)換的情況下引用派生類對象。
class TableTennisPlayer{// .......
}
class RatedPlayer : public TableTennisPlayer{// .......
}

假設有上述繼承關系,那么基類的指針和引用可以在不進行顯示類型轉(zhuǎn)換的情況下指向或引用派生類對象:

TableTennisPlayer* pt = &RatedPlayer;
TableTennisPlayer& rt = RatedPlayer;

但注意,基類指針或引用只能用于調(diào)用基類方法或成員,不能使用 rt 或 pt 調(diào)用派生類的方法。通常,C++要求引用和指針類型與賦給的類型匹配,但這一規(guī)則對繼承來說是例外。

可以說基類的指針和引用可以在不進行顯示類型轉(zhuǎn)換的情況下指向或引用派生類對象,派生類指針或引用不能指向或引用基類對象;也可以說派生類對象可以復制或賦值給基類對象(只針對二者共有的成員),但不能說不能將基類對象賦值或復制給派生類對象(雖然系統(tǒng)沒有默認函數(shù)支持,但我們可以定義重載函數(shù)實現(xiàn),不過一般情況下是不允許將基類對象賦值或復制給派生類對象的)。

基類和派生類還可以進行轉(zhuǎn)換:

  • 將派生類引用或指針轉(zhuǎn)換為基類引用或指針被稱為向上強制轉(zhuǎn)換(upcasting),這使得公有繼承不需要進行顯式類型轉(zhuǎn)換,該規(guī)則是 is-a 關系的一部分。
  • 將基類指針或引用轉(zhuǎn)換為派生類指針或引用稱為向下強制轉(zhuǎn)換(downcasting)。如果不使用顯式類型轉(zhuǎn)換,則向下強制轉(zhuǎn)換是不被允許的,原因是 is-a 關系通常是不可逆的。

但我們可以通過顯式強制轉(zhuǎn)換將基類指針或引用轉(zhuǎn)換為派生類指針或引用,但這可能會帶來不安全的操作,因為派生類的一些方法在基類中可能不存在。如下代碼:

Base t1; // 基類
Baseplus* t2 = (Baseplus*)&t1; // 將基類強制轉(zhuǎn)換為派生類
t2->print();

如果 print()虛函數(shù),此時調(diào)用的是 基類的版本,并不會因為強制轉(zhuǎn)換調(diào)用派生類的 print 函數(shù),而是因為 t1 是一個 基類對象,它的虛函數(shù)表(vtable)指向的是基類的虛函數(shù)表。即使通過強制轉(zhuǎn)換獲得了一個派生類指針,虛函數(shù)調(diào)用依然由對象的動態(tài)類型(這里是 Base)決定,而不是指針的靜態(tài)類型。

如果 print() 不是虛函數(shù),則調(diào)用的是指針類型(即 Baseplus*)對應的函數(shù)版本。在這種情況下,結果是未定義行為,因為 t1 是基類對象,但你嘗試通過派生類指針調(diào)用派生類的方法,可能會訪問未初始化的派生類成員。

2.2.6 繼承的底層實現(xiàn)

在底層,繼承通過對象布局指針偏移來實現(xiàn)。每個對象都有一個虛函數(shù)表(vtable),用于支持多態(tài)(如果使用了虛函數(shù))。當你創(chuàng)建一個子類對象時,它不僅包含自己的數(shù)據(jù)成員,還會包含父類的數(shù)據(jù)成員(如果父類有數(shù)據(jù)成員的話)。

內(nèi)存布局:

  • 對象的內(nèi)存布局包含了父類部分子類部分。父類的成員變量和成員函數(shù)會存儲在內(nèi)存中,子類會在父類的基礎上添加額外的成員。
  • 如果有虛函數(shù),編譯器會為類創(chuàng)建一個虛函數(shù)表,虛函數(shù)表包含所有虛函數(shù)的指針,確保子類能夠重寫(覆蓋)父類的虛函數(shù)。

示例內(nèi)存布局:

假設有以下類繼承關系:

  • A 是基類,B 是從 A 繼承的子類,C 是從 B 繼承的子類。
內(nèi)存布局說明
A 類的成員基類 A 中的成員數(shù)據(jù)存儲在內(nèi)存中
B 類的成員子類 B 擴展的成員數(shù)據(jù)存儲在內(nèi)存中
C 類的成員子類 C 擴展的成員數(shù)據(jù)存儲在內(nèi)存中

2.2.7 繼承的類型

繼承可以分為不同類型,常見的包括:

  • 單繼承:子類只繼承一個父類。
  • 多重繼承:子類可以繼承多個父類。
  • 多級繼承:子類繼承自父類,孫類繼承自子類等。

2.2.8 繼承的優(yōu)缺點

優(yōu)點:

  • 代碼重用:子類繼承父類的行為,可以減少代碼重復,提升代碼復用性。
  • 模塊化設計:通過繼承可以構建層次結構,使得代碼更具組織性。
  • 擴展性:子類可以繼承父類的功能,并在此基礎上擴展或重寫,滿足更多需求。

缺點:

  • 緊密耦合:繼承會導致類之間的緊密耦合,子類對父類的依賴較強,修改父類可能影響子類的行為。
  • 繼承層次復雜:多層繼承可能導致類關系復雜,尤其是多重繼承時,可能出現(xiàn)二義性(例如“菱形繼承問題”)。
  • 不利于靈活性:過度使用繼承可能導致代碼不易擴展或維護,過度繼承會使類層次過于復雜。

2.3 多態(tài)

2.3.1 多態(tài)的概念

多態(tài)(Polymorphism)是面向?qū)ο缶幊?#xff08;OOP)中的一個核心概念,它允許不同類的對象通過相同的接口(方法名)來調(diào)用不同的實現(xiàn)。簡單來說,多態(tài)使得不同類型的對象可以通過相同的接口執(zhí)行不同的操作。多態(tài)性使得程序更加靈活和可擴展。

有兩種機制可用于實現(xiàn)多態(tài)公有繼承:

  • 在派生類中重新定義基類的方法。這種方式不需要額外的語法支持,但只有當通過子類對象直接調(diào)用方法時,才能體現(xiàn)多態(tài)性。通過基類的指針或引用調(diào)用時,仍然會調(diào)用基類的方法。
  • 使用虛方法,基類中將函數(shù)聲明為 virtual,派生類可以重寫該函數(shù)。當通過基類的指針或引用調(diào)用時,會根據(jù)對象的實際類型調(diào)用重寫后的函數(shù),而不是基類的版本。

但注意:

  • 虛函數(shù)必須通過基類的指針或引用調(diào)用,才能實現(xiàn)動態(tài)綁定,即調(diào)用派生類中重寫后的方法。
  • 如果直接通過對象調(diào)用,不管有沒有使用虛函數(shù),無論基類還是派生類對象,調(diào)用的都是對象所屬類的版本。
  • 沒有被重寫的虛函數(shù),調(diào)用時會使用基類的默認實現(xiàn)。
  • 如果需要在派生類中調(diào)用基類的版本,必須顯式指定 Base::,否則會調(diào)用派生類重寫的方法。
#include <iostream>
using namespace std;class Animal {
public:virtual void makeSound() { // 虛函數(shù)cout << "Animal makes a sound" << endl;}
};class Dog : public Animal {
public:void makeSound() override { // 重寫虛函數(shù)cout << "Dog barks" << endl;}
};class Cat : public Animal {
public:void makeSound() override { // 重寫虛函數(shù)cout << "Cat meows" << endl;}
};
class Bird : public Animal {
public:void makeSound() override { // 重寫虛函數(shù)cout << "Bird meows" << endl;// 規(guī)則4Animal::makeSound(); // 顯式調(diào)用基類的 makeSound() 方法}
};int main() {// 規(guī)則1Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->makeSound(); // 輸出: Dog barksanimal2->makeSound(); // 輸出: Cat meowsdelete animal1;delete animal2;// 規(guī)則2Dog dog();Cat cat();dog().makeSound(); // 輸出: Dog barkscat().makeSound(); // 輸出: Cat meowsreturn 0;
}

上段代碼中分別對規(guī)則1 和規(guī)則 2進行的描述,如果我們通過基類的引用或指針調(diào)用,則程序?qū)⒏鶕?jù)引用或指針指向的對象類型來選擇方法(使用了虛函數(shù));如果直接通過派生類對象調(diào)用,即使沒有使用虛函數(shù),也會調(diào)用派生類的方法。

2.3.2 多態(tài)的類型

  1. 編譯時多態(tài)(靜態(tài)多態(tài)):在編譯時決定調(diào)用哪個函數(shù),常見的實現(xiàn)方式是方法重載(Overloading)和運算符重載(Operator Overloading)。
  2. 運行時多態(tài)(動態(tài)多態(tài)):在程序運行時決定調(diào)用哪個函數(shù),常通過虛函數(shù)和繼承實現(xiàn)。運行時多態(tài)通常通過虛函數(shù)來實現(xiàn)。虛函數(shù)是基類中聲明為 virtual 的函數(shù),子類可以重寫這個函數(shù)。當通過基類指針或引用調(diào)用該函數(shù)時,程序會根據(jù)對象的實際類型(而不是指針或引用的類型)來決定調(diào)用哪個函數(shù)實現(xiàn)。

2.3.3 存在類繼承的情況下,為何需要虛析構函數(shù)

使用虛析構函數(shù)是為了確保析構函數(shù)序列被正確調(diào)用。如果基類的析構函數(shù)不是虛函數(shù),通過基類指針刪除派生類對象時,只會調(diào)用基類的析構函數(shù),而不會調(diào)用派生類的析構函數(shù)。這樣可能導致派生類中動態(tài)分配的資源沒有正確釋放,進而產(chǎn)生資源泄漏。如下:

#include <iostream>
using namespace std;class Base {
public:~Base() { // 非虛析構函數(shù)cout << "Base destructor called" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "Derived destructor called" << endl;}
};int main() {Base* ptr = new Derived(); // 基類指針指向派生類對象delete ptr; // 只調(diào)用了基類的析構函數(shù)return 0;
}

在上段代碼中,Derived 類的析構函數(shù)沒有被調(diào)用,因此派生類持有的資源無法正確釋放。輸出如下:

Base destructor called

將基類析構函數(shù)設為虛函數(shù),可確保先調(diào)用派生類析構函數(shù),再調(diào)用基類析構函數(shù),結果如下:

class Base {
public:virtual ~Base() { // 虛析構函數(shù)cout << "Base destructor called" << endl;}
};// 輸出
Derived destructor called
Base destructor called

2.3.4 多態(tài)的底層實現(xiàn)(虛函數(shù)表的實現(xiàn))

多態(tài)的底層實現(xiàn)依賴于虛函數(shù)表(vtable)。每個包含虛函數(shù)的類,在編譯時會生成一個虛函數(shù)表,其中存儲著類的所有虛函數(shù)指針。當通過父類指針調(diào)用虛函數(shù)時,程序會查找虛函數(shù)表,找到對應的子類實現(xiàn)并調(diào)用。

虛函數(shù)表的工作原理:

我們一般利用虛表和虛表指針來實現(xiàn)動態(tài)綁定,那么具體是如何實現(xiàn)的?

通常,編譯器處理虛函數(shù)的方法是:給每個對象添加一個隱藏成員。隱藏成員中保存了一個指向函數(shù)地址數(shù)組的指針,這種數(shù)組稱為虛函數(shù)表。虛函數(shù)表中存儲了該類所有虛函數(shù)的地址。例如,基類對象包含一個指針,該指針指向基類中所有虛函數(shù)的地址表。派生類對象將包含一個指向獨立地址表的指針。如果派生類重新定義了基類的虛函數(shù),虛函數(shù)表會更新該函數(shù)的地址,指向派生類的新定義;如果派生類沒有重寫基類的虛函數(shù),虛函數(shù)表會保留基類的虛函數(shù)地址。如果派生類新增了虛函數(shù),這些虛函數(shù)的地址會被添加到虛函數(shù)表中。

注意,無論類中包含的虛函數(shù)是1個還是10個,對象中的隱藏指針始終只有一個,占用固定的內(nèi)存,只是指向表的大小不同而已。虛表是屬于類的,而不是屬于某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。

在這里插入圖片描述

如上圖,我們定義了基類 Scientist,并聲明了兩個虛函數(shù) show_name()show_all()。同時定義了一個繼承自 Scientist 的子類 Physicist,子類中重定義了 show_all() 并新增了虛函數(shù) show_field()。

基類 Scientist 和派生類 Physicist 的虛函數(shù)表分別如下圖所示:

在這里插入圖片描述

基類 Scientist 中聲明了兩個虛函數(shù),所以它的虛函數(shù)表存在兩個徐函數(shù)地址 40646400,且虛函數(shù)表的地址為 2008;派生類 Physicist 中將虛函數(shù) show_all() 重新定義,并聲明了新的虛函數(shù) show_field(),所以它的虛函數(shù)表中更新 show_all() 的地址為 6820,并新增了對應 show_field() 的地址 7280,且它的虛函數(shù)表地址為 2096。

并且二者都有一個隱藏的指針成員用于指向各自的虛函數(shù)表,如下圖所示:

在這里插入圖片描述

基類 Scientist 的內(nèi)存空間如上圖所示,其私有成員 name 的地址存儲內(nèi)容為 Sopjoe Fant,但它還有一個隱藏指針成員 vptr 用于指向它的虛函數(shù)表;同樣,派生類 Physicist 中也有一個隱藏指針成員 vptr 用于指向它的虛函數(shù)表,同樣它的私有成員 field 內(nèi)容為 Nuclear Structure。

那么調(diào)用虛函數(shù)時,虛函數(shù)表是如何作用的呢?

調(diào)用虛函數(shù)時,程序?qū)⒉榭创鎯υ趯ο笾械奶摵瘮?shù)表地址,然后轉(zhuǎn)向相應的函數(shù)地址表。如果使用類聲明中定義的第一個虛函數(shù),則程序?qū)⑹褂脭?shù)組中的第一個函數(shù)地址,并執(zhí)行具有該地址的函數(shù)。如果使用類聲明中的第三個虛函數(shù),程序?qū)⑹褂玫刂窞閿?shù)組中第三個元素的函數(shù)。如下圖所示:

在這里插入圖片描述

當我們調(diào)用派生類 Physicist 的虛函數(shù) show_all() 時,我們首先獲取派生類 Physicist 的隱藏指針成員 vtpr 指向的地址 2096,并前往該處獲取對應的虛函數(shù)表,然后我們依據(jù)順序獲悉表中對應函數(shù)的地址 6820(由于虛表在編譯階段就可以構造出來了,所以可以根據(jù)所調(diào)用的函數(shù)定位到虛表中的對應條目),編譯器前往 6820 處執(zhí)行這里的虛函數(shù)。

注意:非虛函數(shù)的調(diào)用不用經(jīng)過虛表,故不需要虛表中的指針指向這些函數(shù)。而且虛函數(shù)需要消費一定的資源,所以無繼承以及無虛函數(shù)的情況下,虛函數(shù)表不會生成。

什么時候會執(zhí)行函數(shù)的動態(tài)綁定?這需要符合以下三個條件。

  • 通過指針來調(diào)用函數(shù)
  • 指針upcast向上轉(zhuǎn)型(繼承類向基類的轉(zhuǎn)換稱為upcast
  • 調(diào)用的是虛函數(shù)

2.3.5 使用虛方法時需注意的一些點

  1. 構造函數(shù)不能是虛函數(shù)。創(chuàng)建派生類對象時,將調(diào)用派生類的構造函數(shù),而不是基類的構造函數(shù),然后,派生類的構造函數(shù)將使用基類的一個構造函數(shù),這種順序不同于繼承機制。因此,派生類不繼承基類的構造函數(shù),所以將類構造函數(shù)聲明為虛函數(shù)沒有意義。
  2. 析構函數(shù)應當是虛函數(shù),除非類不用做基類。
  3. 友元不能是虛函數(shù),因為友元不是類成員,而只有成員才能是虛函數(shù)。
  4. 如果派生類沒有重新定義函數(shù),將使用該函數(shù)的基類版本。
  5. 派生類重新定義函數(shù)會隱藏基類方法。

前面四條很淺顯易懂,這里詳細說一下第五條。第五條有以下兩個個規(guī)則:

  • 如果基類的函數(shù)被聲明為virtual,而派生類定義了一個函數(shù)名、參數(shù)列表和返回類型完全相同的函數(shù),那么派生類的函數(shù)將覆蓋基類的函數(shù)。
  • 如果基類和派生類的函數(shù)名相同,但參數(shù)列表不同,則派生類的函數(shù)會隱藏基類的同名函數(shù),無論基類的函數(shù)是否是virtual。

隱藏、覆蓋和重載是三個不同的概念。重載發(fā)生在同一個類內(nèi),通過定義參數(shù)列表不同的同名函數(shù)實現(xiàn)。隱藏覆蓋則出現(xiàn)在基類與派生類之間。

當派生類重新定義基類中的虛函數(shù)時:

  • 如果參數(shù)列表(特征標)相同,派生類的函數(shù)會覆蓋基類的虛函數(shù)。
  • 如果參數(shù)列表不同,派生類的函數(shù)會隱藏基類的虛函數(shù)。

如果基類的函數(shù)被隱藏或覆蓋了,但仍需要調(diào)用,使用基類類名加作用域運算符::,顯式調(diào)用基類的函數(shù)。

2.3.6 純虛函數(shù)

純虛函數(shù)(Pure Virtual Function)是C++中的一種特殊成員函數(shù),通常用于定義抽象類,為派生類提供一個必須實現(xiàn)的接口。抽象類不能實例化。它的定義形式在基類中包含= 0的語法,例如:

virtual void display() = 0;
  • 不能在基類中實現(xiàn):純虛函數(shù)不包含函數(shù)體,只定義接口,具體實現(xiàn)必須由派生類完成。
  • 定義抽象類:包含純虛函數(shù)的類稱為抽象類,不能直接實例化。
  • 派生類的義務:派生類必須重寫所有繼承的純虛函數(shù),否則派生類本身也會變成抽象類。

在原型中使用 =0 指出類是一個抽象基類,在類中不可以定義該函數(shù),應在派生類中定義。

純虛函數(shù)的主要作用是定義接口規(guī)范,強制要求派生類必須實現(xiàn)這些函數(shù),從而實現(xiàn)接口的統(tǒng)一和標準化。

舉個例子說明:

假設我們要設計一個繪圖系統(tǒng),可以繪制不同的形狀,如圓形、矩形等。每種形狀都有一個draw()函數(shù)負責繪圖,但每種形狀的繪圖方式不同。我們可以用純虛函數(shù)實現(xiàn):

#include <iostream>
#include <vector>
using namespace std;// 抽象類:Shape
class Shape {
public:virtual void draw() = 0;  // 純虛函數(shù),強制派生類實現(xiàn)virtual double area() = 0; // 純虛函數(shù),計算面積virtual ~Shape() {}       // 虛析構函數(shù),確保按正確順序釋放資源
};// 派生類:Circle
class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}void draw() override {cout << "Drawing a Circle with radius: " << radius << endl;}double area() override {return 3.14159 * radius * radius;}
};// 派生類:Rectangle
class Rectangle : public Shape {
private:double length, width;
public:Rectangle(double l, double w) : length(l), width(w) {}void draw() override {cout << "Drawing a Rectangle with length: " << length << ", width: " << width << endl;}double area() override {return length * width;}
};int main() {// 用基類指針管理不同的形狀對象vector<Shape*> shapes;shapes.push_back(new Circle(5.0));        // 添加一個圓shapes.push_back(new Rectangle(4.0, 6.0)); // 添加一個矩形// 使用多態(tài)調(diào)用派生類實現(xiàn)for (Shape* shape : shapes) {shape->draw();cout << "Area: " << shape->area() << endl;}// 釋放資源for (Shape* shape : shapes) {delete shape;}return 0;
}

輸出為:

Drawing a Circle with radius: 5
Area: 78.53975
Drawing a Rectangle with length: 4, width: 6
Area: 24

這樣僅僅把抽象類 Shape 當作一個接口規(guī)范類,我們在每一個繼承它的子類中都定義了專屬于自身的實現(xiàn)(多態(tài)),而且因為抽象類中有一些共用的屬性,所以相比單獨的定義 Circle、Rectangle類,通過抽象類衍生派生類更加方便。

2.3.7 動態(tài)聯(lián)編和靜態(tài)聯(lián)編

當我們在程序中寫下一個函數(shù)并調(diào)用它時,編譯器會決定如何執(zhí)行這個函數(shù)。這一過程不僅僅是簡單地“代碼怎么寫,編譯器就怎么執(zhí)行”。特別是在C++中,由于引入了函數(shù)重載、重寫(虛函數(shù))等機制,同一個函數(shù)名可能對應多個實現(xiàn),因此編譯器需要進一步確定到底調(diào)用哪個具體的函數(shù)實現(xiàn)。

什么是聯(lián)編?

聯(lián)編就是將程序中的函數(shù)調(diào)用與具體的函數(shù)實現(xiàn)關聯(lián)起來的過程。通俗來說,聯(lián)編相當于讓程序知道“這個函數(shù)名對應的具體操作在哪里”。

在C語言中,聯(lián)編相對簡單:每個函數(shù)名唯一地對應一個函數(shù)實現(xiàn),因此函數(shù)調(diào)用和具體實現(xiàn)之間的關系在編譯時就能完全確定。但在C++中,函數(shù)重載(同名函數(shù)參數(shù)不同)和虛函數(shù)(子類覆蓋父類方法)等特性增加了聯(lián)編的復雜性,編譯器需要更多信息來決定調(diào)用哪一個具體的函數(shù)實現(xiàn)。

聯(lián)編的類型

靜態(tài)聯(lián)編是在程序的編譯階段完成的,也叫早期聯(lián)編。它在編譯時確定函數(shù)調(diào)用與具體實現(xiàn)之間的關系,運行時無需再做額外的判斷,效率較高。通常用于普通函數(shù)調(diào)用,包括非虛函數(shù)的調(diào)用和函數(shù)重載。編譯器會根據(jù)函數(shù)名和參數(shù)列表,直接找到匹配的函數(shù)實現(xiàn)。代碼執(zhí)行時,已經(jīng)明確知道調(diào)用的是哪段代碼。

動態(tài)聯(lián)編是在程序的運行階段完成的,也叫晚期聯(lián)編。它允許程序在運行時,根據(jù)實際的對象類型或上下文,動態(tài)選擇函數(shù)的實現(xiàn)。動態(tài)聯(lián)編通常用于虛函數(shù)的調(diào)用,因為在多態(tài)場景中,編譯器無法在編譯階段確定具體調(diào)用的是哪個函數(shù)。編譯器會為每個類生成一個虛函數(shù)表(vtable),運行時根據(jù)對象類型從虛函數(shù)表中查找并調(diào)用正確的函數(shù)。

雖然動態(tài)聯(lián)編的靈活性很高,但是因為虛函數(shù)表的生成、調(diào)用需要消耗一定的資源,所以靜態(tài)聯(lián)編被用作C++的默認選擇,因為靜態(tài)聯(lián)編在編譯時完成,效率高于動態(tài)聯(lián)編。

http://www.risenshineclean.com/news/2741.html

相關文章:

  • 網(wǎng)站建設有哪些需要注意的關鍵細節(jié)百度推廣客服中心
  • 特價做網(wǎng)站谷歌搜索引擎入口2022
  • 網(wǎng)站開發(fā)參考文獻期刊seo常用工具網(wǎng)站
  • wordpress修改主題文件做seo需要哪些知識
  • 重慶企業(yè)vi設計公司安卓優(yōu)化大師下載安裝
  • 牛網(wǎng)網(wǎng)站建設互聯(lián)網(wǎng)營銷推廣怎么做
  • 蘇州市住房和城鄉(xiāng)建設局網(wǎng)站首頁手機如何做網(wǎng)站
  • 查詢建筑企業(yè)網(wǎng)站太原seo快速排名
  • 網(wǎng)站url和網(wǎng)站域名重慶森林經(jīng)典臺詞 鳳梨罐頭
  • 如何下載ppt模板免費短視頻關鍵詞seo優(yōu)化
  • 武漢市建設委員會網(wǎng)站如何使用網(wǎng)絡營銷策略
  • 電器網(wǎng)站建設策劃書百度信息流推廣技巧
  • 茂名做網(wǎng)站公司外貿(mào)營銷網(wǎng)站建設介紹
  • 優(yōu)化網(wǎng)站作用永久免費域名申請
  • 手機做網(wǎng)站多少錢中小企業(yè)管理培訓課程
  • 昆明住房和城鄉(xiāng)建設部網(wǎng)站關鍵詞制作軟件
  • 免費英文建設網(wǎng)站企點qq官網(wǎng)
  • 購買手表的網(wǎng)站百度收錄需要多久
  • 網(wǎng)站規(guī)劃明細表昆明seo關鍵字推廣
  • 如何做 試題類 網(wǎng)站百度競價投放
  • 哪些做展架圖的網(wǎng)站好代運營服務
  • 網(wǎng)站后臺是怎么做的網(wǎng)頁怎么做出來的
  • 西安網(wǎng)站建設hyk123網(wǎng)站制作培訓
  • 網(wǎng)站開發(fā)所需費用自學seo能找到工作嗎
  • seo是什么意思網(wǎng)絡用語seo外包公司哪家好
  • 專做外貿(mào)的網(wǎng)站有哪些免費十八種禁用網(wǎng)站
  • 凡客做網(wǎng)站廣州搜發(fā)網(wǎng)絡科技有限公司
  • 蘇州好的做網(wǎng)站的公司哪家好seo雙標題軟件
  • 怎樣用jsp做網(wǎng)站微信推廣平臺
  • 網(wǎng)站怎么做模板切換山東免費網(wǎng)絡推廣工具