1. 程式人生 > >C++之繼承與派生、多繼承、C++向上轉型

C++之繼承與派生、多繼承、C++向上轉型

1.引數的傳遞本質上是一次賦值的過程,賦值就是對記憶體進行拷貝。所謂記憶體拷貝,是指將一塊記憶體上的資料複製到另一塊記憶體上。

  對於像 charboolintfloat等基本型別的資料,它們佔用的記憶體往往只有幾個位元組,對它們進行記憶體拷貝非常快速。而陣列、結構體、物件是一系列資料的集合,資料的數量沒有限制,可能很少,也可能成千上萬,對它們進行頻繁的記憶體拷貝可能會消耗很多時間,拖慢程式的執行效率。C/C++禁止在函式呼叫時直接傳遞陣列的內容,而是強制傳遞陣列指標,而對於結構體和物件沒有這種限制,呼叫函式時既可以傳遞指標,也可以直接傳遞內容;

但是在 C++ 中,我們有了一種比指標更加便捷的傳遞聚合型別資料的方式,那就是引用(

Reference)。

2.  引用(Reference)是C++ 相對於C語言的又一個擴充。引用可以看做是資料的一個別名,通過這個別名和原來的名字都能夠找到這份資料。引用類似於Windows 中的快捷方式,一個可執行程式可以有多個快捷方式,通過這些快捷方式和可執行程式本身都能夠執行程式;引用還類似於人的綽號(筆名),使用綽號(筆名)和本名都能表示一個人。

    引用的定義方式類似於指標,只是用& 取代了* ,語法格式為:

    type &name = data;

    type 是被引用的資料的型別

    name 是引用的名稱

    data 是被引用的資料。

引用必須在定義的同時初始化,並且以後也要從一而終,不能再引用其它資料,這有點類似於常量(const變數)。

3. 本例中,變數 b 就是變數 a 的引用,它們用來指代同一份資料;也可以說變數b 是變數a 的另一個名字。從輸出結果可以看出,ab 的地址一樣,或者說這塊地址的記憶體有兩個名字,ab,想要訪問該記憶體上的資料時,使用哪個名字都行。

注意,引用在定義時需要新增&,在使用時不能新增& ,使用時新增& 表示取地址。如上面程式碼所示,的&表示引用,第 8行中的&表示取地址。除了這兩種用法, &還可以表示位運算中的與運算。

4. 如果不希望通過引用來修改原始的資料,那麼可以在定義時新增

const限制,形式為:

const type &name = value;

    也可以是:

type const &name = value;

這種引用方式為常引用

5. 在定義或宣告函式時,我們可以將函式的形參指定為引用的形式,這樣在呼叫函式時就會將實參和形參繫結在一起,讓它們都指代同一份資料。

如此一來,如果在函式體中修改了形參的資料,那麼實參的資料也會被修改,從而擁有“在函式內部影響函式外部資料”的效果。

一個能夠展現按引用傳參的優勢的例子就是交換兩個數的值

6.swap1() 直接傳遞引數的內容,不能達到交換兩個數的值的目的。對於 swap1()來說,ab是形參,是作用範圍僅限於函式內部的區域性變數,它們有自己獨立的記憶體,和 num1num2指代的資料不一樣。呼叫函式時分別將num1num2的值傳遞給 ab,此後num1num2ab再無任何關係,在 swap1()內部修改 ab的值不會影響函式外部的 num1num2,更不會改變num1num2的值。

swap2() 傳遞的是指標,能夠達到交換兩個數的值的目的。呼叫函式時,分別將 num1num2的指標傳遞給 p1p2,此後p1p2指向 ab所代表的資料,在函式內部可以通過指標間接地修改 ab的值。

swap3() 是按引用傳遞,能夠達到交換兩個數的值的目的。呼叫函式時,分別將 ab繫結到 num1num2所指代的資料,此後 a num1bnum2就都代表同一份資料了,通過 a修改資料後會影響 num1,通過b 修改資料後也會影響num2

以上程式碼的編寫中可以發現,按引用傳參在使用形式上比指標更加直觀。在以後的 C++ 程式設計中,鼓勵大家大量使用引用,它一般可以代替指標(當然指標在C++中也不可或缺),C++標準庫也是這樣做的。

