天天看點

第十五章:面向對象程式設計

  • 面向對象程式設計(OOP)

    基于三個基本概念:

    資料抽象

    繼承

    動态綁定

  • 繼承和動态綁定對程式的影響:
    • 可以更容易地定義與其他類相似但不完全相同的新類
    • 使用這些相似的類寫程式時,可在一定程度上忽略它們的差別

OOP:概述

  • 面向對象程式設計的核心思想是:資料抽象、繼承、動态綁定
    • 使用

      資料抽象

      ,可将類的接口與實作分離
    • 使用

      繼承

      ,可定義相似的類型并對其相似關系模組化
    • 使用

      動态綁定

      ,可在一定程度上忽略相似類型的差別,以統一的方式使用它們的對象
  • 通過繼承聯系在一起的類有一種層次關系:通常在層次關系的根部有一個基類,其他類直接或間接地由基類繼承而來,稱為派生類。
  • 基類

    定義層次關系中的共同成員,每個

    派生類

    定義各自特有的成員
  • 虛函數

    :基類希望它的派生類各自定義自身版本的這種函數,則在基類中聲明為虛函數,形式為傳回類型前加關鍵字

    virtual

  • 類派生清單

    :派生類必須通過類派生清單來明确指出從哪個/哪些基類繼承而來。形式為冒号後緊跟逗号分隔的基類清單,每個基類前可有通路說明符
  • 例子:虛函數和類派生清單
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
           
//基類
class Quote{
public:
    string isbn() const;
    //虛函數,允許派生類重新定義
    virtual double net_price(size_t n) const;
};
//派生類Bulk_quote繼承自基類Quote
class Bulk_quote: public Quote {
public:
    //實作派生類的虛函數,并覆寫基類中的版本
    double net_price(size_t n) const override;
};
           
  • 派生類重新定義的虛函數可在聲明時加

    virtual

    ,但并非強制(基類中定義為虛的函數,在派生類中預設為虛)
  • 派生類必須在内部對需要重新定義的虛函數進行聲明。
  • C++11允許派生類顯式注明用哪個成員函數覆寫基類的虛函數,形式是在其形參清單後加

    override

    關鍵字
  • 通過動态綁定,可用同一段代碼分别處理基類和派生類的對象
  • 動态綁定/運作時綁定

    :使用基類的引用/指針調用虛函數時,函數的版本由運作時的對象類型決定
  • 例子:同一段代碼分别處理基類和派生類的對象
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
           
double print_total(ostream &os,const Quote &item,size_t n){
    //item是基類類型的引用,它可以引用基類或派生類的對象
    //net_price是虛函數,則調用的版本取決于運作時item指向的真正的類型
    double ret=item.net_price(n);
    os<<"ISBN: "<<item.isbn()
      <<" # sold: "<<n
      <<" total due: "<<ret<<endl;
    return ret;
}
/* 假設basic是基類對象,bulk是派生類對象 */
print_total(cout,basic,20);
print_total(cout,bulk,20);
           

定義基類和派生類

定義基類

  • 例子:定義基類
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
           
class Quote{
public:
    //顯式合成預設構造函數
    Quote()=default;
    //構造函數初始化資料成員
    Quote(const std::string &book, double sales_price):
         bookNo(book),price(sales_price) {}
    std::string isbn() const {return bookNo;}
    //虛函數
    virtual double net_price(std::size_t n) const {return n*price;}
    //虛析構函數
    virtual ~Quote()=default;
private:
    std::string bookNo;
//protected成員僅允許其派生類通路
protected:
    double price=0.0;
};
           
  • 繼承關系中根節點的類通常應定義一個

    虛析構函數

    ,即使它不執行任何操作
  • 對于虛函數,派生類經常要提供自己的新定義來

    覆寫

    從基類繼承而來的舊定義
  • 基類的兩種成員函數:
    • 基類希望其派生類進行覆寫:定義為虛函數,使用指針/引用調用時,在運作時動态綁定
    • 基類希望其派生類直接繼承:解析過程發生在編譯期而非運作時
  • 基類在成員函數聲明語句前加關鍵字

    virtual

    将其聲明為虛函數,使用動态綁定。
  • 任何

    除構造函數之外

    非static

    函數都可以是虛函數
  • 關鍵字virtual隻能出現在類内部的聲明語句前,不能用于類外的定義
  • 若基類把一個函數聲明為虛函數,則在其派生類中也隐式地是虛函數
  • 派生類可繼承基類的成員,但派生類的成員函數不能通路從基類繼承而來的

    private

    成員
  • 基類的

    protected

    成員可允許其派生類通路,但禁止其他使用者通路

定義派生類

  • 派生類必須使用

    類派生清單

    明确指出是從哪個/哪些基類繼承而來的
  • 類派生清單的形式是一個冒号後緊跟以逗号分隔的基類清單,每個基類前可有通路說明符public/protected/private
  • 對于需要覆寫的成員函數,派生類必須重新聲明
  • 例子:定義派生類
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
           
