1. 程式人生 > >C++繼承和派生——派生類成員的標識與訪問(作用域分辨符和虛基類技術)

C++繼承和派生——派生類成員的標識與訪問(作用域分辨符和虛基類技術)

在派生類中,成員可以按訪問屬性劃分為以下四種:

  • 不可訪問成員
    準確說是不可以直接訪問。這種成員是從基類私有成員繼承而來,派生類或者派生類物件的模組都無法訪問這些成員,當然,派生類繼續派生的新類也是無法訪問它們的。
  • 私有成員
    這個可以是從基類繼承過來的成員和新增加的成員,在派生類內部可以訪問,但是建立派生類物件的模組中無法訪問,繼續派生,這些成員就會變成新的派生類中的不可訪問成員。
  • 保護成員
    可以是從基類繼承過來的也可以是新增的,在派生類的內部可以訪問,建立派生類物件的模組無法訪問,繼續派生,這些成員在新的派生類中可能成為私有成員或者保護成員。
  • 公有成員
    派生類、派生類的物件模組都可以訪問它們,繼續派生,這些公有成員可能成為新派生類中的私有、保護或者公有成員。

因為派生類可能會多重繼承,所以在對派生類的訪問中,需要解決兩個問題:
     ①唯一識別符號問題,②成員屬性問題(成員可見性問題)

只有能夠唯一標識的可見性物件我們才可直接訪問。如果通過某一個表示式能夠引用的成員不止一個,那麼就有二義性。

下面將介紹兩種方法:作用域分辨符和虛基類,來解決以上問題。

作用域分辨符

在C++中,經常見到的“::”就是作用域分辨符,它的作用就是限定訪問的成員所在的類的名稱。
使用作用域分辨符的一般形式為:


類名::成員名             //資料成員
類名::成員名             //函式成員


在說明作用域分辨符如何在類族中唯一標識成員時,先介紹一個現象。

在巢狀的作用域中,當外層作用域聲明瞭一個識別符號,而內層沒有再次宣告此識別符號,那麼外層識別符號在內層仍然可見;否則,外層識別符號在內層將不可見,內層重新宣告的這個識別符號會隱藏了外層同名識別符號,這個現象稱為隱藏規則

============== 假設條件:當所有基類之間都沒有繼承關係 ==============

當派生類繼承的多個基類擁有同名的成員,並且派生類新增了這樣的同名成員,如上所述,派生類的這個成員會隱藏所有基類中的同名成員。使用“物件.成員名”或者“物件指標->成員名”可以唯一標識和訪問派生類新增的成員,訪問隱藏的基類成員則需要使用基類名和作用域分辨符訪問

若是派生類中沒有新增同名成員,使用“物件.成員名”或者“物件指標->成員名”就無法唯一標識成員!因為從不同基類繼承過來的成員具有相同的名稱,以及相同的作用域,系統根據這些資訊無法判斷呼叫哪一個基類的成員,這時就必須要使用基類名和作用域識別符號來表示成員。

這裡需要注意:子類中若是有與父類同名但是引數不同的函式,這不屬於函式過載,子類中的函式將會隱藏父類中的同名函式,此時要呼叫父類中的函式需要使用父類名來限定。只有作用域相同函式可以進行過載。

訪問基類的隱藏同名成員示例

