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