《C++Primer》第十五章-面向對象編程-學習筆記(1)-基類&派生類&轉換&繼承

《C++Primer》第十五章-面向對象編程-學習筆記(1)

日誌:
1,2020-03-07 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)

摘要

面向對象編程(object-oriented programming)基於三個基本概念:數據抽象繼承動態綁定
在 C++ 中,用類進行數據抽象,用類派生從一個類繼承另一個:派生類繼承基類的成員。
動態綁定(dynamic binding)使編譯器能夠在運行時決定是使用基類中定義的函數還是派生類中定義的函數。
繼承和動態綁定在兩個方面簡化了我們的程序:

  • 能夠容易地定義與其他類相似但又不相同的新類
  • 能夠更容易地編寫忽略這些相似類型之間區別的程序

許多應用程序的特性可以用一些相關但略有不同的概念來描述。例如,書店可以爲不同的書提供不同的定價策略,有些書可以只按給定價格出售,另一些書可以根據不同的折扣策略出售。可以給購買某書一定數量的顧客打折,或者,購買一定數量以內可以打折而超過給定限制就付全價。
面向對象編程(Object-oriented programming,OOP)與這種應用非常匹配。
通過繼承可以定義一些類型,以模擬不同種類的書,通過動態綁定可以編寫程序,使用這些類型而又忽略與具體類型相關的差異。

面向對象編程:概述

面向對象編程的關鍵思想是多態性(polymorphism)。多態性派生於一個希臘單詞,意思是“許多形態”。之所以稱通過繼承而相關聯的類型爲多態類型,是因爲在許多情況下可以互換地使用派生類型或基類型的“許多形態”。在 C++ 中,多態性僅用於通過繼承而相關聯的類型的引用或指針。

繼承

繼承(inheritance)使我們能夠定義這樣的類,它們對類型之間的關係建模,共享公共的東西,僅僅特化本質上不同的東西。
派生類(derived class)能夠繼承基類(base class)定義的成員,派生類可以無須改變而使用那些與派生類型具體特性不相關的操作,派生類可以重定義那些與派生類型相關的成員函數,將函數特化,考慮派生類型的特性。最後,除了從基類繼承的成員之外,派生類還可以定義更多的成員。
我們經常稱因繼承而相關聯的類爲構成了一個繼承層次(inheritance hierarchy)。其中有一個類稱爲,所以其他類直接或間接繼承根類
在書店例子中,我們將定義一個基類,命名爲 Item_base,命名爲 Bulk_item,表示帶數量折扣銷售的書。這些類至少定義如下操作:
• 名爲 book 的操作,返回 ISBN。
• 名爲 net_price 的操作,返回購買指定數量的書的價格。
Item_base 的派生類將無須改變地繼承 book 函數:派生類不需要重新定義獲取 ISBN 的含義。另一方面,每個派生類需要定義自己的 net_price 函數版本,以實現適當的折扣價格策略。
C++ 中基類必須指出希望派生類重寫哪些函數,定義爲 virtual 的函數是基類期待派生類重新定義的,基類希望派生類繼承的函數不能定義爲虛函數。
討論過這些之後,可以看到我們的類將定義三個(const)成員函數:
• 非虛函數 std::string book(),返回 ISBN。由 Item_base 定義,Bulk_item 繼承。
• 虛函數 double net_price(size_t) 的兩個版本,返回給定數目的某書的總價。Item_base 類和 Bulk_item 類將定義該函數自己的版本。

動態綁定

動態綁定(dynamic binding)使我們能夠編寫程序使用繼承層次中任意類型的對象,無須關心對象的具體類型。使用這些類的程序無須區分函數是在基類還是在派生類中定義的。
例如,書店應用程序可以允許顧客在一次交易中選擇幾本書,當顧客購書時,應用程序可以計算總的應付款,指出最終賬單的一個部分將是爲每本書打印一行,以顯示總數和售價。可以定義一個名爲 print_total 的函數管理應用程序的這個部分。
給定一個項目和數量,函數應打印 ISBN 以及購買給定數量的某書的總價。這個函數的輸出應該像這樣:

ISBN: 0-201-54848-8 number sold: 3 total price: 98
ISBN: 0-201-82470-1 number sold: 5 total price: 202.5

可以這樣編寫 print_total 函數:

// calculate and print price for given number of copies, applying any discounts
void print_total(ostream &os,const Item_base &item, size_t n) //第二形參是 Item_base 的引用但可以將 Item_base對象或 Bulk_item 對象傳給它。
{
	os << "ISBN: " << item.book() // calls Item_base::book
	<< "\tnumber sold: " << n << "\ttotal price: "
// virtual call: which version of net_price to call isresolved at run time
	<< item.net_price(n) << endl; //net_price 是虛函數,所以對 net_price 的調用將在運行時確定
}

