1. 程式人生 > >第14章 C++中的程式碼重用

第14章 C++中的程式碼重用

本章內容包括:

  • has-a關係
  • 包含物件成員的類
  • 模板類valarray
  • 私有和保護繼承
  • 多重繼承
  • 虛基類
  • 建立類模板
  • 使用類模板
  • 模板的具體化

通常,包含,私有繼承和保護繼承用於實現has-a關係,即新的類將包含另一個類的物件. 
類模板使我們能夠使用通用術語定義類,然後使用模板來建立針對特定型別定義的特殊類.

14.1 包含物件成員的類 
14.1.1 valarray類簡介

  • valarray類是由標頭檔案valarray支援的.valarray被定義為一個模板類,以便能夠處理不同的資料型別.

14.1.2Student類的設計

  • 通常,用於建立has-a關係的C++技術是組合(包含),即建立一個包含其他類物件的類.
  • 介面和實現:使用公有繼承時,類可以繼承介面,可能還有實現(基類的純虛擬函式提供介面,但不提供實現).獲得介面是is-a關係的組成部分.而使用組合,類可以獲得實現,但不能獲得介面.不繼承介面是has-a關係的組成部分.
  • 對於has-a關係來說,類物件不能自動獲得被包含物件的介面是一件好事.

14.1.3 Student類示例

  • 程式清單14.1 studentc.h
  • 可以用一個引數呼叫的建構函式將用作從引數型別到類型別的隱式轉換函式;但這通常不是好主意.
  • 使用explicit關閉隱式轉換.
  • C++和約束:C++包含讓程式設計師能夠限制程式結構的特性—使用explicit防止單引數建構函式的隱式轉換,使用const限制方法修改資料,等等.這樣做的根本原因是:在編譯階段出現錯誤優於在執行階段出現錯誤.
  • 1.初始化被包含的物件. 
    • 對於繼承的物件,建構函式在成員初始化列表彙總使用類名來呼叫特定的基類建構函式.對於成員物件,建構函式則使用成員名.
    • C++要求在構建物件的其他部分之前,先構建繼承物件的所有成員物件.因此,如果省略初始化列表,C++將使用成員物件所屬類的預設建構函式.
    • 初始化順序:當初始化列表包含多個專案時,這些專案被初始化的順序為他們被宣告的順序,而不是它們在初始化列表中的順序.例如,假設Student建構函式如下:
Student(const char * str,const double * pd,int n)
    : scores(pd,n), name(str){}
  • 1
  • 2
  • 則name成員仍將首先被初始化,因為在類定義中它首先被宣告.對於這個例子來說,初始化順序並不重要,但如果程式碼使用一個成員的值作為另一個成員的初始化表示式的一部分時,初始化順序就非常重要了.
  • 2.使用被包含物件的介面 
    • 程式清單14.2 studentc.cpp
  • 3.使用新的Student類 
    • 程式清單14.3 use_stuc.cpp

14.2 私有繼承

  • 私有繼承提供的特性與包含相同:獲得實現,但不獲得介面.所以,私有繼承也可以用來實現has-a關係.

14.2.1 Student類示例(新版本)

  • 實際上,private是預設值,因此省略訪問限定符也將導致私有繼承.
  • 使用多個基類的繼承被稱為多重繼承multiple inheritance,MI.通常,MI尤其是公有MI將導致一些問題,必須使用額外的語法規則來解決它們.
  • 1.初始化基類元件 
    • 對於繼承類,新版本的建構函式將使用成員初始化列表語法,它使用類名而不是成員名來標識建構函式.
    • 程式清單14.4 studenti.h
  • 2.訪問基類的方法 
    • 使用包含時將使用物件名來呼叫方法,而使用私有繼承時將使用類名和作用域解析運算子來呼叫方法.
  • 3.訪問基類物件 
    • 使用作用域解析運算子可以訪問基類的方法,但如果要使用基類物件本身,該如何做呢?答案是使用強制型別轉換.
  • 4.訪問基類的友元函式 
    • 用類名顯式的限定函式名不適合於友元函式,這是因為友元不屬於類.然而,可以通過顯示地轉換為基類來呼叫正確的函式.
    • 在私有繼承中,在不進行顯式型別轉換的情況下,不能將指向派生類的引用或指標賦給基類引用或指標.
    • 程式清單14.5 studenti.cpp
  • 5.使用修改後的Student類 
    • 程式清單14.6 use_stui.cpp