8.引用作為函式返回值

  在將引用作為函式返回值時應該注意一個小問題,就是不能返回區域性資料(例如區域性變數、區域性物件、區域性陣列等)的引用,因為當函式呼叫完成後區域性資料就會被銷燬,有可能在下次使用時資料就不存在了,C++編譯器檢測到該行為時也會給出警告。

9.繼承是類與類之間的關係,是一個很簡單很直觀的概念,與現實世界中的繼承類似。

繼承(Inheritance)可以理解為一個類從另一個類獲取成員變數和成員函式的過程。例如類B 繼承於類A,那麼B 就擁有A 的成員變數和成員函式。被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。派生類除了擁有基類的成員,還可以定義自己的新成員,以增強類的功能

10.以下是兩種典型的使用繼承的場景:

1) 當你建立的新類與現有的類相似,只是多出若干成員變數或成員函式時,可以使用繼承,這樣不但會減少程式碼量,而且新類會擁有基類的所有功能。

2) 當你需要建立多個類,它們擁有很多相似的成員變數或成員函式時,也可以使用繼承。可以將這些類的共同成員提取出來,定義為基類,然後從基類繼承,既可以節省程式碼,也方便後續修改成員。

11. 繼承的一般語法為:

class 派生類名:[繼承方式] 基類名{

     派生類新增加的成員

};

例項

class Student: public People{

}

繼承方式包括 public(公有的)、private(私有的)和protected(受保護的),此項是可選的,如果不寫,那麼預設為private

1) public繼承方式

基類中所有 public 成員在派生類中為 public 屬性;

基類中所有 protected 成員在派生類中為 protected 屬性;

基類中所有 private 成員在派生類中不能使用。

2) protected繼承方式

基類中的所有 public 成員在派生類中為 protected 屬性;

基類中的所有 protected 成員在派生類中為 protected 屬性;

基類中的所有 private 成員在派生類中不能使用。

3) private繼承方式

基類中的所有 public 成員在派生類中均為 private 屬性;

    基類中的所有 protected 成員在派生類中均為 private 屬性;

基類中的所有 private 成員在派生類中不能使用。

繼承方式/基類成員

public成員

protected成員

private成員

public繼承

public

protected

不可見

protected繼承

protected

protected

不可見

private繼承

private

private

不可見

12.由於 privateprotected繼承方式會改變基類成員在派生類中的訪問許可權,導致繼承關係複雜,所以實際開發中我們一般使用 public

13.1) 基類成員在派生類中的訪問許可權不得高於繼承方式中指定的許可權。例如,當繼承方式為 protected 時,那麼基類成員在派生類中的訪問許可權最高也為protected,高於protected 的會降級為protected,但低於protected 不會升級。再如,當繼承方式為public 時,那麼基類成員在派生類中的訪問許可權將保持不變。

也就是說,繼承方式中的 publicprotectedprivate是用來指明基類成員在派生類中的最高訪問許可權的。

2) 不管繼承方式如何,基類中的 private成員在派生類中始終不能使用(不能在派生類的成員函式中訪問或呼叫)。

3) 如果希望基類的成員能夠被派生類繼承並且毫無障礙地使用,那麼這些成員只能宣告為 publicprotected;只有那些不希望在派生類中使用的成員才宣告為private

4)如果希望基類的成員既不向外暴露(不能通過物件訪問),還能在派生類中使用,那麼只能宣告為 protected

注意,我們這裡說的是基類的 private 成員不能在派生類中使用,並沒有說基類的 private 成員不能被繼承。實際上,基類的private 成員是能夠被繼承的,並且(成員變數)會佔用派生類物件的記憶體,它只是在派生類中不可見,導致無法使用罷了。private成員的這種特性,能夠很好的對派生類隱藏基類的實現,以體現面向物件的封裝性。

14.改變訪問許可權

使用 using 關鍵字可以改變基類成員在派生類中的訪問許可權,例如將public 改為private、將private 改為public

class Student: public People{

public:

    void learning();

public:

    using People::m_name;  //private改為public

    using People::m_age;  //private改為public

