-
基于三個基本概念:面向對象程式設計(OOP)
、資料抽象
、繼承
動态綁定
- 繼承和動态綁定對程式的影響:
- 可以更容易地定義與其他類相似但不完全相同的新類
- 使用這些相似的類寫程式時,可在一定程度上忽略它們的差別
OOP:概述
- 面向對象程式設計的核心思想是:資料抽象、繼承、動态綁定
- 使用
,可将類的接口與實作分離資料抽象
- 使用
,可定義相似的類型并對其相似關系模組化繼承
- 使用
,可在一定程度上忽略相似類型的差別,以統一的方式使用它們的對象動态綁定
- 使用
- 通過繼承聯系在一起的類有一種層次關系:通常在層次關系的根部有一個基類,其他類直接或間接地由基類繼承而來,稱為派生類。
-
定義層次關系中的共同成員,每個基類
定義各自特有的成員派生類
-
:基類希望它的派生類各自定義自身版本的這種函數,則在基類中聲明為虛函數,形式為傳回類型前加關鍵字虛函數
virtual
-
:派生類必須通過類派生清單來明确指出從哪個/哪些基類繼承而來。形式為冒号後緊跟逗号分隔的基類清單,每個基類前可有通路說明符類派生清單
- 例子:虛函數和類派生清單
| |
- 派生類重新定義的虛函數可在聲明時加
,但并非強制(基類中定義為虛的函數,在派生類中預設為虛)virtual
- 派生類必須在内部對需要重新定義的虛函數進行聲明。
- C++11允許派生類顯式注明用哪個成員函數覆寫基類的虛函數,形式是在其形參清單後加
關鍵字override
- 通過動态綁定,可用同一段代碼分别處理基類和派生類的對象
-
:使用基類的引用/指針調用虛函數時,函數的版本由運作時的對象類型決定動态綁定/運作時綁定
- 例子:同一段代碼分别處理基類和派生類的對象
| |
定義基類和派生類
定義基類
- 例子:定義基類
| |
- 繼承關系中根節點的類通常應定義一個
,即使它不執行任何操作虛析構函數
- 對于虛函數,派生類經常要提供自己的新定義來
從基類繼承而來的舊定義覆寫
- 基類的兩種成員函數:
- 基類希望其派生類進行覆寫:定義為虛函數,使用指針/引用調用時,在運作時動态綁定
- 基類希望其派生類直接繼承:解析過程發生在編譯期而非運作時
- 基類在成員函數聲明語句前加關鍵字
将其聲明為虛函數,使用動态綁定。virtual
- 任何
的除構造函數之外
函數都可以是虛函數非static
- 關鍵字virtual隻能出現在類内部的聲明語句前,不能用于類外的定義
- 若基類把一個函數聲明為虛函數,則在其派生類中也隐式地是虛函數
- 派生類可繼承基類的成員,但派生類的成員函數不能通路從基類繼承而來的
成員private
- 基類的
成員可允許其派生類通路,但禁止其他使用者通路protected
定義派生類
- 派生類必須使用
明确指出是從哪個/哪些基類繼承而來的類派生清單
- 類派生清單的形式是一個冒号後緊跟以逗号分隔的基類清單,每個基類前可有通路說明符public/protected/private
- 對于需要覆寫的成員函數,派生類必須重新聲明
- 例子:定義派生類
| |
- 類派生清單中的通路說明符是控制派生類從基類繼承而來的成員是否對派生類的使用者可見
- public派生:
- 若一個派生是public的,則基類的public成員也是派生類接口的一部分
- 可将public派生類型的對象綁定到基類的引用/指針上
- 大多數類都隻繼承自一個基類,這稱為
單繼承
- 派生類經常(但不總是)覆寫它繼承的虛函數,若未覆寫則直接繼承基類中的版本(類似普通成員函數)
- 派生類可在其覆寫的函數前使用
關鍵字(并非必要),基類中的虛函數在派生類中隐式地也是虛函數virtual
- C++11可用
關鍵字顯式注明覆寫基類中的虛函數,此時若未覆寫則報錯override
- override出現在形參清單後、const函數的const關鍵字後、引用成員函數的引用限定符後
- 派生類對象包含多個組成部分:
-
:從基類中繼承而來的部分,若繼承自多個基類,則有多個基類部分基類部分
-
:派生類自己定義的非static成員派生類部分
-
- C++标準并未規定派生類對象在記憶體中如何分布,基類部分和派生類部分并不一定是各自連續的
-
:可将基類的指針/引用綁定到派生類對象的基類部分,這種轉換是隐式的派生類到基類的類型轉換
- 例子:派生類到基類的類型轉換
| |
-
:派生類不能直接初始化從基類繼承而來的成員,必須使用基類的構造函數來初始化其基類部分每個類控制自己成員的初始化
- 在派生類的構造函數初值清單中,将實參傳遞給基類的構造函數來初始化基類部分,否則基類預設初始化
- 派生類構造函數運作過程:
- 初始化基類部分:在初值清單中執行基類構造函數,否則預設初始化
- 按聲明順序初始化派生類部分的成員
- 執行派生類構造函數體
- 例子:派生類構造函數初值清單中初始化基類部分
| |
- 派生類成員可通路基類的public/protected成員
- 派生類的作用域嵌套在基類作用域内部,故在派生類中可直接使用基類成員
-
:派生類不能直接初始化基類成員,而應遵循基類接口,使用基類構造函數每個類定義自己的接口
- 若基類定義了
,則在整個繼承體系中隻有該成員的唯一定義。無論派生出多少個派生類,對static成員來說都隻有唯一的執行個體static成員
- static成員遵循通用的通路控制。若某static成員可通路,則既可通過基類使用也可通過派生類使用
- 例子:static成員
| |
- 派生類隻聲明不定義時,不可包含派生清單。聲明是讓程式知道名字的存在和實體類型,派生清單是定義的一部分。
- 若要将某類用作基類,則必須已定義,不可隻聲明。因為定義派生類時必須已知基類,才可包含并使用基類部分。
- 一個類不能派生它本身
- 一個類可以是派生類,也可是其他類的基類
-
出現在派生清單中,直接基類
通過直接基類繼承而來間接基類
- 每個類都繼承其直接基類的所有成員,故最終的派生類包含其直接基類的子對象以及每個間接基類的子對象
- C++11使用
關鍵字禁止一個類被繼承final
- 例子:禁止類被繼承
| |
類型轉換與繼承
- 把引用/指針綁定到一個對象的情況:
- 引用/指針的類型與對象一緻
- 對象的類型含有可接收的const轉換規則
- 可将基類類型的引用/指針綁定到派生類對象
- 使用基類的引用/指針時,并不知道它綁定的對象的真實類型(運作時才可确定)
- 基類類型的智能指針也支援動态綁定
- 靜态類型和動态類型:
-
在編譯期已知,是變量/表達式聲明時的類型靜态類型
-
到運作期才可知,是變量/表達式在記憶體中對象的類型動态類型
-
- 隻有基類的引用/指針才可能發生靜态類型和動态類型不一緻的情況
- 基類和派生類之間的自動類型轉換:
- 存在派生類向基類轉換,即
:每個派生類都有基類部分,基類引用/指針可綁定到基類部分基類引用/指針可指向派生類
- 不存在基類向派生類的轉換,即
:基類的對象可能是派生類的一部分,也可能不是派生類引用/指針不可指向基類
- 特别是,即使基類的引用/指針綁定到派生類,也不可将其指派給該派生類類型的引用/指針
- 存在派生類向基類轉換,即
- 例子:派生類的引用/指針不可指向基類
| |
- 基類向派生類的顯式轉換:
- 編譯器隻能檢查引用/指針的靜态類型來判斷轉換是否合法,故無法确定基類向派生類的轉換在運作時是否安全,隐式轉換會報錯
- 若基類中有虛函數,則可用
來請求向派生類的類型轉換,該轉換的安全檢查将在運作時執行dynamic_cast
- 若已知某個基類向派生類的轉換一定是安全的,則可用
來強制覆寫編譯器的檢查static_cast
- 派生類對象向基類對象的隐式轉換(slice down):
- 派生類向基類的自動轉換隻對指針/引用有效,在派生類對象和基類對象之間不存在隐式轉換。直接轉換對象得到的可能并非預期
- 對類類型的對象初始化/指派時,實質上是在調用構造函數/指派算符,它們參數的類型經常是該類類型的引用。
- 由于參數是引用,故允許給基類的構造/拷貝/移動/指派操作傳遞派生類對象。這些操作不是虛函數,故實際上運作的是基類的版本,它隻能處理基類成員。
- 給基類的構造/拷貝/移動/指派操作傳遞派生類對象時,隻處理基類成員,忽略派生類自己的成員,派生類部分被
了切掉(sliced down)
- 例子:派生類對象用于構造基類對象時,派生類部分被切掉
| |
- 具有繼承關系的類之間的轉換規則:
- 從派生類到基類的類型轉換隻對引用/指針有效
- 基類向派生類不存在隐式轉換
- 派生類向基類的轉換也可能因為通路受限而不可行(隻有public繼承,即派生類中的基類部分可被使用者通路時,使用者才可用基類指針通路派生類成員)
- 由于拷貝控制成員參數是引用,故經常可将派生類拷貝/移動/指派給基類,此時隻處理基類部分
虛函數
- 由于隻有運作時才知道調用了哪個虛函數,故所有虛函數都必須有定義
- 虛函數調用的版本:
- 通過引用/指針調用虛函數時,被調用的版本是引用/指針綁定的動态類型對應的版本
- 通過非引用非指針的表達式調用虛函數時,編譯期決定調用的版本為靜态類型對應的版本
- 例子:引用/指針調用虛函數執行動态版本,非引用非指針調用虛函數執行靜态版本
| |
-
:具有繼承關系的多個了類型稱為多态類型,因為可使用它們的多種形式而無需在意它們的差異多态
- 允許引用/指針的靜态類型和動态類型不一緻是C++支援運作時多态的根本
- 使用基類的引用/指針調用基類成員函數時:
- 若該函數為虛,則運作時才可确定調用的是動态類型對應的版本
- 若該函數非虛,則編譯期即可确定調用的是靜态類型對應的版本
- 當且僅當引用/指針調用虛函數時,對象的靜态類型和動态類型才會不同,使得解析調用發生在運作時
- 派生類中覆寫了虛函數時,可再次使用virtual關鍵字聲明,但并非必須。基類中被聲明為虛的函數在派生類中隐式為虛
- 虛函數的形參清單和傳回類型:
- 派生類虛函數的形參必須與被它覆寫的基類虛函數完全一緻。
- 派生類虛函數的傳回類型必須與基類虛函數一緻。除非傳回類型是類自身的引用/指針,此時要求從派生類到基類的轉換可通路(即派生類中的基類部分可被使用者通路)。
- 若派生類定義了函數,它與基類中虛函數同名但形參清單不同,則是
而不是重載
。編譯器認為新函數與繼承自基類的函數是獨立的,新函數不會被基類的引用/指針調用。覆寫
- C++11允許使用
關鍵字來說明派生類中的虛函數覆寫了基類的虛函數。若使用override标記了某函數但它未覆寫基類的虛函數,則報錯override
-
标記的函數未覆寫基類虛函數則報錯override
- 隻有虛函數才可被覆寫,非虛函數要麼重載要麼重複定義
- 将某函數指定為
,禁止覆寫該函數final
- final和override說明符出現在形參清單(包括const和引用修飾符)和尾置傳回類型之後
- 例子:override和final
| |
- 虛函數可以有預設實參,若某次函數調用使用了預設實參,則實參值由靜态類型确定
- 通過基類的引用/指針調用函數,則使用基類中的預設實參,即使運作的是派生類版本的函數。是以虛函數的預設實參應與基類一緻
- 若希望對虛函數的調用不要動态綁定,而是指定某個類的版本,則可用
作用域算符
- 通常隻有成員函數或友元的代碼才需要使用作用域算符來回避動态綁定
- 當派生類的虛函數調用它覆寫的基類虛函數時,需要手動指定虛函數版本,回避動态綁定(否則調用自身,無限遞歸)
- 例子:用作用域算符手動指定虛函數版本
| |
抽象基類
- 若一個基類隻用于對其派生類提供抽象,但不希望産生該基類的執行個體,則可将該基類定義為
抽象基類(ABC)
- 将一個虛函數定義為
,可明确告訴編譯器這個函數隻用于抽象,沒有實際意義,無需被定義純虛函數
- 将虛函數定義為純虛函數的方法是在函數體的位置寫
,且隻能出現在類内部的虛函數聲明語句處=0
- 例子:純虛函數和抽象基類
| |
- 不可直接定義抽象基類的對象,但其派生類的構造函數可使用抽象基類的構造函數來建構派生類的基類部分
- 也可為純虛函數提供定義,但函數體必須在類外部。即,類内部不可為=0的函數再提供函數體
- 含有(或未經覆寫直接繼承)
的類是純虛函數
。抽象基類定義接口,其派生類可覆寫其接口。不能直接建立抽象基類的對象抽象基類
- 例子:繼承自抽象基類
| |
-
負責重新設計類的體系,以便将操作/資料從一個類中移到另一個類中。對OOP而言重構很普遍重構
通路控制與繼承
- 每個類控制自己成員的初始化,還控制自己的成員對派生類是否可通路
- 使用
說明符來說明它希望被派生類通路但不希望被其他使用者通路的成員:protected
- 類似private,protected成員對類的使用者不可通路
- 類似public,protected成員對派生類的成員和友元可通路
- 派生類的成員和友元隻能通過派生類對象來通路其基類部分的protected成員,對基類對象中的protected成員不可通路
- 例子:派生類的成員和友元隻能通過派生類對象來通路其基類部分的protected成員
| |
- 派生類的成員/友元不可通路基類對象的protected成員的原因是:若可以通路,則隻需繼承基類并聲明友元(類似上例),即可規避protected的保護機制。
- 某個類對其繼承而來的成員的通路權限受兩方面影響:
-
:說明基類成員的權限(派生類能否通路該成員,使用者能否通路該成員)基類中該成員的通路說明符
-
:說明派生類中基類部分的權限(派生類的使用者能否通路其基類部分)類派生清單中的通路說明符
-
- 派生類的成員/友元能否通路直接基類的成員,隻與直接基類成員的通路說明符有關,與派生通路說明符無關
- 派生通路說明符的目的是控制派生類使用者(包括派生類對象和派生類的派生類)能否通路該派生類的基類部分
- 假設D繼承自B,則基類部分的通路控制:
- 若是
:D的基類部分在D中public,D的所有使用者都可通路其基類部分(基類部分的成員在D中保持基類中定義的通路控制)public繼承
- 若是
:D的基類部分在D中protected,D的派生類成員/友元可通路其基類部分(基類部分的public成員在D中變為protected)protected繼承
- 若是
:D的基類部分在D中private,隻有D的成員/友元可通路其基類部分(基類部分的成員在D中都變為private)private繼承
- 若是
- 例子:繼承中的通路控制
| |
- 派生類向基類的類型轉換是否可通路,由使用轉換的代碼和派生類的派生通路說明符共同決定。假定D繼承自B:
- 隻有D是public繼承B時,使用者代碼才能使用D向B的轉換,protected/private繼承不可轉換
- D以任何方式繼承B,D的成員函數/友元都可使用D向B的轉換
- 若D是public/protected繼承B,則D的派生類成員/友元可使用D向B的轉換
- 對代碼中的某個節點而言,
,否則不可轉換。若派生類中基類部分是可通路的,則派生類向基類的轉換是可行的
- 三種使用者:
-
:使用類的對象,隻能通路類的public成員普通使用者
-
:類的成員和友元,它們可通路類中的所有成員實作者
-
:由類派生而來,可通路public成員和protected成員派生類
-
- 友元關系不可傳遞,也不可繼承。即,基類的友元不是派生類的友元,派生類的友元不是基類的友元
- 一個類的友元類的派生類不是這個類的友元
- 每個類負責控制自己成員的通路權限,即基類也控制派生類中基類部分的權限。特别的,基類的友元可通路派生類中基類部分的private
- 例子:友元與繼承
| |
- 若需改變派生類繼承的某個名字的通路級别,可使用using聲明
- 類内部使用
,可對該類的直接/間接基類的任何可通路成員重定義通路權限。新的通路權限是該using語句所在處的權限using聲明
- 隻有該類内部自己可通路的成員,才可用using改變權限
- 例子:using改變通路權限
| |
- struct和class差別:
- 成員通路說明符:struct預設是public成員,class預設是private成員
- 派生通路說明符:struct預設是public繼承,class預設是private繼承
- 沒有其他差別
- 例子:預設派生通路說明符
| |
繼承中的類作用域
- 每個類定義一個自己的作用域
- 類存在繼承關系時,派生類的作用域嵌套在基類作用域中。若一個名字在派生類中無法解析,則去基類中尋找定義
- 由于繼承關系的類作用域嵌套,是以派生類可直接通路基類成員(而不需指定基類作用域)
- 派生類調用成員時名字的解析過程,例如
:Derv.func()
- 查找調用類型Derv的作用域
- 查找調用類型Derv的基類的作用域
- 沿着繼承鍊向最終的基類查找
- 對象/引用/指針的靜态類型決定該對象的哪些成員可見(即名字查找),即使靜态類型與動态類型不一緻。
- 例子:成員的名字查找取決于靜态類型
| |
- 當靜态類型與動态類型不一緻時,隻有虛函數會查找動态類型中的重定義。其他成員都取決于靜态類型,包括虛函數的名字查找也取決于靜态類型
- 派生類可重用直接/間接基類中的名字,此時内層作用域的定義将
外層作用域的同名定義隐藏
- 可用
來顯式使用被隐藏的基類成員作用域算符
- 例子:派生類的成員隐藏同名的基類成員
| |
- 最佳實踐:除了覆寫繼承而來的虛函數外,派生類最好不要重用基類中的其他名字
- 函數調用的解析過程,假定調用
或p->mem()
:obj.mem()
- 确定p/pbj的靜态類型
- 在靜态類型對應的類中查找名字mem,若未找到則依次在直接基類中查找直到繼承鍊頂端
- 找到名字mem後,進行正常的類型檢查判斷調用是否合法
- 假設調用合法,則編譯器根據mem是否為虛而産生不同代碼:
- 若mem是虛函數且通過引用/指針調用,則在運作時才會根據動态類型确定調用哪個版本的虛函數
- 否則編譯器産生正常的函數調用
- 聲明在内層作用域的函數不會重載外層作用域的函數,故派生類中的函數也不會重載其基類的成員。
-
:若派生類的成員與基類成員同名,則在派生類作用域中隐藏該基類成員。即使形參清單不一緻也會隐藏而不是重載名字查找先于類型檢查
- 不同作用域中的函數不是重載關系。但可手動指定作用域來通路
- 例子:派生類的函數隐藏而不是重載基類同名函數,通路基類函數時需指定作用域
| |
- 基類與派生類的同名虛函數必須有相同的形參清單。若它們形參清單不同,則是隐藏而不是覆寫
- 例子:形參清單不同則是隐藏而不是覆寫
| |
- 成員函數無論是否是虛函數都可被重載。派生類可覆寫重載函數的0個或多個執行個體。
- 若派生類希望基類的所有重載虛函數都對它可見,則應或者覆寫所有的版本,或者一個也不覆寫。因為隻要覆寫了一個,基類的函數名就會被隐藏
- 若派生類隻需覆寫重載集合中的一些而非全部,可使用using聲明解決名字被隐藏的問題
- 在派生類中使用
語句指定名字,可将基類的所有重載版本都添加到派生類作用域。此時,派生類隻需覆寫需要覆寫的重載版本即可,不需覆寫所有重載版本。對派生類未覆寫的重載版本的通路,實際上是對using聲明點的通路using聲明
構造函數與拷貝控制
- 繼承體系中的類也需要控制其對象執行構造/拷貝/移動/指派/析構時發生什麼。
- 若類未定義構造函數和拷貝控制,則編譯器會合成
虛析構函數
- 基類通常應定義虛析構函數,使得派生類可用自己的析構函數覆寫它,這樣就可動态配置設定繼承體系中的對象。
- delete動态對象的指針時,用此指針調用該對象的析構函數。若該對象處于繼承體系中,則指針的靜态類型可能與指向對象的動态類型不比對。基類中将析構函數定義為虛,可確定執行正确的析構版本
- 若基類的析構函數不是虛,則delete一個指向派生類對象的基類指針是未定義
- 例子:基類中的虛析構函數
| |
- 三五法則的特例:基類需要虛析構函數是為了讓派生類動态綁定析構函數,其函數體不一定有操作, 是以不一定需要其他的拷貝控制操作。
- 若一個類定義了虛析構函數,即使用=default手動指定合成版本,編譯器也不會為該類合成移動操作
合成拷貝控制與繼承
- 基類/派生類的合成拷貝控制成員:
- 對類本身的成員依次初始化/拷貝/移動/指派/銷毀
- 派生類的合成拷貝控制成員還負責調用直接基類的對應操作來對直接基類部分初始化/拷貝/移動/指派/銷毀,要求對應成員可被派生類通路且非删除
- 例如,派生類的析構函數除銷毀自己的成員外,還調用直接基類的析構函數析構基類部分,依次調用直到繼承鍊頂端
- 基類/派生類也可将合成的預設構造函數/拷貝控制成員定義為删除,某些定義基類的方式也可能導緻派生類的合成成員被删除:
- 若基類的預設構造函數/拷貝構造函數/拷貝指派算符/析構函數是删除的或不可被派生類通路,則派生類中相應函數被删除。(編譯器無法對派生類的基類部分進行初始化/拷貝/指派/銷毀)
- 若基類的析構函數是删除的或不可被派生類通路,則派生類的預設構造函數/拷貝構造函數将被删除(無法銷毀派生類的基類部分)
- 若派生類使用=default請求移動操作,且基類中對應的成員是删除的或不可被派生類通路,則派生類中該操作被删除(無法移動派生類的基類部分)
- 若基類的析構函數是删除的或不可通路,則派生類的移動構造函數被删除
- 若基類沒有預設/拷貝/移動構造函數,則派生類中也不會定義相應操作
- 例子:基類删除拷貝構造函數
| |
- 大多數基類都會定義虛析構函數,此時基類沒有合成的移動操作,是以派生類中也沒有合成的移動操作
- 基類缺少移動操作會阻止派生類擁有自己的合成移動操作,故派生類需要移動時應在基類中定義移動操作
- 例子:在基類中定義所有拷貝構造成員
| |
派生類的拷貝控制成員
- 派生類的構造函數不僅要初始化自己的成員,還要初始化其基類部分。類似的,拷貝/移動構造函數/指派算符也必須處理基類部分的成員,即手動調用基類的對應成員
- 但析構函數隻負責銷毀派生類自己的成員(隐式銷毀)。類似的,派生類的基類部分也是被隐式銷毀,析構函數自動被調用。即不需要手動調用基類虛構函數
- 派生類的拷貝控制成員調用基類拷貝控制成員:
- 派生類定義拷貝/移動構造函數時,通常應在
中顯式調用基類的對應函數來初始化對象的基類部分。否則基類部分被初值清單
預設初始化
- 派生類的指派算符也必須顯式調用基類的指派算符,來為基類部分指派
- 派生類的析構函數體執行完成後,成員(包括基類部分)被隐式銷毀。故派生類析構函數不需顯式調用基類析構函數(基類部分銷毀時隐式調用),隻需要管理自己的資源。
- 成員析構的順序與構造相反:先執行派生類析構函數,再執行基類析構函數,直到繼承鍊頂端
- 派生類定義拷貝/移動構造函數時,通常應在
- 例子:派生類拷貝/移動構造函數/指派算符的初值清單中應顯式調用基類的對應函數,析構則不用
| |
- 構造和析構基類部分時派生類部分未完成:
- 派生類構造對象時,基類部分首先被構造。故執行基類構造函數時派生類部分還未初始化
- 派生類析構對象時,基類部分最後被析構。故執行基類析構函數時派生類部分不存在
- 由于執行派生類構造/析構函數時派生類部分是未完成狀态,故不可調用派生類版本的函數,調用的虛函數都是基類版本
- 在構造/析構對象過程中,正在執行哪個類的構造/析構函數,就認為正在構造/析構的對象是這個類型
- 若構造/析構函數調用了某個虛函數,則應該執行與構造/析構函數所屬類型對應的虛函數版本
繼承的構造函數
- C++11中,派生類可重用其直接基類定義的構造函數,但并非正常繼承(但為了友善仍稱為繼承)
- 一個類隻初始化它的直接基類,也隻繼承其直接基類的構造函數
- 類不能繼承預設/拷貝/移動構造函數。若派生類未定義它們,編譯器将合成它們
- 派生類繼承基類構造函數的方式是提供一條
語句。這條using語句不是為了使名字可見,而是令編譯器産生代碼:對于基類的每個構造函數,編譯器都生成一個對應的形參清單完全相同的派生類構造函數using聲明
- 例子:用using聲明來繼承構造函數
| |
- 繼承構造函數用的using聲明生成的構造函數形如:
derived(params):base(args){}
- derived是派生類名,base是基類名
- params是派生類構造函數的形參清單,args使用派生類形參調用基類構造函數
- 該構造函數隻初始化基類部分。若派生類有自己的成員,則預設初始化
- 與普通using聲明不一樣,繼承構造函數的using聲明不會改變該函數的通路權限,權限仍與基類保持一緻
- 用using聲明産生的派生類構造函數不可指定
或explicit
,這兩個屬性與基類保持一緻constexpr
- 若基類的構造函數有預設實參,則using産生的派生類構造函數不會繼承這些實參,而是産生多個版本的構造函數,每個版本分别省略一個含預設實參的形參。例如,若基類構造函數有兩個形參,其中一個有預設實參,則派生類繼承得到兩個構造函數,一個接受兩個形參(無預設實參),另一個隻接受一個形參(是基類中無預設實參的那個)
- 若基類有多個構造函數,則派生類繼承時一般繼承所有,除兩個例外:
- 派生類可繼承一部分構造函數,而為其他構造函數定義自己的版本。若自定義的版本與基類版本形參清單相同,則這個構造函數不會被繼承
- 預設/拷貝/移動構造函數不能被繼承,它們按照正正常則合成。若一個類隻有繼承的構造函數,則他也将有一個合成的預設構造函數
容器與繼承
- 使用容器存儲繼承體系的對象時,由于容器不可儲存不同類型的元素,故不可直接存儲具有繼承關系的多種對象
- 當派生類對象被指派給基類對象時,派生類部分被“切掉”,隻保留基類部分。是以若把派生類對象儲存在基類類型的容器中,它們就不再是派生類對象了
- 例子:派生類對象被放在基類容器中,被切掉
| |
- 希望在容器中存放具有繼承關系的對象時,實際存放的通常是基類的指針/智能指針。指針所指的動态類型可以是基類/派生類
- 可将派生類的内置指針/智能指針轉換為基類的内置指針/智能指針
- 例子:在容器中儲存基類指針
| |
編寫Basket類
- C++做OOP的一個悖論是,無法直接使用對象進行面向對象程式設計,而是必須使用指針/引用
- 大量使用指針會增加程式的複雜性,故經常定義一些輔助類(
)來處理需要大量指針操作的情況句柄類
-
語句不能處理多态,需要多态時應将new封裝進虛函數中new
- 例子:使用句柄類管理指針
| |