//public繼承
class Bulk_quote: public Quote{
public:
    Bulk_quote()=default;
    Bulk_quote(const std::string &,double,std::size_t,double);
    //基類中是虛函數,派生類中隐式地也是虛函數
    double net_price(std::size_t) const override;
private:
    std::size_t min_qty=0;
    double discount=0.0;
};
           
  • 類派生清單中的通路說明符是控制派生類從基類繼承而來的成員是否對派生類的使用者可見
  • public派生:
    • 若一個派生是public的,則基類的public成員也是派生類接口的一部分
    • 可将public派生類型的對象綁定到基類的引用/指針上
  • 大多數類都隻繼承自一個基類,這稱為

    單繼承

  • 派生類經常(但不總是)覆寫它繼承的虛函數,若未覆寫則直接繼承基類中的版本(類似普通成員函數)
  • 派生類可在其覆寫的函數前使用

    virtual

    關鍵字(并非必要),基類中的虛函數在派生類中隐式地也是虛函數
  • C++11可用

    override

    關鍵字顯式注明覆寫基類中的虛函數,此時若未覆寫則報錯
  • override出現在形參清單後、const函數的const關鍵字後、引用成員函數的引用限定符後
  • 派生類對象包含多個組成部分:
    • 基類部分

      :從基類中繼承而來的部分,若繼承自多個基類,則有多個基類部分
    • 派生類部分

      :派生類自己定義的非static成員
  • C++标準并未規定派生類對象在記憶體中如何分布,基類部分和派生類部分并不一定是各自連續的
  • 派生類到基類的類型轉換

    :可将基類的指針/引用綁定到派生類對象的基類部分,這種轉換是隐式的
  • 例子:派生類到基類的類型轉換
1
2
3
4
5
6
           
/* 假設Bulk_quote從Quote派生而來 */
Quote item;         //基類對象
Bulk_quote bulk;    //派生類對象
Quote *p=&item;     //基類指針
p=&bulk;            //可将派生類對象綁定到基類指針,p指向派生類的基類部分
Quote &r=bulk;      //可将派生類對象綁定到基類引用,r綁定到派生類的基類部分
           
  • 每個類控制自己成員的初始化

    :派生類不能直接初始化從基類繼承而來的成員,必須使用基類的構造函數來初始化其基類部分
  • 在派生類的構造函數初值清單中,将實參傳遞給基類的構造函數來初始化基類部分,否則基類預設初始化
  • 派生類構造函數運作過程:
    • 初始化基類部分:在初值清單中執行基類構造函數,否則預設初始化
    • 按聲明順序初始化派生類部分的成員
    • 執行派生類構造函數體
  • 例子:派生類構造函數初值清單中初始化基類部分
1
2
3
4
           
/* 上下文:15.2.1中Quote的定義,15.2.2中Bulk_quote的定義,Quote是Bulk_quote的基類 */
//在派生類的構造函數初值清單中顯式構造基類部分
Bulk_quote::Bulk_quote(const std::string &book,double p,std::size_t qty,double disc):
                      Quote(book,p),min_qty(qty),discount(disc) {}
           
  • 派生類成員可通路基類的public/protected成員
  • 派生類的作用域嵌套在基類作用域内部,故在派生類中可直接使用基類成員
  • 每個類定義自己的接口

    :派生類不能直接初始化基類成員,而應遵循基類接口,使用基類構造函數
  • 若基類定義了

    static成員

    ,則在整個繼承體系中隻有該成員的唯一定義。無論派生出多少個派生類,對static成員來說都隻有唯一的執行個體
  • static成員遵循通用的通路控制。若某static成員可通路,則既可通過基類使用也可通過派生類使用
  • 例子:static成員
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
           
class Base{
public:
    static void statmem();  //static成員函數
};
class Derived: public Base{
    void f(const Derived &);
};
void Derived::f(const Derived &derived_obj){
    Base::statmem();        //Base定義了statmem
    Derived::statmem();     //Derived繼承了statmem
    derived_obj.statmem();  //通過Derived對象通路
    statmem();              //通過this對象通路
}
           
  • 派生類隻聲明不定義時,不可包含派生清單。聲明是讓程式知道名字的存在和實體類型,派生清單是定義的一部分。
  • 若要将某類用作基類,則必須已定義,不可隻聲明。因為定義派生類時必須已知基類,才可包含并使用基類部分。
  • 一個類不能派生它本身
  • 一個類可以是派生類,也可是其他類的基類
  • 直接基類

    出現在派生清單中,

    間接基類

    通過直接基類繼承而來
  • 每個類都繼承其直接基類的所有成員,故最終的派生類包含其直接基類的子對象以及每個間接基類的子對象
  • C++11使用

    final

    關鍵字禁止一個類被繼承
  • 例子:禁止類被繼承
1
2
3
4
5
           
class NoDerived final {/* 定義 */}; //不可被繼承
class Base{/* 定義 */};
class Last final: Base{/* 定義 */}; //不可被繼承
class Bad: NoDerived{/* 定義 */};   //錯,NoDerived是final
class Bad2: Last{/* 定義 */};       //錯,Last是final
           

類型轉換與繼承

  • 把引用/指針綁定到一個對象的情況:
    • 引用/指針的類型與對象一緻
    • 對象的類型含有可接收的const轉換規則
    • 可将基類類型的引用/指針綁定到派生類對象
  • 使用基類的引用/指針時,并不知道它綁定的對象的真實類型(運作時才可确定)
  • 基類類型的智能指針也支援動态綁定
  • 靜态類型和動态類型:
    • 靜态類型

      在編譯期已知,是變量/表達式聲明時的類型
    • 動态類型

      到運作期才可知,是變量/表達式在記憶體中對象的類型
  • 隻有基類的引用/指針才可能發生靜态類型和動态類型不一緻的情況
  • 基類和派生類之間的自動類型轉換:
    • 存在派生類向基類轉換,即

      基類引用/指針可指向派生類

      :每個派生類都有基類部分,基類引用/指針可綁定到基類部分
    • 不存在基類向派生類的轉換,即

      派生類引用/指針不可指向基類

      :基類的對象可能是派生類的一部分,也可能不是
    • 特别是,即使基類的引用/指針綁定到派生類,也不可将其指派給該派生類類型的引用/指針
  • 例子:派生類的引用/指針不可指向基類
1
2
3
4
           
