C++三大特性之繼承

原文地址:https://qunxinghu.github.io/2016/09/12/C++%20%E4%B8%89%E5%A4%A7%E7%89%B9%E6%80%A7%E4%B9%8B%E7%BB%A7%E6%89%BF/ios

基本概念

繼承 : 類的繼承,就是新的類從已有類那裏獲得已有的特性。原有的類稱爲基類或父類,產生的新類稱爲派生類或子類。c++

基本語法

派生類的聲明:git

class 派生類名:繼承方式 基類名1, 繼承方式 基類名2,...,繼承方式 基類名n
{
    派生類成員聲明;
};

在 c++ 中,一個派生類能夠同時有多個基類,這種狀況稱爲多重繼承。若是派生類只有一個基類,稱爲單繼承。派生類繼承基類中除構造和析構函數之外的全部成員。github

類的繼承方式

C++類繼承方式 繼承方式規定了如何訪問基類繼承的成員。繼承方式有public, private, protected。若是不顯示給出繼承方式,默認爲private繼承。繼承方式指定了派生類成員以及類外對象對於從基類繼承來的成員的訪問權限。函數

  • 公有繼承 當類的繼承方式爲公有繼承時,基類的公有和保護成員的訪問屬性在派生類中不變,而基類的私有成員不可訪問。即基類的公有成員和保護成員被繼承到派生類中仍做爲派生類的公有成員和保護成員。派生類的其餘成員能夠直接訪問它們。不管派生類的成員仍是派生類的對象都沒法訪問基類的私有成員。
  • 私有繼承 當類的繼承方式爲私有繼承時,基類中的公有成員和保護成員都以私有成員身份出如今派生類中,而基類的私有成員在派生類中不可訪問。基類的公有成員和保護成員被繼承後做爲派生類的私有成員,派生類的其餘成員能夠直接訪問它們,可是在類外部經過派生類的對象沒法訪問。不管是派生類的成員仍是經過派生類的對象,都沒法訪問從基類繼承的私有成員。經過屢次私有繼承後,對於基類的成員都會成爲不可訪問。所以私有繼承比較少用。
  • 保護繼承 保護繼承中,基類的公有成員和私有成員都以保護成員的身份出如今派生類中,而基類的私有成員不可訪問。派生類的其餘成員能夠直接訪問從基類繼承來的公有和保護成員,可是類外部經過派生類的對象沒法訪問它們,不管派生類的成員仍是派生類的對象,都沒法訪問基類的私有成員。

派生類的構造函數

  1. 派生類中由基類繼承而來的成員的初始化工做仍是由基類的構造函數完成,派生類中新增的成員在派生類的構造函數中初始化派生類構造函數的語法:
派生類名::派生類名(參數總表):基類名1(參數表1),基類名(參數名2)....基類名n(參數名n),內嵌子對象1(參數表1),內嵌子對象2(參數表2)....內嵌子對象n(參數表n)
{
   派生類新增成員的初始化語句;
}

注:構造函數的初始化順序並不以上面的順序進行,而是根據聲明的順序初始化。 2. 若是基類中沒有不帶參數的構造函數,那麼在派生類的構造函數中必須調用基類構造函數,以初始化基類成員。 3. 派生類構造函數執行的次序: 1. 調用基類構造函數,調用順序按照它們 被繼承時聲明的順序 (從左到右); 2. 調用內嵌成員對象的構造函數,調用順序按照它們在類中聲明的順序; 3. 派生類的構造函數體中的內容。spa

派生類的析構函數

派生類的析構函數的功能是在該對象消亡以前進行一些必要的清理工做,析構函數沒有類型,也沒有參數。析構函數的執行順序與構造函數相反。指針

實例:code

#include <iostream>
#include <time.h>
using namespace std;
// 基類 B1
class B1
{
public:
    B1(int i)
    {
        cout<<"constructing B1 "<<i<<endl;
    }
    ~B1()
    {
        cout<<"destructing B1"<<endl;
    }
};

//基類 B2
class B2
{
public:
    B2(int j)
    {
        cout<<"constructing B2 "<<j<<endl;
    }
     ~B2()
    {
        cout<<"destructing B2"<<endl;
    }
};

//基類 B3
class B3
{
public:
    B3()
    {
        cout<<"constructing B3"<<endl;
    }
    ~B3()
    {
        cout<<"destructing B3"<<endl;
    }
};

//派生類 C, 繼承B2, B1,B3(聲明順序從左至右。 B2->B1->B3)
class C: public B2, public B1, public B3
{
public:
    C(int a, int b, int c, int d):B1(a), memberB2(d), memberB1(c),B2(b)
    {
		//B1,B2的構造函數有參數,B3的構造函數無參數
      	//memberB2(d), memberB1(c)是派生類對本身的數據成員進行初始化的過程、
        //構造函數執行順序, 基類(聲明順序)-> 內嵌成員對象的構造函數(聲明順序) -> 派生類構造函數中的內容
    }
private:
    B1 memberB1;
    B2 memberB2;
    B3 memberB3;
};

int main() 
{ 
    C obj(1,2,3,4);
    return 0; 
}

/* 輸出結果 */
/*
constructing B2 2
constructing B1 1
constructing B3
constructing B1 3
constructing B2 4
constructing B3
destructing B3
destructing B2
destructing B1
destructing B3
destructing B1
destructing B2
*/

二義性問題

