C++ primer 第15章 面向对象程序设计
文章目錄
- 前言
- OOP:概述
- 繼承
- 動(dòng)態(tài)綁定
- 定義基類和派生類
- 定義基類
- 成員函數(shù)與繼承
- 訪問(wèn)控制與繼承
- 定義派生類
- 派生類中的虛函數(shù)
- 派生類對(duì)象及派生類向基類的類型轉(zhuǎn)換
- 派生類構(gòu)造函數(shù)
- 派生類使用基類的成員
- 繼承與靜態(tài)成員
- 派生類的聲明
- 被用作基類的類
- 防止繼承的發(fā)生
- 類型轉(zhuǎn)換與繼承
- 靜態(tài)類型與動(dòng)態(tài)類型
- 在對(duì)象之間不存在類型轉(zhuǎn)換
- 虛函數(shù)
- 對(duì)虛函數(shù)的調(diào)用可能在運(yùn)行時(shí)才被解析
- c++的多態(tài)性
- 派生類中的虛函數(shù)
- final和override說(shuō)明符
- 回避虛函數(shù)的機(jī)制
- 抽象基類
- 純虛函數(shù)
- 含有純虛函數(shù)的類是抽象基類
- 派生類構(gòu)造函數(shù)只初始化它的直接基類
- 重構(gòu)
- 訪問(wèn)控制與繼承
- 受保護(hù)的成員 protected
- 公有、私有和受保護(hù)繼承
- 派生類向基類轉(zhuǎn)換的可訪問(wèn)性
- 友元與繼承
- 改變各個(gè)成員的可訪問(wèn)性
- 默認(rèn)的繼承保護(hù)級(jí)別
- 繼承中的類作用域
- 在編譯時(shí)進(jìn)行名字查找
- 名字沖突與繼承
- 名字查找先于類型檢查
- 虛函數(shù)與作用域,通過(guò)基類調(diào)用隱藏的虛函數(shù)
- 構(gòu)造函數(shù)與拷貝控制
- 虛析構(gòu)函數(shù)
- 虛析構(gòu)函數(shù)將阻止合成移動(dòng)操作
- 合成拷貝控制與繼承
- 派生類中刪除的拷貝控制與基類的關(guān)系型
- 派生類的拷貝控制成員
- 定義派生類的拷貝或移動(dòng)構(gòu)造函數(shù)
- 派生類賦值運(yùn)算符
- 派生類析構(gòu)函數(shù)
- 在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù)
- 繼承的構(gòu)造函數(shù)
- 繼承的構(gòu)造函數(shù)的特點(diǎn)
- 容器與繼承
- 在容器中放置(智能)指針而非對(duì)象
- 編寫(xiě)B(tài)asket類
- decltype的意義
- [upper_bound可參考chapter 11](https://blog.csdn.net/weixin_43116900/article/details/105916425)
- 模擬虛拷貝
前言
面向?qū)ο蟪绦蛟O(shè)計(jì)基于三個(gè)基本概念:數(shù)據(jù)抽象、繼承和動(dòng)態(tài)綁定。
繼承和動(dòng)態(tài)綁定對(duì)程序的編寫(xiě)有兩方面的影響:一是我們可以更容易地定義與其他類相似但不完全相同的新類:二是在使用這些彼此相似的類編寫(xiě)程序時(shí), 我們可以在一定程度上忽略掉它們的區(qū)別。
OOP:概述
面向?qū)ο蟪绦蛟O(shè)計(jì)的核心思想是數(shù)據(jù)抽象、繼承和動(dòng)態(tài)綁定。通過(guò)使用數(shù)據(jù)抽象,我們可以將類的接口與實(shí)現(xiàn)分離;使用繼承,可以定義相似的類型并對(duì)其相似關(guān)系建模;使用動(dòng)態(tài)綁定,可以在一定程度上忽略相似類型的區(qū)別,而以統(tǒng)一的方式使用它們的對(duì)象。
繼承
基類負(fù)責(zé)定義在層次關(guān)系中所有類共同擁有的成員,而每個(gè)派生類定義各自特有的成員。
在c++語(yǔ)言中,基類將類型相關(guān)的函數(shù)與派生類不做改變直接繼承的函數(shù)區(qū)分對(duì)待。對(duì)于某些函數(shù),基類希望它的派生類各自定義適合自身的版本,此時(shí)基類就將這些函數(shù)聲明成虛函數(shù)。
基類:
class Quote{ public:string isbn() const;virtual double net_price(size_t n) const; };派生類:
class Bulk_quote : public Quote{ public:double net_price(size_t n) const override; };因?yàn)锽ulk_quote在它的派生列表中使用了public關(guān)鍵字,因此我們完全可以把Bulk_quote的對(duì)象當(dāng)成Quote的對(duì)象來(lái)使用。
派生類必須在其內(nèi)部對(duì)所有重新定義的虛函數(shù)進(jìn)行聲明。派生類可以在這樣的函數(shù)之前加上virtual關(guān)鍵字, 但是并不是非得這么做。C++11新標(biāo)準(zhǔn)允許派生類顯式地注明它將使用哪個(gè)成員函數(shù)改寫(xiě)基類的虛函數(shù), 具體措施是在該函數(shù)的形參列表之后增加一個(gè)override關(guān)鍵字。
動(dòng)態(tài)綁定
通過(guò)使用動(dòng)態(tài)綁定,我們能用同一段代碼分別處理Quote和Bulk_quote的對(duì)象。
double print_total(ostream &os,const Quote &item,size_t n){//根據(jù)item的形參對(duì)象類型調(diào)用Quote::net_price或者Bulk_quote::net_pricedouble ret = item.net_price(n);os<<"ISBN: "<<item.isbn()<<"# sold: "<<n<<"total due: "<<ret<<endl;return ret; }因?yàn)楹瘮?shù)print_total的item形參是基類Quote的一個(gè)引用,所以,我們既能使用基類Quote的對(duì)象調(diào)用該函數(shù),也能使用派生類Bulk_quote的對(duì)象調(diào)用它。又因?yàn)閜rint_total是使用引用類型調(diào)用net_price函數(shù)的,所以實(shí)際傳入print_total的對(duì)象類型將決定到底執(zhí)行net_price的哪個(gè)版本。
在c++語(yǔ)言中,當(dāng)我們使用基類的引用(或指針)調(diào)用一個(gè)虛函數(shù)時(shí)將發(fā)生動(dòng)態(tài)綁定。
定義基類和派生類
定義基類
基類通常都應(yīng)該定義一個(gè)虛析構(gòu)函數(shù),即使該函數(shù)不執(zhí)行任何實(shí)際操作也是如此。
成員函數(shù)與繼承
在c++語(yǔ)言中,基類必須將它的兩種成員函數(shù)區(qū)分開(kāi)來(lái):一種是基類希望其派生類進(jìn)行覆蓋的函數(shù);另一種是基類希望派生類直接繼承而不要改變的函數(shù)。對(duì)于前者,基類通常將其定義為虛函數(shù),當(dāng)我們使用指針或引用來(lái)調(diào)用虛函數(shù)時(shí),該調(diào)用將被動(dòng)態(tài)綁定。根據(jù)引用或指針?biāo)壎ǖ膶?duì)象類型不同,該調(diào)用可能執(zhí)行基類的版本,也可能執(zhí)行某個(gè)派生類的版本。
基類通過(guò)在其成員函數(shù)的聲明語(yǔ)句之前加上關(guān)鍵字virtual使得該函數(shù)執(zhí)行動(dòng)態(tài)綁定。任何構(gòu)造函數(shù)之外的非靜態(tài)函數(shù)都可以是虛函數(shù)。關(guān)鍵字 virtual只能出現(xiàn)在類內(nèi)部的聲明語(yǔ)句之前而不能用于類外部的函數(shù)定義。如果基類把一個(gè)函數(shù)聲明成虛函數(shù),則該函數(shù)在派生類中隱式地也是虛函數(shù)。
成員函數(shù)如果沒(méi)被聲明為虛函數(shù),則其解析過(guò)程發(fā)生在編譯時(shí)而非運(yùn)行時(shí)。isbn函數(shù)的執(zhí)行與派生類的細(xì)節(jié)無(wú)關(guān),無(wú)論是Quote對(duì)象還是Bulk_quote對(duì)象,isbn函數(shù)的行為都一樣。
訪問(wèn)控制與繼承
派生類可以繼承定義在基類中的成員,但是派生類的成員函數(shù)不一定有權(quán)訪問(wèn)從基類繼承而來(lái)的成員。和其他使用基類的代碼一樣,派生類能訪問(wèn)公有成員,而不能訪問(wèn)私有成員。不過(guò)在某些時(shí)候基類中還有這樣一種成員,基類希望它的派生類有權(quán)訪問(wèn)該成員同時(shí)禁止其他用戶訪問(wèn)。我們用受保護(hù)的(protected)訪問(wèn)運(yùn)算符說(shuō)明這樣的成員。
我們的Quote類希望它的派生類定義各自的net_price函數(shù),因此派生類需要訪問(wèn)Quote的price成員。此時(shí)我們將price定義成受保護(hù)的。與之相反,派生類訪問(wèn)bookNo成員的方式與其他用戶是樣的,都是通過(guò)調(diào)用isbn函數(shù),因此bookNo被定義成私有的,即使是Quote派生出來(lái)的類也不能直接訪問(wèn)它。
定義派生類
派生類必須通過(guò)使用類派生列表明確指出它是從哪個(gè)(哪些)基類繼承而來(lái)的。類派生列表的形式是:首先是一個(gè)冒號(hào),后面緊跟以逗號(hào)分隔的基類列表,其中每個(gè)基類前面可以有以下三種訪問(wèn)說(shuō)明符中的一個(gè):public、protected或者private。
訪問(wèn)說(shuō)明符的作用是控制派生類從基類繼承而來(lái)的成員是否對(duì)派生類的用戶可見(jiàn)。
如果一個(gè)派生類是公有的,則基類的公有成員也是派生類接口的組成部分。此外,我們能將公有派生類型的對(duì)象綁定到基類的引用或指針上。因?yàn)槲覀冊(cè)谂缮斜碇惺褂昧藀ublic,所以Bulk_quote的接口隱式地包含isbn函數(shù),同時(shí)在任何需要Quote的引用或指針的地方我們都能使用Bulk_quote的對(duì)象。
派生類中的虛函數(shù)
派生類經(jīng)常(但不總是)覆蓋它繼承的虛函數(shù)。如果派生類沒(méi)有覆蓋其基類中的某個(gè)虛函數(shù),則該虛函數(shù)的行為類似于其他的普通成員,派生類會(huì)直接繼承其在基類中的版本。
派生類對(duì)象及派生類向基類的類型轉(zhuǎn)換
一個(gè)派生類對(duì)象包含多個(gè)組成部分:一個(gè)含有派生類自己定義的(非靜態(tài))成員的子對(duì)象,以及一個(gè)與該派生類繼承的基類對(duì)應(yīng)的子對(duì)象,如果有多個(gè)基類,那么這樣的子對(duì)象也有多個(gè)。
因?yàn)樵谂缮悓?duì)象中含有與其基類對(duì)應(yīng)的組成部分,所以我們能把派生類的對(duì)象當(dāng)成基類對(duì)象來(lái)使用,而且我們也能將基類的指針或引用綁定到派生類對(duì)象中的基類部分。
Quote item; //基類對(duì)象 Bulk_quote bulk; //派生類對(duì)象 Quote *p = &item; //p指向Quote對(duì)象 p = &bulk; //p指向bulk的Quote部分 Quote &r = bulk; //r綁定到bulk的Quote部分這種轉(zhuǎn)換通常稱為派生類到基類的類型轉(zhuǎn)換。和其他類型一樣,編譯器會(huì)隱式地執(zhí)行派生類到基類的轉(zhuǎn)換。我們可以把派生類對(duì)象或者派生類對(duì)象的引用用在需要基類引用的地方;同樣的,我們也可以把派生類對(duì)象的指針用在需要基類指針的地方。
派生類構(gòu)造函數(shù)
盡管在派生類對(duì)象中含有從基類繼承而來(lái)的成員,但是派生類并不能直接初始化這些成員。派生類必須使用基類的構(gòu)造函數(shù)來(lái)初始化它的基類部分。
派生類構(gòu)造函數(shù)同樣是通過(guò)構(gòu)造函數(shù)初始化列表來(lái)將實(shí)參傳遞給基類構(gòu)造函數(shù)的。例如:
Bulk_quote::Bulk_quote(const string& book, double p, size_t qty, double disc):Quote(book,p),min_qty(qty),discount(disc){}除非我們特別指出,否則派生類對(duì)象的基類部分會(huì)像數(shù)據(jù)成員一樣執(zhí)行默認(rèn)初始化。如果想使用其他的基類構(gòu)造函數(shù),我們需要以類名加圓括號(hào)內(nèi)的實(shí)參列表的形式為構(gòu)造函數(shù)提供初始值。
首先初始化基類的部分,然后按照聲明的順序依次初始化派生類的成員。
派生類使用基類的成員
派生類可以訪問(wèn)基類的公有成員和受保護(hù)成員。
繼承與靜態(tài)成員
**如果基類定義了一個(gè)靜態(tài)成員,則在整個(gè)繼承體系中只存在該成員的唯一定義。**不論從基類中派生出來(lái)多少個(gè)派生類,對(duì)于每個(gè)靜態(tài)成員來(lái)說(shuō)都只存在唯一的實(shí)例。
靜態(tài)成員遵循通用的訪問(wèn)控制規(guī)則,如果基類中的成員是private的,則派生類無(wú)權(quán)訪問(wèn)它。假設(shè)某靜態(tài)成員是可訪問(wèn)的,則我們既能通過(guò)基類使用它也能通過(guò)派生類使用它。
class Base { public:static void statmem() {cout << "這是Base的靜態(tài)函數(shù)statmem()"<< endl;}}; class Derived:public Base { public:void f(const Derived& obj) {Base::statmem();Derived::statmem();obj.statmem(); //通過(guò)Derived對(duì)象訪問(wèn)statmem(); //通過(guò)this對(duì)象訪問(wèn)} };輸出結(jié)果:
這是Base的靜態(tài)函數(shù)statmem() 這是Base的靜態(tài)函數(shù)statmem() 這是Base的靜態(tài)函數(shù)statmem() 這是Base的靜態(tài)函數(shù)statmem()派生類的聲明
派生類的聲明與其他類差別不大,聲明中包含類名但是不包含它的派生列表:
class Bulk_quote : public Quote; 錯(cuò)誤,聲明不包含派生列表 class Bulk_quote; 正確被用作基類的類
如果我們想將某個(gè)類用作基類,則該類必須已經(jīng)定義而非僅僅聲明。所以,一個(gè)類不能派生它本身。
每個(gè)類都會(huì)繼承直接基類的所有成員。對(duì)于一個(gè)最終類來(lái)說(shuō),它會(huì)繼承其直接基類的成員,該直接基類的成員又含有其基類的成員,以此類推,最終的派生類將包含它的直接基類的子對(duì)象以及每個(gè)間接基類的子對(duì)象。
防止繼承的發(fā)生
防止繼承發(fā)生的方法,即在類名后跟一個(gè)關(guān)鍵字final:
class NoDerived final{}; NoDerived不能作為基類 class Last final:public Base{}; Last不能作為基類類型轉(zhuǎn)換與繼承
通常情況下,如果我們想把引用或指針綁定到一個(gè)對(duì)象上,則引用或指針的類型應(yīng)與對(duì)象的類型一致,或者對(duì)象的類型含有一個(gè)可接受的const類型轉(zhuǎn)換規(guī)則。存在繼承關(guān)系的類是一個(gè)重要的例外:我們可以將基類的指針或引用綁定到派生類對(duì)象上。
我們可以將基類的指針或引用綁定到派生類對(duì)象上,這意味著,當(dāng)使用基類的引用(或指針)時(shí),實(shí)際上我們并不清楚該引用(或指針)所綁定對(duì)象的真實(shí)類型。該對(duì)象可能是基類的對(duì)象,也可能是派生類的對(duì)象。
和內(nèi)置指針一樣,智能指針類也支持派生類向基類的類型轉(zhuǎn)換,這意味著我們可以將一個(gè)派生類對(duì)象的指針存儲(chǔ)在一個(gè)基類的智能指針類。
靜態(tài)類型與動(dòng)態(tài)類型
當(dāng)我們使用存在繼承關(guān)系的類型時(shí),必須將一個(gè)變量或其他表達(dá)式的靜態(tài)類型與該表達(dá)式表示對(duì)象的動(dòng)態(tài)類型區(qū)分開(kāi)來(lái)。表達(dá)式的靜態(tài)類型在編譯時(shí)總是已知的,它的變量聲明時(shí)的類型或表達(dá)式生成的類型,動(dòng)態(tài)類型則是變量或表達(dá)式表示的內(nèi)存中的對(duì)象的類型。動(dòng)態(tài)類型直到運(yùn)行時(shí)才可知。
如果一個(gè)變量非指針也非引用,則它的靜態(tài)類型和動(dòng)態(tài)類型永遠(yuǎn)一致。但基類的指針或引用的動(dòng)態(tài)類型可能與其動(dòng)態(tài)類型不一致。
不存在從基類向派生類的隱式類型轉(zhuǎn)換。
在對(duì)象之間不存在類型轉(zhuǎn)換
派生類向基類的自動(dòng)類型轉(zhuǎn)換只對(duì)指針或引用類型有效,在派生類類型和基類類型之間不存在這樣的轉(zhuǎn)換。
當(dāng)我們用一個(gè)派生類對(duì)象為一個(gè)基類對(duì)象初始化或賦值時(shí),只有該派生類對(duì)象中的基類部分會(huì)被拷貝、移動(dòng)或賦值,它的派生類部分將被忽略掉。
虛函數(shù)
當(dāng)我們使用基類的引用或指針調(diào)用一個(gè)虛成員函數(shù)時(shí)會(huì)執(zhí)行動(dòng)態(tài)綁定。
我們必須為每一個(gè)虛函數(shù)都提供定義,而不管它是否被用到了。
對(duì)虛函數(shù)的調(diào)用可能在運(yùn)行時(shí)才被解析
當(dāng)某個(gè)虛函數(shù)通過(guò)指針或引用調(diào)用時(shí),編譯器產(chǎn)生的代碼直到運(yùn)行時(shí)才能確定應(yīng)該調(diào)用哪個(gè)版本的函數(shù)。被調(diào)用的函數(shù)是與綁定到指針或引用上的對(duì)象的動(dòng)態(tài)類型相匹配的那一個(gè)。
當(dāng)我們通過(guò)一個(gè)具有普通類型(非引用非指針)的表達(dá)式調(diào)用虛函數(shù)時(shí),在編譯時(shí)就會(huì)將調(diào)用的版本確定下來(lái)。
c++的多態(tài)性
OOP的核心思想是多態(tài)性(polymorphism)。多態(tài)性這個(gè)詞源自希臘語(yǔ),其含義是“多種形式”。我們把具有繼承關(guān)系的多個(gè)類型稱為多態(tài)類型,因?yàn)槲覀兡苁褂眠@些類型的“多種形式”而無(wú)須在意它們的差異。引用或指針的靜態(tài)類型與動(dòng)態(tài)類型不同這一事實(shí)正是C++語(yǔ)言支持多態(tài)性的根本所在。
當(dāng)我們使用基類的引用或指針調(diào)用基類中定義的一個(gè)函數(shù)時(shí),我們并不知遞該函數(shù)真正作用的對(duì)象是什么類型,因?yàn)樗赡苁且粋€(gè)基類的對(duì)象也可能是一個(gè)派生類的對(duì) 象。如果該函數(shù)是虛函數(shù),則直到運(yùn)行時(shí)才會(huì)決定到底執(zhí)行哪個(gè)版本,判斷的依據(jù)是引用或指針?biāo)壎ǖ膶?duì)象的真實(shí)類型。
另一方面,對(duì)非虛函數(shù)的調(diào)用在編譯時(shí)進(jìn)行綁定。類似的,通過(guò)對(duì)象進(jìn)行的函數(shù)(虛函數(shù)或非虛函數(shù))調(diào)用也在編譯時(shí)綁定。 對(duì)象的類型是確定不變的,我們無(wú)論如何都不可能令對(duì)象的動(dòng)態(tài)類型與靜態(tài)類型不一致。因此,通過(guò)對(duì)象進(jìn)行的函數(shù)調(diào)用將在編譯時(shí)綁定到該對(duì)象所屬類中的函數(shù)版本上。
當(dāng)且僅當(dāng)對(duì)通過(guò)指針或引用調(diào)用虛函數(shù)時(shí),才會(huì)在運(yùn)行時(shí)解析該調(diào)用,也只有在這種情況下對(duì)象的動(dòng)態(tài)類型才有可能與靜態(tài)類型不同。
派生類中的虛函數(shù)
當(dāng)我們?cè)谂缮愔叙⑸w了某個(gè)虛函數(shù)時(shí),可以再一次使用virtual關(guān)鍵字指出該函數(shù)的性質(zhì)。然而這么做并非必須,因?yàn)橐坏┠硞€(gè)函數(shù)被聲明成虛函數(shù),則在所有派生類中它都是虛函數(shù)。
派生類中虛函數(shù)的返回類型也必須與基類函數(shù)匹配。該規(guī)則存在一個(gè)例外,當(dāng)類的虛函數(shù)返回類型是類本身的指針或引用時(shí),上述規(guī)則無(wú)效。也就是說(shuō),如果D由B派生得到則基類的虛函數(shù)可以返回B*而派生類的對(duì)應(yīng)函數(shù)可以返回D*,只不過(guò)這樣的返回類型要求從D到B的類型轉(zhuǎn)換是可訪問(wèn)的。
基類中的虛函數(shù)在派生類中隱含地也是一個(gè)虛函數(shù)。當(dāng)派生類覆蓋了某個(gè)虛函數(shù)時(shí),該函數(shù)在基類中的形參必須與派生類中的形參嚴(yán)格匹配。
final和override說(shuō)明符
派生類如果定義了一個(gè)函數(shù)與基類中虛函數(shù)的名字相同但是形參列表不同,這仍然是合法的行為,編譯器將認(rèn)為新定義的這個(gè)函數(shù)與基類中原有的函數(shù)是相互獨(dú)立的。
在c++11新標(biāo)準(zhǔn)中,我們可以使用override關(guān)鍵字來(lái)說(shuō)明派生類中的虛函數(shù)。如果我們使用override標(biāo)記了某個(gè)函數(shù),但該函數(shù)并沒(méi)有覆蓋已存在的虛函數(shù),此時(shí)編譯器將會(huì)報(bào)錯(cuò)。
class B { virtual void fl(int) const; virtual void f2(); void f3 (); };class Dl : B { void fl(int) const override; //正確:fl與基類中的fl匹配 void f2(int) override; //錯(cuò)誤:B沒(méi)有形如f2(int)的函數(shù)void f3() override; //錯(cuò)誤:f3不是虛函數(shù) void f4 () override; //錯(cuò)誤:B沒(méi)有名為f4的函數(shù) };我們還能把某個(gè)函數(shù)指定為final,如果我們已經(jīng)把函數(shù)定義成final了,則之后任何嘗試覆蓋該函數(shù)的操作都將會(huì)引發(fā)錯(cuò)誤。
class D2:B{ //從B繼承f2()和f3(),覆蓋f1(int)void f1(int)const final; // 不允許后續(xù)的其他類覆蓋f1(int) }class D3:D2{void f2(); //正確:覆蓋從間接基類B繼承而來(lái)的f2void f1(int)const; //錯(cuò)誤:D2已經(jīng)將f2聲明成final }回避虛函數(shù)的機(jī)制
在某些情況下,我們希望對(duì)虛函數(shù)的調(diào)用不要進(jìn)行動(dòng)態(tài)綁定,而是強(qiáng)迫其執(zhí)行虛函數(shù)的某個(gè)特定版本。使用作用域運(yùn)算符可以實(shí)現(xiàn)這一目的。
如果一個(gè)派生類虛函數(shù)需要調(diào)用它的基類版本,但是沒(méi)有使用作用域運(yùn)算符,則在運(yùn)行時(shí)該調(diào)用將被解析為對(duì)派生類版本自身的調(diào)用,從而導(dǎo)致無(wú)限遞歸。
抽象基類
純虛函數(shù)
和普通的虛函數(shù)不一樣,一個(gè)純虛函數(shù)無(wú)須定義。我們通過(guò)在函數(shù)體的位置(即在聲明語(yǔ)句的分號(hào)之前)書(shū)寫(xiě) =0 就可以將一個(gè)虛函數(shù)說(shuō)明為純虛函數(shù)。其中 =0 只能出現(xiàn)在類內(nèi)部的虛函數(shù)聲明語(yǔ)句處。
class Disc_quote : public Quote { public:Disc_quote() = default;Disc_quote(const string& book, double p, size_t qty, double disc):Quote(book, p), min_qty(qty), discount(disc) {}double net_price(size_t n) const = 0; protected:size_t min_qty=0; //折扣適用的購(gòu)買量double discount = 0.0; //表示折扣的小數(shù)值 };我們也可以為純虛函數(shù)提供定義,不過(guò)函數(shù)體必須定義在類的外部,也就是說(shuō),我們不能在類的內(nèi)部為一個(gè) =0 的函數(shù)提供函數(shù)體。
含有純虛函數(shù)的類是抽象基類
含有(或者未經(jīng)覆蓋直接繼承)純虛函數(shù)的類是抽象基類。抽象基類負(fù)責(zé)定義接口,而后續(xù)其他類可以覆蓋該接口。我們不能(直接)創(chuàng)建一個(gè)抽象基類的對(duì)象。因?yàn)镈isc_quote將net_price定義成了純虛函數(shù),所以我們不能定義Disc_quote的對(duì)象,我們可以定義Disc_quote的派生類的對(duì)象,前提是這些類覆蓋了net_price函數(shù)。
Disc_quote的派生類必須給出自己的net_price定義,否則它們?nèi)詫⑹浅橄蠡悺?/p>
派生類構(gòu)造函數(shù)只初始化它的直接基類
重構(gòu)
在Quote的繼承體系中增加Disc_quote類是重構(gòu)(refactoring)的一個(gè)典型示例。重構(gòu)負(fù)責(zé)重新設(shè)計(jì)類的體系以便將操作和/或數(shù)據(jù)從一個(gè)類移動(dòng)到另一個(gè)類中。對(duì)于面向?qū)ο蟮膽?yīng)用程序來(lái)說(shuō),重構(gòu)是一種很普遍的現(xiàn)象。
值得注意的是,即使我們改變了整個(gè)繼承體系,那些使用了Bulk_quote或Quote 的代碼也無(wú)須進(jìn)行任何改動(dòng)。不過(guò)一旦類被重構(gòu)(或以其他方式被改變),就意味著我們必須重新編譯含有這些類的代碼了。
訪問(wèn)控制與繼承
每個(gè)類分別控制自己的成員初始化過(guò)程,與之類似,每個(gè)類還分別控制著其成員對(duì)于派生類來(lái)說(shuō)是否可訪問(wèn)。
受保護(hù)的成員 protected
- 和私有成員類似,受保護(hù)的成員對(duì)于類的用戶來(lái)說(shuō)是不可訪問(wèn)的
- 和公有成員類似,受保護(hù)的成員對(duì)千派生類的成員和友元來(lái)說(shuō)是可訪問(wèn)的。
- 派生類的成員或友元只能通過(guò)派生類對(duì)象來(lái)訪問(wèn)基類的受保護(hù)成員。派生類對(duì)于一個(gè)基類對(duì)象中的受保護(hù)成員沒(méi)有任何訪問(wèn)特權(quán)。
公有、私有和受保護(hù)繼承
| 公有繼承 | public | protected | 不可見(jiàn) |
| 私有繼承 | private | private | 不可見(jiàn) |
| 保護(hù)繼承 | protected | protected | 不可見(jiàn) |
某個(gè)類對(duì)其繼承而來(lái)的成員的訪問(wèn)權(quán)限受到兩個(gè)因素影響:一是在基類中該成員的訪問(wèn)說(shuō)明符,二是在派生類的派生列表中的訪問(wèn)說(shuō)明符。
派生訪問(wèn)說(shuō)明符對(duì)于派生類的成員(及友元)能否訪問(wèn)其直接基類的成員沒(méi)什么影響。 對(duì)基類成員的訪問(wèn)權(quán)限只與基類中的訪問(wèn)說(shuō)明符有關(guān)。
派生訪問(wèn)說(shuō)明符的目的是控制派生類用戶(包括派生類的派生類在內(nèi))對(duì)于基類成員的訪問(wèn)權(quán)限。
故,示例如下:
繼承代碼:
class Base2 { public:void publicFun() {cout << "Base2的publicFun()" << endl;} private:void privateFun() {cout << "Base2的privateFun()" << endl;} protected:void protectedFun() {cout << "Base2的protectedFun()" << endl;}};class Public_derv : public Base2 { public:void f1() { cout << "Public_derv調(diào)用publicFun() ";publicFun(); }//void f2() { privateFun(); } //派生類不能訪問(wèn)private成員void f3() { cout << "Public_derv調(diào)用protectedFun() ";protectedFun(); } };class Private_derv : private Base2 { public:void f1() { cout << "Private_derv調(diào)用publicFun() ";publicFun(); }//void f2() { privateFun(); } //派生類不能訪問(wèn)private成員void f3() { cout << "Private_derv調(diào)用protectedFun() ";protectedFun(); } };class Protected_derv : protected Base2 { public:void f1() { cout << "Protected_derv調(diào)用publicFun() ";publicFun(); }//void f2() { privateFun(); } //派生類不能訪問(wèn)private成員void f3() { cout << "Protected_derv調(diào)用protectedFun() ";protectedFun(); } };測(cè)試代碼:
Public_derv public_d;public_d.f1();public_d.f3();public_d.publicFun();Private_derv private_d;private_d.f1();private_d.f3();//private_d.publicFun(); publicFun()在派生類中是private的,不可訪問(wèn)Protected_derv protected_d;protected_d.f1();protected_d.f3();//protected_d.publicFun(); publicFun()在派生類中是protected的,不可訪問(wèn)輸出結(jié)果:
Public_derv調(diào)用publicFun() Base2的publicFun() Public_derv調(diào)用protectedFun() Base2的protectedFun() Base2的publicFun() Private_derv調(diào)用publicFun() Base2的publicFun() Private_derv調(diào)用protectedFun() Base2的protectedFun() Protected_derv調(diào)用publicFun() Base2的publicFun() Protected_derv調(diào)用protectedFun() Base2的protectedFun()派生類向基類轉(zhuǎn)換的可訪問(wèn)性
- 只有當(dāng)D公有的繼承B時(shí),用戶代碼才能使用派生類向基類的轉(zhuǎn)換;如果D繼承B的方式是受保護(hù)的或者私有的,則用戶代碼不能使用該轉(zhuǎn)換
- 不論D以什么方式繼承B,D的成員函數(shù)和友元都能使用派生類向基類的轉(zhuǎn)換;派生類向其直接基類的類型轉(zhuǎn)換對(duì)于派生類的成員和友元來(lái)說(shuō)永遠(yuǎn)是可訪問(wèn)的。
- 如果D繼承B的方式是公有的或者受保護(hù)的,則D的派生類的成員和友元可以使用D向B的類型轉(zhuǎn)換,反之,如果D繼承B是私有的,則不能使用。
友元與繼承
就像友元關(guān)系不能傳遞一樣,友元關(guān)系同樣也不能繼承。基類的友元在訪問(wèn)派生類成員時(shí)不具有特殊性,類似的,派生類的友元也不能隨意訪問(wèn)基類的成員。
對(duì)于f2函數(shù),Pal是Base的友元,Pal能夠訪問(wèn)Base對(duì)象的成員,這種可訪問(wèn)性包括了Base對(duì)象內(nèi)嵌在其派生類對(duì)象中的情況。
當(dāng)一個(gè)類將另一個(gè)類聲明為友元時(shí),這種友元關(guān)系只對(duì)做出聲明的類有效。對(duì)于原來(lái)那個(gè)類來(lái)說(shuō),其友元的基類或者派生類不具有特殊的訪問(wèn)能力。
改變各個(gè)成員的可訪問(wèn)性
有時(shí)我們需要改變派生類繼承的某個(gè)名字的訪問(wèn)級(jí)別,通過(guò)使用using聲明可以達(dá)到這一目的。
class Base3 { public:size_t size()const { return n; } protected:size_t n; }; class Derived : private Base3 { public:using Base3::size; protected:using Base3::n; };因?yàn)镈erived是私有繼承,所以繼承而來(lái)的成員size和n是Derived的私有成員,然而我們使用using聲明語(yǔ)句,將size變成了public成員,Derived變成了protected成員,改變了這些成員的可訪問(wèn)性。
默認(rèn)的繼承保護(hù)級(jí)別
class Base{ }; struct D1 : Base{ }; //默認(rèn)public繼承 class D2 : Base{ }; //默認(rèn)private繼承繼承中的類作用域
每個(gè)類定義自己的作用域,在這個(gè)作用域內(nèi)我們定義類的成員。當(dāng)存在繼承關(guān)系時(shí),派生類的作用域嵌套在其基類的作用域之內(nèi),如果一個(gè)名字在派生類的作用域內(nèi)無(wú)法正確解析,則編譯器將繼續(xù)在外層的基類作用域中尋找該名字的定義。
恰恰因?yàn)轭愖饔糜蛴羞@種繼承嵌套的關(guān)系,所以派生類才能像使用自己的成員一樣使用基類的成員。
在編譯時(shí)進(jìn)行名字查找
一個(gè)對(duì)象、引用或指針的靜態(tài)類型決定了該對(duì)象的哪些成員是可見(jiàn)的。即使靜態(tài)類型與動(dòng)態(tài)類型可能不一致(當(dāng)使用基類的引用或指針時(shí)會(huì)發(fā)生這種情況),但是我們能使用哪些成員仍然是由靜態(tài)類型決定的。
示例:
class D_Quote : public Quote{ public:void fun(){} }D_Quote tmp; D_Quote *dq = &tmp; Quote *q = &tmp;dq->fun(); //正確,dq的類型是D_Quote* q->fun(); //錯(cuò)誤,q的類型是Quote*,沒(méi)有fun()函數(shù)名字沖突與繼承
和其他作用域一樣,派生類也能重用定義在其直接基類或間接基類中的名字,此時(shí)定義在內(nèi)層作用域(即派生類)的名字將隱藏定義在外層作用域(即基類)的名字。
派生類的成員將隱藏同名的基類成員。
除了覆蓋繼承而來(lái)的虛函數(shù)之外,派生類最好不要重用其他定義在基類中的名字。
名字查找先于類型檢查
如果派生類的成員與基類的某個(gè)成員同名,則派生類將在其作用域內(nèi)隱藏該基類成員。即使派生類成員和基類成員的形參列表不一致,基類成員也仍然會(huì)被隱藏掉。
示例:
class Base{ public:void fun(); };class Derived : public Base{ public:void fun(int); //即使派生類成員和基類成員的形參列表不一致,基類成員fun()也仍然會(huì)被隱藏掉 };Derived d; Base b; d.fun(10); d.fun();//錯(cuò)誤,fun()被隱藏 d.Base::fun(); //正確,調(diào)用Base::fun()虛函數(shù)與作用域,通過(guò)基類調(diào)用隱藏的虛函數(shù)
class BaseHide { public:virtual void fcn() { cout << "BaseHide virtual fcn()" << endl; }//虛函數(shù) };class DerivedHide1 : public BaseHide { public:void fcn(int) { cout << "DerivedHide1 fcn(int)" << endl; } //隱藏BaseHide的fcn()virtual void f2() { cout << "DerivedHide1 virtual f2( )" << endl; } };class DerivedHide2 : public DerivedHide1 { public:void fcn(int) { cout << "DerivedHide2 fcn(int)" << endl; }void fcn() { cout << "DerivedHide2 fcn()" << endl; }void f2() { cout << "DerivedHide2 f2( )" << endl; } };測(cè)試代碼:
BaseHide bh;DerivedHide1 dh1;DerivedHide2 dh2;BaseHide *bp1 = &bh, *bp2 = &dh1, *bp3 = &dh2;bp1->fcn(); //虛調(diào)用,BaseHide::fcn()bp2->fcn(); //虛調(diào)用,BaseHide::fcn()bp3->fcn(); //虛調(diào)用,DerivedHide2::fcn()cout << endl;DerivedHide1 *dp1 = &dh1;DerivedHide2 *dp2 = &dh2;//bp2->f2(); 錯(cuò)誤,BaseHide沒(méi)有名為f2的成員dp1->f2(); //虛調(diào)用,DerivedHide1::f2()dp2->f2(); //虛調(diào)用,DerivedHide2::f2()cout << endl;BaseHide *p1= &dh2;DerivedHide1 *p2 = &dh2;DerivedHide2 *p3 = &dh2;//p1->fcn(42); 錯(cuò)誤,BaseHide沒(méi)有fcn(int)p2->fcn(42); //靜態(tài)綁定,DerivedHide1::fcn(int)p3->fcn(42); //靜態(tài)綁定,DerivedHide2::fcn(int)結(jié)果:
BaseHide virtual fcn() BaseHide virtual fcn() DerivedHide2 virtual fcn()DerivedHide1 virtual f2( ) DerivedHide2 virtual f2( )DerivedHide1 fcn(int) DerivedHide2 fcn(int)DerivedHide1的fcn函數(shù)并沒(méi)有覆蓋BaseHide的虛函數(shù)fcn,原因是參數(shù)列表不同,將隱藏BaseHide的fcn。
dh1不能調(diào)用fcn(),因?yàn)楸浑[藏了,但bp2指針能調(diào)用fcn(),因?yàn)閒cn是虛函數(shù),bp2實(shí)際綁定的對(duì)象是DerivedHide1類型,而DerivedHide1并沒(méi)有覆蓋不接受實(shí)參的fcn(),所以通過(guò)bp2進(jìn)行的調(diào)用將在運(yùn)行時(shí)解析為BaseHide定義的版本。
bp2指向派生類對(duì)象DerivedHide1,但由于Base類中沒(méi)有f2(),所以bp2不能調(diào)用f2()。
同理,p1不能調(diào)用fcn(int),但p2可以調(diào)用fcn(int),因?yàn)镈erivedHide1中有fcn(int),由于fcn(int)是非虛函數(shù),所以不會(huì)發(fā)生動(dòng)態(tài)綁定,實(shí)際調(diào)用的函數(shù)版本由指針的靜態(tài)類型決定。
構(gòu)造函數(shù)與拷貝控制
和其他類一樣,位于繼承體系中的類也需要控制當(dāng)其對(duì)象執(zhí)行一系列操作時(shí)發(fā)生什么樣的行為,這些操作包括創(chuàng)建、拷貝、移動(dòng)、賦值和銷毀。如果一個(gè)類(基類或派生類)沒(méi)有定義拷貝控制操作,則編譯器將為它合成一個(gè)版本。
虛析構(gòu)函數(shù)
基類通常應(yīng)該定義一個(gè)虛析構(gòu)函數(shù),這樣我們就能動(dòng)態(tài)分配繼承體系中的對(duì)象了。
如果我們delete一個(gè)Base * 類型的指針,則該指針有可能實(shí)際上指向了一個(gè)Derived類型的對(duì)象, 因此編譯器必須清楚它應(yīng)該執(zhí)行的的Derived類型的析構(gòu)函數(shù)。和其他函數(shù)一樣,我們通過(guò)在基類中將析構(gòu)函數(shù)定義成虛函數(shù)以確保執(zhí)行正確的析構(gòu)函數(shù)版本。
如果基類的析構(gòu)函數(shù)不是虛函數(shù),則delete一個(gè)指向派生類對(duì)象的基類指針將產(chǎn)生未定義的行為。
之前我們?cè)榻B過(guò)一條經(jīng)驗(yàn)準(zhǔn)則, 即如果一個(gè)類需要析構(gòu)函數(shù), 那么它也同樣需要 貝和賦值操作。 基類的析構(gòu)函數(shù)并不遵循上述準(zhǔn)則, 它是一個(gè)重要的例外。 一個(gè)基類總是需要析構(gòu)函數(shù), 而且它能將析構(gòu)函數(shù)設(shè)定為虛函數(shù)。 此時(shí),該析構(gòu)函數(shù)為了成為虛函數(shù)而令內(nèi)容為空,我們顯然無(wú)法由此推斷該基類還需要賦值運(yùn)算符或拷貝構(gòu)造函數(shù)。
虛析構(gòu)函數(shù)將阻止合成移動(dòng)操作
基類需要一個(gè)虛析構(gòu)函數(shù)這一事實(shí)還會(huì)對(duì)基類和派生類的定義產(chǎn)生另外一個(gè)間接的影響:如果一個(gè)類定義了析構(gòu)函數(shù),即使它通過(guò) =default 的形式使用了合成的版本,編譯器也不會(huì)為這個(gè)類合成移動(dòng)操作。
合成拷貝控制與繼承
派生類中刪除的拷貝控制與基類的關(guān)系型
- 如果基類中的默認(rèn)構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、拷貝賦值運(yùn)算符或析構(gòu)函數(shù)是被刪除的函數(shù)或者不可訪問(wèn),則派生類中對(duì)應(yīng)的成員將是被刪除的,原因是編譯器不能使用基類成員來(lái)執(zhí)行派生類對(duì)象基類部分的構(gòu)造、賦值或銷毀操作。
- 如果在基類中有一個(gè)不可訪問(wèn)或刪除掉的析構(gòu)函數(shù),則派生類中合成的默認(rèn)和拷貝構(gòu)造函數(shù)將是被刪除的,因?yàn)榫幾g器無(wú)法銷毀派生類對(duì)象的基類部分。
- 和過(guò)一樣,編譯器將不會(huì)合成一個(gè)刪除掉的移動(dòng)操作。 當(dāng)我們使用=default請(qǐng)求一個(gè)移動(dòng)操作時(shí),如果基類中的對(duì)應(yīng)操作是刪除的或不可訪問(wèn)的, 那么派生類中該函數(shù)將是被刪除的, 原因 是派生類對(duì)象的基類部分不可移動(dòng)。 同樣, 如果基類的析構(gòu)函數(shù)是刪除的或不可訪問(wèn)的, 則派生類的移動(dòng)構(gòu)造函數(shù)也將是被刪除的。
派生類的拷貝控制成員
派生類構(gòu)造函數(shù)在其初始化階段中不但要初始化派生類自己的成員,還負(fù)責(zé)初始化派生類對(duì)象的基類部分。因此,派生類的拷貝和移動(dòng)構(gòu)造函數(shù)在拷貝和移動(dòng)自有成員的同時(shí),也要拷貝和移動(dòng)基類部分的成員。類似的,派生類賦值運(yùn)算符也必須為其基類部分的成員賦值。
和構(gòu)造函數(shù)及賦值運(yùn)算符不同的是,析構(gòu)函數(shù)只負(fù)責(zé)銷毀派生類自己分配的資源。對(duì)象的成員是被隱式銷毀的;類似的,派生類對(duì)象的基類部分也是自動(dòng)銷毀的。
對(duì)象銷毀的順序正好與其創(chuàng)建的順序相反:派生類析構(gòu)函數(shù)首先執(zhí)行,然后是基類的析構(gòu)函數(shù),以此類推, 沿著繼承體系的反方向直至最后。
當(dāng)派生類定義了拷貝或移動(dòng)操作時(shí),該操作負(fù)責(zé)拷貝或移動(dòng)包括基類部分成員在內(nèi)的整個(gè)對(duì)象。
定義派生類的拷貝或移動(dòng)構(gòu)造函數(shù)
在默認(rèn)情況下,基類默認(rèn)構(gòu)造函數(shù)初始化派生類對(duì)象的基類部分。如果我們想拷貝(或移動(dòng))基類部分,則必須在派生類的構(gòu)造函數(shù)初始值列表中顯式地使用基類的拷貝(或移動(dòng))構(gòu)造函數(shù)。派生類的賦值運(yùn)算符也必須顯式地為其基類部分賦值。
派生類賦值運(yùn)算符
與拷貝和移動(dòng)構(gòu)造函數(shù)一樣,派生類的賦值運(yùn)算符也必須顯式地為其基類部分賦值。
// Base::operator=(const Base&); 不會(huì)被自動(dòng)調(diào)用D &D::operator=(const D &rhs){Base::operator=(rhs); //為其基類部分賦值//按照過(guò)去的方式為派生類的成員賦值//酌情處理自賦值及釋放已有資源等情況return *this; }派生類析構(gòu)函數(shù)
如前所述,在析構(gòu)函數(shù)體執(zhí)行完成后,對(duì)象的成員會(huì)被隱式銷毀。類似的,對(duì)象的基類部分也是隱式銷毀的。因此,和構(gòu)造函數(shù)及賦值運(yùn)算符不同的是,派生類析構(gòu)函數(shù)只負(fù)責(zé)銷毀由派生類自己分配的資源。
對(duì)象銷毀的順序正好與其創(chuàng)建的順序相反:派生類析構(gòu)函數(shù)首先執(zhí)行,然后是基類的析構(gòu)函數(shù),以此類推,沿著繼承體系的反方向直至最后。
在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù)
如果構(gòu)造函數(shù)或析構(gòu)函數(shù)調(diào)用了某個(gè)虛函數(shù),則我們應(yīng)該執(zhí)行與構(gòu)造函數(shù)或析構(gòu)函數(shù)所屬類型的虛函數(shù)版本。
測(cè)試類
class TestBase { public:TestBase() { cout << "這是TestBase的構(gòu)造函數(shù)" << endl;fun();cout << endl; }virtual void fun() { cout << "這是TestBase的虛函數(shù)" << endl; }~TestBase() { cout << "這是TestBase的析構(gòu)函數(shù)" << endl; fun();cout << endl;}};class TestDerived:public TestBase { public:TestDerived() { cout << "這是TestDerived的構(gòu)造函數(shù)" << endl;fun();cout << endl;}virtual void fun() { cout << "這是TestDerived的虛函數(shù)" << endl; }~TestDerived() { cout << "這是TestDerived的析構(gòu)函數(shù)" << endl;fun();cout << endl;} };測(cè)試函數(shù):
void testTestBase() {TestBase tb1;TestDerived td1; }以上代碼,先創(chuàng)建一個(gè)TestBase的類,故先執(zhí)行TestBase的構(gòu)造函數(shù),然后創(chuàng)建一個(gè)TestDerived的類,由于其繼承了TestBase,故先執(zhí)行TestBase的構(gòu)造函數(shù),然后執(zhí)行TestDerived的構(gòu)造函數(shù),最后當(dāng)testTestBase()函數(shù)執(zhí)行完成后,將td1進(jìn)行析構(gòu),先執(zhí)行TestDerived的析構(gòu)函數(shù),然后執(zhí)行TestBase的析構(gòu)函數(shù),然后將tb1執(zhí)行析構(gòu),即執(zhí)行TestBase的析構(gòu)函數(shù)。在構(gòu)造和析構(gòu)函數(shù)調(diào)用的虛函數(shù),其執(zhí)行與構(gòu)造函數(shù)或析構(gòu)函數(shù)所屬類型的虛函數(shù)版本。
輸出結(jié)果:
這是TestBase的構(gòu)造函數(shù) 這是TestBase的虛函數(shù)這是TestBase的構(gòu)造函數(shù) 這是TestBase的虛函數(shù)這是TestDerived的構(gòu)造函數(shù) 這是TestDerived的虛函數(shù)這是TestDerived的析構(gòu)函數(shù) 這是TestDerived的虛函數(shù)這是TestBase的析構(gòu)函數(shù) 這是TestBase的虛函數(shù)這是TestBase的析構(gòu)函數(shù) 這是TestBase的虛函數(shù)測(cè)試函數(shù)二:
void testTestBase() {TestDerived td1;TestBase *tb1 = new TestDerived();cout << "調(diào)用fun函數(shù):" << endl;tb1->fun(); //此處調(diào)用的是派生類的fun函數(shù)cout << endl; }輸出結(jié)果:
這是TestBase的構(gòu)造函數(shù) 這是TestBase的虛函數(shù)這是TestDerived的構(gòu)造函數(shù) 這是TestDerived的虛函數(shù)這是TestBase的構(gòu)造函數(shù) 這是TestBase的虛函數(shù)這是TestDerived的構(gòu)造函數(shù) 這是TestDerived的虛函數(shù)調(diào)用fun函數(shù): 這是TestDerived的虛函數(shù)這是TestDerived的析構(gòu)函數(shù) 這是TestDerived的虛函數(shù)這是TestBase的析構(gòu)函數(shù) 這是TestBase的虛函數(shù)繼承的構(gòu)造函數(shù)
一個(gè)類只初始化它的直接基類,出于同樣的原因,一個(gè)類也只繼承其直接基類的構(gòu)造函數(shù)。
類不能繼承默認(rèn)、拷貝和移動(dòng)構(gòu)造函數(shù)。如果派生類沒(méi)有直接定義這些構(gòu)造函數(shù),則編譯器將為派生類合成它們。
派生類繼承基類構(gòu)造函數(shù)的方式是提供一條注明了(直接)基類名的using聲明語(yǔ)句。通常情況下,using聲明語(yǔ)句只是令某個(gè)名字在當(dāng)前作用域內(nèi)可見(jiàn),而當(dāng)作用于構(gòu)造函數(shù)時(shí),using聲明語(yǔ)句將令編譯器產(chǎn)生代碼。對(duì)于基類的每個(gè)構(gòu)造函數(shù),編譯器都生成一個(gè)與之對(duì)應(yīng)的派生類構(gòu)造函數(shù)。換句話說(shuō),對(duì)于基類的每個(gè)構(gòu)造函數(shù),編譯器都在派生類中生成一個(gè)形參列表完全相同的構(gòu)造函數(shù)。
繼承的構(gòu)造函數(shù)的特點(diǎn)
和普通成員的using聲明不一樣,一個(gè)構(gòu)造函數(shù)的using聲明不會(huì)改變?cè)摌?gòu)造函數(shù)的訪問(wèn)級(jí)別。例如,不管using聲明出現(xiàn)在哪,基類的私有構(gòu)造函數(shù)在派生類中還是一個(gè)私有構(gòu)造函數(shù),受保護(hù)的構(gòu)造函數(shù)和公有構(gòu)造函數(shù)也是同樣的規(guī)則。
當(dāng)一個(gè)基類構(gòu)造函數(shù)含有默認(rèn)實(shí)參時(shí),這些實(shí)參并不會(huì)被繼承。相反,派生類將獲得多個(gè)繼承的構(gòu)造函數(shù),其中每個(gè)構(gòu)造函數(shù)分別省略掉一個(gè)含有默認(rèn)實(shí)參的形參。
如果基類有多個(gè)構(gòu)造函數(shù),則除了兩個(gè)例外情況,大多數(shù)時(shí)候派生類會(huì)繼承所有這些構(gòu)造函數(shù)。第一個(gè)例外情況是,如果派生類定義的構(gòu)造函數(shù)于基類的構(gòu)造函數(shù)具有相同的參數(shù)列表,則該構(gòu)造函數(shù)將不會(huì)被繼承。定義在派生類中的構(gòu)造函數(shù)將替換繼承而來(lái)的構(gòu)造函數(shù)。
第二個(gè)例外是默認(rèn)、拷貝和移動(dòng)構(gòu)造函數(shù)不會(huì)被繼承。
測(cè)試代碼:
class TestBase { public:TestBase(){ }TestBase(int i):id(i) { }void getId() { cout <<"id:"<<id << endl; }private:int id;};class TestDerived:public TestBase { public:using TestBase::TestBase;};//測(cè)試函數(shù): void testTestBase() {TestDerived td1(5);td1.getId(); }輸出:
id:5若將using TestBase::TestBase;注釋掉,則TestDerived td1(5);會(huì)報(bào)錯(cuò)。
容器與繼承
當(dāng)派生類對(duì)象被賦值給基類對(duì)象時(shí),其中的派生類部分將被“切掉”,因此,當(dāng)我們使用容器存放繼承體系中的對(duì)象時(shí),通常必須采取間接存儲(chǔ)的方式。
在容器中放置(智能)指針而非對(duì)象
當(dāng)我們希望在容器中存放具有繼承關(guān)系的對(duì)象時(shí),我們實(shí)際上存放的通常是基類的指針。和往常一樣,這些指針?biāo)笇?duì)象的動(dòng)態(tài)類型可能是基類類型,也可能是派生類類型。
測(cè)試代碼:
class TestBase { public:TestBase(int i):id(i) { }virtual void getId() { cout <<"id:"<<id << endl; }private:int id; };class TestDerived:public TestBase { public:TestDerived(int i,int j) :TestBase(i),num(j) { }virtual void getId() { TestBase::getId();cout << "num:" << num << endl; } private:int num;}; //測(cè)試函數(shù) void testTestBase() {TestBase tb1(1);TestDerived td1(2, 3);vector<shared_ptr<TestBase>>vec;vec.push_back(make_shared<TestBase>(tb1));vec.push_back(make_shared<TestDerived>(td1));for (auto v:vec) {v->getId();cout << "======" << endl;} }輸出結(jié)果:
id:1 ====== id:2 num:3 ======編寫(xiě)B(tài)asket類
對(duì)于c++面向?qū)ο蟮木幊虂?lái)說(shuō),我們必須使用指針和引用來(lái)進(jìn)行面向?qū)ο缶幊獭R驗(yàn)橹羔槙?huì)增加程序的復(fù)雜性,所以我們經(jīng)常定義一些輔助的類來(lái)處理這種復(fù)雜情況。
定義一個(gè)表示購(gòu)物籃的類:
decltype的意義
有時(shí)我們希望從表達(dá)式的類型推斷出要定義的變量類型,但是不想用該表達(dá)式的值初始化變量(初始化可以用auto)。為了滿足這一需求,C++11新標(biāo)準(zhǔn)引入了decltype類型說(shuō)明符,它的作用是選擇并返回操作數(shù)的數(shù)據(jù)類型,在此過(guò)程中,編譯器分析表達(dá)式并得到它的類型,卻不實(shí)際計(jì)算表達(dá)式的值。
upper_bound可參考chapter 11
Basket類:
class Basket { public:// Basket使用合成的默認(rèn)構(gòu)造函數(shù)和拷貝控制成員void add_item(const shared_ptr<Quote>&sale){items.insert(sale);}//打印每本書(shū)的總價(jià)和購(gòu)物籃中所有書(shū)的總價(jià)double total_receipt(ostream&)const; private://該函數(shù)用于比較shared_ptr,multiset成員會(huì)用它static bool compare(const shared_ptr<Quote>&lhs, const shared_ptr<Quote>&rhs) {return lhs->isbn() < rhs->isbn();}//multiset保存多個(gè)報(bào)價(jià),按照compare成員排序multiset<shared_ptr<Quote>, decltype(compare)*>items{compare}; };double Basket::total_receipt(ostream&os)const {double sum = 0;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; }Quote類和QuoteDerived類:
class Quote { public:Quote() = default;Quote(const string &book,double sales_price) :bookNo(book),price(sales_price){}string isbn()const { return bookNo; }//返回給定數(shù)量的書(shū)籍的銷售總額//派生類負(fù)責(zé)改寫(xiě)并使用不同的折扣計(jì)算算法virtual double net_price(size_t n)const { return n*price; }virtual ~Quote() = default;//對(duì)析構(gòu)函數(shù)進(jìn)行動(dòng)態(tài)綁定virtual void debug() {cout << "Quote類的bookNo:" << bookNo << ",price: " << price << endl;}virtual ostream & print(ostream &os) {os << "bookNo: " << bookNo << " price: " << price;return os;}private:string bookNo; //書(shū)籍的ISBN編號(hào) protected:double price = 0.0; //代表普通狀態(tài)下不打折的價(jià)格 };double print_total(ostream &os, Quote &qt,int num) {qt.print(os);os << " number: " << num << endl;return qt.net_price(num); }class QuoteDerived :public Quote { public:QuoteDerived() = default;QuoteDerived(const string &book, double sales_price,double d):Quote(book, sales_price),discount(d){}virtual double net_price(size_t cnt) const {return cnt*price*discount;}private:double discount; };測(cè)試函數(shù):
void testBasket(){Basket bsk;bsk.add_item(make_shared<Quote>("123",60));bsk.add_item(make_shared<QuoteDerived>("123", 60,0.5));bsk.add_item(make_shared<QuoteDerived>("345", 100, 0.5));bsk.add_item(make_shared<Quote>("345", 100));bsk.total_receipt(cout); }輸出結(jié)果:
bookNo: 123 price: 60 number: 2 bookNo: 345 price: 100 number: 2 Total Sale: 220若測(cè)試函數(shù)改為:
void testBasket(){Basket bsk;bsk.add_item(make_shared<Quote>("123",60));bsk.add_item(make_shared<QuoteDerived>("123", 60,0.5));bsk.add_item(make_shared<Quote>("345", 100));bsk.add_item(make_shared<QuoteDerived>("345", 100, 0.5));bsk.total_receipt(cout); }則輸出結(jié)果為:
bookNo: 123 price: 60 number: 2 bookNo: 345 price: 100 number: 2 Total Sale: 320顯然,結(jié)果不是我們預(yù)期的結(jié)果,我們想要的結(jié)果應(yīng)該是,60+600.5+100+1000.5=240。該程序的結(jié)果是由于其iter=items.upper_bound(*iter) 導(dǎo)致的,因?yàn)樗鼘⑺衖sbn相同的歸為一類,且,按照該類別的第一個(gè)進(jìn)行統(tǒng)一計(jì)算價(jià)錢(qián)。
模擬虛拷貝
在Quote中添加虛函數(shù):
//該虛函數(shù)返回當(dāng)前對(duì)象的一份動(dòng)態(tài)分配的拷貝virtual Quote*clone()const & { return new Quote(*this); }virtual Quote*clone()const && { return new Quote(move(*this)); }在QuoteDerived中添加虛函數(shù):
virtual QuoteDerived*clone()const & { return new QuoteDerived(*this); }virtual QuoteDerived*clone()const && { return new QuoteDerived(move(*this)); }改寫(xiě)B(tài)asket類的add_item:
void add_item(const Quote &sale) {items.insert(shared_ptr<Quote>(sale.clone()));}void add_item(const Quote &&sale) {items.insert(shared_ptr<Quote>(move(sale).clone()));}此時(shí)的測(cè)試函數(shù)為:
void testBasket(){Basket bsk;bsk.add_item(Quote("123",60) );bsk.add_item(QuoteDerived("123", 60,0.5));bsk.add_item(Quote("345", 100));bsk.add_item(QuoteDerived("345", 100, 0.5));bsk.total_receipt(cout); }總結(jié)
以上是生活随笔為你收集整理的C++ primer 第15章 面向对象程序设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 世界公认的通用货币有哪些
- 下一篇: C++内存管理(1)