    float m_score;

private:

    using People::show;  //public改為private

};

void Student::learning(){

    cout<<"我是"<<m_name<<",今年"<<m_age<<"歲,這次考了"<<m_score<<"分!"<<endl;

}

程式碼中首先定義了基類 People,它包含兩個protected 屬性的成員變數和一個public 屬性的成員函式。定義Student 類時採用public 繼承方式,People類中的成員在 Student類中的訪問許可權預設是不變的。

15.C++繼承時名字的遮蔽

如果派生類中的成員(包括成員變數和成員函式)和基類中的成員重名,那麼就會遮蔽從基類繼承過來的成員。所謂遮蔽,就是在派生類中使用該成員(包括在定義派生類時使用,也包括通過派生類物件訪問該成員)時,實際上使用的是派生類新增的成員,而不是從基類繼承來的。

基類 People 和派生類Student 都定義了成員函式show(),它們的名字一樣,會造成遮蔽。第37 行程式碼中,stuStudent類的物件,預設使用 Student類的 show()函式。

但是,基類 People 中的 show() 函式仍然可以訪問,不過要加上類名和域解析符

基類成員函式和派生類成員函式不構成過載

基類成員和派生類成員的名字一樣時會造成遮蔽,這句話對於成員變數很好理解對於成員函式要引起注意,不管函式的引數如何,只要名字一樣就會造成遮蔽。換句話說,基類成員函式和派生類成員函式不會構成過載,如果派生類有同名函式,那麼就會遮蔽基類中的所有同名函式,不管它們的引數是否一樣。

Base 類的func()func(int)Derived 類的func(char *)func(bool)四個成員函式的名字相同,引數列表不同,它們看似構成了過載,能夠通過物件d 訪問所有的函式,實則不然,Derive類的 func遮蔽了 Base類的 func,導致程式碼沒有匹配的函式,所以呼叫失敗。

     如果說有過載關係,那麼也是 Base 類的兩個 func 構成過載,而Derive 類的兩個func 構成另外的過載。

16.C++派生類的建構函式

基類的成員函式可以被繼承,可以通過派生類的物件訪問,但這僅僅指的是普通的成員函式,類的建構函式不能被繼承。建構函式不能被繼承是有道理的,因為即使繼承了,它的名字和派生類的名字也不一樣,不能成為派生類的建構函式,當然更不能成為普通的成員函式。

在設計派生類時,對繼承過來的成員變數的初始化工作也要由派生類的建構函式完成,但是大部分基類都有 private屬性的成員變數,它們在派生類中無法訪問,更不能使用派生類的建構函式來初始化。

這種矛盾在C++繼承中是普遍存在的,解決這個問題的思路是:在派生類的建構函式中呼叫基類的建構函式。

17.Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

    People(name, age)就是呼叫基類的建構函式,並將 nameage作為實參傳遞給它,m_score(score)是派生類的引數初始化表,它們之間以逗號,隔開。也可以將基類建構函式的呼叫放在引數初始化表後面:Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

    但是不管它們的順序如何,派生類建構函式總是先呼叫基類建構函式再執行其他程式碼(包括引數初始化表以及函式體中的程式碼),總體上看和下面的形式類似:

Student::Student(char *name, int age, float score){

    People(name, age);

    m_score = score;

}

函式頭部是對基類建構函式的呼叫,而不是宣告,所以括號裡的引數是實參,它們不但可以是派生類建構函式引數列表中的引數,還可以是區域性變數、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16),m_score(score){ }

18.建構函式呼叫順序

基類建構函式總是被優先呼叫,這說明建立派生類物件時,會先呼叫基類建構函式,再呼叫派生類建構函式,如果繼承關係有好幾層的話,例如:A --> B --> C

那麼建立 C 類物件時建構函式的執行順序為:

A類建構函式 --> B類建構函式--> C類建構函式

建構函式的呼叫順序是按照繼承的層次自頂向下、從基類再到派生類的。

還有一點要注意,派生類建構函式中只能呼叫直接基類的建構函式,不能呼叫間接基類的。以上面的 ABC類為例,C是最終的派生類,B就是 C的直接基類,A就是 C的間接基類。