在單繼承下,基類的public 和protected 成員能夠直接被訪問,就像它們是派生類的成員同樣,對多繼承這也是正確的。可是在多繼承下,派生類能夠從兩個或者更多個基類中繼承同名的成員。然而在這種狀況下,直接訪問是二義的,將致使編譯時刻錯誤。 示例:對象

#include <iostream>
using namespace std;

class A
{
public:
void f();
};

class B
{
public:
void f();
void g();
};

class C : public A, public B
{
public:
void g();
void h();
};

int main(){
	C c1;
    // c1.f();    產生二義性問題,訪問A中的 f()? or B的 f() ?
    //經過指定成員名,限定消除二義性
    c1.A::f();
    c1.B::f();
}

使用成員名限定法能夠消除二義性,可是更好的解決辦法是在類C中定義一個同名函數 f(), 類C中的 f() 再根據須要來決定調用 A::f() or B::f(), 這樣 c1.f() 將調用 C::f().blog

當一個派生類從多個基類派生類,而這些基類又有一個共同的基類,則對該基類中說明的成員進行訪問時,也可能會出現二義性。 示例:

//  派生類 B1,B2 繼承相同的基類 A, 派生類 C 繼承 B1, B2
class A
{
public:
int a;
};
class B1 : public A
{
private:
int b1;
};
class B2 : public A
{
private:
int b2;
};
class C : public B1, public B2
{
public:
int f();
private:
int c;
};

int main(){
	C c1;
    c1.a();
    c1.A::a();
    c1.B1::a();
    c1.B2::a();
	return 0;
}

c1.a; c1.A::a; 這兩個訪問都有二義性,c1.B1::a; c1.B2::a;是正確的: 類C的成員函數 f() 用以下定義能夠消除二義性:

int C::f()
{ 
	retrun B1::a + B2::a; 
}

因爲二義性的緣由,一個類不能夠從同一個類中直接繼承一次以上。

虛基類

多繼承時很容易產生命名衝突,即便咱們很當心地將全部類中的成員變量和成員函數都命名爲不一樣的名字,命名衝突依然有可能發生,好比很是經典的菱形繼承層次。以下圖所示:

graph TD;
    A-->B;
    A-->C;
    B-->D;
    C-->D;

類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變量和成員函數繼承到類D中變成了兩份,一份來自 A-->B-->D 這一路,另外一份來自 A-->C-->D 這一條路。當D訪問從A中繼承的數據時,變一塊兒將沒法決定採用哪一條路傳過來的數據,因而便出現了虛基類。

在一個派生類中保留間接基類的多份同名成員,雖然能夠在不一樣的成員變量中分別存放不一樣的數據,但大多數狀況下這是多餘的:由於保留多份成員變量不只佔用較多的存儲空間,還容易產生命名衝突,並且不多有這樣的需求。使用虛基類,可使得在派生類中只保留間接基類的一份成員。

聲明虛基類只須要在繼承方式前面加上 virtual 關鍵字,以下面示例:

#include <iostream>
using namespace std;

class A{
protected:
    int a;
public:
    A(int a):a(a){}
};

class B: virtual public A{  //聲明虛基類
protected:
    int b;
public:
    B(int a, int b):A(a),b(b){}
};

class C: virtual public A{  //聲明虛基類
protected:
    int c;
public:
    C(int a, int c):A(a),c(c){}
};

class D: virtual public B, virtual public C{  //聲明虛基類
private:
    int d;
public:
    D(int a, int b, int c, int d):A(a),B(a,b),C(a,c),d(d){}
    void display();
};
void D::display(){
    cout<<"a="<<a<<endl;
    cout<<"b="<<b<<endl;
    cout<<"c="<<c<<endl;
    cout<<"d="<<d<<endl;
}

int main(){
    (new D(1, 2, 3, 4)) -> display();
    return 0;
}

/* 
運行結果:
a=1
b=2
c=3
d=4
*/

本例中咱們使用了虛基類,在派生類D中只有一份成員變量 a 的拷貝,因此在 display() 函數中能夠直接訪問 a,而不用加類名和域解析符。

虛基類的初始化 : 請注意派生類D的構造函數,與以往的用法有所不一樣。 以往,在派生類的構造函數中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。如今,因爲虛基類在派生類中只有一份成員變量,因此對這份成員變量的初始化必須由派生類直接給出。若是不禁最後的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能因爲在類B和類C的構造函數中對虛基類給出不一樣的初始化參數而產生矛盾。因此規定:在最後的派生類中不只要負責對其直接基類進行初始化,還要負責對虛基類初始化。

在上述代碼中,類D的構造函數經過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也經過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?你們沒必要過慮,C++編譯系統只執行最後的派生類對虛基類的構造函數的調用,而忽略虛基類的其餘派生類(如類B和類C)對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被屢次初始化。

最後請注意: 爲了保證虛基類在派生類中只繼承一次,應當在該基類的全部直接派生類中聲明爲虛基類,不然仍然會出現對基類的屢次繼承。

賦值兼容原則

賦值兼容 : 賦值兼容規則是指在須要基類對象的任何地方均可以使用公有派生類的對象來替代。

賦值兼容規則中所指的替代包括:

  • 派生類的對象能夠賦值給基類對象;
  • 派生類的對象能夠初始化基類的引用;
  • 派生類對象的地址能夠賦給指向基類的指針。 在替代以後,派生類對象就能夠做爲基類的對象使用,但只能使用從基類繼承的成員。
相關文章
相關標籤/搜索