一、基礎知識
- 繼承,表示has-a(是一種),即派生類也是一種基類。
- 繼承關系下,派生類繼承了基類的成員變量,繼承了基類成員函數的調用權。
- 派生類必須通過使用類派生清單明确指出它是從哪個(哪些)基類繼承而來的。
- 類派生清單的形式是:首先是一冒号,後面緊跟以逗号分隔的基類清單,其中每個基類前面有通路說明符。
二、定義基類和派生類
(一)定義基類
- 基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
(1)成員函數與繼承
- 對于成員函數,如果基類不希望派生類自定義,則聲明為non-virtual函數,即普通函數;如果基類希望派生類自定義,且派生類有預設定義,則聲明為virtual函數;如果基類要求派生類自定義,則聲明為純虛函數。
(2)通路控制與繼承
- 派生類能通路公有成員,不能通路私有成員。
- 如果基類希望它的派生類有權通路某成員,同時禁止其他使用者通路,可以将通路權限設定為protected。
(二)定義派生類
(1)派生類中的虛函數
- 派生類可以在它覆寫的函數前使用virtual關鍵字,但不是非得這樣做。
- C++11新标準允許向派生類顯式地注明它使用某個成員函數覆寫了它繼承的虛函數。具體做法是在形參清單後面、或者在const成員函數的const關鍵字的後面、或者在引用成員函數的引用限定符後面添加一個關鍵字override。
(2)派生類對象及派生類向基類的類型轉換
- 派生類也是一種基類,派生類繼承了基類的成員變量和成員函數的調用權。
- 因為在派生類對象中含有與其基類對應的組成部分,是以我們能把派生類的對象當作基類來使用,而且能将基類的指針或引用綁定到派生類對象中的基類部分上。這種轉換通常稱為派生類到基類的類型轉換,編譯器會隐式地執行這種轉換。
(3)派生類構造函數
- 派生類必須使用基類的構造函數來初始化它的基類部分。
- 除非我們特别指出,否則派生類對象的基類部分會像資料成員一樣執行預設初始化。
- 先初始化基類的部分,然後按照聲明的順序依次初始化派生類的成員。
(4)派生類使用基類的成員
- 派生類可以通路基類的公有成員和受保護成員。
(5)繼承與靜态成員
- 如果基類定義了一靜态成員,則在整個繼承體系中隻存在該成員的唯一定義。
(6)派生類的聲明
- 派生類的聲明與其他類差别不大,聲明中包含類名但是不包含它的派生清單。
class Base{…};
class Derived :public Base; //錯誤:派生清單不能出現在這裡
class Derived; //正确
(7)被用作基類的類
- 如果我們将某個類用作基類,則該類必須已經定義而非僅僅聲明。
- 因為派生類中包含并且可以使用它從基類繼承而來的成員,為了使用這些成員,派生類要知道它們時什麼。
(8)防止繼承的發生
- C++11新标準提供了一種防止繼承發生的方法——在類名後跟一個關鍵字final。
(三)類型轉換與繼承
- 我們可以将基類的指針或引用綁定到派生類的對象上。
- 當使用基類的引用(或指針)時,實際上我們并不清楚該引用(或指針)所綁定對象的真實類型。
(1)靜态類型和動态類型
- 表達式的靜态類型在編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型。
- 動态類型則是變量或表達式表示的記憶體中的對象的類型,動态類型直到運作時才可知。
(2)類型轉換
- 不存在基類向派生類的類型轉換。
- 在對象之間不存在類型轉換。派生類向基類的自動類型轉換隻對指針或引用有效,在派生類類型和基類類型之間不存在這樣的轉換。
- 當用派生類對象初始化基類對象時,實際上用的是派生類對象中的基類部分去初始化基類對象。
三、虛函數
- 當我們使用基類的引用或指針調用一個虛成員函數時會執行動态綁定。
- 因為我們直到運作時才能知道到底調用了哪個版本的虛函數,是以虛函數必須都有定義。
(一)對虛函數的調用可能在運作時才被解析
- 當我們通過一個具有普通類型(非引用非指針)的表達式調用虛函數時,在編譯時就會将調用的版本确定下來。
- 當用引用或者指針類型調用虛函數且是up-cast時,在程式運作時才能确定下來。
(二)派生類中的虛函數
- 一個派生類的函數如果覆寫了某個繼承而來的虛函數,則它的形參類型必須與被它覆寫的基類函數完全一緻。
- 派生類中虛函數的傳回類型也必須與基類函數比對。但當類的虛函數傳回類型是類本身的指針或引用時,上述規則無效。
(三)final和override說明符
- 如果我們使用override标記了某個函數,但該函數并沒有覆寫已存在的虛函數,此時編譯器會報錯。
- 如果我們把某個函數指定為final,則之後任何嘗試覆寫該函數的操作都将引發錯誤。
- final和override說明符出現在形參清單(包括任何const或引用修飾符)以及尾置傳回類型之後。
(四)虛函數與預設實參
- 虛函數也可以擁有預設實參,如果某次函數調用使用了預設實參,則該實參值由本次調用的靜态類型決定。
- 如果虛函數使用預設實參,則基類和派生類中定義的預設實參最好一緻。
(五)回避虛函數的機制
- 在某些情況下,我們希望對虛函數的調用不要進行動态綁定,而是強迫其執行虛函數的某個特定版本,使用作用域運算符可以實作這一目的。
四、抽象基類
- 在虛函數聲明語句的分号之前書寫=0就可以将一個虛函數聲明為純虛函數,且=0隻能出現在類内部的虛函數聲明語句處。
- 我們可以為純虛函數提供定義,不過函數體必須定義在類的外部。
(一)含有純虛函數的類是抽象基類
- 抽象基類負責定義接口,後續的其他類可以覆寫該接口。
- 不能直接建立抽象基類的對象。
(二)派生類構造函數隻初始化它的直接基類
五、通路控制與繼承
(一)受保護的成員
- 受保護的成員對于類的使用者來說是不可通路的。
- 受保護的對于派生類的成員和友元是可以通路的。
- 派生類的成員或友元隻能通過派生類對象來通路基類的受保護成員。
(二)派生類向基類轉換的可通路性
假定D繼承自B:
- 隻有當D公有地繼承B時,使用者代碼才能使用派生類向基類的轉換;如果D繼承B的方式是受保護的或者私有的,則使用者代碼不能使用該轉換。
#include<iostream>
using namespace std;
class A {
public:
virtual void print() { cout << "I'm A"; }
};
class B : public A {
public:
void print() override { cout << "I'm B"; }
};
class C : private A {
public:
void print() override { cout << "I'm C"; }
};
int main() {
A *p1, *p2;
B b;
C c;
p1 = &b;
p2 = &c; //Cannot cast 'C' to its private base class 'A'
return 0;
}
- 不論D以什麼方式繼承B,D的成員函數和友元都能使用派生類向基類的轉換;派生類向其基類的類型轉換對于派生類的成員和友元來說永遠是可通路的。
#include<iostream>
using namespace std;
class A {
public:
void Print() { print(); }
virtual void print() { cout << "I'm A"; }
};
class B : public A {
public:
void print() override { cout << "I'm B"; }
};
class C : private A {
public:
void Print_c() { Print(); }
void print() override { cout << "I'm C"; }
};
int main() {
C c;
c.Print_c(); //C會調用A中的Print();
//Print實作up-cast,Print會調用print虛函數;
//進而實作動态綁定
return 0;
}
- 如果D繼承B的方式是公有的或者受保護的,則D的派生類的成員和友元可以使用D向B的類型轉換;反之,如果D繼承B的方式是私有的,則不能使用。
(三)友元與繼承
- 友元關系不能繼承,基類的友元在通路派生類成員時不具有特殊性。
- 類似的,派生類的友元也不能随意通路基類的成員。
(四)改變個别成員的可通路性
- 可以通過using聲明改變派生類繼承的某個名字通路級别。
#include<iostream>
using namespace std;
class Base {
public:
size_t size() const { return n; }
protected:
size_t n;
};
class Derived : private Base {
public:
using Base::size;
protected:
using Base::n;
};
int main() {
system("pause");
return 0;
}
- using聲明語句中名字的通路權限由該using聲明語句之前的通路說明符決定。
- 派生類隻能為那些它能通路的名字提供using聲明。
(五)預設的繼承保護級别
- 預設情況下,使用class關鍵字定義的派生類是私有繼承的;而使用struct關鍵字定義的派生類是公有繼承的。
class Base {};
struct D1:Base{}; //預設public繼承
class D2:Base{}; //預設private繼承
六、繼承中的類作用域
- 每個類定義自己的作用域,在這個作用域内我們定義類的成員。
- 當存在繼承關系時,派生類的作用域嵌套在其基類的作用域之内。
- 如果一個名字在派生類的作用域内無法正确解析,則編譯器将繼續在基類的作用域内繼續尋找。
(一)名字沖突與繼承
- 派生類能重用定義在其直接基類或間接基類中名字,此時定義在内層作用域(即派生類)的名字将隐藏定義在其外層作用域(即基類)的名字。
- 我們可以通過作用域運算符來使用一個被隐藏的基類成員。
- 聲明在内層作用域的函數并不會重載聲明在外層作用域的函數,是以,定義派生類中的函數也不會重載其基類中的成員,即使派生類成員和基類成員的形參清單不一緻,基類成員仍然會被隐藏的。