例1
#include <iostream>
#include <cstdlib>
using namespace std;
class Base1
{
public:
    int var;
    void fun() { cout << "In Base1,var is " << var << endl; }
};
class Base2
{
public:
    int var;
    void fun() { cout << "In Base2, var is " << var << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived objd;
    Derived * objp = new Derived;

    objd.var = 1;           //物件.成員名訪問 Derived中的同名物件
    objd.fun();

    objp->Base1::var = 2;   //類名加作用域分辨符訪問 唯一標識訪問成員
    objp->Base1::fun();

    objp->Base2::var = 3;
    objp->Base2::fun();

    system("pause");
    return 0;
}

執行結果

In Derived,var is 1
In Base1,var is 2
In Base2, var is 3

上述舉例是以派生類Derived中新增了同名成員為例,若是Derived中沒有新增同名成員,main函式中物件.成員名的訪問方式就會出錯,提示訪問物件不明確即這種訪問方式具有二義性。
若要使物件.成員名的訪問方式沒有二義性,則可在Derived類的定義中使用using關鍵字具體說明訪問成員為哪個基類的。
如下所示,使用using關鍵字說明,要訪問的成員為基類Base1中的。

class Derived:public Base1,public Base2
{
public:
    using Base1::var;
    using Base1::fun;
};

>>>>>> using <<<<<<
using的一般功能是將一個作用域中的名字引入到另一個作用域中
利用這個作用可以做一件有趣的事情:使用using將基類的函式名引入到派生類中,在派生類中定義同名但引數不同的函式,實現函式過載。基類的這個函式不會被隱藏,與派生類中的同名函式共存在派生類的作用域中。

class Derived:public Base1,public Base2
{
public:
    using Base1::fun;
    void fun(int a,int b){...}
};

============== 基類之間有繼承關係 ==============

假設所有的基類都沒有繼承關係不太可能存在,若是派生類的部分或者全部直接基類是從另外一個共同基類派生而來,那麼在這些直接基類中,從上一級基類繼承來的成員就擁有相同的名稱,因此派生類中也會出現同名現象,對這種型別的成員要使用作用域分辨符來唯一標識,並且必須用直接基類限定!

類之間的一個派生關係如下:

派生關係

例2
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    int var0;
    void fun0(){ cout << "In Base1,var is " << var0 << endl; }
};

class Base1:public Base0
{
public:
    int var1;
    void fun() { cout << "In Base1,var is " << var1 << endl; }
};
class Base2:public Base0
{
public:
    int var2;
    void fun() { cout << "In Base2, var is " << var2 << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived d;
    d.Base1::var0 = 1;  //使用直接基類訪問
    d.Base1::fun0();

    d.Base2::var0 = 2;
    d.Base2::fun0();

    system("pause");
    return 0;
}

執行結果

In Base1,var is 1
In Base1,var is 2

在上述程式中,派生類物件在記憶體中擁有var0的兩份同名副本,很多時候我們只需要一份,多的一份增加了記憶體開銷。C++中提供了虛基類技術來解決這個問題。

需要注意上例中Base0類的函式成員fun0的程式碼始終只有一個副本,之所以還要加直接基類名限定是因為呼叫非靜態成員函式是針對特定的物件,執行函式呼叫時需要將指向該類的一個物件指標作為隱含地引數傳遞給被呼叫的函式初始化this指標。
上例中,Derived類的物件中存在兩個Base0類的物件,所以在呼叫fun0時,需要使用Base1和Base1限定,明確針對的是哪個Base0物件

虛基類

當派生類中的部分或者全部直接基類從另外一個共同基類派生而來時,在這些直接基類中,從上一級基類繼承來的成員就擁有相同的名稱。在派生類物件中同名數據成員會有多個副本,同一個函式名會有多個對映。
除了使用作用域分辨符唯一識別成員,還可以將共同基類設定為虛基類,這樣從不同路徑繼承過來的同名數據成員在記憶體中只有一個副本,同名函式成員也只有一個對映。這樣也可以解決同名成員唯一標識問題。

虛基類的宣告是在派生類的定義過程中進行的,語法形式如下:

class 派生類名:virtual 繼承方式 基類名

這樣聲明瞭基類為派生類的虛基類。在多繼承的情況下,虛基類關鍵字的作用範圍和繼承方式關鍵字相同,只對緊跟其後的基類起作用。

虛基類示例

例3
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    int var0;
    void fun0(){ cout << "In Base1,var is " << var0 << endl; }
};

class Base1:virtual public Base0
{
public:
    int var1;
    void fun() { cout << "In Base1,var is " << var1 << endl; }
};
class Base2:virtual public Base0
{
public:
    int var2;
    void fun() { cout << "In Base2, var is " << var2 << endl; }
};
class Derived:public Base1,public Base2
{
public:
    int var;
    void fun() { cout << "In Derived,var is " << var << endl; }
};

int main(int argc, char * argv[])
{
    Derived d;
    d.var0 = 0;//直接訪問虛基類資料成員和函式成員
    d.fun0();
    system("pause");
    return 0;
}

執行結果

In Base1,var is 0

作用域分辨符和虛基類技術的區別

在例1和例2中可以看到,派生類中擁有的同名成員的多個副本,在其中使用直接基類名和作用域分辨符唯一標識成員,可以存放不同資料和進行不同操作。在例3中,只維護了一份成員副本。前者可以容納更多的資料,而後者使用更為簡潔,記憶體空間也更為節省。

虛基類及其派生類建構函式