14.2.2 使用包含還是私有繼承

  • 由於既可以使用包含,也可以使用私有繼承來建立has-a關係,那麼應使用哪種方式呢?大多數C++程式設計師傾向於使用包含. 
    • 首先,它易於理解.
    • 其次,繼承會引起很多問題,尤其從多個基類繼承時.
    • 另外,包含能夠包括多個同類的子物件.
  • 然而,私有繼承所提供的特性確實比包含多.
  • 另一種需要使用私有繼承的情況是需要重新定義虛擬函式.派生類可以重新定義虛擬函式,但包含類不能.
  • 提示:通常,應使用包含來建立has-a關係;如果新類需要訪問原有類的保護成員,或需要重新定義虛擬函式,則應使用私有繼承.

14.2.3 保護繼承

  • 保護繼承是私有繼承的辯題.保護繼承在列出基類時使用關鍵字protected.

表14.1 各種繼承方式

特徵 公有繼承 保護繼承 私有繼承
公有成員變成 派生類的公有成員 派生類的保護成員 派生類的私有成員
保護成員變成 派生類的保護成員 派生類的保護成員 派生類的私有成員
私有成員變成 只能通過基類介面訪問 只能通過基類介面訪問 只能通過基類介面訪問
能否隱式向上轉換 是(但只能在派生類中)

14.2.4 使用using重新定義訪問許可權

  • 注意,using宣告只使用成員名—沒有圓括號,函式特徵標和返回型別.
  • using宣告只適用於繼承,而不適用於包含.
  • 有一種老式方式可用於在私有派生類中重新宣告基類方法,極簡啊ing防發明放在派生類的公有部分.這看起來像不包含關鍵字using的using宣告.這種方法已被摒棄,即將停止使用.

14.3 多重繼承

  • MI帶來的新問題: 
    • 從兩個不同的基類繼承同名方法;
    • 從兩個或更多相關基類那裡繼承同一個類的多個例項.
  • 程式清單14.7 worker0.h
  • 程式清單14.8 worker0.cpp
  • 程式清單14.9 worktest.cpp

14.3.1 有多少worker

  • C++引入多重繼承的同時引入了一種新技術—虛基類virtual base class,使MI成為可能.
  • 1.虛基類 
    • 虛基類使得從多個類(它們的基類相同)派生出的物件只繼承一個基類物件.
    • C++對這種新特性也使用關鍵字virtual—有點像關鍵字過載.
  • 2.新的建構函式規則 
    • C++在基類是虛的時,禁止資訊通過中間類自動傳遞給基類.
    • 如果不希望預設建構函式來構造虛基類物件,則需要顯式地呼叫所需的基類建構函式.
    • 警告:如果類有間接虛基類,則除非只需使用該虛基類的預設建構函式,否則必須顯式的呼叫該虛基類的某個建構函式.

14.3.2 哪個方法?

  • 對於單繼承,如果沒有重新定義Show(),則將使用最近祖先中的定義.而在多重繼承中,每個直接祖先都有一個Show()函式,這使得上述呼叫是二義性的.
  • 警告:多重繼承可能導致函式呼叫的二義性.
  • 總之,在祖先相同時,使用MI必須引入虛基類,並修改建構函式初始化列表的規則.另外,如果在編寫這些類時沒有考慮到MI,則還可能需要重新編寫它們.
  • 程式清單14.10 workermi.h
  • 程式清單14.11 workermi.cpp
  • 程式清單14.12 workmi.cpp
  • 1.混合使用虛基類和非虛基類 
    • 當類通過多條虛途徑和非虛途徑繼承某個特定的基類時,該類包含一個表示所有的虛途徑的基類子物件和分別表示各條非虛途徑的多個基類子物件.
  • 2.虛基類和支配 
    • 一個成員名如何優先於另一個成員名呢?派生類中的名稱優先於直接或間接祖先類中的相同名稱.
    • 虛二義性規則與訪問規則無關.

14.3.3 MI小結

  • 從虛基類的一個或多個例項派生而來的類將只繼承了一個基類物件.為實現這種特性,必須滿足其他要求: 
    • 有間接虛基類的派生類包含直接呼叫間接呼叫間接基類建構函式的建構函式,這對於間接非虛基類來說是非法的.
    • 通過優先規則解決名稱二義性.
  • MI會增加程式設計的複雜程度.然而,這種複雜性主要是由於派生類通過多條途徑繼承同一個基類引起的.避免這種情況後,唯一需要注意的是,在必須時對繼承的名稱進行限定.

14.4 類模板 
14.4.1 定義類模板

  • 模板類以下面這樣的程式碼開頭:template ,較新的C++實現允許在這種情況下使用不太容易混淆的關鍵字typename代替class:template //newer choice.
  • 不能將模板成員函式放在獨立的實現檔案彙總,但支援該關鍵字的編譯器不多:C++11不再這樣使用關鍵字export,而將其保留用於其他用途.
  • 由於模板不是函式,他們不能單獨編譯.模板必須與特定的模板示例化請求一起使用.
  • 程式清單14.13 stacktp.h