該函數的工作很普通:調用其 item 形參的 book 和 net_price 函數,打印結果。關於這個函數,有兩點值得注意。
第一,雖然這個函數的第二形參是 Item_base 的引用但可以將 Item_base對象或 Bulk_item 對象傳給它。
第二,因爲形參是引用且 net_price 是虛函數,所以對 net_price 的調用將在運行時確定。調用哪個版本的 net_price 將依賴於傳給 print_total 的實參。如果傳給 print_total 的實參是一個 Bulk_item 對象,將運行 Bulk_item中定義的應用折扣的 net_price;如果實參是一個 Item_base 對象,則調用由
Item_base 定義的版本。

在 C++ 中,通過基類的引用(或指針)調用虛函數時,發生動態綁定。引用(或指針)既可以指向基類對象也可以指向派生類對象,這一事實是動態綁定的關鍵。用引用(或指針)調用 的虛函數在運行時確定,被調用的函數是引用(或指針)所指 對象的實際類型所定義的。

//總結一下就是先定義了虛函數,然後派生類繼承虛函數,重新定義它。然後使用的時候,通常情況下是在形參中傳入一個對象,根據這個對象看使用哪個函數。

定義基類和派生類

在繼承層次中定義類還需要另外一些特性。這些特性的使用對類以及使用繼承類編寫的程序有一些影響。

定義基類

像任意其他類一樣,基類也有定義其接口和實現的數據和函數成員。在(非常簡化的)書店定價應用程序的例子中,Item_base 類定義了 book 和net_price 函數並且需要存儲每本書的 ISBN 和標準價格:

// Item sold at an undiscounted price
// derived classes will define various discount strategies
class Item_base {  //這個是我們用來作爲例子的基類
public:
	Item_base(const std::string &book = "",double sales_price = 0.0):isbn(book), price(sales_price) { }
	std::string book() const { return isbn; }
// returns total sales price for a specified number of items
// derived classes will override and apply different discount algorithms
virtual double net_price(std::size_t n) const{ return n * price; }  //基類中的虛函數
virtual ~Item_base() { }
private:
	std::string isbn; // identifier for the item
protected:
	double price; // normal, undiscounted price繼承層次的根類一般都要定義虛析構函數
};

這個類的大部分看起來像我們已見過的其他類一樣。它定義了一個構造函數以及我們已描述過的函數,該構造函數使用默認實參,允許用 0個、1 個或兩個實參進行調用,它用這些實參初始化數據成員。
新的部分是 protected 訪問標號以及對析構函數和 net_price 函數所使用的保留字 virtual。之後解釋虛析構函數,現在只需注意到繼承層次的根類一般都要定義虛析構函數即可。

基類成員函數

Item_base 類定義了兩個函數,其中一個前面帶有保留字 virtual。保留字virtual 的目的是啓用動態綁定。成員默認爲非虛函數,對非虛函數的調用在編譯時確定。爲了指明函數爲虛函數,在其返回類型前面加上保留字 virtual。除了構造函數之外,任意非 static 成員函數都可以是虛函數保留字只在類內部的成員函數聲明中出現,不能用在類定義體外部出現的函數定義上。

基類通常應將派生類需要重定義的任意函數定義爲虛函數。

訪問控制和繼承

在基類中,public 和 private 標號具有普通含義:用戶代碼可以訪問類的public 成員而不能訪問 private 成員,private 成員只能由基類的成員和友元訪問。派生類對基類的 public 和 private 成員的訪問權限與程序中任意其他部分一樣:它可以訪問 public 成員而不能訪問 private 成員。
有時作爲基類的類具有一些成員,它希望允許派生類訪問但仍禁止其他用戶訪問這些成員。對於這樣的成員應使用受保護的訪問標號protected 成員可以被派生類對象訪問但不能被該類型的普通用戶訪問。
我們的 Item_base 類希望它的派生類重定義 net_price 函數,爲了重定義net_price 函數,這些類將需要訪問 price 成員。希望派生類用與普通用戶一樣通過 book 訪問函數訪問 isbn,因此,isbn 成員爲 private,不能被Item_base 的繼承類所訪問。

protected 成員

可以認爲 protected 訪問標號是 private 和 public 的混合:

  • 像 private 成員一樣,protected 成員不能被類的用戶訪問。
  • 像 public 成員一樣,protected 成員可被該類的派生類訪問。
    此外,protected 還有另一重要性質:
  • 派生類只能通過派生類對象訪問其基類的 protected 成員,派生類對其基類類型對象的 protected 成員沒有特殊訪問權限

例如,假定 Bulk_item 定義了一個成員函數,接受一個 Bulk_item 對象的引用和一個 Item_base 對象的引用,該函數可以訪問自己對象的 protected 成員以及 Bulk_item 形參的 protected 成員,但是,它不能訪問 Item_base 形參的 protected 成員。