當類中沒有宣告建構函式時,編譯器會為類自動生成預設建構函式。但是虛基類中聲明瞭帶形參的建構函式,那情況可能有點複雜。

例4
#include <iostream>
#include <cstdlib>
using namespace std;
class Base0
{
public:
    Base0(int var):var0(var){
        cout << "Constructor in Base1,var is " << var0 << endl; }
    ~Base0() { cout << "Destroy Base0" << endl;}
    int var0;
};

class Base1:virtual public Base0  //Base0為虛基類
{
public:
    Base1(int var):Base0(var),var1(var){
        cout << "Constructor in Base1,var is " << var1 << endl; }
    ~Base1() { cout << "Destroy Base1" << endl; }
    int var1;
};
class Base2:virtual public Base0
{
public:
    Base2(int var):Base0(var),var2(var0){
        cout << "Constructor in Base2,var is " << var2 << endl; }
    ~Base2() { cout << "Destroy Base2" << endl; }
    int var2;

};
class Derived:public Base1,public Base2
{
public:
    Derived(int var):Base0(var),Base1(var),Base2(var),var(var){
        cout << "Constructor in Derived,var is " << var << endl;
    }
    ~Derived(){ cout << "Destroy Derived" << endl; }
    int var;
};

int main(int argc, char * argv[])
{
    Derived d(1);
    system("pause");
    return 0;
}

以上述派生關係為例,可能會疑惑:Base0基類中聲明瞭帶形參的建構函式,當派生類Derived建立物件時,會呼叫Base0、Base1和Base2的建構函式,在呼叫Base0的建構函式時,對成員var0進行了初始化,而Base1和Base2的初始化列表中也會呼叫Base0建構函式對var0進行初始化,這樣var0豈不是被初始化了3次?!

C++編譯器中有解決這個問題的方法:假設建立物件的派生類為最遠派生類這裡為Derived。若是最遠派生類建立物件,這個物件中含有從虛基類那裡繼承過來的成員,那麼虛基類的這個成員的初始化是由最遠派生類呼叫虛基類的建構函式進行初始化。
也就是說只有最遠派生類才會呼叫虛基類的建構函式,該派生類的其他基類如Base1和Base2對虛基類建構函式的呼叫都會自動被忽略!

例4執行結果

Constructor in Base0,var is 1
Constructor in Base1,var is 1
Constructor in Base2,var is 1
Constructor in Derived,var is 1
Destroy Derived
Destroy Base2
Destroy Base1
Destroy Base0

可以看出,虛基類的建構函式只被呼叫了一次。

建立派生類的物件,建構函式的執行順序

當一個類建立一個物件時,建構函式的呼叫是有一定順序的:
(但是與派生類建構函式初始化形參類表出現次序無關)

  1. 如果該類有直接或者間接的虛基類,則先執行虛基類的建構函式

  2. 該類若有其他基類,則按它們在繼承宣告時的順序分別執行它們的建構函式,但不執行它們虛基類中的建構函式

  3. 按照在類的定義中出現的順序,對派生類中新增的成員物件進行初始化。

  4. 執行建構函式的函式體

解構函式的執行次序相反

例5
#include <iostream>
#include <cstdlib>
using namespace std;

class Base1
{
public:
Base1(int i) { cout << "Constructing Base1 " << i << endl; }
~Base1() { cout << "Destructing Base1..." << endl; }
};

class Base2
{
public:
Base2(int i) { cout << "Constructing Base2 " << i << endl; }
~Base2() { cout << "Destructing Base2..." << endl; }
};

class Base3
{
public:
Base3() { cout << "Constructing Base3" << endl; }
~Base3() { cout << "Destructing Base3..." << endl; }
};

class Derived :public Base1, public Base2, public Base3
{
public://執行次序(與派生類建構函式初始化形參類表出現次序無關)
Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b){}
private:
Base1 member1;
Base2 member2;
Base3 member3;
};

int main()
{
Derived(1, 2, 3, 4);
system("pause");
return 0;
}

例5執行結果

Constructing Base1 1
Constructing Base2 2
Constructing Base3
Constructing Base1 3
Constructing Base2 4
Constructing Base3
Destructing Base3...
Destructing Base2...
Destructing Base1...
Destructing Base3...
Destructing Base2...
Destructing Base1...

執行結果符合上述介紹的執行順序

這篇部落格主要記錄了在唯一標識類中成員的兩種方法:作用域分辨符和虛基類技術,並且附加一些細節點。