C++中的繼承(2)類的默認成員


在繼承關系里面, 在派生類中如果沒有顯示定義這六個成員函數, 編譯系統則會默認合成這六個默認的成員函數。

1、構造與析構函數的調用關系

調用關系先看一段代碼:

 1 class Base
 2 {
 3     public :
 4     Base()
 5     {
 6         cout << "B() " << endl;
 7     }
 8     ~Base()
 9     {
10         cout << "~B() " << endl;
11     }
12 private:
13     int _pri;
14 protected:
15     int _pro;
16 public:
17     int _pub;
18 };
19 class Derived : public Base
20 {
21     public :
22     Derived()
23     {
24         cout << "D() " << endl;
25     }
26     ~Derived()
27     {
28         cout << "~D() " << endl;
29     }
30 private:
31     int _d_pri;
32 protected:
33     int _d_pro;
34 public:
35     int _d_pub;
36 }; 
37 void Test()
38 {
39     Derived d;
40 }
41 int main()
42 {
43     Test();
44     getchar();
45     return 0;
46 }

輸出結果為:

 

代碼中,我們利用派生類Derived,創建了一個對象d,根據輸出結果看到,貌似創建對象d的過程是:先調用基類的構造函數,再調用子類的構造函數;而析構對象時先調用子類的析構函數,再調用基類的析構函數。但是我們不能被表象所迷惑,我們轉到反匯編來看看具體是怎么實現的:

我們看到創建對象d的時候是先調用Derived類的構造函數D(),但是在cout << "D() " << endl;這句代碼執行之前,編譯器還做了一堆的其他工作,其中最重要的是  call        Base::Base (0E41041h)這條指令它跳轉到了基類中,如圖示:

屏幕上輸出:

B() 、D()即先執行cout << "B() " << endl;語句,再執行cout << "D() " << endl;語句,所以,才有屏幕上的結果。在析構對象d的時候先調用~D()但是沒那么簡單,再看圖中

在語句cout << "~D() " << endl;執行完之后用掉call        Base::~Base (0E410D7h)指令跳轉到~B()中 

此時~D()輸出,接着~B()輸出,然后,~D()才算執行完。

  分析:基類是派生類的一部分,創建派生類對象時必須調用派生類構造函數,而派生類構造函數必須使用基類的構造函數。程序首先創建基類對象,所以基類對象在程序進入派生類構造函數之前被創建。實際上C++使用成員初始化列表語法來完成這項工作,即B()相當於在函數D()的初始化列表中被使用,如果不調用基類構造函數,程序將使用默認的基類構造函數。在執行完B()的函數體之后,繼承的數據成員被初始化,執行D()函數體初始化新增的數據成員。析構對象時,先調用派生類的析構函數,執行完函數體析構完新增部分之后,使用基類的析構函數析構繼承自基類的部分。所以才有上述現象。調用關系總結如下圖:

  總結:創建派生類對象時程序調用派生類構造函數,然后在初始化列表部分調用基類構造函數初始化繼承的數據成員,而派生類構造函數主要初始化新增的數據成員。派生類總是調用一個基類構造函數。可以使用初始化列表語法指明要使用的基類構造函數,否則將使用默認的基類構造函數。派生類對象過期時,程序將先調用派生類析構函數,在函數體執行完之后調用基類析構函數。(可以看到,繼承的數據成員生命周期長, 新增的數據成員生命周期短。)

構造函數帶參情況

  構造派生對象時,派生類構造函數默認調用參數缺省的基類構造函數,若基類構造函數帶有參數,則派生類中必須顯式定義構造函數,並在初始化列表中傳參。本例中,若B()帶有參數,則D()中必須顯式定義構造函數並傳參;

如圖,B()帶有參數,則D()中構造函數無參時編譯不能通過。

傳參之后可以編譯用過。

  當基類中顯示定義構造函數,而派生類中沒有定義構造函數,則使用默認合成的派生類構造函數,並在默認合成的派生類構造函數調用基類構造函數。即基類Base顯式定義了構造函數,而派生類Derived中沒有定義,則用默認合成的構造函數D()實例化對象,且在初始化參數列表部分調用基類構造函數B()。

同理當派生類中顯示定義構造函數,而基類中沒有定義構造函數,則在派生類構造函數中調用默認合成的基類構造函數。

2、拷貝構造函數

使用拷貝構造函數的情況有:

  1. 將新的對象初始化為一個同類對象
  2. 按值將對象傳遞給函數
  3. 函數按值返回對象
  4. 編譯器生成臨時對象