void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b)
{
// attempt to use protected member
double ret = price; // ok: uses this->price
ret = d.price; // ok: uses price from a Bulk_item object
ret = b.price; // error: no access to price from an Item_base派生類對其基類類型對象的 protected 成員沒有特殊訪問權限
}

d.price 的使用正確,因爲是通過 Bulk_item 類型對象引用 price;b.price 的使用非法,因爲對 Base_item 類型的對象沒有特殊訪問訪問權限。

派生類

爲了定義派生類,使用類派生列表指定基類。類派生列表指定了一個或多個基類,具有如下形式:

class classname: access-label base-class

這裏 access-label 是 public、protected 或 private,base-class 是已定義的類的名字。類派生列表可以指定多個基類。繼承單個基類是爲常見。現在,只需要瞭解訪問標號決定了對繼承成員的訪問權限。如果想要繼承基類的接口,則應該進行public 派生。
派生類繼承基類的成員並且可以定義自己的附加成員。每個派生類對象包含兩個部分:從基類繼承的成員自己定義的成員。一般而言,派生類只(重)定義那些與基類不同或擴展基類行爲的方面。

定義派生類

在書店應用程序中,將從 Item_base 類派生 Bulk_item 類,因此Bulk_item 類將繼承 book、isbn 和 price 成員。Bulk_item 類必須重定義net_price 函數定義該操作所需要的數據成員:

// discount kicks in when a specified number of copies of same book are sold
// the discount is expressed as a fraction used to reduce the normal price
class Bulk_item : public Item_base {
public:
// redefines base version so as to implement bulk purchase discount policy
double net_price(std::size_t) const;
private:
std::size_t min_qty; // minimum purchase for discount to apply
double discount; // fractional discount to apply
};

每個 Bulk_item 對象包含四個數據成員:從 Item_base 繼承的 isbn 和price,自己定義的 min_qty 和 discount,後兩個成員指定最小數量以及購買超過該數量時給的折扣。Bulk_item 類還需要定義一個構造函數。

派生類和虛函數

儘管不是必須這樣做,派生類一般會重定義所繼承的虛函數。派生類沒有重定義某個虛函數,則使用基類中定義的版本。
派生類型必須對想要重定義的每個繼承成員進行聲明。Bulk_item 類指出,它將重定義 net_price 函數但將使用 book 的繼承版本。
派生類中虛函數的聲明必須與基類中的定義方式完全匹配,但有一個例外:返回對基類型的引用(或指針)的虛函數。派生類中的虛函數可以返回基類函數所返回類型的派生類的引用(或指針)。
例如,Item_base 類可以定義返回 Item_base* 的虛函數,如果這樣,Bulk_item 類中定義的實例可以定義爲返回 Item_base* 或 Bulk_item*。
一旦函數在基類中聲明爲虛函數,它就一直爲虛函數,派生類無法改變該函數爲虛函數這一事實。派生類重定義虛函數時,可以使用 virtual 保留字,但不是必須這樣做。

派生類對象包含基類對象作爲子對象

派生類對象由多個部分組成:派生類本身定義的(非 static)成員加上由基類(非 static)成員組成的子對象。可以認爲 Bulk_item 對象由圖 1 表示的兩個部分組成。
在這裏插入圖片描述
圖1. Bulk_item 對象的概念結構
C++ 語言不要求編譯器將對象的基類部分和派生部分和派生部分連續排列,因此,圖 15.1 是關於類如何工作的概念表示而不是物理表示。

派生類中的函數可以使用基類的成員

像任意成員函數一樣,派生類函數可以在類的內部或外部定義,正如這裏的net_price 函數一樣:

// if specified number of items are purchased, use discounted price
double Bulk_item::net_price(size_t cnt) const
{
if (cnt >= min_qty)
	return cnt * (1 - discount) * price;
else
	return cnt * price;  //price是基類的protected成員
}

該函數產生折扣價格:如果給定數量多於 min_qty,就對 price 應用 discount(discount 存儲爲分數)。因爲每個派生類對象都有基類部分,類可以訪問共基類的public 和 protected 成員,就好像那些成員是派生類自己的成員一樣

用作基類的類必須是已定義的

已定義的類纔可以用作基類。如果已經聲明瞭 Item_base 類,但沒有定義它,則不能用 Item_base 作基類:

class Item_base; // declared but not defined
// error: Item_base must be defined
class Bulk_item : public Item_base { ... };

這一限制的原因應該很容易明白:每個派生類包含並且可以訪問其基類的成員,爲了使用這些成員,派生類必須知道它們是什麼。這一規則暗示着不可能從類自身派生出一個類。

用派生類作基類

基類本身可以是一個派生類

class Base { /* ... */ };
class D1: public Base { /* ... */ };
class D2: public D1 { /* ... */ };

