1. 程式人生 > 實用技巧 >派生類建構函式和多重繼承的二義性問題

派生類建構函式和多重繼承的二義性問題

轉載:https://blog.csdn.net/zhangchen1003/article/details/48242393

一、派生類建構函式的寫法

(1)冒號前面是派生類建構函式的頭部,這和我們以前介紹的建構函式的形式一樣,但它的形參列表包括了初始化基類和派生類的成員變數所需的資料;冒號後面是對基類建構函式的呼叫,這和普通建構函式的引數初始化表非常類似。
(2)需要注意的是:冒號後面是對基類建構函式的呼叫,而不是宣告,所以括號裡的引數是實參


二、基類建構函式呼叫規則

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

(2)如果基類有預設建構函式,那麼在派生類建構函式中可以不指明,系統會預設呼叫;如果沒有,那麼必須要指明,否則系統不知道如何呼叫基類的建構函式。


三、建構函式的呼叫順序

(1)當建立派生類物件時,先呼叫基類建構函式,再呼叫派生類建構函式。如果繼承關係有好幾層的話,例如:
A –> B –> C
那麼則建立C類物件時,建構函式的執行順序為:
A類建構函式 –> B類建構函式 –> C類建構函式

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


四、有子類物件的派生類

(1)即一個類中,如果有物件被定義為一個使用者自定義型,那麼就稱該類含有子物件,比如

