C++面向對象程序設計:虛函數、抽象基類、訪問控制與繼承

前提

​ 有如下代碼,表示一本書籍的信息以及該書的打折方案:

class Quote {
//    friend double print_total(ostream &os,const Quote &item,size_t n);
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;     // 書的 isbn
protected:
    double price = 0.0;
};

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_qy = 0;     // 使用折扣政策的最低購買量
    double discount = 0;        // 以小數表示的折扣額
};

double print_total(ostream &os,const Quote &item,size_t n) {
    // 根據傳入 item 形參的對象類型,調用 Quote::net_price
    // 或者 Bulk_quote::net_price
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn()
       << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}

Bulk_quote::Bulk_quote(const std::string &book, double p, std::size_t qty, double disc):
    Quote(book,p), min_qy(qty), discount(disc) { }

double Bulk_quote::net_price(std::size_t cnt) const {
    if(cnt >= min_qy)
        return cnt * (1 - discount) * price;
    return cnt * price;
}

虛函數

​ 我們知道,在 C++ 中,使用基類的引用或指針調用一個虛成員函數時會執行動態綁定。直到運行時才能知道到底調用了哪個版本的虛函數,所以所有虛函數都必須有定義。我們必須爲每一個虛函數都提供函數定義,不管它是否被用到了。

對虛函數的調用可能在運行時才被解析

​ 我們知道,當編譯器產生的代碼直到運行時才能確定調用哪個版本的虛函數。如 print_total,其函數通過名爲 item 的參數來調用 net_price,其中 item 的類型是 Quote&。因爲 item 是引用,而且 net_price 是虛函數,所以我們調用的 net_price 版本依賴於運行時綁定到 item 的實參的動態類型:

Quote base("0-201-82470-1", 50);
print_total(cout, base, 10);		// 調用 Quote::net_price
Bulk_quote derived("0-201-82470-1",50,5,0.19);
print_total(cout, derived, 10);		// 調用 Bulk_quote::net_price;

必須記住,動態綁定只有當我們通過指針或引用調用虛函數時纔會發生。

​ 如果我們通過對象進行函數 (虛函數或非虛函數) 調用,其靜態類型和動態類型是一致的,不會出現動態綁定的情況。

派生類中的虛函數

​ 我們可以在派生類中使用 virtual 關鍵字對繼承而來的虛函數進行覆蓋,① 但它的形參類型必須與被它覆蓋的基類函數完全一致。同時,② 派生類中虛函數的返回類型也必須與基類函數匹配。該規則有一個列外,當類的虛函數返回類型是類本身的指針或引用時,② 規則無效,例如:D 由 B 派生得到,基類的虛函數可以返回 B*,而派生類的對應函數可以返回 D*,只不過這樣的返回類型要求從 D 到 B 的轉換是可訪問的。

final 和 override 說明符

​ 派生類如果定義了一個函數與基類中虛函數的名字相同但是形參列表不同,這是合法的。並且,編譯器會將認爲新定義的這個函數與基類中原有的函數是相互獨立的。

​ 我們有時可能是想覆蓋虛函數,但是由於這種類似的問題,而發生錯誤,而且這種錯誤是很難發現的。所以 C++11 允許使用 override 關鍵字來說明派生類中的虛函數。如果我們使用 override 標記了某個函數,但該函數並沒有覆蓋已存在的虛函數,此時編譯器將報錯