/* 上下文:Bulk_quote由Quote派生而來 */
Bulk_quote bulk;
Quote *itemP=&bulk;         //對,基類指針可指向派生類對象
Bulk_quote *bulkP=itemp;    //錯,基類指針不可轉為派生類指針,即使該基類指針實際指向該派生類類型
           
  • 基類向派生類的顯式轉換:
    • 編譯器隻能檢查引用/指針的靜态類型來判斷轉換是否合法,故無法确定基類向派生類的轉換在運作時是否安全,隐式轉換會報錯
    • 若基類中有虛函數,則可用

      dynamic_cast

      來請求向派生類的類型轉換,該轉換的安全檢查将在運作時執行
    • 若已知某個基類向派生類的轉換一定是安全的,則可用

      static_cast

      來強制覆寫編譯器的檢查
  • 派生類對象向基類對象的隐式轉換(slice down):
    • 派生類向基類的自動轉換隻對指針/引用有效,在派生類對象和基類對象之間不存在隐式轉換。直接轉換對象得到的可能并非預期
    • 對類類型的對象初始化/指派時,實質上是在調用構造函數/指派算符,它們參數的類型經常是該類類型的引用。
    • 由于參數是引用,故允許給基類的構造/拷貝/移動/指派操作傳遞派生類對象。這些操作不是虛函數,故實際上運作的是基類的版本,它隻能處理基類成員。
    • 給基類的構造/拷貝/移動/指派操作傳遞派生類對象時,隻處理基類成員,忽略派生類自己的成員,派生類部分被

      切掉(sliced down)

  • 例子:派生類對象用于構造基類對象時,派生類部分被切掉
1
2
3
4
           
/* 上下文:Bulk_quote由Quote派生而來 */
Bulk_quote bulk;
Quote item(bulk);   //調用基類構造函數Quote::Quote(const Quote &)
item=bulk;          //調用基類指派算符Quote::operator=(const Quote &)
           
  • 具有繼承關系的類之間的轉換規則:
    • 從派生類到基類的類型轉換隻對引用/指針有效
    • 基類向派生類不存在隐式轉換
    • 派生類向基類的轉換也可能因為通路受限而不可行(隻有public繼承,即派生類中的基類部分可被使用者通路時,使用者才可用基類指針通路派生類成員)
    • 由于拷貝控制成員參數是引用,故經常可将派生類拷貝/移動/指派給基類,此時隻處理基類部分

虛函數

  • 由于隻有運作時才知道調用了哪個虛函數,故所有虛函數都必須有定義
  • 虛函數調用的版本:
    • 通過引用/指針調用虛函數時,被調用的版本是引用/指針綁定的動态類型對應的版本
    • 通過非引用非指針的表達式調用虛函數時,編譯期決定調用的版本為靜态類型對應的版本
  • 例子:引用/指針調用虛函數執行動态版本,非引用非指針調用虛函數執行靜态版本
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
           
/* 上下文:
 * Bulk_quote由Quote派生而來,
 * print_total第二個形參是Quote類型的引用,該函數中調用Quote的net_price方法
 * net_price是虛函數,在Bulk_quote中被覆寫
 */
Quote base("0-201-82470-1",50);
Bulk_quote derived("0-201-82470-1",50,5,0.19);
//執行動态類型的版本
print_total(cout,base,10);      //引用形參綁定到基類對象,内部調用Quote::net_price
print_total(cout,derived,10);   //引用形參綁定到派生類對象,内部調用Bulk_quote::net_price
//執行靜态類型的版本
base=derived;                   //拷貝派生類的基類部分
base.net_price(20);             //base是基類類型,調用Quote::net_price
           
  • 多态

    :具有繼承關系的多個了類型稱為多态類型,因為可使用它們的多種形式而無需在意它們的差異
  • 允許引用/指針的靜态類型和動态類型不一緻是C++支援運作時多态的根本
  • 使用基類的引用/指針調用基類成員函數時:
    • 若該函數為虛,則運作時才可确定調用的是動态類型對應的版本
    • 若該函數非虛,則編譯期即可确定調用的是靜态類型對應的版本
  • 當且僅當引用/指針調用虛函數時,對象的靜态類型和動态類型才會不同,使得解析調用發生在運作時
  • 派生類中覆寫了虛函數時,可再次使用virtual關鍵字聲明,但并非必須。基類中被聲明為虛的函數在派生類中隐式為虛
  • 虛函數的形參清單和傳回類型:
    • 派生類虛函數的形參必須與被它覆寫的基類虛函數完全一緻。
    • 派生類虛函數的傳回類型必須與基類虛函數一緻。除非傳回類型是類自身的引用/指針,此時要求從派生類到基類的轉換可通路(即派生類中的基類部分可被使用者通路)。
  • 若派生類定義了函數,它與基類中虛函數同名但形參清單不同,則是

    重載

    而不是

    覆寫

    。編譯器認為新函數與繼承自基類的函數是獨立的,新函數不會被基類的引用/指針調用。
  • C++11允許使用

    override

    關鍵字來說明派生類中的虛函數覆寫了基類的虛函數。若使用override标記了某函數但它未覆寫基類的虛函數,則報錯
  • override

    标記的函數未覆寫基類虛函數則報錯
  • 隻有虛函數才可被覆寫,非虛函數要麼重載要麼重複定義
  • 将某函數指定為

    final

    ,禁止覆寫該函數
  • final和override說明符出現在形參清單(包括const和引用修飾符)和尾置傳回類型之後
  • 例子:override和final
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
           
struct B{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D1: B{
    void f1(int) const override;    //對,f1與基類參數相同,覆寫
    void f2(int) override;          //錯,f2與基類參數不同,未覆寫
    void f3() override;             //錯,基類中f3不是虛函數
    void f4() override;             //錯,基類中無f4,故不是虛函數
}
struct D2: B{
    void f1(int) const final;       //使用final,禁止派生類覆寫
};
struct D3: D2{
    void f2();                      //對,覆寫從間接基類B中繼承的f2
    void f1(int) const;             //錯,D2中f1是final,禁止派生類覆寫
};
           