C++ 這樣規定是有道理的,因為我們在 C中呼叫了 B的建構函式,B又呼叫了 A的建構函式,相當於 C間接地(或者說隱式地)呼叫了 A的建構函式,如果再在 C中顯式地呼叫 A的建構函式,那麼 A的建構函式就被呼叫了兩次,相應地,初始化工作也做了兩次,這不僅是多餘的,還會浪費CPU時間以及記憶體,毫無益處,所以C++ 禁止在C 中顯式地呼叫A 的建構函式。

事實上,通過派生類建立物件時必須要呼叫基類的建構函式,這是語法規定。換句話說,定義派生類建構函式時最好指明基類建構函式;如果不指明,就呼叫基類的預設建構函式(不帶引數的建構函式);如果沒有預設建構函式,那麼編譯失敗。

建立物件 stu1 時,執行派生類的建構函式Student::Student(),它並沒有指明要呼叫基類的哪一個建構函式,從執行結果可以很明顯地看出來,系統預設呼叫了不帶引數的建構函式,也就是People::People()

    建立物件 stu2 時,執行派生類的建構函式Student::Student(char *name, int age, float score),它指明瞭基類的建構函式。

    如果將People(name, age)去掉,也會呼叫預設建構函式,第37 行的輸出結果將變為:

    xxx的年齡是0,成績是90.5

    如果將基類 People 中不帶引數的建構函式刪除,那麼會發生編譯錯誤,因為建立物件 stu1 時需要呼叫 People類的預設建構函式, 而 People類中已經顯式定義了建構函式,編譯器不會再生成預設的建構函式。

    和建構函式類似,解構函式也不能被繼承。與建構函式不同的是,在派生類的解構函式中不用顯式地呼叫基類的解構函式,因為每個類只有一個解構函式,編譯器知道如何選擇,無需程式設計師干涉。

    另外解構函式的執行順序和建構函式的執行順序也剛好相反:

    建立派生類物件時,建構函式的執行順序和繼承順序相同,即先執行基類建構函式,再執行派生類建構函式

    而銷燬派生類物件時,解構函式的執行順序和繼承順序相反,即先執行派生類解構函式,再執行基類解構函式。

19.C++類的多繼承

  在前面的例子中,派生類都只有一個基類,稱為單繼承(Single Inheritance)。除此之外,C++也支援多繼承(Multiple Inheritance),即一個派生類可以有兩個或多個基類。

    多繼承容易讓程式碼邏輯複雜、思路混亂,一直備受爭議,中小型專案中較少使用,後來的 JavaC#PHP等乾脆取消了多繼承。】

    多繼承的語法也很簡單,將多個基類用逗號隔開即可。例如已聲明瞭類A、類B和類C,那麼可以這樣來宣告派生類D

class D: public A, private B, protected C{

    //D新增加的成員

}

D 是多繼承形式的派生類,它以公有的方式繼承 A類,以私有的方式繼承 B類,以保護的方式繼承 C類。D 根據不同的繼承方式獲取 ABC中的成員,確定它們在派生類中的訪問許可權。

多繼承形式下的建構函式和單繼承形式基本相同,只是要在派生類的建構函式中呼叫多個基類的建構函式。以上面的 ABCD類為例,D類建構函式的寫法為:

D(形參列表): A(實參列表), B(實參列表), C(實參列表){

    //其他操作

}

基類建構函式的呼叫順序和和它們在派生類建構函式中出現的順序無關,而是和宣告派生類時基類出現的順序相同。仍然以上面的 ABCD類為例,即使將 D類建構函式寫作下面的形式:

D(形參列表): B(實參列表), C(實參列表), A(實參列表){

    //其他操作

}

那麼也是先呼叫 A 類的建構函式,再呼叫B 類建構函式,最後呼叫C 類建構函式

多繼承形式下解構函式的執行順序和建構函式的執行順序與單繼承相同。

20.命名衝突

當兩個或多個基類中有同名的成員時,如果直接訪問該成員,就會產生命名衝突,編譯器不知道使用哪個基類的成員。這個時候需要在成員名字前面加上類名和域解析符::,以顯式地指明到底使用哪個類的成員,消除二義性。