class person{
public:
      person();
private:
      int n;
class student:public person{
public:
      student();
private:
      person stu2;//稱為含有子物件
      int k;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

(2)初始化方法
派生類建構函式的任務應該包括3個部分:對基類資料成員初始化;對子物件資料成員初始化;對派生類資料成員初始化。比如:

student(int x,int y,int w,int h,int z):person(x,y),stu2(w,h),k(z){}
  • 1

在上面的建構函式中有5個形參,前兩個作為基類建構函式的引數,第3、第4個作為子物件建構函式的引數,第5個是用作派生類資料成員初始化的。基類建構函式和子物件的次序可以是任意的,如上面的派生類建構函式首部可以寫成

student(int x,int y,int w,int h,int z):stu2(w,h),person(x,y),k(z){}
  • 1

編譯系統是根據相同的引數名(而不是根據引數的順序)來確立它們的傳遞關係的。但是習慣上一般先寫基類建構函式

(3)定義派生類建構函式的一般形式為:

派生類建構函式名(總引數表列): 基類建構函式名(引數表列), 子物件名(引數表列)
{
    派生類中新增數成員據成員初始化語句
}
  • 1
  • 2
  • 3
  • 4

(4)執行派生類建構函式的順序是:呼叫基類建構函式,對基類資料成員初始化;呼叫子物件建構函式,對子物件資料成員初始化;再執行派生類建構函式本身,對派生類資料成員初始化。


五、多層派生的建構函式

(1)多層派生定義
一個類不僅可以派生出一個派生類,派生類還可以繼續派生,形成派生的層次結構;

(2)不要列出每一層派生類的建構函式,只需寫出其上一層派生類(即它的直接基類)的建構函式即可;比如:

class Base{
public:
       Base(int x):n(x){}
private:
       int n;
};
class D1:public Base{
public:
       D1(int x,int y):Base(x),m(y){}
private:
       int m;
};
class D2:public D1{
piblic:
       D2(int x,int y,int z):D1(x,y),k(z){}
private:
       int k;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

那麼寫D2的建構函式,只需要寫清楚其上層的建構函式即可D2(int x,int y,int z):D1(x,y),k(z){}

(3)執行順序
在宣告D2類物件時,呼叫D2建構函式;在執行D2建構函式時,先呼叫D1建構函式;在執行D1建構函式時,先呼叫基類Base建構函式。

初始化的順序是:先初始化基類的資料成員n;再初始化D1的資料成員m;最後再初始化D2的資料成員k。


六、注意事項
(1)如果在基類中既定義無參的建構函式,又定義了有參的建構函式(建構函式過載),則在定義派生類建構函式時,既可以包含基類建構函式及其引數,也可以不包含基類建構函式。(因為有預設的,可以不寫明)

(2)如果在基類或子物件型別的宣告中定義了帶引數的建構函式,那麼就必須顯式地定義派生類建構函式,並在派生類建構函式中寫出基類或子物件型別的建構函式及其引數表。

(3)如果在基類中沒有定義建構函式,或定義了沒有引數的建構函式,那麼在定義派生類建構函式時可不寫基類建構函式。


七、多繼承的建構函式

(1)多繼承宣告形式

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

}
  • 1
  • 2
  • 3

(2)多繼承的建構函式宣告形式

D類建構函式名(總引數表列): A建構函式(實參表列), B類建構函式(實參表列), C類建構函式(實參表列){
    新增成員初始化語句
}
  • 1
  • 2
  • 3

(3)派生類建構函式執行順序
派生類建構函式的執行順序同樣為:先呼叫基類的建構函式,再呼叫派生類建構函式。基類建構函式的呼叫順序是按照宣告派生類時基類出現的順序。(即: class D: public A, private B, protected C 中A、B、C出現的順序)

(4)命名衝突

考慮下列程式碼:

class Base1{
public:
       Base1(int x):n(x){}
       void display(){
       cout<<"Base1"<<endl;
       }
private:
       int n;
};
class Base2{
public:
       Base2(int y):m(y){}
       void display(){
       cout<<"Base2"<<endl;
       }
private:
       int m;
};
class D:public Base1,public Base2{
public:
       D(int x,int y,int z):Base1(x),Base2(y),k(z){}
private:
       int k;
};


int main(){
     D d(1,2,3);
     d.display();//錯誤,不知道呼叫哪個
     d.Base1::display();//正確,呼叫Base1中的display()
     d.Base2::display();//正確,呼叫Base2中的diaplay()
     return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

從上面程式碼中可以知道,Base1和Base2中都有display();D的物件訪問時必須加域名操作符;(注意:即使以private繼承Base2,那麼派生類外部,不能訪問繼承過來的display(),此時,若D d; d.display()還是會造成不能區分,必須加::指明哪個基類中的函式)


八、多重繼承的二義性問題

(1)二義性問題的定義
由多次繼承,導致成員同名而產生的二義性(ambiguous)問題

(2)二義性問題分為以幾類

  1. 兩個基類有同名成員
    那麼派生類繼承這兩個基類,派生類物件呼叫這個同名成員,編譯器就不知道到底是呼叫哪個基類的函式,必須加域名操作符限定

  2. 兩個基類和派生類三者都有同名成員
    假設這三個類都有display()函式,那麼通過派生類物件去呼叫display()函式,能不能通過編譯?
    此時,程式能通過編譯,也可以正常執行。請問:執行時訪問的是哪一個類中的成員?答案是:訪問的是派生類中的成員。規則是:基類的同名成員在派生類中被遮蔽,成為“不可見”的,或者說,派生類新增加的同名成員覆蓋了基類中的同名成員。因此如果在定義派生類物件的模組中通過物件名訪問同名的成員,則訪問的是派生類的成員。請注意:不同的成員函式,只有在函式名和引數個數相同、型別相匹配的情況下才發生同名覆蓋,如果只有函式名相同而引數不同,不會發生同名覆蓋,而屬於函式過載。
    若要訪問基類的同名成員函式,則要加域名操作符限定

  3. 類A和類B是從同一個基類派生的
    類A和類B是從同一個基類Base派生的,然後類C又繼承類A和類B;

class N
{
public:
   int a;
   void display(){ cout<<"A::a="<<a<<endl; }
};

class A: public N
{
public:
   int al;
};

class B: public N
{
public:
   int a2;
};

class C: public A, public B
{
public:
   int a3;
   void show(){ cout<<"a3="<<a3<<endl; }
}

int main()
{
   C cl;  //定義C類物件cl
   // 其他程式碼
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

在類A和類B中雖然沒有定義資料成員a和成員函式display,但是它們分別從類N繼承了資料成員a和成員函式display,這樣在類A和類B中同時存在著兩個同名的資料成員a和成員函式display。在程式中可以通過類A和類B的建構函式去呼叫基類N的建構函式,分別對類A和類B的資料成員a初始化。怎樣才能訪問類A中從基類N繼承下來的成員呢?顯然不能用
cl.a = 3; cl.display();

cl.N::a = 3; cl. N::display();
因為這樣依然無法區別是類A中從基類N繼承下來的成員,還是類B中從基類N繼承下來的成員。應當通過類N的直接派生類名來指出要訪問的是類N的哪一個派生類中的基類成員。如
cl.A::a=3; cl.A::display(); //要訪問的是類N的派生類A中的基類成員

(3)虛基類
類A派生出類B和類C,類D繼承自類B和類C,這個時候類A中的成員變數和成員函式繼承到類D中變成了兩份,一份來自 A–>B–>D 這一路,另一份來自 A–>C–>D 這一條路;

為了解決命名衝突,C++定義了虛基類,宣告虛基類只需要在繼承方式前面加上 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

注意以下幾點:

  1. 派生類D的建構函式,與以往的用法有所不同。以往,在派生類的建構函式中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。現在,由於虛基類在派生類中只有一份成員變數,所以對這份成員變數的初始化必須由派生類直接給出。如果不由最後的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能由於在類B和類C的建構函式中對虛基類給出不同的初始化引數而產生矛盾。所以規定:在最後的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化。
  2. C++編譯系統只執行最後的派生類對虛基類的建構函式的呼叫,而忽略虛基類的其他派生類(如類B和類C)對虛基類的建構函式的呼叫,這就保證了虛基類的資料成員不會被多次初始化。
  3. 為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中宣告為虛基類,否則仍然會出現對基類的多次繼承。
  4. 只有在比較簡單和不易出現二義性的情況或實在必要時才使用多重繼承,能用單一繼承解決的問題就不要使用多重繼承

參考原文地址:http://c.biancheng.net/cpp/biancheng/cpp/rumen/