  • 虛函數可以有預設實參,若某次函數調用使用了預設實參,則實參值由靜态類型确定
  • 通過基類的引用/指針調用函數,則使用基類中的預設實參,即使運作的是派生類版本的函數。是以虛函數的預設實參應與基類一緻
  • 若希望對虛函數的調用不要動态綁定,而是指定某個類的版本,則可用

    作用域算符

  • 通常隻有成員函數或友元的代碼才需要使用作用域算符來回避動态綁定
  • 當派生類的虛函數調用它覆寫的基類虛函數時,需要手動指定虛函數版本,回避動态綁定(否則調用自身,無限遞歸)
  • 例子:用作用域算符手動指定虛函數版本
1
2
           
double discounted=baseP->net_price(42);             //指針調用虛函數,在運作時确定版本
double undiscounted=baseP->Quote::net_price(42);    //手動指定執行Quote中的版本
           

抽象基類

  • 若一個基類隻用于對其派生類提供抽象,但不希望産生該基類的執行個體,則可将該基類定義為

    抽象基類(ABC)

  • 将一個虛函數定義為

    純虛函數

    ,可明确告訴編譯器這個函數隻用于抽象,沒有實際意義,無需被定義
  • 将虛函數定義為純虛函數的方法是在函數體的位置寫

    =0

    ,且隻能出現在類内部的虛函數聲明語句處
  • 例子:純虛函數和抽象基類
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
           
/* 上下文:Quote是基類,其成員函數net_price是虛函數 */
class Disc_quote: public Quote{
public:
    Disc_quote()=default;
    Disc_quote(const string &book,double price,size_t qty,double disc):
              Quote(book,price),quantity(qty),discount(disc) {}
    //該函數在基類中是virtual,此處=0定義為純虛函數,使得這個類成為抽象基類
    double net_price(size_t) const =0;
protected:
    size_t quantity=0;
    double discount=0.0;
};
           
  • 不可直接定義抽象基類的對象,但其派生類的構造函數可使用抽象基類的構造函數來建構派生類的基類部分
  • 也可為純虛函數提供定義,但函數體必須在類外部。即,類内部不可為=0的函數再提供函數體
  • 含有(或未經覆寫直接繼承)

    純虛函數

    的類是

    抽象基類

    。抽象基類定義接口,其派生類可覆寫其接口。不能直接建立抽象基類的對象
  • 例子:繼承自抽象基類
1
 2
 3
 4
 5
 6
 7
 8
 9
10
           
/* 上下文:15.4中定義的抽象基類Disc_quote,其中net_price是純虛函數 */
class Bulk_quote: public Disc_quote{
public:
    Bulk_quote()=default;
    //抽象基類Disc_quote不可建立對象,但可在派生類Bulk_quote的構造函數中建立基類部分
    Bulk_quote(const string &book,double price,size_t qty,double disc):
              Disc_quote(book,price,qty,disc) {}
    //覆寫了純虛函數,該類不再是抽象基類
    double net_price(size_t) const override;
};
           
  • 重構

    負責重新設計類的體系,以便将操作/資料從一個類中移到另一個類中。對OOP而言重構很普遍

通路控制與繼承

  • 每個類控制自己成員的初始化,還控制自己的成員對派生類是否可通路
  • 使用

    protected

    說明符來說明它希望被派生類通路但不希望被其他使用者通路的成員:
    • 類似private,protected成員對類的使用者不可通路
    • 類似public,protected成員對派生類的成員和友元可通路
    • 派生類的成員和友元隻能通過派生類對象來通路其基類部分的protected成員,對基類對象中的protected成員不可通路
  • 例子:派生類的成員和友元隻能通過派生類對象來通路其基類部分的protected成員
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
           
class Base{
protected:
    int prot_mem;                   //基類中的protected成員
};
class Sneaky: public Base{
    friend void clobber(Sneaky &);  //使用派生類對象來通路
    friend void clobber(Base &);    //使用基類對象來通路
    int j;
};
//對,派生類的友元可通過派生類對象來通路其基類部分的protected
void clobber(Sneaky &s){s.j=s.prot_mem=0;}
//錯,不可通過基類對象來通路其protected
void clobber(Base &b){b.prot_mem=0;}
           
  • 派生類的成員/友元不可通路基類對象的protected成員的原因是:若可以通路,則隻需繼承基類并聲明友元(類似上例),即可規避protected的保護機制。
  • 某個類對其繼承而來的成員的通路權限受兩方面影響:
    • 基類中該成員的通路說明符

      :說明基類成員的權限(派生類能否通路該成員,使用者能否通路該成員)
    • 類派生清單中的通路說明符

      :說明派生類中基類部分的權限(派生類的使用者能否通路其基類部分)
  • 派生類的成員/友元能否通路直接基類的成員,隻與直接基類成員的通路說明符有關,與派生通路說明符無關
  • 派生通路說明符的目的是控制派生類使用者(包括派生類對象和派生類的派生類)能否通路該派生類的基類部分
  • 假設D繼承自B,則基類部分的通路控制:
    • 若是

      public繼承

      :D的基類部分在D中public,D的所有使用者都可通路其基類部分(基類部分的成員在D中保持基類中定義的通路控制)
    • 若是

      protected繼承

      :D的基類部分在D中protected,D的派生類成員/友元可通路其基類部分(基類部分的public成員在D中變為protected)
    • 若是

      private繼承

      :D的基類部分在D中private,隻有D的成員/友元可通路其基類部分(基類部分的成員在D中都變為private)
  • 例子:繼承中的通路控制
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
           
class Base{
public:
    void pub_mem();
protected:
    int prot_mem;
private:
    char priv_mem;
};
//public派生,基類部分對外可見
struct Pub_Derv: public Base{
    int f(){return prot_mem;}           //對,派生類可通路基類protected成員
    char g(){return priv_mem;}          //錯,派生類不可通路基類private成員
};
//private派生,基類部分對外不可見
struct Priv_Derv: private Base{
    int f1() const {return prot_mem;}   //對,派生類可通路基類protected成員
};
Pub_Derv d1;                            //public派生,基類部分是public
Priv_Derv d2;                           //private派生,基類部分是private
d1.pub_mem();                           //對,public派生時基類部分對外可見
d2.pub_mem();                           //錯,private派生時基類部分對外不可見
//Base--(public)-->Pub_Derv--(public)-->Derived_from_Public
struct Derived_from_Public: public Pub_Derv{
    int use_base(){return prot_mem;}    //對,Pub_Derv中的Base::prot_mem仍是protected
};
//Base--(private)-->Priv_Derv--(public)-->Derived_from_Private
struct Derived_from_Private: public Priv_Derv{
    int use_base(){return prot_mem;}    //錯,Priv_Derv中的Base::prot_mem是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
  • 例子:友元與繼承
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
           
class Base{
    friend class Pal;                       //Pal是其友元類
protected:
    int prot_mem;                           //基類中的protected成員
};
class Sneaky: public Base{
    int j;                                  //派生類中的private成員
};
class Pal{
public:
    int f(Base b){return b.prot_mem;}       //對,該類是Base的友元
    int f2(Sneaky s){return s.j;}           //錯,基類友元不可通路派生類中非基類部分的private
    int f3(Sneaky s){return s.prot_mem;}    //對,基類友元可通路派生類中基類部分的private
};
class D2: public Pal{
public:
    int mem(Base b){return b.prot_mem;}     //錯,友元類的派生類不是友元
};
           
  • 若需改變派生類繼承的某個名字的通路級别,可使用using聲明
  • 類内部使用

    using聲明

    ,可對該類的直接/間接基類的任何可通路成員重定義通路權限。新的通路權限是該using語句所在處的權限
  • 隻有該類内部自己可通路的成員,才可用using改變權限
  • 例子:using改變通路權限
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
           
class Base{
public:
    size_t size() const {return n;}
protected:
    size_t n;
};
//private繼承,則基類部分所有成員變為派生類的private
class Derived: private Base{
public:
    using Base::size;   //将基類部分的size變為public
protected:
    using Base::n;      //将基類部分的n變為protected
};
           
  • struct和class差別:
    • 成員通路說明符:struct預設是public成員,class預設是private成員
    • 派生通路說明符:struct預設是public繼承,class預設是private繼承
    • 沒有其他差別
  • 例子:預設派生通路說明符
1
2
3
           
class Base {/* 定義 */};
struct D1: Base{/* 定義 */};    //預設public繼承
class D2: Base{/* 定義 */};     //預設private繼承
           

繼承中的類作用域

  • 每個類定義一個自己的作用域
  • 類存在繼承關系時,派生類的作用域嵌套在基類作用域中。若一個名字在派生類中無法解析,則去基類中尋找定義
  • 由于繼承關系的類作用域嵌套,是以派生類可直接通路基類成員(而不需指定基類作用域)
  • 派生類調用成員時名字的解析過程,例如

    Derv.func()

    1. 查找調用類型Derv的作用域
    2. 查找調用類型Derv的基類的作用域
    3. 沿着繼承鍊向最終的基類查找
  • 對象/引用/指針的靜态類型決定該對象的哪些成員可見(即名字查找),即使靜态類型與動态類型不一緻。
  • 例子:成員的名字查找取決于靜态類型
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
           
/* 上下文:
 * Quote(定義于15.2.1)派生出Disc_quote(定義于15.3),
 * Disc_quote派生出Bulk_quote(定義于15.3)
 */
class Disc_quote: public Quote{
public:
    pair<size_t,double> discount_policy() const {return {quantity,discount};}
    /* 其他成員與15.3中一緻 */
};
Bulk_quote bulk;
Bulk_quote *bulkP=&bulk;    //靜态類型與動态類型一緻
Quote *itemP=&bulk;         //靜态類型與動态類型不一緻
bulkP->discount_policy();   //對,該成員屬于派生類,故派生類指針(靜态類型)可通路
itemP->discount_policy();   //錯,該成員不屬于基類,故基類指針(靜态類型)不可通路,即使指向的是派生類對象也不行
           
  • 當靜态類型與動态類型不一緻時,隻有虛函數會查找動态類型中的重定義。其他成員都取決于靜态類型,包括虛函數的名字查找也取決于靜态類型
  • 派生類可重用直接/間接基類中的名字,此時内層作用域的定義将

    隐藏

    外層作用域的同名定義
  • 可用

    作用域算符

    來顯式使用被隐藏的基類成員
  • 例子:派生類的成員隐藏同名的基類成員
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
           
struct Base{
    Base(): mem(0) {}
protected:
    int mem;
};
struct Derived: Base{
    Derived(int i): mem(i) {}
    int gen_mem() {return mem;}             //優先查找該類中的名字
    int get_base_mem() {return Base::mem;}  //顯式指定是基類中的該成員
protected:
    int mem;                                //重新定義成員,隐藏基類中的同名成員
};
Derived d(42);
cout<<d.get_mem()<<endl;                    //列印42,是派生類中重定義的mem
cout<<d.get_base_mem()<<endl;               //列印0,是基類中的Base::mem
           
  • 最佳實踐:除了覆寫繼承而來的虛函數外,派生類最好不要重用基類中的其他名字
  • 函數調用的解析過程,假定調用

    p->mem()

    obj.mem()

