《C++ Primer 4th》讀書筆記程序員
面向對象編程基於三個基本概念:數據抽象、繼承和動態綁定。在 C++ 中,用類進行數據抽象,用類派生從一個類繼承另外一個:派生類繼承基類的成員。動態綁定使編譯器可以在運行時決定是使用基類中定義的函數仍是派生類中定義的函數。編程
繼承和動態綁定在兩個方面簡化了咱們的程序:可以容易地定義與其餘類類似但又不相同的新類,可以更容易地編寫忽略這些類似類型之間區別的程序。數組
面向對象編程(Object-oriented programming,OOP)與這種應用很是匹配。經過繼承能夠定義一些類型,以模擬不一樣種類的書,經過動態綁定能夠編寫程序,使用這些類型而又忽略與具體類型相關的差別。安全
面向對象編程的關鍵思想是多態性(polymorphism)。多態性派生於一個希臘單詞,意思是「許多形態」。之因此稱經過繼承而相關聯的類型爲多態類型,是由於在許多狀況下能夠互換地使用派生類型或基類型的「許多形態」。less
經過繼承咱們可以定義這樣的類,它們對類型之間的關係建模,共享公共的東西,僅僅特化本質上不一樣的東西。基類必須指出但願派生類重寫哪些函數,定義爲 virtual 的函數是基類期待派生類從新定義的,基類但願派生類繼承的函數不能定義爲虛函數。ide
經過動態綁定咱們可以編寫程序使用繼承層次中任意類型的對象,無須關心對象的具體類型。咱們常常稱因繼承而相關聯的類爲構成了一個繼承層次。函數
經過基類的引用(或指針)調用虛函數時,發生動態綁定。引用(或指針)既能夠指向基類對象也能夠指向派生類對象,這一事實是動態綁定的關鍵。用引用(或指針)調用的虛函數在運行時肯定,被調用的函數是引用(或指針)所指對象的實際類型所定義的。this
定義基類spa
繼承層次的根類通常都要定義虛析構函數便可。設計
保留字virtual 的目的是啓用動態綁定。成員默認爲非虛函數,對非虛函數的調用在編譯時肯定。爲了指明函數爲虛函數,在其返回類型前面加上保留字 virtual。除了構造函數以外,任意非 static 成員函數均可以是虛函數。保留字只在類內部的成員函數聲明中出現,不能用在類定義體外部出現的函數定義上。基類一般應將派生類須要重定義的任意函數定義爲虛函數。
C++ 中的函數調用默認不使用動態綁定。要觸發動態綁定,知足兩個條件:第一,只有指定爲虛函數的成員函數才能進行動態綁定,成員函數默認爲非虛函數,非虛函數不進行動態綁定;第二,必須經過基類類型的引用或指針進行函數調用。要理解這一要求,須要理解在使用繼承層次中某一類型的對象的引用或指針時會發生什麼。
能夠認爲 protected 訪問標號是 private 和 public 的混合:
• 像 private 成員同樣,protected 成員不能被類的用戶訪問。
• 像 public 成員同樣,protected 成員可被該類的派生類訪問。
• 派生類只能經過派生類對象訪問其基類的 protected 成員,派生類對其基類類型對象的 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 }
關鍵概念:類設計與受保護成員
若是沒有繼承,類只有兩種用戶:類自己的成員和該類的用戶。將類劃分爲 private 和 public 訪問級別反映了用戶種類的這一分隔:用戶只能訪問 public 接口,類成員和友元既能夠訪問 public 成員也能夠訪問 private 成員。
有了繼承,就有了類的第三種用戶:從類派生定義新類的程序員。派生類的提供者一般(但並不老是)須要訪問(通常爲 private 的)基類實現,爲了容許這種訪問而仍然禁止對實現的通常訪問,提供了附加的protected 訪問標號。類的 protected 部分仍然不能被通常程序訪問,但能夠被派生類訪問。只有類自己和友元能夠訪問基類的 private 部分,派生類不能訪問基類的 private 成員。
定義類充當基類時,將成員設計爲 public 的標準並無改變:仍然是接口函數應該爲 public 而數據通常不該爲 public。被繼承的類必須決定實現的哪些部分聲明爲 protected 而哪些部分聲明爲 private。但願禁止派生類訪問的成員應該設爲 private,提供派生類實現所需操做或數據的成員應設爲 protected。換句話說,提供給派生類型的接口是protected 成員和 public 成員的組合。
儘管不是必須這樣作,派生類通常會重定義所繼承的虛函數。派生類中虛函數的聲明必須與基類中的定義方式徹底匹配,但有一個例外:返回對基類型的引用(或指針)的虛函數。派生類中的虛函數能夠返回基類函數所返回類型的派生類的引用(或指針)。
一旦函數在基類中聲明爲虛函數,它就一直爲虛函數,派生類沒法改變該函數爲虛函數這一事實。派生類重定義虛函數時,可使用 virtual 保留字,但不是必須這樣作。
用做基類的類必須是已定義的
已定義的類才能夠用做基類。若是已經聲明瞭 Item_base 類,但沒有定義它,則不能用 Item_base 做基類:
class Item_base; // declared but not defined // error: Item_base must be defined class Bulk_item : public Item_base { ... };
這一限制的緣由應該很容易明白:每一個派生類包含而且能夠訪問其基類的成員,爲了使用這些成員,派生類必須知道它們是什麼。這一規則暗示着不可能從類自身派生出一個類。
若是須要聲明(但並不實現)一個派生類,則聲明包含類名但不包含派生列表。例如,下面的前向聲明會致使編譯時錯誤:
// 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++ 中動態綁定的關鍵。
關鍵概念:C++ 中的多態性
引用和指針的靜態類型與動態類型能夠不一樣,這是 C++ 用以支持多態性的基石。經過基類引用或指針調用基類中定義的函數時,咱們並不知道執行函數的對象的確切類型,執行函數的對象多是基類類型的,也多是派生
類型的。
若是調用非虛函數,則不管實際對象是什麼類型,都執行基類類型所定義的函數。若是調用虛函數,則直到運行時才能肯定調用哪一個函數,運行的虛函數是引用所綁定的或指針所指向的對象所屬類型定義的版本。
從編寫代碼的角度看咱們無需擔憂。只要正確地設計和實現了類,無論實際對象是基類類型或派生類型,操做都將完成正確的工做。
另外一方面,對象是非多態的——對象類型已知且不變。對象的動態類型老是與靜態類型相同,這一點與引用或指針相反。運行的函數(虛函數或非虛函數)是由對象的類型定義的。
只有經過引用或指針調用,虛函數纔在運行時肯定。只有在這些狀況下,直到運行時才知道對象的動態類型。
注:多態實現形式包括:接口,抽象類 和 重寫。而重載不該該認爲是多態的實現。
成員函數的重載、覆蓋與隱藏
重載與覆蓋
成員函數被重載的特徵:
(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不一樣;
(4)virtual 關鍵字無關緊要。
覆蓋是指派生類函數覆蓋基類函數,特徵是:
(1)不一樣的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;
(4)基類函數必須有virtual 關鍵字。
「隱藏」是指派生類的函數屏蔽了與其同名的基類函數,規則以下:
(1)若是派生類的函數與基類的函數同名,可是參數不一樣。此時,不論有無virtual關鍵字,基類的函數將被隱藏(注意別與重載混淆)。
(2)若是派生類的函數與基類的函數同名,而且參數也相同,可是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏(注意別與覆蓋混淆)。
在編譯時肯定非 virtual 調用
非虛函數老是在編譯時根據調用該函數的對象、引用或指針的類型而肯定。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);
只有成員函數中的代碼才應該使用做用域操做符覆蓋虛函數機制。
爲何會但願覆蓋虛函數機制?最多見的理由是爲了派生類虛函數調用基類中的版本。在這種狀況下,基類版本能夠完成繼承層次中全部類型的公共任務,而每一個派生類型只添加本身的特殊工做。
派生類虛函數調用基類版本時,必須顯式使用做用域操做符。若是派生類函數忽略了這樣作,則函數調用會在運行時肯定而且將是一個自身調用,從而致使無窮遞歸。
虛函數與默認實參
虛函數也能夠有默認實參。經過基類的引用或指針調用虛函數時,默認實參爲在基類虛函數聲明中指定的值,若是經過派生類的指針或引用調用虛函數,則默認實參是在派生類的版本中聲明的值。在同一虛函數的基類版本和派生類版本中使用不一樣的默認實參幾乎必定會引發麻煩。
公用、私有和受保護的繼承
對類所繼承的成員的訪問由基類中的成員訪問級別和派生類派生列表中使用的訪問標號共同控制。
派生類不能訪問基類的 private 成員,也不能使本身的用戶可以訪問那些成員。若是基類成員爲 public 或
protected,則派生列表中使用的訪問標號決定該成員在派生類中的訪問級別:
• 若是是公用繼承,基類成員保持本身的訪問級別:基類的 public 成員爲派生類的 public 成員,基類的 protected 成員爲派生類的 protected成員。
• 若是是受保護繼承,基類的 public 和 protected 成員在派生類中爲protected 成員。
• 若是是私有繼承,基類的的全部成員在派生類中爲 private 成員。
接口繼承與實現繼承
public 派生類繼承基類的接口,它具備與基類相同的接口。設計良好的類層次中,public 派生類的對象能夠用在任何須要基類對象的地方。
使用 private 或 protected 派生的類不繼承基類的接口,相反,這些派生一般被稱爲實現繼承。派生類在實現中使用被繼承但繼承基類的部分並未成爲其接口的一部分。
迄今爲止,最多見的繼承形式是 public。
去除個別成員
派生類能夠恢復繼承成員的訪問級別,但不能使訪問級別比基類中原來指定的更嚴格或更寬鬆。
class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { . . . };
爲了使 size 在 Derived 中成爲 public,能夠在 Derived 的 public部分增長一個 using 聲明。以下這樣改變 Derived 的定義,可使 size 成員可以被用戶訪問,並使 n 可以被從 Derived 派生的類訪問:
也可使用using 聲明訪問基類中的名字,除了在做用域操做符左邊用類名字代替命名空間名字以外,使用形式是相同的。
class Derived : private Base { public: // maintain access levels for members related to the size of the object using Base::size; protected: using Base::n; // ... };
默認繼承保護級別
用 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 保留字時是默認狀況,但這在實踐中相對罕見。由於私有繼承是如此罕見,一般顯式指定 private 是比依賴於默認更好的辦法。顯式指定可清楚指出想要私有繼承而不是一時疏忽。
友元關係與繼承
友元關係不能繼承。基類的友元對派生類的成員沒有特殊訪問權限。若是基類被授予友元關係,則只有基類具備特殊訪問權限,該基類的派生類不能訪問授予友元關係的類。
若是派生類想要將本身成員的訪問權授予其基類的友元,派生類必須顯式地這樣作:基類的友元對從該基類派生的類型沒有特殊訪問權限。一樣,若是基類和派生類都須要訪問另外一個類,那個類必須特意將訪問權限授予基類的和每個派生類。
轉換與繼承
基類類型對象既能夠做爲獨立對象存在,也能夠做爲派生類對象的一部分而存在,所以,一個基類對象多是也可能不是一個派生類對象的部分。
若是有一個派生類型的對象,則可使用它的地址對基類類型的指針進行賦值或初始化。一樣,可使用派生類型的引用或對象初始化基類類型的引用。嚴格說來,對對象沒有相似轉換。編譯器不會自動將派生類型對象轉換爲基類類型對象。
引用轉換不一樣於轉換對象
將對象傳給但願接受引用的函數時,引用直接綁定到該對象,雖然看起來在傳遞對象,實際上實參是該對象的
引用,對象自己未被複制,而且,轉換不會在任何方面改變派生類型對象,該對象還是派生類型對象。
將派生類對象傳給但願接受基類類型對象(而不是引用)的函數時,狀況完全不一樣。在這種狀況下,形參的類型是固定的——在編譯時和運行時形參都是基類類型對象。若是用派生類型對象調用這樣的函數,則該派生類對象的基類部分被複制到形參。
一個是派生類對象轉換爲基類類型引用,一個是用派生類對象對基類對象進行初始化或賦值,理解它們之間的區別很重要。
用派生類對象對基類對象進行初始化或賦值
對基類對象進行初始化或賦值,其實是在調用函數:初始化時調用構造函數,賦值時調用賦值操做符。用派生類對象對基類對象進行初始化或賦值時,有兩種可能性。
第一種(雖然不太可能的)可能性是,基類可能顯式定義了將派生類型對象複製或賦值給基類對象的含義,這能夠經過定義適當的構造函數或賦值操做符實現:
class Derived; class Base { public: Base(const Derived&); // create a new Base from a Derived Base &operator=(const Derived&); // assign from a Derived // ... };
第二種可能性是,基類通常(顯式或隱式地)定義本身的複製構造函數和賦值操做符,這些成員接受一個形參,該形參是基類類型的(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 的 Bulk_item 部分在對 item 進行初始化或賦值時被「切掉」了。
派生類到基類轉換的可訪問性
若是是 public 繼承,則用戶代碼和後代類均可以使用派生類到基類的轉換。若是類是使用 private 或 protected 繼承派生的,則用戶代碼不能將派生類型對象轉換爲基類對象。若是是 private 繼承,則從 private 繼承類派生的類不能轉換爲基類。若是是 protected 繼承,則後續派生類的成員能夠轉換爲基類類型。
基類到派生類的轉換
從基類到派生類的自動轉換是不存在的。
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申請在運行時進行檢查
構造函數和複製控制
構造函數和複製控制成員不能繼承,每一個類定義本身的構造函數和複製控制成員。像任何類同樣,若是類不定義本身的默認構造函數和複製控制成員,就將使用合成版本。
繼承對基類構造函數的惟一影響是,在肯定提供哪些構造函數時,必須考慮一類新用戶。像任意其餘成員同樣,構造函數能夠爲 protected 或 private,某些類須要只但願派生類使用的特殊構造函數,這樣的構造函數應定義爲protected。
派生類構造函數
派生類的構造函數受繼承關係的影響,每一個派生類構造函數除了初始化本身的數據成員以外,還要初始化基類。
派生類的合成默認構造函數除了初始化派生類的數據成員以外,它還初始化派生類對象的基類部分。基類部分由基類的默認構造函數初始化。
定義默認構造函數
由於 Bulk_item 具備內置類型成員,因此應定義本身的默認構造函數:
class Bulk_item : public Item_base { public: Bulk_item(): min_qty(0), discount(0.0) { } // as before };
運行這個構造函數的效果是,首先使用 Item_base 的默認構造函數初始化Item_base 部分,那個構造函數將 isbn 置爲空串並將 price 置爲 0。Item_base 的構造函數執行完畢後,再初始化 Bulk_item 部分的成員並執行構造函數的函數體(函數體爲空)。
向基類構造函數傳遞實參
派生類構造函數的初始化列表只能初始化派生類的成員,不能直接初始化繼承成員。相反派生類構造函數經過將基類包含在構造函數初始化列表中來間接初始化繼承成員。
class Bulk_item : public Item_base { public: Bulk_item(const std::string& book, double sales_price, std::size_t qty = 0, double disc_rate = 0.0): Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { } // as before };
構造函數初始化列表爲類的基類和成員提供初始值,它並不指定初始化的執行次序。首先初始化基類,而後根據聲明次序初始化派生類的成員。
一個類只能初始化本身的直接基類。直接就是在派生列表中指定的類。
關鍵概念:重構
將 Disc_item 加到 Item_base 層次是重構(refactoring)的一個例子。重構包括從新定義類層次,將操做和/或數據從一個類移到另外一個類。爲了適應應用程序的須要而從新設計類以便增長新函數或處理其餘改變時,最有可能須要進行重構。重構常見在面向對象應用程序中很是常見。值得注意的是,雖然改變了繼承層次,使用 Bulk_item 類或 Item_base 類的代碼不須要改變。然而,對類進行重構,或以任意其餘方式改變類,使用這些類的任意代碼都必須從新編譯。
關鍵概念:尊重基類接口
構造函數只能初始化其直接基類的緣由是每一個類都定義了本身的接口。定義 Disc_item 時,經過定義它的構造函數指定了怎樣初始化Disc_item 對象。一旦類定義了本身的接口,與該類對象的全部交互都應該經過該接口,即便對象是派生類對象的一部分也不例外。一樣,派生類構造函數不能初始化基類的成員且不該該對基類成員賦值。
若是那些成員爲 public 或 protected,派生構造函數能夠在構造函數函數體中給基類成員賦值,可是,這樣作會違反基類的接口。派生類應經過使用基類構造函數尊重基類的初始化意圖,而不是在派生類構造函數函數體中對這些成員賦值。
複製控制和繼承
只包含類類型或內置類型數據成員、不含指針的類通常可使用合成操做,複製、賦值或撤銷這樣的成員不須要特殊控制。具備指針成員的類通常須要定義本身的複製控制來管理這些成員。
派生類析構函數
析構函數的工做與複製構造函數和賦值操做符不一樣:派生類析構函數不負責撤銷基類對象的成員。編譯器老是顯式調用派生類對象基類部分的析構函數。每一個析構函數只負責清除本身的成員:
class Derived: public Base { public: // Base::~Base invoked automatically ~Derived() { /* do what it takes to clean up derived members */ } };
對象的撤銷順序與構造順序相反:首先運行派生析構函數,而後按繼承層次依次向上調用各基類析構函數。
若是派生類定義了本身的複製構造函數,該複製構造函數通常應顯式使用基類複製構造函數初始化對象的基類部分:
class Base { /* ... */ }; class Derived: public Base { public: // Base::Base(const Base&) not invoked automatically Derived(const Derived& d): Base(d) /* other member initialization */ { /*... */ } };
若是派生類定義了本身的賦值操做符,則該操做符必須對基類部分進行顯式賦值, 賦值操做符必須防止自身賦值。
// Base::operator=(const Base&) not invoked automatically Derived &Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // assigns the base part // do whatever needed to clean up the old value in the derived part // assign the members from the derived } return *this; }
虛析構函數
自動調用基類部分的析構函數對基類的設計有重要影響。刪除指向動態分配對象的指針時,須要運行析構函數在釋放對象的內存以前清除對象。處理繼承層次中的對象時,指針的靜態類型可能與被刪除對象的動態類型不一樣,可能會刪除實際指向派生類對象的基類類型指針。若是刪除基類指針,則須要運行基類析構函數並清除基類的成員,若是對象實際是派生類型的,則沒有定義該行爲。要保證運行適當的析構函數,基類中的析構函數必須爲虛函數:
class Item_base { public: // no work, but virtual destructor needed // if base pointer that points to a derived object is ever deleted virtual ~Item_base() { } };
若是析構函數爲虛函數,那麼經過指針調用時,運行哪一個析構函數將因指針所指對象類型的不一樣而不一樣:
Item_base *itemP = new Item_base; // same static and dynamic type delete itemP; // ok: destructor for Item_base called itemP = new Bulk_item; // ok: static and dynamic types differ delete itemP; // ok: destructor for Bulk_item called
若是在構造函數或析構函數中調用虛函數,則運行的是爲構造函數或析構函數自身類型定義的版本。
要理解這種行爲,考慮若是從基類構造函數(或析構函數)調用虛函數的派生類版本會怎麼樣。虛函數的派生類版本極可能會訪問派生類對象的成員,畢竟,若是派生類版本不須要使用派生類對象的成員,派生類多半可以使用基類中的定義。可是,對象的派生部分的成員不會在基類構造函數運行期間初始化,實際上,若是容許這樣的訪問,程序極可能會崩潰。
繼承狀況下的類做用域
在繼承狀況下,派生類的做用域嵌套在基類做用域中。若是不能在派生類做用域中肯定名字,就在外圍基類做用域中查找該名字的定義。
名字查找在編譯時發生
對象、引用或指針的靜態類型決定了對象可以完成的行爲。甚至當靜態類型和動態類型可能不一樣的時候,就像使用基類類型的引用或指針時可能會發生的,靜態類型仍然決定着可使用什麼成員。
名字衝突與繼承
雖然能夠直接訪問基類成員,就像它是派生類成員同樣,可是成員保留了它的基類成員資格。通常咱們並不關心是哪一個實際類包含成員,一般只在基類和派生類共享同一名字時才須要注意。與基類成員同名的派生類成員將屏蔽對基類成員的直接訪問。可使用做用域操做符訪問被屏蔽的基類成員.設計派生類時,只要可能,最好避免與基類成員的名字衝突。
做用域與成員函數
在基類和派生類中使用同一名字的成員函數,其行爲與數據成員同樣:在派生類做用域中派生類成員將屏蔽基類成員。即便函數原型不一樣,基類成員也會被屏蔽:
struct Base { int memfcn(); }; struct Derived : Base { int memfcn(int); // hides memfcn in the base }; Derived d; Base b; b.memfcn(); // calls Base::memfcn d.memfcn(10); // calls Derived::memfcn d.memfcn(); // error: memfcn with no arguments is hidden d.Base::memfcn(); // ok: calls Base::memfcn
局部做用域中聲明的函數不會重載全局做用域中定義的函數,一樣,派生類中定義的函數也不重載基類中定義的成員。經過派生類對象調用函數時,實參必須與派生類中定義的版本相匹配,只有在派生類根本沒有定義該函數時,才考慮基類函數。
重載函數
若是派生類想經過自身類型使用的重載版本,則派生類必需要麼重定義全部重載版本,要麼一個也不重定義。
有時類須要僅僅重定義一個重載集中某些版本的行爲,而且想要繼承其餘版本的含義,在這種狀況下,爲了重定義須要特化的某個版本而不得不重定義每個基類版本,可能會使人厭煩.
派生類不用重定義所繼承的每個基類版本,它能夠爲重載成員提供 using聲明。一個 using 聲明只能指定一個名字,不能指定形參表,所以,爲基類成員函數名稱而做的 using 聲明將該函數的全部重載實例加到派生類的做用域。將全部名字加入做用域以後,派生類只須要重定義本類型確實必須定義的那些函數,對其餘版本可使用繼承的定義。
關鍵概念:名字查找與繼承
理解 C++ 中繼承層次的關鍵在於理解如何肯定函數調用。肯定函數調用遵循如下四個步驟:
1. 首先肯定進行函數調用的對象、引用或指針的靜態類型。
2. 在該類中查找函數,若是找不到,就在直接基類中查找,如此循着類的繼承鏈往上找,直到找到該函數或者查找完最後一個類。若是不能在類或其相關基類中找到該名字,則調用是錯誤的。
3. 一旦找到了該名字,就進行常規類型檢查,查看若是給定找到的定義,該函數調用是否合法。
4. 假定函數調用合法,編譯器就生成代碼。若是函數是虛函數且通過引用或指針調用,則編譯器生成代碼以肯定根據對象的動態類型運行哪一個函數版本,不然,編譯器生成代碼直接調用函數。
純虛函數
在函數形參表後面寫上 = 0 以指定純虛函數:
class Disc_item : public Item_base { public: double net_price(std::size_t) const = 0; };
將函數定義爲純虛可以說明,該函數爲後代類型提供了能夠覆蓋的接口,但是這個類中的版本決不會調用。重要的是,用戶將不能建立 Disc_item 類型的對象。
試圖建立抽象基類的對象將發生編譯時錯誤:
// Disc_item declares pure virtual functions Disc_item discounted; // error: can't define a Disc_item object Bulk_item bulk; // ok: Disc_item subobject within Bulk_item
含有(或繼承)一個或多個純虛函數的類是抽象基類。除了做爲抽象基類的派生類的對象的組成部分,不能建立抽象類型的對象。
容器與繼承
咱們但願使用容器(或內置數組)保存因繼承而相關聯的對象。可是,對象不是多態的,這一事實對將容器用於繼承層次中的類型有影響。記住,將派生類對象複製到基類對象時,派生類對象將被切掉.
惟一可行的選擇多是使用容器保存對象的指針。這個策略可行,但代價是須要用戶面對管理對象和指針的問題,用戶必須保證只要容器存在,被指向的對象就存在。若是對象是動態分配的,用戶必須保證在容器消失時適當地釋放對象。包裝(cover)類或句柄類對這個問題更好更通用的解決方案。
C++ 中面向對象編程的一個頗具諷刺意味的地方是,不能使用對象支持面向對象編程,相反,必須使用指針或引用。
句柄類與繼承
C++ 中一個通用的技術是定義包裝(cover)類或句柄類。句柄類存儲和管理基類指針。指針所指對象的類型能夠變化,它既能夠指向基類類型對象又能夠指向派生類型對象。用戶經過句柄類訪問繼承層次的操做。由於句柄類使用指針執行操做,虛成員的行爲將在運行時根據句柄實際綁定的對象的類型而變化。所以,句柄的用戶能夠得到動態行爲但無須操心指針的管理。包裝了繼承層次的句柄有兩個重要的設計考慮因素:
• 像對任何保存指針的類同樣,必須肯定對複製控制作些什麼。包裝了繼承層次的句柄一般表現得像一個智能指針或者像一個值。
• 句柄類決定句柄接口屏蔽仍是不屏蔽繼承層次,若是不屏蔽繼承層次,用戶必須瞭解和使用基本層次中的對象。
// use counted handle class for the Item_base hierarchy class Sales_item { public: // default constructor: unbound handle Sales_item(): p(0), use(new std::size_t(1)) { } // attaches a handle to a copy of the Item_base object Sales_item(const Item_base&); // copy control members to manage the use count and pointers Sales_item(const Sales_item &i): p(i.p), use(i.use) { ++*use; } ~Sales_item() { decr_use(); } Sales_item& operator=(const Sales_item&); // member access operators const Item_base *operator->() const { if (p) return p; else throw std::logic_error("unbound Sales_item"); } const Item_base &operator*() const { if (p) return *p; else throw std::logic_error("unbound Sales_item"); } private: Item_base *p; // pointer to shared item std::size_t *use; // pointer to shared use count // called by both destructor and assignment operator to free pointers void decr_use() { if (--*use == 0) { delete p; delete use; } } }; // use-counted assignment operator; use is a pointer to a shared use count Sales_item& Sales_item::operator=(const Sales_item &rhs) { ++*rhs.use; decr_use(); p = rhs.p; use = rhs.use; return *this; }
句柄類常常須要在不知道對象的確切類型時分配書籍對象的新副本。解決這個問題的通用方法是定義虛操做進行復制,咱們稱將該操做命名爲 clone。
對於派生類的返回類型必須與基類實例的返回類型完全匹配的要求,但有一個例外。若是虛函數的基類實例返回類類型的引用或指針,則該虛函數的派生類實例能夠返回基類實例返回的類型的派生類(或者是類類型的指針或引用)。
class Item_base { public: virtual Item_base* clone() const { return new Item_base(*this); } }; class Bulk_item : public Item_base { public: Bulk_item* clone() const { return new Bulk_item(*this); } }; Sales_item::Sales_item(const Item_base &item): p(item.clone()), use(new std::size_t(1)) { }