《C++ primer》(第5版) chapter15 讀書筆記
阿新 • • 發佈:2021-02-04
文章目錄
15.1 OOP:概述
- 面向物件程式設計的核心思想是
資料抽象
、繼承
和動態繫結
(有時也被稱為執行時繫結
)- 通過使用資料抽象,我們可以將類的介面與實現分離
- 使用繼承,可以定義相似的型別並對其相似關係建模
- 使用動態繫結,可以在一定程度上忽略相似型別的區別,而以統一的方式使用它們的物件
15.2 定義基類和派生類
- 基類通常都應該定義一個虛解構函式,即使該函式不執行任何實際操作也是如此
- 基類通過在其成員函式的宣告語句之前加上關鍵字
virtual
使得該函式執行動態繫結:- 任何建構函式之外的非靜態函式都可以是虛擬函式
- 關鍵字
virtual
只能出現在類內部的宣告語句之前而不能用於類外部的函式定義 - 如果基類把一個函式宣告成虛擬函式,則該函式在派生類中隱式地也是虛擬函式
- 如果派生類沒有覆蓋其基類中的某個虛擬函式,則該函式的行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本
- 派生類可以在它覆蓋的函式前使用
virtual
關鍵字,但不是非得這麼做 - 如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。不論從基類中派生出多少個派生類,對於每個靜態成員來說都只存在唯一的例項
- 靜態成員遵循通用的訪問控制規則
- 表示式的
靜態型別
在編譯時總是已知的,它是變數宣告時的型別或表示式生成的型別;動態型別則是變數或表示式表示的記憶體中的物件的型別,動態型別直到執行時才可知- 繫結到派生類的基類的指標或引用其動態型別只有在執行時才只知道
- 當我們用一個派生類物件為一個基類物件初始化或賦值時,只有該派生類物件中的基類部分會被拷貝、移動或賦值,它的派生類部分將被忽略掉
15.3 虛擬函式
- 當且僅當對通過指標或引用呼叫虛擬函式時,才會在執行時解析該呼叫,也只有在這種情況下物件的動態型別才有可能與靜態型別不同
- 基類中的虛擬函式在派生類中隱含地也是一個虛擬函式。當派生類覆蓋了某個虛擬函式時,該函式在基類中的形參必須與派生類中的形參嚴格匹配;否則會產生意想不到的結果。
- 在c++11新標準猴子那個我們可以使用
override
關鍵字來說明派生類中虛擬函式,這麼做的好處是在使得程式設計師的意圖更加清晰的同時讓編譯器可以為我們發現一些錯誤(見例子一) - 我們還能把某個函式指定為
final
,指定為final
的函式不能被覆蓋,否則將引發錯誤(見例子二)final
和override
說明符出現在形參列表(包括任何const或引用修飾符)以及尾置返回型別之後,即放在修飾符最後
- 如果虛擬函式使用預設引數,則基類和派生類中定義的預設實參最好一致,因為使用的始終是基類中定義的預設實參
//例子一:使用override宣告被覆蓋的virtual函式
struct B{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1:B{
void f1(int) const override; //正確:f1與基類中的f1匹配
void f2(int) override; //錯誤:B沒有形如f2(int)的函式
void f3() override; //錯誤:f3不是虛擬函式
void f4() override; //錯誤:B沒有名為f4的函式
};
//例子二:指定函式為final防止被覆蓋
struct D2:B{
//從B繼承f2()和f3(),覆蓋f1(int)
void f1(int) const final; //不允許後續的其他類覆蓋f1(int)
};
struct D3:D2{
void f2(); //正確:覆蓋從間接基類B繼承而來的f2
void f1(int) const; //錯誤:D2已經將f2宣告成final
};
15.4 抽象基類
- 我們通過在函式體的位置(即在宣告語句的分號之前)書寫
=0
就可以建個一個虛擬函式說明為純虛擬函式。其中=0
只能出現在類內部的虛擬函式宣告語句。 - 我們也可以為純虛擬函式提供定義,不過函式體必須定義在類的外部。也就是說,我們不能在類的內部為一個
=0
的函式提供函式體 - 含有(或未經覆蓋直接繼承)純虛擬函式的類是
抽象基類
。我們不能(直接)建立一個抽象基類的物件。
15.5 訪問控制與繼承
public | protected | private | |
---|---|---|---|
類成員是否可以訪問 | Yes | Yes | Yes |
友元函式是否可以訪問 | Yes | Yes | Yes |
子類是否可以訪問 | Yes | Yes | No |
類的例項化物件是否可以訪問 | Yes | No | No |
public | protected | private | |
---|---|---|---|
public繼承 | public | protected | private |
protected繼承 | protected | protected | private |
private | private | private | private |
-
訪問控制的許可權如上表一所示(參考C++ 類訪問控制)
-
某個類對繼承而來的成員的訪問許可權受到兩個因素影響:一是在基類中該成員的訪問說明符,二是在派生類的派生列表中的訪問說明符。三種繼承方式導致的許可權變化見上表二(參考C++ 類訪問控制)
-
(*重要)派生類向基類的轉換是否可訪問由使用該轉換的程式碼決定,同時派生類的派生訪問說明符也會有影響。假定
D
繼承自B
(見例子一):- 只有當
D
公有地繼承B
時,使用者程式碼才能使用派生類向基類的轉換;如果D
繼承B
的方式是受保護的或者私有的,則使用者程式碼不能使用該轉換。 - 不論
D
以什麼方式繼承B
,D
的成員函式和友元都能使用派生類向基類的轉換;派生類向其直接基類的型別轉換對於派生類的成員和友元來說永遠是可訪問的。 - 如果
D
繼承B
的方式是公有的或者受保護的,則D
的派生類的成員和友元可以使用D
向B
的型別轉換;反之,如果D
繼承B
的方式是私有的,則不能使用。
- 只有當
-
使用
class
關鍵字定義的派生類是私有繼承的;而使用struct
關鍵字定義的派生類是公有繼承的。(見例子二)
//例子一:派生類向基類轉換的可見性
class Base{};
//下面三條判斷的依據是:
// 派生類向其直接基類的型別轉換對於派生類的成員和友元來說永遠是可訪問的
struct Pub_Derv:public Base{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Prot_Derv:protected Base{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Priv_Derv:private Base{
//合法
void memfcn(Base &b){ b=*this;}
};
//下面三條判斷的依據是:
// 如果`D`繼承`B`的方式是公有的或者受保護的,則`D`的派生類的成員和友元可以使用`D`向`B`的型別轉換
struct Derived_from_Public:public Pub_Derv{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Derived_from_Protected:public Prot_Derv{
//合法
void memfcn(Base &b){ b=*this;}
};
struct Derived_from_Private:public Priv_Derv{
//不合法
void memfcn(Base &b){ b=*this;}
};
//下面六條判斷的依據是:
// 只有當`D`公有地繼承`B`時,使用者程式碼才能使用派生類向基類的轉換
int main(){
Pub_Derv d1;
Base* p1=&d1; //正確
Prot_Derv d2;
Base* p2=&d2; //錯誤
Priv_Derv d3;
Base* p3=&d3; //錯誤
Derived_from_Public dd1;
Base* p4=&dd1; //正確
Derived_from_Private dd2;
Base* p5=&dd2; //錯誤
Derived_from_Protected dd3;
Base* p6=&dd3; //錯誤
};
//例子二
class Base { /* ... */};
struct D1:Base { /* ... */}; //預設public繼承
class D2: Base { /* ... */}; //預設private繼承
15.6 繼承中的類作用域
- 如果派生類(即內層作用域)的成員與基類(即外層作用域)的某個成員同名,則派生類將在其作用域內隱藏此基類成員。即使派生類成員和基類成員的形參列表不一致,基類成員也仍然會被隱藏掉(只要重名就被隱藏)
- (*重要)在c++的繼承體系中函式呼叫的解析過程如下所示(以
p->mem()
為例)(見例子一):- 首先確定
p
的靜態型別 - 在
p
的靜態型別對應的類中查詢mem
。如果找不到,則依次在直接基類中不斷查詢直至到達繼承鏈的頂端。如果找遍了該類及其基類仍然找不到,則編譯器將報錯。 - 一旦找到了
mem
,就進行常規的型別檢測以確認對於當前找到mem
,本次呼叫是否合法 - 假設呼叫合法,編譯器將根據呼叫的是否是虛擬函式而產生不同的程式碼:
- 如果
mem
是虛擬函式且我們是通過引用或指標進行的呼叫,則編譯器產生的程式碼在執行時確定到底執行該虛擬函式的哪個版本依據是物件的動態型別 - 反之,如果
mem
不是虛擬函式或者我們是通過物件(而非引用或指標)進行的呼叫,則編譯器將產生一個常規函式呼叫
- 如果
- 首先確定
//例子一:繼承體系中的函式解析
class Base{
public:
virtual int fcn();
};
class D1:public Base{
public:
//隱藏基類的fcn,這個fcn不是虛擬函式
//D1繼承了Base::fcn()的定義
int fcn(int); //形參列表與Base中的fcn不一致
virtual void f2(); //是一個新的虛擬函式,在Base中不存在
};
class D2:public D1{
public:
int fcn(int); //是一個非虛擬函式,隱藏了D1::fcn(int)
int fcn(); //覆蓋了Base的虛擬函式fcn!!!
void f2(); //覆蓋了D1的虛擬函式f2
};
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1=&bobj,*bp2=&d1obj,*bp3=&d2obj;
bp1->fcn(); //虛呼叫,將在執行時呼叫Base::fcn
bp2->fcn(); //虛呼叫,將在執行時呼叫D1::fcn()
bp3->fcn(); //虛呼叫,將在執行時呼叫D2::fcn()
D1 *d1p=&d1obj; D2 *d2p=&d2obj;
bp2->f2(); //錯誤:Base沒有名為f2的成員
d1p->f2(); //虛呼叫,將在執行時呼叫D1::f2()
d2p->f2(); //虛呼叫,將在執行時呼叫D2::f2()
15.7 建構函式與拷貝控制
- 派生類刪除的拷貝控制與基類的關係:基類中某個成員(預設建構函式,拷貝建構函式,拷貝賦值運算子,解構函式,移動操作)被刪除,則派生類中對應的成員也將是被刪除的。原因是編譯器不能使用基類對應的成員來執行派生類中基類部分的構造、賦值、銷燬或移動等操作。
- 派生類的拷貝和移動建構函式在拷貝和移動自有成員的同時,也要拷貝和移動基類部分的成員。
- 如果我們想拷貝(或移動)基類部分,則必須在派生類的建構函式初始值列表中顯示地使用基類的拷貝(或移動)建構函式(見例子一)
//例子一
class Base { /* ... */};
class D:public Base{
public:
D(const D& d):Base(d) //拷貝基類成員
/* D的成員的初始值 */ { /* ... */ }
D(D&& d):Base(std::move(d) //移動基類成員
/* D的成員的初始值 */ { /* ... */ }
};
15.8 容器與繼承
- 當我們希望在容器中存放具有繼承關係的物件時,應當在容器中放置(智慧)指標而非物件(見例子一)
//例子一
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-84470-1",50));
//Bulk_quote是Quote的子類
basket.push_back(make_shared<Bulk_quote>("0-201-54848",50,10,.25));
//實際呼叫net_price版本指標所指物件的動態型別即Bulk_quote
cout<<basket.back()->net_price(15)<<endl;
15.9 文字查詢程式再探
(本節主要是程式設計實踐)