struct B {
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D1 : B {
    void f1(int) const override;	// 正確,f1 與基類的 f1 匹配
    void f2(int) override;			// 錯誤,B 沒有形如 f2(int) 的函數
    void f3() override;				// 錯誤,f3 不是虛函數
    void f4() override;				// 錯誤,B 沒有名爲 f4 的函數
};

只有虛函數才能被覆蓋,所以編譯器會拒絕 D1 的 f3。

​ 我們還能把某個函數指定爲 final,如果我們已經把函數定義成 final 了,則之後任何嘗試覆蓋該函數的操作都將引發錯誤:

struct D2 : B {
    // 從 B 繼承 f2() 和 f3(),覆蓋 f1(int)
    void f1(int) const final;		//不允許後續的其他類覆蓋
};
struct D3 : D2 {
    void f2();						// 正確,覆蓋從間接基類 B 繼承而來的 f2
    void f1(int) const;				// 錯誤,D2 已經將 f1 聲明成 final
};

final 和 override 說明符出現在形參列表 (包括任何 const 或引用修飾符) 以及尾置返回類型之後。

虛函數和默認實參

​ 虛函數可以擁有默認實參,如果某次函數調用使用了默認實參,則該實參值由本次調用的靜態類型決定。換句話說,如果我們通過基類的引用或指針調用函數,則使用基類中定義的默認實參,即使實際運行的派生類中的函數版本也是如此。此時,傳入派生類函數的將是基類函數定義的默認實參。

如果虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致。

迴避虛函數的機制

​ 在某些情況下,我們希望對虛函數的調用不要進行動態綁定,而是強迫其執行虛函數的某個特定版本。使用作用域運算符實現這一目的,如:

// 強行調用基類中定義的函數版本而不管 baseP 的動態類型是什麼
double undiscounted = baseP -> Quote::net_price(42);

該代碼強行調用 Quote 的 net_price 函數,而不管 baseP 實際指向的對象類型到底是什麼。該調用在編譯時完成解析。

通常情況下,只有成員函數 (或友元) 中的代碼才需要使用作用域運算符來回避虛函數機制。

​ 什麼時候需要回避虛函數的默認機制呢?通常是當一個派生類的虛函數調用它覆蓋的基類的虛函數版本時。

!注意:如果一個派生類虛函數需要調用它的基類版本,但是沒用使用作用域運算符,則在運行時將被解析爲對派生類自身的調用,從而導致無限遞歸。

抽象基類

​ 我們可能出現的打折策略不止一種。除了購買某一種書超過一定量的書籍享受折扣以外,也可能是購買不超過一定數量才享受折扣,或者是購買量超過一定數量後,購買的全部書籍都折扣。

​ 上面的策略都要求一個購買量和折扣值。我們用 Disc_quote 來保存購買量的值和折扣值。其他特定的策略將分別繼承自 Disc_quote,每個派生類都通過定義自己的 net_price 來實現各自的折扣策略。

​ 我們知道,每個策略的 net_price 函數都不相同,而最爲基類的 Disc_quote 沒有任何折扣策略,所以我們可以在 Disc_quote 中不定義新的 net_price,讓其直接繼承 Quote 中的 net_price。

​ 但是,這樣的 Disc_quote 會帶來另一個問題,如果我們將 Disc_quote 傳入像 print_total 的函數,那豈不是毫無意義嗎。

純虛函數

​ 認真思考可以發現,關鍵的問題不僅僅是如何定義 net_price 函數,而是我們根本不希望用戶創建一個 Disc_quote 對象。Disc_quote 僅僅是一本打折書籍所通用的概念,並不是打折方式。

​ 我們可以將 net_price 定義成純虛函數,從而告訴用戶在 Disc_quote 這個類中,net_price 函數沒有實際意義。我們可以在函數體的位置書寫 =0,來表示這個虛函數時純虛函數,純虛函數只需要聲明不需要定義。注意,=0 只能出現在類內部的虛函數聲明語句處。

class Disc_quote : public Quote {		// !! 記住是否使用訪問說明符,經常忘記
public:
    Disc_quote() = default;
    Disc_quote(const std::string &book,double price,std::size_t qty,double disc):
            Quote(book,price), quantity(qty), discount(disc) { }
    double net_price(std::size_t) const =0;
protected:
    std::size_t quantity = 0;     // 使用折扣政策的最低購買量
    double discount = 0.0;        // 以小數表示的折扣額
};

​ 我們也可以在函數外部定義純虛函數。但類的內部不能對 =0 的函數提供函數體。

含有純虛函數的類是抽象基類

​ 含有 (或者未經覆蓋直接繼承) 純虛函數的類是抽象基類。抽象基類負責定義接口,而後續的其他來可以覆蓋該接口。我們不能直接創建一個抽象基類的對象。如:

Disc_quote discounted;		// 錯誤,不能定義 Disc_quote 的對象,Disc_quote 是抽象基類
Bulk_quote bulk;			// ok

Disc_quote 的派生類必須給出自己的 net_price 定義,否則它們仍將是抽象基類。

派生類構造函數只初始化它的直接基類

​ 現在我們重新實現 Bulk_quote,繼承自 Disc_quote 而非 Quote:

// 當同一書籍的銷售量超過某個值時啓用折扣,這樣折扣是小於 1 的正數
class Bulk_quote : public Disc_quote {
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string &book, double p, std::size_t qty, double disc):
        Disc_quote(book, p, qty, disc) { }
    // 覆蓋基類的函數版本以實現基於大量購買的折扣政策
    double net_price(std::size_t) const override;
};

可以發現,我們是調用直接基類的構造函數,因爲直接基類的構造函數函數中會調用間接基類中的構造函數。所以,派生類構造函數只初始化它的直接基類即可

訪問控制與繼承

​ 每個類分別控制自己的成員初始化過程,與之類似,每個類還分別控制着其成員對於派生類來說是否可訪問

受保護的成員