    1. 确定p/pbj的靜态類型
    2. 在靜态類型對應的類中查找名字mem,若未找到則依次在直接基類中查找直到繼承鍊頂端
    3. 找到名字mem後,進行正常的類型檢查判斷調用是否合法
    4. 假設調用合法,則編譯器根據mem是否為虛而産生不同代碼:
      • 若mem是虛函數且通過引用/指針調用,則在運作時才會根據動态類型确定調用哪個版本的虛函數
      • 否則編譯器産生正常的函數調用
  • 聲明在内層作用域的函數不會重載外層作用域的函數,故派生類中的函數也不會重載其基類的成員。
  • 名字查找先于類型檢查

    :若派生類的成員與基類成員同名,則在派生類作用域中隐藏該基類成員。即使形參清單不一緻也會隐藏而不是重載
  • 不同作用域中的函數不是重載關系。但可手動指定作用域來通路
  • 例子:派生類的函數隐藏而不是重載基類同名函數,通路基類函數時需指定作用域
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
           
struct Base{
    int memfcn();
};
struct Derived: Base{
    int memfcn(int);    //隐藏基類中的該名字
};
Base b;
Derived d;
b.memfcn();             //對,調用Base::memfcn
d.memfcn(10);           //對,調用Derived::memfcn
d.memfcn();             //錯,基類的函數在派生類中被隐藏而非重載
d.Base::memfcn();       //對,顯式調用基類函數Base::memfcn
           
  • 基類與派生類的同名虛函數必須有相同的形參清單。若它們形參清單不同,則是隐藏而不是覆寫
  • 例子:形參清單不同則是隐藏而不是覆寫
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
           
class Base{
public:
    virtual int fcn();  //虛函數
};
class D1: public Base{
public:
    int fcn(int);       //非虛函數,隐藏了Base::fcn,但未覆寫
    virtual void f2();  //虛函數
};
class D2: public D1{
public:
    int fcn(int);       //非虛函數,隐藏了D1::fcn和Base::fcn,但未覆寫
    int fcn();          //虛函數,覆寫了Base::fcn
    void f2();          //虛函數,覆寫了D1::f2
};
Base bobj; D1 d1obj; D2 d2obj;
//考察fcn(),基類指針通路各種對象
Base *bp1=&bobj, *bp2=&d1obj, *bp3=&d2obj;
bp1->fcn();     //虛調用,運作時調用Base::fcn()
bp2->fcn();     //虛調用,運作時調用Base::fcn()(D1中隻是隐藏,未覆寫)
bp3->fcn();     //虛調用,運作時調用D2::fcn()(D2中覆寫了)
//考察f2()
D1 *d1p=&d1obj; D2 *d2p=&d2obj;
bp2->f2();      //錯,基類無f2成員,靜态類型找不到名字
d1p->f2();      //虛調用,運作時調用D1::f2()
d2p->f2();      //虛調用,運作時調用D2::f2()
//考察fcn(int),各類指針通路最終的派生類
Base *p1=&d2obj; D1 *p2=&d2obj; D2 *p3=&d2obj;
p1->fcn(42);    //錯,基類無fcn(int)成員
p2->fcn(42);    //非虛,靜态綁定,調用D1::fcn(int)
p3->fcn(42);    //非虛,靜态綁定,調用D2::fcn(int)
           
  • 成員函數無論是否是虛函數都可被重載。派生類可覆寫重載函數的0個或多個執行個體。
  • 若派生類希望基類的所有重載虛函數都對它可見,則應或者覆寫所有的版本,或者一個也不覆寫。因為隻要覆寫了一個,基類的函數名就會被隐藏
  • 若派生類隻需覆寫重載集合中的一些而非全部,可使用using聲明解決名字被隐藏的問題
  • 在派生類中使用

    using聲明

    語句指定名字,可将基類的所有重載版本都添加到派生類作用域。此時,派生類隻需覆寫需要覆寫的重載版本即可,不需覆寫所有重載版本。對派生類未覆寫的重載版本的通路,實際上是對using聲明點的通路

構造函數與拷貝控制

  • 繼承體系中的類也需要控制其對象執行構造/拷貝/移動/指派/析構時發生什麼。
  • 若類未定義構造函數和拷貝控制,則編譯器會合成

虛析構函數

  • 基類通常應定義虛析構函數,使得派生類可用自己的析構函數覆寫它,這樣就可動态配置設定繼承體系中的對象。
  • delete動态對象的指針時,用此指針調用該對象的析構函數。若該對象處于繼承體系中,則指針的靜态類型可能與指向對象的動态類型不比對。基類中将析構函數定義為虛,可確定執行正确的析構版本
  • 若基類的析構函數不是虛,則delete一個指向派生類對象的基類指針是未定義
  • 例子:基類中的虛析構函數
1
2
3
4
5
6
7
8
9
           
/* 上下文:Bulk_quote由Quote派生而來 */
class Quote{
public:
    virtual ~Quote()=default;   //動态綁定析構函數
};
Quote *itemP=new Quote;         //基類指針
delete itemP;                   //調用基類的析構函數
itemP=new Bulk_quote;
delete itemP;                   //調用派生類的析構函數
           
  • 三五法則的特例:基類需要虛析構函數是為了讓派生類動态綁定析構函數,其函數體不一定有操作, 是以不一定需要其他的拷貝控制操作。
  • 若一個類定義了虛析構函數,即使用=default手動指定合成版本,編譯器也不會為該類合成移動操作

合成拷貝控制與繼承

