文章目錄
- 一、 繼承基本概念
- 二、派生類對象及派生類向基類的類型轉換
- 三、繼承中的公有、私有和受保護的訪問控制規則
- 四、派生類的作用域
- 五、繼承中的靜態成員
一、 繼承基本概念
通過繼承(inheritance)聯系在一起的類構成一種層次關系。通常在層次關系的根部都有一個基類(base class),其他類則直接或間接地從基類繼承而來,這些繼承得到的類稱為派生類(derived class)。基類負責定義在層次關系中所有類所共同擁有的成員,而每個派生類定義自己特有的成員。
這個層次結構是如何體現的呢?繼承作為面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,也就是派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。上一篇文章中【 C++私房菜】模板的入門與進階-CSDN博客的都是函數復用,繼承是類設計層次的復用。
繼承的定義格式如下:
class 派生類名 : 繼承方式 基類1,繼承方式 基類2{//...};
從上述格式可以看出C++是支持多繼承的。派生類必須通過使用**類派生列表(class derivation list)**明確指出它是從哪個(哪些)基類繼承而來的。類派生列表的形式是:首先是一個冒號,后面緊跟以逗號分隔的基類列表,其中每個基類前面可以有以下三種訪問說明符中的一個:public、protected或者private。
此處我們定義一個基類和派生類來做說明,我們可以看到Person是基類。Student是派生類:
class Person{//... };class Student:public Person{//...};
派生類必須將其繼承而來的成員函數中需要覆蓋的那些重新聲明。我們觀察下文代碼:
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;protected:double price = 0;private:string bookNo;};class Bulk_quote :public Quote {public:Bulk_quote() = default;Bulk_quote(const string&, double, size_t, double);double net_price(size_t) const override;private:size_t min_qty = 0;double discount = 0;};
上述代碼完成了哪些工作呢?Bulk_quote對象具有以下特征:
派生類對象存儲了積累的數據成員(派生類繼承了基類的實現)。
派生類對象可以使用基類的方法(派生類繼承了基類的接口)。
因此Bulk_quote 類中必須包含一個 net_price 成員。Bulk_quote 類從它的基類繼承了 isBn 函數和 bookNo、 price 等數據成員,還定義了新的版本,同時用于兩個新增加的數據成員 min_qty 和 discount。這兩個成員分別用于說明享受折扣所需購買的最低數量以及一旦該數量達到后具體的折扣信息。
需要在繼承特性中添加什么呢?
派生類需要自己的構造函數。
派生類可以根據需要添加額外的數據成員和成員函數。
上文中只繼承自一個類的這種繼承被稱為“單繼承“。
現在需要記住的是作為繼承關系中的根節點的類通常都會定義一個虛析構函數,即使該函數不執行任何實際操作。
本文我們暫時忽略 virtual 關鍵字,我將在后續的文章中對此進行敘述。
當然我們也可以防止繼承的發生,有時我們可能會定義一些類且不希望其他類繼承它,或者不想考慮它是否適合作為一個基類。
為了這一目的,C++11提供了一種防止繼承發生的方式,即在類名后跟一個關鍵字 final。 如class NoDerived final{ //... };
。或者我們也可以將父類構造函數私有化,派生類實例化不出對象,也就不能被繼承。被final修飾的類我們通常稱為最終類。
但是如果我們在派生類定義了一個函數與基類虛函數名字相同但形參列表不同的函數,這仍是合法的行為。編譯器將認為這個新定義的函數與基類中的是相互獨立的。這時派生類的函數并沒有覆蓋掉基類中的版本。就實際的編程習慣而言,這種聲明往往意味著發生了錯誤,因為我們可能原本希望派生類能覆蓋掉基類中的虛函數,但是一不小心把形參列表弄錯了。 要想調試并發現這樣的錯誤顯然非常困難。在C++11新標準中我們可以使用 override 關鍵字來說明派生類中的虛函數。這么做的好處是在使得程序員的意圖更加清晰的同時讓編譯器可以為我們發現一些錯誤,后者在編程實踐中顯得更加重要。如果我們使用override標記了某個函數,但該函數并沒有覆蓋已存在的虛函數,此時編譯器將報錯,下面我們舉幾個例子:
class Base {public:virtual void f1(int)const;virtual void f2();void f3();};class Derived1 :public Base{void f1(int)const override; //正確: f1與基類中的 f1 匹配void f2(int) override; //錯誤: Base沒有形如f2(int)的函數void f3()override; //錯誤: f3不是虛函數void f4()override; //錯誤: Base沒有名為f4的函數};
因為只有虛函數才會被覆蓋,所以編譯器會認為 Derived 中的 f3是錯誤的。相同的 f4 的聲明也是錯誤的,Base中沒有為 f4的虛函數。我們在此處將 override與final 一起討論:
class Derived2 :public Derived1 {void f1(int)const final; //不允許后續的其他函數覆蓋 f1(int)};class Derived3 :public Derived2 {void f2(); //正確: 覆蓋從間接基類Base繼承而來的 fvoid f1(int)const; //錯誤: Derived2已經將f2聲明為final};
??成員變量所有的都會被繼承,無論公有私有。
二、派生類對象及派生類向基類的類型轉換
理解基類和派生類之間的類型轉換是理解C++語言面向對象編程的關鍵所在。
一個派生類對象包含多個組成部分:一個含有派生類自己定義的(非靜態)成員的子對象,以及一個與該派生類繼承的基類對應的子對象,如果有多個基類,那么這樣的子對象也有多個。因此,一個 Bulk_quote對象將包含四個數據元素:它從基類 Quote 繼承而來的 bookNo 和 price 數據成員,以及 Bulk_quote 自己定義的 min_qty 和 discount 成員。
因為在派生類中含有與基類相對于的組成部分,所以我們可以把派生類的對象當成基類對象來使用,而且我們也能將基類的指針或引用綁定到派生類對象的基類部分上。
通常情況下,如果我們想把引用或指針綁定到一個對象上,則引用或指針的類型應與對象的類型一致,或者對象的類型含有一個可接受的 const 類型轉換規則。存在繼承關系的類是一個重要的例外:我們可以將基類的指針或引用綁定到派生類對象上。例如,我們可以用 Quote& 指向一個 Bulk_quote對象,也可以把一個 Bulk_quote 對象的地址賦給一個Quote*。
Quote item;Bulk_quote bulk;// 子類對象可以賦值給父類對象/指針/引用Quote *p = &item;p = &bulk;Quote &r = bulk;Quote obj=bulk;
上述代碼均是合法的,我們通常把這種轉換稱為派生類到基類的類型轉換。和其他類型轉換一樣,編譯器會隱式地執行此種轉換。
這種隱式特性意味著我們可以把派生類對象或者派生類對象的引用用在需要基類引用的地方;同樣的,我們也可以把派生類對象的指針用在需要基類指針的地方。
??注意:
派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片 或者切割。寓意把派生類中父類那部分切來賦值過去。
基類對象不能賦值給派生類對象。
在派生類對象中含有與其基類對應的組成部分,這一事實是繼承的關鍵所在。
但是在對象之間并不存在類型轉換,基類向派生類的隱式類型轉換也不存在。為什么呢?
派生類向基類的自動類型轉換只對指針或引用類型有效。在派生類類型和基類類型之間不存在這樣的轉換。很多時候,我們確實希望派生類對象轉換成它的基類類型,但是這種轉換的實際發生過程往往與我們期望的有所差別。
請注意,當我們初始化或賦值一個類類型的對象時,實際上是在調用某個函數。當執行初始化時,我們調用構造函數;而當執行賦值操作時,我們調用賦值運算符。這些成員通常都包含一個參數,該參數的類型是類類型的 const 版本的引用。 因為這些成員接受引用作為參數,所以派生類向基類的轉換允許我們給基類的傳遞一個派生類的對象。這些操作不是虛函數。當我們給基類的構造函數傳遞一個派生類對象時,實際運行的構造函數是基類中定義的那個,顯然該構造函數只能處理基類自己的成員。類似的,如果我們將一個派生類對象賦值給一個基類對象,則實際運行的賦值運算符也是基類中定義的那個,該運算符同樣只能處理基類自己的成員。
Bulk_quote bulk; //派生類對象Quote item(bulk); //使用Quote::Quote(const Quote&)構造函數item =bulk; //調用Quote::operator=(const Quote&)
當構造 item 時,運行 Quote 的拷貝構造函數。該函數只能處理 bookNo 和 price 兩個成員,它負責拷貝 bulk 中Quote部分的成員,同時忽略掉 bulk 中 Bulk_quote 部分的成員。類似的,對于將bulk賦值給item的操作來說,只有bulk中Quote部分的成員被賦值給 item。因為在上述過程中會忽略 Bulk_quote 部分,所以我們可以說 bulk 的 Bulk_quote 部分被切割掉了,這就是派生類向基類賦值的過程。
之所以存在派生類向基類的類型轉換是因為每個派生類對象都包含一個基類部分,而基類的引用或指針可以綁定到該基類部分上。一個基類的對象既可以以獨立的形式存在,也可以作為派生類對象的一部分存在。如果基類對象不是派生類對象的一部分,則它只含有基類定義的成員,而不含有派生類定義的成員。因為一個基類的對象可能是派生類對象的一部分,也可能不是,所以不存在從基類向派生類的自動類型轉換。
Quote base;Bulk_quote* bulkP=&base; //錯誤,不能將基類轉換成派生類Bulk_quote& bulkRef= base; //錯誤,不能將基類轉換成派生類Bulk_quote bulk;Quote *itemP =&bulk; //正確,動態類型是 Bulk_quoteBulk_quote *bulkP=itemP; //錯誤,不能將基類轉換成派生類
如若此方式合法,則我們可能會使用到 bulkP 或 bulkRef 訪問 base 中不存在的成員。
當我們使用存在繼承關系的類型時,必須將一個變量或其他表達式的**靜態類型(static type)與該表達式表示對象的動態類型(dynamic type)**區分開來。表達式的靜態類型是編譯時總是已知的,它是變量聲明時的類型或表達式生成的類型:動態類型則是變量或表達式表示的內存中的對象的類型。動態類型直到運行時才可知。此部分我們將在后文多態中詳細介紹。
三、繼承中的公有、私有和受保護的訪問控制規則
每個類分別控制自己的成員初始化過程,與之類似,每個類還分別控制著其成員對于派生類來說是否是可訪問的。繼承方式有三種分別為 public繼承、protected繼承和private繼承。訪問限定符同樣也是三種: public訪問、protected訪問和private訪問。
protected成員:如前所述,一個類使用protected關鍵字來聲明那些它希望與派生類分享但是不想被其他公共訪問使用的成員。protected說明符可以看做是public和private中和后的產物。
和私有成員類似,受保護的成員對于類的用戶來說是不可訪問的。
和公有成員類似,受保護的成員對于派生類的成員和友元來說是可訪問的。
派生類的成員或友元只能通過派生類對象來訪問基類的受保護成員。派生類對于一個基類對象中的受保護成員沒有任何訪問特權。
我們舉個例子來說明:
class Base {protected:int prot_mem;};class Derived :public Base {friend void clobber(Derived&);friend void clobber(Base&);private:int j;};//錯誤: clobber不能訪問Base的對象的private和protected成員void clobber(Base& b) { b.prot_mem = 0; } //正確: clobber可以訪問Derived的對象的private和protected成員void clobber(Derived& s) { s.j = s.prot_mem = 0; }
某個類對其繼承而來的成員的訪問權限受到兩個因素影響:一是在基類中該成員的訪問說明符,二是在派生類的派生列表中的訪問說明符。舉個例子,考慮如下的繼承關系:
class Base {public:void pub_mem(); //public成員protected:int prot_mem; //protected成員private:char priv_mem; //private成員};struct Pub_Derv :public Base {int f() { return prot_mem; } //正確:派生類能訪問protected成員char g() { return priv_mem; } //錯誤:private成員對于派生類來說是不可訪問的};struct Priv_Derv :private Base {int fl()const { return prot_mem; } //private 不影響派生類的訪問權限};
派生訪問說明符對于派生類的成員(及友元)能否訪問其直接基類的成員沒什么影響。對基類成員的訪問權限只與基類中的訪問說明符有關。PubDerv和PrivDerv都能訪問受保護的成員protmem,同時它們都不能訪問私有成員privmem。 派生訪問說明符的目的是控制派生類用戶(包括派生類的派生類在內)對于基類成員的訪問權限:
Pub_Derv dl; //繼承自Base的成員是public的Priv_Derv d2; //繼承自Base的成員是private的d1.pub_mem(); //正確:pub mem在派生類中是public的d2.pub_mem(); //錯誤:pub mem在派生類中是private的
Pub_Derv和Priv_Derv都繼承了pub_mem函數。如果繼承是公有的,則成員將遵循其原有的訪問說明符,此時d1可以調用pub_mem。在Priv_Derv中,Base 的成員是私有的,因此類的用戶不能調用pub_mem。 上述內容總結成如下內容:
特征類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
public成員變量 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
protected成員變量 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
private成員變量 | 在派生類中不可見,只能通過基類接口訪問 | 在派生類中不可見,只能通過基類接口訪問 | 在派生類中不可見,只能通過基類接口訪問 |
能否隱式向上轉換 | 是 | 是(但只能在派生類中) | 否 |
從上述表格我們可以觀察到:
基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私 有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面 都不能去訪問它。
實際上面的表格我們進行一下總結會發現,基類的私有成員在子類都是不可見。基類的其他成員在子類的訪問方式 = Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public。
派生類向基類的轉換是否可訪問由使用該轉換的代碼決定,同時派生類的訪問說明符也會由影響。假定 D 繼承自 B:
只有當 D 公有地繼承 B 時,用戶代碼才能使用派生類向基類的轉換;如果 D 繼承 B 的方式是受保護的或者私有的,則用戶代碼不能使用該轉換。
class A {public:virtual void print() { cout << "我是A" << endl; }};class B :public A{public:void print() { cout << "我是B 繼承A" << endl; }};class C : private A{ //此處換為protected同理public: void print() { cout << "我是C 繼承A" << endl; }};int main(){A* p; B b; C c;p = &b;//p = &c; // 錯誤:無法將'C'轉換為其私有基類'A'。p->print();}
不論 D 以什么方式繼承 B ,D 的成員函數和友元都能使用派生類向基類的轉換;派生類向其直接基類的類型轉換對于派生類的成員和友元來說永遠是可訪問的。
class B {};class D :public B {void function(D& d) { B b = d; }friend void friendFunction(D& d) { B b = d; }};class E :protected B {void function(E& e) { B b = e; }friend void friendFunction(E& e) { B b = e; }};class F :private B {void function(F& f) { B b = f; }friend void friendFunction(F& f) { B b = f; }};
如果 D 繼承 B 的方式是公有的或者受保護的,則 D 的派生類的成員和友元可以使用 D 向 B 的類型轉換;反之,如果 D 繼承 B 的方式是私有的,則不能使用。
class B {};class D :public B {void function(D& d) { B b = d; }friend void friendFunction(D& d) { B b = d; }};class E :protected B {void function(E& e) { B b = e; }friend void friendFunction(E& e) { B b = e; }};class F :private B {void function(F& f) { B b = f; }friend void friendFunction(F& f) { B b = f; }};class G :private D {void function(D& d) { B b = d; }};class H :private E {void function(E& e) { B b = e; }};class I :private F {void function(F& d) {B b = f; //錯誤: B 是 B 的私有成員}friend void friendFunction2(F& f){B b = f; //錯誤: B 是 B 的私有成員}};
對于代碼中的某個給定節點來說,如果基類的公有成員是可訪問的,則派生類向基類的類型轉換也是可訪問的;反之則不行。
??友元關系不能繼承,基類的友元在訪問派生類成員時不具有特殊性,類似的,派生類的友元也不能隨意訪問基類的成員。即友元關系只對作出聲明的類有效,每個類負責控制各自成員的訪問權限。基類友元不能訪問子類私有和保護成員。
當然有時候我們可以改變派生類繼承的某個名字的訪問級別,通過使用using 聲明可以達到此目的。
class Base{public:size_t size()const { return n; }protected:size_t n = 3;private:int s = 2;};class Derived :private Base {public:using Base::size;protected:using Base::n;private:using Base::s;//錯誤: 派生類只能為那些它可以訪問的名字提供 using聲明。};
因為 Derived 使用了私有繼承,所以繼承而來的成員 size 和 n (在默認情況下)是Derived 的私有成員。然而,我們使用 using 聲明語句改變了這些成員的可訪問性。改變之后,Derived的用戶將可以使用 size 成員,而 Derived 的派生類將能使用 n。 通過在類的內部使用 using 聲明語句,我們可以將該類的直接或間接基類中的任何可訪問成員(例如,非私有成員)標記出來。using 聲明語句中名字的訪問權限由該using聲明語句之前的訪問說明符來決定。也就是說,如果一條 using 聲明語句出現在類的private部分,則該名字只能被類的成員和友元訪問;如果 using 聲明語句位于 public部分,則類的所有用戶都能訪問它;如果using聲明語句位于protected部分,則該名字對于成員、友元和派生類是可訪問的。
四、派生類的作用域
每個類定義自己的作用域,在這個作用域內我們定義類的成員。當存在繼承關系時,派生類的作用域嵌套在其基類的作用域之內。如果一個名字在派生類的作用域內無法正確解析,則編譯器將繼續在外層的基類作用域中尋找該名字的定義。 派生類的作用域位于基類作用域之內這一事實可能有點兒出人意料,畢竟在我們的程序文本中派生類和基類的定義是相互分離開來的。不過也恰恰因為類作用域有這種繼承嵌套的關系,所以派生類才能像使用自己的成員一樣使用基類的成員。
Bulk_quote bulk;cout<<bulk.isBn();
下面我們來敘述 isBn() 的解析過程:
因為我們是通過 Bulk_quote的對象調用isbn的,所以首先在 Bulk_quote中查找,這一步沒有找到名字isbn。 因為Disc_quote是Quote的派生類,所以接著查找Quote。此時找到了名字isBn,所以我們使用的isBn 最終被解析為 Quote中的 isBn。
通常在編譯時進行名字查找,一個對象、引用或只在的靜態類型決定了該對象的哪些成員是可見的。即使動態類型與靜態類型不匹配。但是我們仍能使用哪些成員仍然是靜態類型決定的。
當名字沖突時,和其他作用域相同,派生類也能重新定義在其之間基類或間接基類的成員變量和成員函數,此時定義在內存作用域的名字將**隱藏(也稱為重定義)**定義在外層作用域的名字。
🔲派生類的成員將隱藏同名的基類成員。當然我們仍然可以通過域運算符來使用一個被隱藏的基類成員。(使用 基類::基類成員 顯示訪問)
class Base{public:int func();};class Derived :public Base {public:int func(int); //隱藏基類的 int func();};int main() {Derived d;Base b;b.func(); //正確: 調用Base::func()d.func(1); //正確: 調用Derived::func(int)d.func(); //錯誤: 參數列表為空的func被隱藏了d.Base::func(); //正確: 調用Base::func()}
Derived 中的 func 聲明隱藏了 Base 中的 func 聲明。在上面的代碼中前兩條調用語句容易理解,第一個通過 Base對象 b 進行的調用執行基類的版本;類似的,第二個通過 d 進行的調用執行 Derived的版本;第三條調用語句有點特殊,d.func()是非法的。 為了解析這條調用語句,編譯器首先在 Derived 中查找名字func 。因為 Derived確實定義了一個名為func 的成員,所以查找過程終止。一旦名字找到,編譯器就不再繼續查找了。Derived 中的func 版本需要一個int實參,而當前的調用語句無法提供任何實參,所以該調用語句是錯誤的。
五、繼承中的靜態成員
如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。不管從基類派生出來多少個派生類,對于每個靜態成員來說都只存在唯一的實例。
class Base{public:static void statmen();};class Derived:public Base{void f(const Derived&); };
靜態成員遵循通用的訪問控制規則,如果基類的成員是 private 的,則派生類無權訪問它。假設某靜態成員是可訪問的,則我們既能通過基類使用它也能通過派生類使用它:
void Derived::f(const Derived& dd){Base::statmen(); //正確,Base定義了statmenDerived::statmen(); //正確,Derived繼承了statmendd.statmen(); //正確,通過Derived對象訪問statmen(); //正確,通過this對象訪問//派生類的對象可以訪問基類的靜態成員。}
靜態成員屬于整個類,不屬于任何對象,所以在整體體系中只有一份。