每個類繼承其基類的所有成員。最底層的派生類繼承其基類的成員,基類又繼承自己的基類的成員,如此沿着繼承鏈依次向上。從效果來說,最底層的派生類對象包含其每個直接基類和間接基類的子對象。

派生類的聲明

如果需要聲明(但並不實現)一個派生類,則聲明包含類名但不包含派生列表。例如,下面的前向聲明會導致編譯時錯誤:

// error: a forward declaration must not include the derivation list
class Bulk_item : public Item_base;

正確的前向聲明爲:

// forward declarations of both derived and nonderived class
class Bulk_item;
class Item_base;

virtual 與其他成員函數

C++ 中的函數調用默認不使用動態綁定。要觸發動態綁定,滿足兩個條件:

  • 第一,只有指定爲虛函數的成員函數才能進行動態綁定,成員函數默認爲非虛函數,非虛函數不進- 行動態綁定;
  • 第二,必須通過基類類型的引用或指針進行函數調用。要理解這一要求,需要理解在使用繼承層次中某一類型的對象的引用或指針時會發生什麼。

從派生類型到基類的轉換

因爲每個派生類對象都包含基類部分,所以可將基類類型的引用綁定到派生類對象的基類部分,也可以用指向基類的指針指向派生類對象

// function with an Item_base reference parameter
double print_total(const Item_base&, size_t); //形參基類對象的引用
Item_base item; // 基類對象
// ok: use pointer or reference to Item_base to refer to an Item_base object
print_total(item, 10); // passes reference to an Item_base object
Item_base *p = &item; // p points to an Item_base object
Bulk_item bulk; // object of derived type 派生類對象
// ok: can bind a pointer or reference to Item_base to a Bulk_item object
print_total(bulk, 10); // passes reference to the Item_base part of bulk
p = &bulk; // p points to the Item_base part of bulk

這段代碼使用同一基類類型指針指向基類類型的對象和派生類型的對象,該代碼還傳遞基類類型和派生類型的對象來調用需要基類類型引用的函數,兩種使用都是正確的,因爲每個派生類對象都擁有基類部分。
因爲可以使用基類類型的指針或引用來引用派生類型對象,所以,使用基類類型的引用或指針時,不知道指針或引用所綁定的對象的類型:基類類型的引用或指針可以引用基類類型對象,也可以引用派生類型對象。無論實際對象具有哪種類型,編譯器都將它當作基類類型對象。將派生類對象當作基類對象是安全的,因爲每個派生類對象都擁有基類子對象。而且,派生類繼承基類的操作,即,
何可以在基類對象上執行的操作也可以通過派生類對象使用

基類類型引用和指針的關鍵點在於靜態類型(在編譯時可知的引用類型或指針類型)和動態類型(指針或引用所綁定的對象的類型這是僅在運行時可知的)可能不同。

可以在運行時確定 virtual 函數的調用

將基類類型的引用或指針綁定到派生類對象對基類對象沒有影響,對象本身不會改變,仍爲派生類對象。對象的實際類型可能不同於該對象引用或指針的靜態類型,這是 C++ 中動態綁定的關鍵。
通過引用或指針調用虛函數時,編譯器將生成代碼,在運行時確定調用哪個函數,被調用的是與動態類型相對應的函數。例如,我們再來看 print_total 函數:

// calculate and print price for given number of copies, applying any discounts
void print_total(ostream &os,const Item_base &item, size_t n)
{
	os << "ISBN: " << item.book() // calls Item_base::book
	<< "\tnumber sold: " << n << "\ttotal price: "
// virtual call: which version of net_price to call is resolved at run time
	<< item.net_price(n) << endl;
}

因爲 item 形參是一個引用且 net_price 是虛函數,item.net_price(n) 所調用的 net_price 版本取決於在運行時綁定到 item 形參的實參類型

Item_base base;
Bulk_item derived;
// print_total makes a virtual call to net_price
print_total(cout, base, 10); // calls Item_base::net_price
print_total(cout, derived, 10); // calls Bulk_item::net_price

在第一個調用中,item 形參在運行時綁定到 Item_base 類型的對象,因此,print_total 內部調用 Item_base 中定義的 net_price 版本。在第二個調用中,item 形參綁定到 Bulk_item 類型的對象,從 print_total 調用的是Bulk_item 類定義的 net_price 版本。

在編譯時確定非 virtual 調用

不管傳給 print_total 的實參的實際類型是什麼,對 book 的調用在編譯時確定爲調用Item_base::book。
即使 Bulk_item 定義了自己的 book 函數版本,這個調用也會調用基類中的版本。
非虛函數總是在編譯時根據調用該函數的對象、引用或指針的類型而確定。
item 的類型是 const Item_base 的引用,所以,無論在運行時 item 引用的實際對象是什麼類型,調用該對象的非虛函數都將會調用 Item_base 中定義的版本。

覆蓋虛函數機制

在某些情況下,希望覆蓋虛函數機制並強制函數調用使用虛函數的特定版本,這裏可以使用作用域操作符

Item_base *baseP = &derived;
// calls version from the base class regardless of the dynamic type of baseP
double d = baseP->Item_base::net_price(42);

這段代碼強制將 net_price 調用確定爲 Item_base 中定義的版本,該調用將在編譯時確定。
只有成員函數中的代碼才應該使用作用域操作符覆蓋虛函數機制。
爲什麼會希望覆蓋虛函數機制?最常見的理由是爲了派生類虛函數調用基類中的版本。在這種情況下,基類版本可以完成繼承層次中所有類型的公共任務,而每個派生類型只添加自己的特殊工作。
例如,可以定義一個具有虛操作的 Camera 類層次。Camera 類中的 display函數可以顯示所有的公共信息,派生類(如 PerspectiveCamera)可能既需要顯示公共信息又需要顯示自己的獨特信息。可以顯式調用 Camera 版本以顯示公共信息,而不是在 PerspectiveCamera 的 display 實現中複製 Camera 的操作。
在這種情況下,已經確切知道調用哪個實例,因此,不需要通過虛函數機制。派生類虛函數調用基類版本時,必須顯式使用作用域操作符。如果派生類函數忽略了這樣做,則函數調用會在運行時確定並且將是一個自身調用,從而導致無窮遞歸。

虛函數與默認實參

像其他任何函數一樣,虛函數也可以有默認實參。通常,如果有用在給定調用中的默認實參值,該值將在編譯時確定。如果一個調用省略了具有默認值的實參,則所用的值由調用該函數的類型定義(也就是用的什麼類類型就是哪個類類型的默認實參),與對象的動態類型無關。

  • 通過基類的引用或指針調用虛函數時,默認實參爲在基類虛函數聲明中指定的值,
  • 如果通過派生類的指針或引用調用虛函數,則默認實參是在派生類的版本中聲明的值。

在同一虛函數的基類版本和派生類版本中使用不同的默認實參幾乎一定會引起麻煩。如果通過基類的引用或指針調用虛函數,但實際執行的是派生類中定義的版本,這時就可能會出現問題。在這種情況下,爲虛函數的基類版本定義的默認實參將傳給派生類定義的版本,而派生類版本是用不同的默認實參定義的。

公用、私有和受保護的繼承

派生類中定義的成員訪問控制的處理與任意其他類中完全一樣。派生類可以定義零個或多個訪問標號,指定跟隨其後的成員的訪問級別。對類所繼承的成員的訪問由基類中的成員訪問級別和派生類派生列表中使用的訪問標號共同控制。
每個類控制它所定義的成員的訪問。派生類可以進一步限制但不能放鬆對所繼承的成員的訪問
基類本身指定對自身成員的最小訪問控制。如果成員在基類中爲 private,則只有基類和基類的友元可以訪問該成員。派生類不能訪問基類的 private 成員,也不能使自己的用戶能夠訪問那些成員

如果基類成員爲 public 或protected,則派生列表中使用的訪問標號決定該成員在派生類中的訪問級別:

  • 如果是公用繼承,基類成員保持自己的訪問級別:基類的 public 成員爲派生類的 public 成員,基類的 protected 成員爲派生類的 protected成員。
  • 如果是受保護繼承,基類的 public 和 protected 成員在派生類中爲protected 成員。
  • 如果是私有繼承,基類的的所有成員在派生類中爲 private 成員。
  • (可以用優先級來記憶,private>protected>privated,繼承的時候,優先級高的覆蓋優先級低的)
    例如,考慮下面的繼承層次:
class Base {
public:
	void basemem(); // public member
protected:
	int i; // protected member
// ...
};
struct Public_derived : public Base {
	int use_base() { return i; } // ok: derived classes can access i
// ...
};
struct Private_derived : private Base {
	int use_base() { return i; } // ok: derived classes can access i
};

無論派生列表中是什麼訪問標號,所有繼承 Base 的類對 Base 中的成員具有相同的訪問。派生訪問標號將控制派生類的用戶對從 Base 繼承而來的成員的訪問:

Base b;
Public_derived d1;
Private_derived d2;
b.basemem(); // ok: basemem is public
d1.basemem(); // ok: basemem is public in the derived class
d2.basemem(); // error: basemem is private in the derived class

Public_derived 和 Private_derived 都繼承了 basemem 函數。當進行public 繼承時,該成員保持其訪問標號,所以,d1 可以調用 basemem。在Private_derived 中,Base 的成員爲 private,Private_derived 的用戶不能調用 basemem。
派生訪問標號還控制來自非直接派生類的訪問:

struct Derived_from Private : public Private_derived {
// error: Base::i is private in Private_derived  //出錯
	int use_base() { return i; }
};
struct Derived_from_Public : public Public_derived {
// ok: Base::i remains protected in Public_derived
	int use_base() { return i; }
};

從 Public_derived 派生的類可以訪問來自 Base 類的 i,是因爲該成員在Public_derived 中仍爲 protected 成員。從 Private_derived 派生的類沒有這樣的訪問,對它們而言,Private_derived 從 Base 繼承的所有成員均爲private。

接口繼承與實現繼承

public 派生類繼承基類的接口,它具有與基類相同的接口。設計良好的類層次中,public 派生類的對象可以用在任何需要基類對象的地方。
使用 private 或 protected 派生的類不繼承基類的接口,相反,這些派生通常被稱爲實現繼承。派生類在實現中使用被繼承但繼承基類的部分並未成爲其接口的一部分。類是使用接口繼承還是實現繼承對派生類的用戶具有重要含義。迄今爲止,最常見的繼承形式是 public。

去除個別成員

如果進行 private 或 protected 繼承,則基類成員的訪問級別在派生類中比在基類中更受限:

class Base {
public:
	std::size_t size() const { return n; }
protected:
	std::size_t n;
};
class Derived : private Base { . . . };

派生類可以恢復繼承成員的訪問級別,但不能使訪問級別比基類中原來指定的更嚴格或更寬鬆。
在這一繼承層次中,size 在 Base 中爲 public,但在 Derived 中爲private。爲了使 size 在 Derived 中成爲 public,可以在 Derived 的 public部分增加一個 using 聲明。如下這樣改變 Derived 的定義,可以使 size 成員能夠被用戶訪問,並使 n 能夠被從 Derived 派生的類訪問:

class Derived : private Base {
public:
// maintain access levels for members related to the size of the object
	using Base::size;
protected:
	using Base::n;
// ...
};

正如可以使用 using 聲明從命名空間使用名字,也可以使用using 聲明訪問基類中的名字,除了在作用域操作符左邊用類名字代替命名空間名字之外,使用形式是相同的。

默認繼承保護級別

用 struct 和 class 保留字定義的類具有不同的默認訪問級別,同樣,默認繼承訪問級別根據使用哪個保留字定義派生類也不相同。
使用 class 保留字定義的派生默認具有 private 繼承,而用 struct 保留字定義的類默認具有 public 繼承

class Base { /* ... */ };
struct D1 : Base { /* ... */ }; // public inheritance by default
class D2 : Base { /* ... */ }; // private inheritance by default

有一種常見的誤解認爲用 struct 保留字定義的類與用 class 定義的類有更大的區別。唯一的不同只是默認的成員保護級別和默認的派生保護級別,沒有其他區別:

class D3 : public Base {
public:
/* ... */
};
// equivalent definition of D3
struct D3 : Base { // inheritance public by default
/* ... */ // initial member access public by default
};
struct D4 : private Base {
private:
/* ... */
};
// equivalent definition of D4
class D4 : Base { // inheritance private by default
/* ... */ // initial member access private by default
};

儘管私有繼承在使用 class 保留字時是默認情況,但這在實踐中相對罕見。因爲私有繼承是如此罕見,通顯式指定 private 是比依賴於默認更好的辦法。顯式指定可清楚指出想要私有繼承而不是一時疏忽。

友元關係與繼承

像其他類一樣,基類或派生類可以使其他類或函數成爲友元友元可以訪問類的 private 和 protected 數據。
友元關係不能繼承。基類的友元對派生類的成員沒有特殊訪問權限。如果基類被授予友元關係,則只有基類具有特殊訪問權限,該基類的派生類不能訪問授予友元關係的類。
每個類控制對自己的成員的友元關係:

class Base {
	friend class Frnd;
protected:
	int i;
};
// Frnd has no access to members in D1
class D1 : public Base {
protected:
	int j;
};
class Frnd {
public:
	int mem(Base b) { return b.i; } // ok: Frnd is friend to Base
	int mem(D1 d) { return d.i; } // error: friendship doesn't inherit
};
// D2 has no access to members in Base
class D2 : public Frnd {
public:
	int mem(Base b) { return b.i; } // error: friendship doesn't inherit
};

如果派生類想要將自己成員的訪問權授予其基類的友元,派生類必須顯式地這樣做(因爲基類的友元對從該基類派生的類型沒有特殊訪問權限)。同樣,如果基類和派生類都需要訪問另一個類,那個類必須特地將訪問權限授予基類的和每一個派生類。

繼承與靜態成員

如果基類定義 static 成員,則整個繼承層次中只有一個樣的成員。無論從基類派生出多少個派生類,每個 static 成員只有一個實例。
static 成員遵循常規訪問控制:如果成員在基類中爲 private,則派生類不能訪問它。我們假定可以訪問成員,則既可以通過基類訪問 static 成員,也可以通過派生類訪問 static 成員。一般而言,既可以使用作用域操作符也可以使用點或箭頭成員訪問操作符。