  • 基類/派生類的合成拷貝控制成員:
    • 對類本身的成員依次初始化/拷貝/移動/指派/銷毀
    • 派生類的合成拷貝控制成員還負責調用直接基類的對應操作來對直接基類部分初始化/拷貝/移動/指派/銷毀,要求對應成員可被派生類通路且非删除
    • 例如,派生類的析構函數除銷毀自己的成員外,還調用直接基類的析構函數析構基類部分,依次調用直到繼承鍊頂端
  • 基類/派生類也可将合成的預設構造函數/拷貝控制成員定義為删除,某些定義基類的方式也可能導緻派生類的合成成員被删除:
    • 若基類的預設構造函數/拷貝構造函數/拷貝指派算符/析構函數是删除的或不可被派生類通路,則派生類中相應函數被删除。(編譯器無法對派生類的基類部分進行初始化/拷貝/指派/銷毀)
    • 若基類的析構函數是删除的或不可被派生類通路,則派生類的預設構造函數/拷貝構造函數将被删除(無法銷毀派生類的基類部分)
    • 若派生類使用=default請求移動操作,且基類中對應的成員是删除的或不可被派生類通路,則派生類中該操作被删除(無法移動派生類的基類部分)
    • 若基類的析構函數是删除的或不可通路,則派生類的移動構造函數被删除
  • 若基類沒有預設/拷貝/移動構造函數,則派生類中也不會定義相應操作
  • 例子:基類删除拷貝構造函數
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
           
class B{
public:
    B();
    //定義了拷貝構造函數,且定義為删除。是以B沒有移動構造函數。是以既不能拷貝又不能移動
    B(const B &)=delete;
};
//基類定義了拷貝構造函數,且定義為删除。則派生類的合成拷貝構造函數也是删除,且沒有移動構造函數
class D: public B{};
D d;
D d2(d);            //錯,D的合成拷貝構造函數被删除
D d3(std::move(d)); //錯,D沒有移動構造函數,而合成拷貝構造函數被删除
           
  • 大多數基類都會定義虛析構函數,此時基類沒有合成的移動操作,是以派生類中也沒有合成的移動操作
  • 基類缺少移動操作會阻止派生類擁有自己的合成移動操作,故派生類需要移動時應在基類中定義移動操作
  • 例子:在基類中定義所有拷貝構造成員
1
 2
 3
 4
 5
 6
 7
 8
 9
10
           
class Quote{
public:
    //基類定義一整套完整的拷貝控制操作,其派生類也将獲得合成的拷貝控制操作
    Quote()=default;
    Quote(const Quote &)=default;
    Quote(Quote &&)=default;
    Quote &operator=(const Quote &)=default;
    Quote &operator=(Quote &&)=default;
    virtual ~Quote()=default;
};
           

派生類的拷貝控制成員

  • 派生類的構造函數不僅要初始化自己的成員,還要初始化其基類部分。類似的,拷貝/移動構造函數/指派算符也必須處理基類部分的成員,即手動調用基類的對應成員
  • 但析構函數隻負責銷毀派生類自己的成員(隐式銷毀)。類似的,派生類的基類部分也是被隐式銷毀,析構函數自動被調用。即不需要手動調用基類虛構函數
  • 派生類的拷貝控制成員調用基類拷貝控制成員:
    • 派生類定義拷貝/移動構造函數時,通常應在

      初值清單

      中顯式調用基類的對應函數來初始化對象的基類部分。否則基類部分被

      預設初始化

    • 派生類的指派算符也必須顯式調用基類的指派算符,來為基類部分指派
    • 派生類的析構函數體執行完成後,成員(包括基類部分)被隐式銷毀。故派生類析構函數不需顯式調用基類析構函數(基類部分銷毀時隐式調用),隻需要管理自己的資源。
    • 成員析構的順序與構造相反:先執行派生類析構函數,再執行基類析構函數,直到繼承鍊頂端
  • 例子:派生類拷貝/移動構造函數/指派算符的初值清單中應顯式調用基類的對應函數,析構則不用
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
           
class Base{/* 定義 */};
class D: public Base{
public:
    //派生類拷貝構造函數需要手動拷貝基類部分
    D(const D &d):Base(d)/* 初值清單剩餘部分拷貝派生類自己的成員 */{/* 函數體 */}
    //派生類移動構造函數需要手動移動基類部分
    D(D &&d):Base(std::move(d))/* 初值清單剩餘部分移動派生類自己的成員 */{/* 函數體 */}
    D &operator=(const D &rhs){
        //派生類指派算符需要手動調用基類指派算符(用作用域算符指定)        
        Base::operator=(rhs);
        /* 另外的操作 */
    }
    //基類部分被自動析構,基類析構函數被隐式調用,不需手動調用
    ~D(){/* 函數體 */}
};
           
  • 構造和析構基類部分時派生類部分未完成:
    • 派生類構造對象時,基類部分首先被構造。故執行基類構造函數時派生類部分還未初始化
    • 派生類析構對象時,基類部分最後被析構。故執行基類析構函數時派生類部分不存在
  • 由于執行派生類構造/析構函數時派生類部分是未完成狀态,故不可調用派生類版本的函數,調用的虛函數都是基類版本
  • 在構造/析構對象過程中,正在執行哪個類的構造/析構函數,就認為正在構造/析構的對象是這個類型
  • 若構造/析構函數調用了某個虛函數,則應該執行與構造/析構函數所屬類型對應的虛函數版本

繼承的構造函數

  • C++11中,派生類可重用其直接基類定義的構造函數,但并非正常繼承(但為了友善仍稱為繼承)
  • 一個類隻初始化它的直接基類,也隻繼承其直接基類的構造函數
  • 類不能繼承預設/拷貝/移動構造函數。若派生類未定義它們,編譯器将合成它們
  • 派生類繼承基類構造函數的方式是提供一條

    using聲明

    語句。這條using語句不是為了使名字可見,而是令編譯器産生代碼:對于基類的每個構造函數,編譯器都生成一個對應的形參清單完全相同的派生類構造函數
  • 例子:用using聲明來繼承構造函數
1
2
3
4
5
6
           
/* 上下文:Bulk_quote繼承自Disc_quote */
class Bulk_quote:public Disc_quote{
public:
    using Disc_quote::Disc_quote;   //使用using聲明繼承基類的構造函數
    double net_price(size_t) const;
};
           