14.4.2 使用模板類

  • 僅在程式包含模板並不能生成模板類,而必須請求示例化.
  • 在kernel宣告中,型別引數Type的值為int
  • 注意:必須顯式地提供所需的型別,這與常規的函式模板是不同的,因為編譯器可以根據函式的引數來信來確定要生成哪種函式.
  • 程式清單14.14 stacktem.cpp

14.4.3 深入探討模板類

  • 可以使用指標棧,但如果不對程式做重大修改,將無法很好地工作.編譯器可以建立類,但使用效果如何就因人而異了.
  • 1.不正確地使用指標棧
  • 2.正確使用指標棧 
    • 使用指標棧的方法之一是,讓呼叫程式提供一個指標資料,其中每個指標都指向不同的字串.注意,建立不同指標是呼叫程式的職責,而不是棧的職責.棧的任務是管理指標,而不是穿件指標.
    • 程式清單14.15 stcktp1.h
    • 程式清單14.16 stckoptr1.cpp

14.4.4 陣列模板示例和非型別引數

  • 程式清單14.17 arraytp.h 
    • 注意模板頭emplate < class T,int n > ,關鍵字class(或在這種上下文中等價的關鍵字typename)指出T為型別引數,int指出n的型別為int.這種引數(制定特殊的型別而不是用作泛型名)稱為非型別non-type或表示式expression引數.
    • 表示式引數有一些限制.表示式引數可以是整型,列舉,引用或指標.
    • 模板程式碼不能修改引數的值,也不能使用引數的地址.

14.4.5 模板多功能性

  • 可以將用於常規類的技術用於模板類.模板類可用作基類,也可用做元件類,還可用作其他模板的型別引數.
  • 1.遞迴使用模板 
    • 請注意,在模板語法中,維的順序與等價的二維陣列相反.
    • 程式清單14.18 twod.cpp
  • 2.使用多個型別引數 
    • 程式清單14.19 pairs.cpp
  • 3.預設型別模板引數 
    • 類模板可以為型別引數提供預設值.雖然可以為類模板型別引數提供預設值,但不能為函式模板引數提供預設值.然而,可以為非型別引數提供預設值,這對於類模板和函式模板都是適用的.

14.4.6 模板的具體化

  • 1.隱式例項化
  • 2.顯式例項化 
    • 當使用關鍵字template並指出所需型別來宣告類時,編譯器將生成類宣告的顯式例項化.宣告必須位於模板定義所在的名稱空間中.
  • 3.顯式具體化 
    • 其是特定型別(用於替換模板眾的泛型)的定義.有時候,可能需要在為特殊型別例項化時,對模板進行修改,使其行為不同.在這種情況下,可以建立顯式具體化.
  • 4.部分具體化 
    • C++還允許部分具體化,即部分限制模板的通用性.

14.4.7 成員模板

  • 模板可用作結構,類或模板類的成員.要完全實現STL的設計,必須使用這項特性.
  • 程式清單14.20 tempmemb.cpp

14.4.8 將模板用作引數

  • 程式清單14.21 tempparm.cpp

14.4.9 模板類和友元

  • 模板類宣告也可以有友元.模板的友元分3類: 
    • 非模板友元;
    • 約束bound模板友元,即友元的型別取決於類被例項化時的型別.
    • 非約束unbound模板友元,即友元的所有具體化都是類的每一個具體化的友元.
  • 1.模板類的非模板友元函式 
    • 程式清單14.22 frnd2tmp.cpp
  • 2.模板類的約束模板友元函式 
    • 要約束模板友元作準備,要使類的每一個具體化都獲得與友元匹配的具體化.
    • 程式清單14.23 tmp2tmp.cpp
  • 3.模板類的非約束模板友元函式 
    • 通過在類內部宣告模板,可以建立非約束友元函式,即每個函式具體化都是每個類具體化的友元.
    • 對於非約束友元,友元模板型別引數與模板類型別引數是不同的.
    • 程式清單14.24 manyfrnd.cpp

14.4.10 模板別名(C++11)

  • 可以使用typedef為模板具體化制定別名.C++11新增了一項功能—使用模板提供一系列別名.
  • C++11允許將語法using=用於非模板.用於非模板時,這種語法與常規typedef等價.
  • 習慣這種語法後,您可能發現其可讀性更強,因為它讓型別名和型別資訊更清晰.
  • C++11新增的另一項模板功能是可變引數模板,讓您能夠定義這樣的模板類和模板函式,即可接受可變數量的引數.

14.5 總結 
14.6 複習題 
14.7 程式設計練習

附件:本章原始碼下載地址