21.c++虛繼承和虛基類

多繼承(Multiple Inheritance)是指從多個直接基類中產生派生類的能力,多繼承的派生類繼承了所有父類的成員。儘管概念上非常簡單,但是多個基類的相互交織可能會帶來錯綜複雜的設計問題,命名衝突就是不可迴避的一個。

多繼承時很容易產生命名衝突,即使我們很小心地將所有類中的成員變數和成員函式都命名為不同的名字,命名衝突依然有可能發生,比如典型的是菱形繼承,如下圖所示:

 

A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,這個時候類 A 中的成員變數和成員函式繼承到類 D 中變成了兩份,一份來自 A-->B-->D 這條路徑,另一份來自 A-->C-->D 這條路徑。

在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變數中分別存放不同的資料,但大多數情況下這是多餘的:因為保留多份成員變數不僅佔用較多的儲存空間,還容易產生命名衝突。假如類 A 有一個成員變數 a,那麼在類 D 中直接訪問 a 就會產生歧義,編譯器不知道它究竟來自 A -->B-->D 這條路徑,還是來自 A-->C-->D 這條路徑。

為了消除歧義,我們可以在 m_a 的前面指明它具體來自哪個類:

void seta(int a){ B::m_a = a; }

這樣表示使用 B 類的 m_a。當然也可以使用 C 類的:

void seta(int a){ C::m_a = a; }

22.虛繼承

    為了解決多繼承時的命名衝突和冗餘資料問題C++ 提出了虛繼承,使得在派生類中只保留一份間接基類的成員。

    在繼承方式前面加上 virtual 關鍵字就是虛繼承

虛繼承的目的是讓某個類做出宣告,承諾願意共享它的基類。其中,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都只包含一份虛基類的成員。

觀察這個新的繼承體系,我們會發現虛繼承的一個不太直觀的特徵:必須在虛派生的真實需求出現前就已經完成虛派生的操作。在上圖中,當定義 D 類時才出現了對虛派生的需求,但是如果 B 類和 C 類不是從 A 類虛派生得到的,那麼 D 類還是會保留 A 類的兩份成員。

換個角度講,虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。

 

在實際開發中,位於中間層次的基類將其繼承宣告為虛繼承一般不會帶來什麼問題。通常情況下,使用虛繼承的類層次是由一個人或者一個專案組一次性設計完成的。對於一個獨立開發的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發者也無法改變已經存在的類體系。

 

C++標準庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream istream ostream 直接繼承而來,而 istream ostream 又都繼承自一個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream ostream 必須採用虛繼承,否則將導致 iostream 類中保留兩份 base_ios 類的成員。

23.虛基類成員的可見性

因為在虛繼承的最終派生類中只保留了一份虛基類的成員,所以該成員可以被直接訪問,不會產生二義性。此外,如果虛基類的成員只被一條派生路徑覆蓋,那麼仍然可以直接訪問這個被覆蓋的成員。但是如果該成員被兩條或多條路徑覆蓋了,那就不能直接訪問了,此時必須指明該成員屬於哪個類。

以菱形繼承為例,假設 A 定義了一個名為 x 的成員變數,當我們在 D 中直接訪問 x 時,會有三種可能性:

如果 B C 中都沒有 x 的定義,那麼 x 將被解析為 A 的成員,此時不存在二義性。

如果 B C 其中的一個類定義了 x,也不會有二義性,派生類的 x 比虛基類的 x 優先順序更高。

如果 B C 中都定義了 x,那麼直接訪問 x 將產生二義性問題。

可以看到,使用多繼承經常會出現二義性問題,必須十分小心。上面的例子是簡單的,如果繼承的層次再多一些,關係更復雜一些,程式設計師就很容易陷人迷魂陣,程式的編寫、除錯和維護工作都會變得更加困難,因此我不提倡在程式中使用多繼承,只有在比較簡單和不易出現二義性的情況或實在必要時才使用多繼承,能用單一繼承解決的問題就不要使用多繼承。也正是由於這個原因,C++ 之後的很多面向物件的程式語言,例如 JavaC#PHP 等,都不支援多繼承。