  • 繼承構造函數用的using聲明生成的構造函數形如:

    derived(params):base(args){}

    • derived是派生類名,base是基類名
    • params是派生類構造函數的形參清單,args使用派生類形參調用基類構造函數
    • 該構造函數隻初始化基類部分。若派生類有自己的成員,則預設初始化
  • 與普通using聲明不一樣,繼承構造函數的using聲明不會改變該函數的通路權限,權限仍與基類保持一緻
  • 用using聲明産生的派生類構造函數不可指定

    explicit

    constexpr

    ,這兩個屬性與基類保持一緻
  • 若基類的構造函數有預設實參,則using産生的派生類構造函數不會繼承這些實參,而是産生多個版本的構造函數,每個版本分别省略一個含預設實參的形參。例如,若基類構造函數有兩個形參,其中一個有預設實參,則派生類繼承得到兩個構造函數,一個接受兩個形參(無預設實參),另一個隻接受一個形參(是基類中無預設實參的那個)
  • 若基類有多個構造函數,則派生類繼承時一般繼承所有,除兩個例外:
    • 派生類可繼承一部分構造函數,而為其他構造函數定義自己的版本。若自定義的版本與基類版本形參清單相同,則這個構造函數不會被繼承
    • 預設/拷貝/移動構造函數不能被繼承,它們按照正正常則合成。若一個類隻有繼承的構造函數,則他也将有一個合成的預設構造函數

容器與繼承

  • 使用容器存儲繼承體系的對象時,由于容器不可儲存不同類型的元素,故不可直接存儲具有繼承關系的多種對象
  • 當派生類對象被指派給基類對象時,派生類部分被“切掉”,隻保留基類部分。是以若把派生類對象儲存在基類類型的容器中,它們就不再是派生類對象了
  • 例子:派生類對象被放在基類容器中,被切掉
1
2
3
4
5
6
7
8
9
           
/* 上下文:Bulk_quote由Quote繼承而來,net_price是虛函數,在基類和派生類中實作不一樣 */
//容器中存放基類對象
vector<Quote> basket;
//基類對象存在基類容器中
basket.push_back(Quote("0-201-82470-1",50));
//派生類對象存在基類容器中,被切掉
basket.push_back(Bulk_quote("0-201-82470-1",50,10,0.25));
//調用原派生類對象的函數,但由于被切掉,實際調用的是基類部分的版本
cout<<basket.back().net_price(15)<<endl;
           
  • 希望在容器中存放具有繼承關系的對象時,實際存放的通常是基類的指針/智能指針。指針所指的動态類型可以是基類/派生類
  • 可将派生類的内置指針/智能指針轉換為基類的内置指針/智能指針
  • 例子:在容器中儲存基類指針
1
2
3
4
5
6
7
8
9
           
/* 上下文:Bulk_quote由Quote繼承而來,net_price是虛函數,在基類和派生類中實作不一樣 */
//容器中存放指向基類的智能指針
vector<shared_ptr<Quote>> basket;
//存放指向基類對象的指針
basket.push_back(make_shared<Quote>("0-201-82470-1",50));
//存放指向派生類對象的指針
basket.push_back(make_shared<Bulk_quote>("0-201-82470-1",50,10,0.25));
//調用派生類版本的虛函數,動态綁定
cout<<basket.back()->net_price(15)<<endl;
           

編寫Basket類

  • C++做OOP的一個悖論是,無法直接使用對象進行面向對象程式設計,而是必須使用指針/引用
  • 大量使用指針會增加程式的複雜性,故經常定義一些輔助類(

    句柄類

    )來處理需要大量指針操作的情況
  • new

    語句不能處理多态,需要多态時應将new封裝進虛函數中
  • 例子:使用句柄類管理指針
1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
           
/* 上下文:
 * print_total定義于15.1
 * Quote定義于15.2.1
 * Bulk_quote定義于15.2.2
 */
class Basket{
public:
    //向底層指針的集合中添加一個指針
    void add_item(const shared_ptr<Quote> &sale)
                 {items.insert(sale);}
    double total_receipt(ostream &) const;
private:
    //自定義的序(謂詞),定義為static是因為可以給所有對象共用
    static bool compare(const shared_ptr<Quote> &lhs,const shared_ptr<Quote> &rhs)
                       {return lhs->isbn()<rhs->isbn();}
    //底層管理的是智能指針的集合,并自定義了序
    multiset<shared_ptr<Quote>,decltype(compare) *> items{compare};
};
double Basket::total_receipt(ostream &os) const{
    double sum=0.0;
    //計算總價,iter解引用後是一個指向基類的指針
    for(auto iter=items.cbegin();iter!=items.cend();iter=items.upper_bound(*iter))
        sum+=print_total(os,**iter,items.count(*iter));
    os<<"Total Sale: "<<sum<<endl;
    return sum;
}
//如上所定義的句柄類添加元素時需要轉為指針(如下),而不能直接使用對象,使用不便
Basket bsk;
bsk.add_item(make_shared<Quote>("123",45));
bsk.add_item(make_shared<Bulk_quote>("345",45,3,0.15));
//需要實作add_item的重載版本,使得可直接在句柄類中添加對象
void add_item(const Quote &sale);   //允許将對象拷貝給句柄類管理
void add_item(Quote &&sale);        //允許将對象移動給句柄類管理

//需要在容器管理的類中添加拷貝和移動操作,允許将對象拷貝/移動給句柄類管理
class Quote{
public:
    //定義為虛函數,則引用/指針調用clone時可正确選擇拷貝/移動的版本,實作運作時多态
    //傳回指針,便于存入句柄類底層的容器中
    virtual Quote *clone() cosnt & {return new