面向對象基於三個基本概念:數據抽象、繼承和動態綁定。通過使用數據抽象,我們可以將類的接口與實現分離;使用繼承,可以定義相似的類型並對其相似關系建模;使用動態綁定,可以在一定程度上忽略相似類型的區別,而以統一的方式使用它們的對象。
繼承是派生類與基類之間的關系,它們共享了一些公共的東西,而派生類特化了一些本質不同的東西。類與類之間的繼承關系構成了繼承層次。在C++中,基類必須指定希望派生類重定義哪些函數,定義為virtual的函數是基類期待派生類重新定義的,需要在派生類中繼承的不能定義為虛函數。
派生類重新定義的虛函數前面可以加virtual,但這不是必要的,在C++11中,允許派生類顯式地注明它將使用哪個成員函數改寫基類虛函數,只用在該函數形式參數列表後增加一個override關鍵字。
當函數接口定義了基類的引用(或指針)時,在函數內調用虛函數時,發生動態綁定。因為這時候函數實參可以為基類類型也可以為派生類類型,虛函數只有在運行階段才能確定需要調用哪個定義。
基類成員函數中希望在派生類中重新定義的函數前面加了virtual,這類函數在調用時,在程序運行階段確定。任何構造函數之外的非靜態函數都可以是虛函數。virtual關鍵字只需要在基類定義的時候加在需要重定義的函數前面,實現的時候不需要。
基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
派生類可以繼承定義在基類中的成員,但是派生類的成員函數不能訪問從基類繼承而來的private成員,只能訪問public和protected成員。而用戶則只能通過派生類訪問從基類繼承而來的public成員。
pubic:用戶可訪問的類成員,繼承類也可以訪問它們。
private:類的成員和友元可以訪問,繼承類不能訪問。
protected:用戶不能訪問,但是可以被類的成員或派生類的成員訪問。
1)派生類的成員變量分兩種:一是從基類繼承到的成員變量,另一種是顯示自己特殊化的變量或者為特殊化接口而准備的變量。
2)一般來說派生類都要重新定義基類中聲明的虛函數,但如果沒有重新定義,則延用基類裡的定義。
3)派生類中的虛函數定義與基類應該完全一致,如果基類返回基類類型的引用,派生類則返回派生類類型的引用。
4)派生類中定義虛函數時,可以不保留virtual。
5)作為基類的類必須是已經定義的,不能僅僅是聲明,因為它的成員將被用到,所以一個類不能從自身派生出一個類。
發生動態綁定的兩個條件:
1)虛函數
2)基類類型的引用或指針進行函數調用
在任何需要基類的地方都可以用派生類對象去代替,所以指向基類的指向或引用可以指向派生類,因為派生類有所有它需要的成員或成員函數。
對象是非多態的——對象類型已知且不變。對象的動態類型是與靜態類型相同,這一些與引用或指針相反。運行的函數是由對象的類型定義的。
如果派生類需要調用虛函數的基類版本,則需要用作用域操作符來覆蓋虛函數機制。
不要在基類和派生類中為虛函數定義不同的默認實參,因為實參是靜態綁定的,在編譯時期確定,所以當你用指向派生類對象的基類類型的指針訪問虛函數時,實際上用的是基類成員函數的默認實參。
派生列表中使用訪問標號用來決定使用派生類的用戶和對基類成員訪問的權限。
首先,基類中只有public和protected的成員可以被派生類訪問。
1) 如果派生類是公用繼承(public inheritance)
派生類的成員或友員可以訪問基類中的public和protected成員,用戶通過派生類可以訪問基類中的public成員,基類中的private只有基類的成員可以訪問。
class Bulk_item:public Item_base{...};
那麼基類中的public與protected就像派生類中的public和protected類型一樣。
2)如果派生類是受保護繼承(protected inheritance)
class Bulk_item:protected Item_base{...};
基類中的public和protected成員 就像派生類的中protected一樣
3)如果派生類是私有繼承(private inheritance)
class Bulk_item:private Item_base{...};
基類中的public和protected成員 就像派生類的中private一樣,用戶不能通過派生類對象訪問基類中的任何成員。
public派生類繼承了基類的接口,可以在需要基類的地方使用public派生類。而private和protected派生類則不可以。
可以用using 聲明恢復基類成員在派生類中的訪問級別。
class和struct在定義繼承類的,默認的繼承類別也不同。
class Base{...};
class D1:Base{...} 等價於 class D1:private Base{...}
struct D2:Base{...} 等價於 class D2:public Base{...}
class 與 struct 用來定義類除了訪問級別的不同外,其他沒有任何不同。
派生類的成員函數中不能直接訪問基類類型對象的protected成員,但是可以通過派生類對象訪問基類的protected成員。
void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b) { // price is protected double ret = price; //ok ret = d.price;//ok ret = b.price;//error }
如果基類定義了static成員,則在整個繼承層次中只有一個這樣的成員。
有時候我們會定義這樣一種類,我們不希望其他類繼承它,或者不想考慮它是否適合作為一個基類。C++11中允許在類名後加一個關鍵字final來防止繼承。
Class NoDerived final{/* */ } Class Bad : public NoDerived{/* */ } // error!
可以將基類的指針或引用綁定到派生類對象上有一層極為重要的含義:當使用基類的引用(或指針)時,實際上我們並不清楚該引用(或指針)所綁定對象的真實類型。該對象可能是基類的對象,也可能是派生類的對象。
表達式的靜態類型在編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型;動態類型則是變量或表達式表示的內存中的對象的類型,動態類型直到運行才可知。如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態類型一致。
引用轉換不同於對象轉換
1)將派生類對象傳遞給希望接受基類引用的函數,實際上傳遞進去的就是原來的派生類對象,這個對象沒有發生任何變化。
2)而如果將派生類對象傳遞給希望接受基類對象的函數,實際上是將實參派生類對象中基類部分復制出來,創建了一個臨時的基類對象。
3)派生類對象轉換為基類對象實際上像是做了“裁切”操作。
沒有從基類類型到派生類型的(自動)轉換,因為派生類中很可能包含了基類中沒有成員。甚至當基類指針或引用實際綁定到派生類對象時,從基類到派生類的轉換也存在限制。
Bulk_item bulk; Item_base *itemP = &bulk; // ok Bulk_item *bulkP = itemP; // error
1)引用或指針的靜態類型與動態類型不同這一事實正是C++語言支持多態性的根本所在。
2)基類中的虛函數在派生類中隱含地也是一個虛函數。當派生類覆蓋了某個虛函數時,該函數在基類中的形參必須與派生類中的形參嚴格匹配。
3)可以是函數的形參表後加override說明函數是派生類中的虛函數,而用final用於說明不希望在繼承類中覆蓋該函數。
4)如果一個派生類虛函數需要調用它的基類版本,但是沒有使用作用域運算符,則在運行時該調用將被解析為對派生類版本自身的調用,從而導致無限遞歸。
double undiscounted = baseP->Quote::net_price(42);
5)如果我們通過基類的引用或指針調用函數,則使用基類中定義的默認實參,即使實際運行的是派生類中的函數版本也是如此。
1)在虛函數的形參表後加=0會讓虛函數變為純虛函數,純虛函數本身不需要定義,它只是為派生類提供一個接口用於表示抽象普適的意義。值得注意的是,我們也可以為純虛函數提供定義,不過函數體必須定義在類的外部。
2)含有(或者未經覆蓋直接繼承)純虛函數的類是抽象基類。抽象基類負責定義接口,而後續的其他類可以覆蓋該接口。我們不能定義一個抽象基類的對象。
3)派生類構造函數只初始化它的直接基類。
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){} double net_price(size_t)const = 0; protected: size_t quantity = 0; double discount = 0.0; }; class Bulk_quote :public Disc_quote { public: Bulk_quote() = default; 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; };
1)名字的查找發生在編譯階段,也就是說不論是對象,指針還是引用,都只能訪問它們靜態類型的成員。
2)當派生類中定義了同基類同名的成員時,基類中的成員將會被屏蔽,當然可以使用作用域操作符進行訪問。
3)在派生類中定義的同名函數,就算形參不一樣,也會被屏蔽。原理同:局部作用域定義的函數不會重載全局作用域中定義的函數。
4)如果想讓派生類使用所有的重載成員,則在派生類裡要麼不定義,要麼全定義。
因為派生類中實際上包含了基類的一些成員,所以這將會影響到派生類的構造、復制、移動、賦值和撤銷。
構造函數與復制控制成員不能繼承,每個類都必須有自己的構造函數和復制控制成員。
但是在C++11中,派生類能夠和其直接基類定義的構造函數,但是也僅限於此,仍然不能繼承默認、拷貝和移動構造函數。
派生類繼承基類構造函數的方式是提供一條注明了基類名的using聲明語句,這樣對於基類的每個構造函數,編譯器都生成一個與之對應的派生類的構造函數。
class Bulk_qute : public Disc_quote{ public: using Disc_quote::Disc_quote; // 繼承Disc_quote的構造函數 double net_price(size_t) const; };
如果派生類定義的構造函數與基類構造函數具有相同的參數列表,則該構造函數將不會被繼承。
基類的構造與復制函數基本不受影響,唯一的影響是,在確定哪些構造函數時,需要考慮使用對象,可以在構造函數前加protected,讓它只能在派生類中使用。
1)合成的派生類默認構造函數
兩個步驟:1,調用基類的默認構成函數完成基類成員的初始化;2,用常規變量初始化規則初始化派生類的特有成員。
2)定義默認構造函數
可以只給派生類的特有成員賦初值,而函數會隱式調用基類的構造函數對基類成員進行定義。
3)向基類構造函數傳遞實參
派生類的構造函數初始化列表,不能直接初始化派生得到的成員,只能將基類包含在初始化列表中來間接初始化那些成員。
Bulk_item(const string& book, double sales_price, size_t qty = 0, double disc_rate = 0) : Item_base(book, sales_price), min_qty(qty), discount(dis_rate){}
4)只能初始化直接基類
在有多重繼承的性況下,派生類的構造函數只能初始化自己的直接基類。
5)尊重基類接口
雖然可以在派生類的構造函數體中直接訪問基類的public和protected成員,從而進行初始化,但是不要這樣做,而是使用基類提供的構造函數接口。
如果派生類顯式定義了自己的復制構造函數或賦值操作符,則該定義將完全覆蓋默認定義。基類的部分也必須由派生類定義的函數來完成,不能希望有隱式的復制和賦值(這一點與構造函數不同)。
在派生類復制構造函數中,會顯式的調用基類的復制構造函數,因為它的形參是基類類型的引用,所以可以直接將派生類對象作為實參傳入。
如果沒有顯式調用,那麼將會隱式地調用基類的默認構造函數完成基類部分成員的定義,這樣最終得到的派生類對象就很奇怪了。
與復制構造函數類似,必須顯式地調用基類的基類的賦值函數。
Derived& Derived::operator=(const Derived &rhs) { if (this != &rhs) { Base::operator=(rhs); // then do } return *this; }
派生類析構函數不負責撤銷基類對象的成員。編譯器總是顯式調用派生類對象基類部分的析構函數。每個析構函數只負責清除自己的成員。
Item_base *itemP = new Item_base; delete itemP; itemP = new Bulk_item; delete itemP;
上面代碼中,在delete指向類的指針時,將調用析構函數,可是這裡itemP可能指向基類也可能指向派生類,所以這時候應該在運行階段根據類型去調用不同的析構函數。
所以基類的析構函數為虛函數:virtual ~Item_base(){}
運行的是為構造函數或析構函數自身類型定義的版本。