​ protected 說明符:

  • protected 成員對類的用戶來說,是不可訪問的
  • protected 成員對於派生類的成員和友元是可訪問的
  • 派生類的成員或友元只能通過派生類對象來訪問基類受保護的成員。派生類對於一個基類對象中的受保護成員沒有任何訪問特權

爲了理解最後一條規則,我們考慮如下例子:

class Base {
protected:
    int prot_mem;
};
class Sneaky : public Base {
    friend void clobber(Sneaky&);	// 能訪問 Sneaky::prot_mem
    friend void clobber(Base&);		// 不能訪問 Base::prot_mem
    int j;							// j 是默認 private
};
// ok,clobber 能訪問 Sneaky 對象的 private 和 protected 成員
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// 錯誤,clobber 不能訪問 Base 的 protected 成員
void clobber(Base &b) { b.prot_mem = 0; }

第二點與第三點不要混淆了:第二點是對於 this 的情況,而第三點不是 this,而是指其他的對象來訪問 protected。

公有、私有和受保護繼承

某個類對其繼承而來的成員的訪問權限受兩個因素的影響:一是在基類中該成員的訪問說明符,二是在派生類的派生列表中的訪問說明符。

​ **派生訪問說明符對於派生類的成員 (或友元) 能否訪問其直接基類的成員沒有影響。**而是控制派生類用戶 (包括派生類的派生類) 對於繼承成員的訪問權限:

  • 當類的繼承方式爲 public 時,基類的 public 成員和 protected 成員被繼承到派生類中仍作爲派生類的 public成員和 protected 成員,派生類的其它成員可以直接訪問它們。
  • 當類的繼承方式爲 private 時,基類的 public 成員和 protected 成員被繼承後成爲派生類的 private 成員,派生類的其它成員可以直接訪問它們,但是在類的外部通過派生類的對象無法訪問。
  • 當類的繼承方式爲 protected 時,基類的 public 成員和 protected 成員被繼承到派生類中都作爲派生類的protected 成員,派生類的其它成員可以直接訪問它們,但是類的外部使用者不能通過派生類的對象訪問它們。
派生類向基類轉換的可訪問性

​ 派生類向基類的轉換是否可訪問由使用該轉換的代碼決定,同時派生類的派生訪問說明符也有影響。假定 D 繼承自 B:

  • 只有當 D 公有地繼承 B 時,用戶的代碼才能使用派生類向基類的轉換;否則,用戶代碼不能使用該轉換。
  • 不論 D 以何方式繼承 B,D 的成員函數和友元都能使用派生類向基類的轉換。
  • 如果 D 以公有或者受保護的方式繼承 B,則 D 的派生類的成員和友元可以使用 D 向 B 的轉換;否則,該轉換不能執行。
友元與繼承

​ 我們知道,友元關係不能傳遞。同樣,友元關係也不能繼承。基類的友元在訪問派生類成員時不具有特殊性。同樣,派生類的友元也不能隨意訪問基類的成員:

class Base {
    // 其他成員與上一致
    friend class Pal;
};
class Pal {
public:
    int f(Base b) { return b.prot_mem; }	// ok,Pal 是 Base 的友元
    int f2(Sneaky s) { return s.j; }		// 錯誤,j 是 private,Pal 不是 Sneaky 的友元
    // 對基類的訪問權限由基類本身控制,即使對於派生類的基類部分也是如此
    int f3(Sneaky s) { return s.prot_mem; }	// ok
};

每個類都控制各自成員的訪問權限。從上 ”基類的訪問權限由基類本身控制,即使對於派生類的基類部分也是如此“ 可知。

​ 當一個類將另一個類聲明爲友元時,這種友元關係只對做出聲明的類有效。對於原來那個類來說,其友元的基類或者派生類不具有特殊的訪問能力,即友元不能被繼承

class D2 : public Pal {
public:
    int mem(Base b) { return b.prot_mem; }	// 錯誤,友元不能被繼承
};
改變個別成員的可訪問性

​ 有時我們需要改變派生類繼承的某個名字的訪問級別,通過使用 using 聲明可以達到這一目的:

class Base {
public:
    std::size_t size() const { return n; }
protected:
    std::size_t n;
};
class Derived : private Base {		// private 繼承方式
public:
	using Base::size;
protected:
    using Base::n;
};

我們的 Derived 繼承 Base,而且是 private 繼承方式,它們應該是 Derived 的私有成員。而我們使用 using 改變了其訪問級別。改變之後,Derived 的用戶將可以使用 size 成員,而 Derived 的派生類也將能使用 n。

默認的繼承保護級別

​ 我們知道,struct 的默認訪問是 public,而 class 是 private。所以默認派生類的繼承方式由派生類關鍵字決定。struct 派生類的默認派生方式是 public,class 派生類的默認派生方式是 private。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章