struct Base {
static void statmem(); // public by default
};
struct Derived : Base {
	void f(const Derived&);
};
void Derived::f(const Derived &derived_obj)
{
	Base::statmem(); // ok: Base defines statmem
	Derived::statmem(); // ok: Derived in herits statmem
// ok: derived objects can be used to access static from base
	derived_obj.statmem(); // accessed through Derived object
	statmem(); // accessed through this class

轉換與繼承

理解基類類型和派生類型之間的轉換,對於理解面向對象編程在 C++ 中如何工作非常關鍵。

我們已經看到,每個派生類對象包含一個基類部分,這意味着可以像使用基類對象一樣在派生類對象上執行操作。因爲派生類對象也是基類對象,所以存在從派生類型引用到基類類型引用的自動轉換,即,可以將派生類對象的引用轉換爲基類子對象的引用,對指針也類似。
基類類型對象既可以作爲獨立對象存在,也可以作爲派生類對象的一部分而存在,因此,一個基類對象可能是也可能不是一個派生類對象的部分,結果,沒有從基類引用(或基類指針)到派生類引用(或派生類指針)的(自動)轉換。
相對於引用或指針而言,對象轉換的情況更爲複雜。雖然一般可以使用派生類型的對象對基類類型的對象進行初始化或賦值,但,沒有從派生類型對象到基類類型對象的直接轉換。

派生類到基類的轉換

如果有一個派生類型的對象,則可以使用它的地址對基類類型的指針進行賦值或初始化。同樣,可以使用派生類型的引用或對象初始化基類類型的引用。嚴格說來,對對象沒有類似轉換。編譯器不會自動將派生類型對象轉換爲基類類型對象。
但是,一般可以使用派生類型對象對基類對象進行賦值或初始化。對對象進行初始化和賦值以及可以自動轉換引用或指針,這之間的區別是微妙的,必須好好理解。

引用轉換不同於轉換對象

我們已經看到,可以將派生類型的對象傳給希望接受基類引用的函數。也許會因此認爲對象進行轉換,但是,事實並非如此。將對象傳給希望接受引用的函數時,引用直接綁定到該對象,雖然看起來在傳遞對象,實際上實參是該對象的引用,對象本身未被複制並且,轉換不會在任何方面改變派生類型對象,該對象仍是派生類型對象。
將派生類對象傳給希望接受基類類型對象(而不是引用)的函數時,情況完全不同。在這種情況下,形參的類型是固定的——在編譯時和運行時形參都是基類類型對象。如果用派生類型對象調用這樣的函數,則該派生類對象的基類部分被複制到形參。
一個是派生類對象轉換爲基類類型引用,一個是用派生類對象對基類對象進行初始化或賦值,理解它們之間的區別很重要。

用派生類對象對基類對象進行初始化或賦值

對基類對象進行初始化或賦值,實際上是在調用函數:初始化時調用構造函數,賦值時調用賦值操作符。
用派生類對象對基類對象進行初始化或賦值時,有兩種可能性。第一種(雖然不太可能的)可能性是,基類可能顯式定義了將派生類型對象複製或賦值給基類對象的含義,這可以通過定義適當的構造函數或賦值操作符實現:

class Derived;
class Base {
public:
	Base(const Derived&); // create a new Base from a Derived
	Base &operator=(const Derived&); // assign from a Derived
// ...
};

在這種情況下,這些成員的定義將控制用 Derived 對象對 Base 對象進行初始化或賦值時會發生什麼。
然而,類顯式定義怎樣用派生類型對象對基類類型進行初始化或賦值並不常見,相反,基類一般(顯式或隱式地)定義自己的複製構造函數和賦值操作符,這些成員接受一個形參,該形參是基類類型的(const)引用。因爲存在從派生類引用到基類引用的轉換,這些複製控制成員可用於從派生類對象對
基類對象進行初始化或賦值:

Item_base item; // object of base type
Bulk_item bulk; // object of derived type
// ok: uses Item_base::Item_base(const Item_base&) constructor
Item_base item(bulk); // bulk is "sliced down" to its Item_base portion
// ok: calls Item_base::operator=(const Item_base&)
item = bulk; // bulk is "sliced down" to its Item_base portion

用 Bulk_item 類型的對象調用 Item_base 類的複製構造函數或賦值操作符時,將發生下列步驟:

  • 將 Bulk_item 對象轉換爲 Item_base 引用,這僅僅意味着將一個Item_base 引用綁定到 Bulk_item 對象。
  • 將該引用作爲實參傳給複製構造函數或賦值操作符。
  • 那些操作符使用 Bulk_item 的 Item_base 部分分別對調用構造函數或賦值的 Item_base 對象的成員進行初始化或賦值。
  • 一旦操作符執行完畢,對象即爲 Item_base。它包含 Bulk_item 的Item_base 部分的副本,但實參的 Bulk_item 部分被忽略。

在這種情況下,我們說 bulk 的 Bulk_item 部分在對 item 進行初始化或賦值時被“切掉”了。Item_base 對象只包含基類中定義的成員,不包含由任意派生類型定義的成員,Item_base 對象中沒有派生類成員的存儲空間。

派生類到基類轉換的可訪問性

像繼承的成員函數一樣,從派生類到基類的轉換可能是也可能不是可訪問的。轉換是否訪問取決於在派生類的派生列表中指定的訪問標號

要確定到基類的轉換是否可訪問,可以考慮基類的 public成員是否訪問,如果可以,轉換是可訪問的,否則,轉換是不可訪問的。

  • 如果是 public 繼承,則用戶代碼和後代類都可以使用派生類到基類的轉換。
  • 如果類是使用 private 或 protected 繼承派生的,則用戶代碼不能將派生類型對象轉換爲基類對象。
  • 如果是 private 繼承,則從 private 繼承類派生的類不能轉換爲基類。
  • 如果是 protected 繼承,則後續派生類的成員可以轉換爲基類類型。
    無論是什麼派生訪問標號,派生類本身都可以訪問基類的 public 成員,因此,派生類本身的成員和友元總是可以訪問派生類到基類的轉換。

基類到派生類的轉換

從基類到派生類的自動轉換是不存在的。需要派生類對象時不能使用基類對象:

Item_base base;
Bulk_item* bulkP = &base; // error: can't convert base to derived
Bulk_item& bulkRef = base; // error: can't convert base to derived
Bulk_item bulk = base; // error: can't convert base to derived

沒有從基類類型到派生類型的(自動)轉換,原因在於基類對象只能是基類對象,它不能包含派生類型成員。如果允許用基類對象給派生類型對象賦值,那麼就可以試圖使用該派生類對象訪問不存在的成員。有時更令人驚訝的是,甚至當基類指針或引用實際綁定到綁定到派生類對象時,從基類到派生類的轉換也存在限制:

Bulk_item bulk;
Item_base *itemP = &bulk; // ok: dynamic type is Bulk_item
Bulk_item *bulkP = itemP; // error: can't convert base to derived

編譯器在編譯時無法知道特定轉換在運行時實際上是安全的編譯器確定轉換是否合法,只看指針或引用的靜態類型。
在這些情況下,如果知道從基類到派生類的轉換是安全的,就可以使用static_cast強制編譯器進行轉換。或者,可以用 dynamic_cast申請在運行時進行檢查。

類設計與受保護成員

如果沒有繼承,類只有兩種用戶:類本身的成員(private|public)和該類的用戶(public)。將類劃分爲 private 和 public 訪問級別反映了用戶種類的這一分隔:用戶只能訪問 public 接口,類成員和友元既可以訪問 public 成員也可以訪問 private 成員。
有了繼承,就有了類的第三種用戶:從類派生定義新類的程序員。派生類的提供者通常(但並不總是)需要訪問(一般爲 private 的)基類實現,爲了允許這種訪問而仍然禁止對實現的一般訪問,提供了附加的protected 訪問標號。類的 protected 部分仍然不能被一般程序訪問,但可以被派生類訪問。只有類本身和友元可以訪問基類的 private 部分,派生類不能訪問基類的 private 成員。
定義類充當基類時,將成員設計爲 public 的標準並沒有改變:仍然是接口函數應該爲 public 而數據一般不應爲 public。被繼承的類必須決定實現的哪些部分聲明爲 protected 而哪些部分聲明爲 private。希望禁止派生類訪問的成員應該設爲 private,提供派生類實現所需操作或數據的成員應設爲protected。換句話說,提供給派生類型的接口是protected 成員和 public 成員的組合。

C++ 中的多態性

引用和指針的靜態類型與動態類型可以不同,這是 C++ 用以支持多態性的基石。
通過基類引用或指針調用基類中定義的函數時,我們並不知道執行函數的對象的確切類型,執行函數的對象可能是基類類型的,也可能是派生類型的。
如果調用非虛函數,則無論實際對象是什麼類型,都執行基類類型所定義的函數。如果調用虛函數,則直到運行時才能確定調用哪個函數,運行的虛函數是引用所綁定的或指針所指向的對象所屬類型定義的版本
從編寫代碼的角度看我們無需擔心。只要正確地設計和實現了類,不管實際對象是基類類型或派生類型,操作都將完成正確的工作。
另一方面,對象是非多態的——對象類型已知且不變。**對象的動態類型總是與靜態類型相同,這一點與引用或指針相反。**運行的函數(虛函數或非虛函數)是由對象的類型定義的。
只有通過引用或指針調用,虛函數纔在運行時確定。只有在這些情況下,直到運行時才知道對象的動態類型。

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

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