如果程序沒有顯式定義拷貝構造函數,編譯器將自動生成一個。當然,如果想在派生類中構造基類對象,那么不僅僅可以用構造函數,也可以用拷貝構造函數

 1 class Base
 2 {
 3     public :
 4     Base()
 5     {
 6         cout << "B() " << endl;
 7     }
 8     ~Base()
 9     {
10         cout << "~B() " << endl;
11     }
12 private:
13     int _pri;
14 protected:
15     int _pro;
16 public:
17     int _pub;
18 };
19 class Derived : public Base
20 {
21     public :
22     Derived()
23     {
24         cout << "D() " << endl;
25     }
26     Derived(const Derived &tp)
27         :Base(tp)//拷貝構造函數
28     {
29         cout << "Derive()" << endl;
30     }
31     ~Derived()
32     {
33         cout << "~D() " << endl;
34     }
35 private:
36     int _d_pri;
37 protected:
38     int _d_pro;
39 public:
40     int _pub;
41 }; 
42 void Test()
43 {
44     Derived d;
45     Derived i(d);
46 }
47 int main()
48 {
49     Test();
50     getchar();
51     return 0;
52 }

運行成功,輸出結果為:

  這里我沒有給基類定義拷貝構造函數,但是編譯器自動給基類生成了一個拷貝構造函數,因為我基類中定義的沒有指針成員,所以淺拷貝可以滿足我的要求,但是如果在基類成員中有指針變量,必須要進行顯式定義拷貝構造函數,即進行深拷貝。不然會造成同一塊內存空間被析構兩次的問題。

3、賦值操作符

1 class Base
2 {};
3 int main()
4 {
5     Base a;
6     Base b = a;//初始化
7     Base c;
8     c = a;//賦值
9 }

  默認的賦值操作符用於處理同類對象之間的賦值,賦值不是初始化,如果語句創建新的對象,則使用初始化,如果語句修改已有對象的值,則為賦值。 賦值運算符是不能被繼承的,原因很簡單。派生類繼承的方法的特征與基類完全相同,但賦值操作符的特征隨類而異,因為它包含一個類型為其所屬類的形參。 
  如果編譯器發現程序將一個對象賦給同一個類的另一個對象,它將自動為這個類提供一個賦值操作符。這個操作符的默認版本將采用成員賦值,即將原對象的相應成員賦給目標對象的每個成員。 
  如果對象屬於派生類,編譯器將使用基類賦值操作符來處理派生對象中基類部分的賦值,如果顯示的為基類提供了賦值操作符,將使用該操作符。
  注意:賦值運算和拷貝構造是不同的,賦值是賦值給一個已有對象,拷貝構造是構造一個全新的對象

將派生類對象賦給基類對象時:

 1 class Base
 2 {
 3    public:
 4      int data;
 5 };
 6 class Derive:public Base
 7 {
 8    public:
 9      int d;
10 };
11 int main()
12 {
13     Base a;
14     Derive dd;
15     a = dd;
16 }

上面的a=dd;語句將使用誰的賦值操作符呢。 實際上,賦值語句將被轉換成左邊的對象調用的一個方法

 a.operator=(dd);//左邊的為基類對象

簡而言之,可以將派生對象賦給基類對象,但這只涉及到基類的成員。如圖示

 

基類對象賦給派生類對象。

 1 class Base
 2  {
 3     public:
 4       int data;
 5 };
 6  class Derive:public Base
 7  {
 8      public:
 9        int d;
10  };
11  int main()
12  {
13      Base a;
14      Derive dd;
15      dd = a;
16  }

上述賦值語句將被轉換為:

d.operator=(a); //Derive::operator=(const Derive&)
左邊的對象為派生類對象,不過派生類引用不能自動引用基類對象,所以上述代碼不能運行。或者運行出錯。除非有函數Derive(const Base&){}

總結:
  • 是否可以將基類對象賦給派生類對象,答案是也許。如果派生類包含了轉換構造函數,即對基類對象轉換為派生類對象進行了定義,則可以將基類對象賦給派生對象。
  • 派生類對象可以賦給基類對象。

4、類的成員初始化列表

(1)類的成員變量總是在構造函數執行前創建完畢,但有此成員變量只能在初始化時賦值 – 如const型常量 和 引用;

(2)使用初始化表可以使指定構造函數中的參數或常量作為成員的初始值;

1 Derived::Derived(int i, int j): x(i), y(j) {}

(3)初始化表只能用於構造函數;

(4)必須使用初始化表來初始化const型常量和引用;

(5)成員初始化的順序與它們出現在類聲明中的位置有關,與初始化表中的順序無關;

(6)C++11允許類內初始化,但初始化表會覆蓋類內初始化:

1 class Derived {
2 private:
3     int x = 10;
4     int y = 20;
5     static const int num = 0;
6 };

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM