1. 程式人生 > >c++中的多型機制

c++中的多型機制

目錄

  1  背景介紹

  2  多型介紹

    2-1  什麼是多型

    2-2  多型的分類 

    2-3  動態多型成立的條件

    2-4  靜態聯編和動態聯編

    2-5  動態多型的實現原理

      2-6   虛解構函式

      2.7  關於虛擬函式的思考題

    2.8  純虛擬函式、抽象類、介面

背景介紹

  虛擬函式重寫:子類重新定義父類中有相同返回值、名稱和引數的虛擬函式;

  非虛函重寫:子類重新定義父類中有相同名稱和引數的非虛擬函式;

  父子間的賦值相容:子類物件可以當作父類物件使用(相容性);具體表現為:

   1. 子類物件可以直接賦值給父類物件;

   2. 子類物件可以直接初始化父類物件;

   3. 父類指標可以直接指向子類物件;

   4. 父類引用可以直接引用子類物件;

  當發生賦值相容時,子類物件退化為父類物件,只能訪問父類中定義的成員,可以直接訪問被子類覆蓋的同名成員;

 1 // 在賦值相容原則中,子類物件退化為父類物件,子類是特殊的父類;
 2 #include <iostream>
 3 #include <string>
 4 
 5 using namespace std;
 6 
 7 class Parent
 8 {
 9 public:
10     int mi;
11 
12     void add(int i)
13     {
14         mi += i;
15     }
16     
17     void add(int a, int b)
18     {
19         mi += (a + b);
20     }
21 };
22 
23 class Child : public Parent
24 {
25 public:
26     int mi;
27     
28     void add(int x, int y, int z)
29     {
30         mi += (x + y + z);
31     }
32 };
33 
34 int main()
35 {
36     Parent p;
37     Child c;
38     
39     c.mi = 100;
40     p = c;             // p.mi = 0; 子類物件退化為父類物件
41     Parent p1(c);   // p1.mi = 0; 同上
42     Parent& rp = c;
43     Parent* pp = &c;
44     
45     rp.add(5);             
46     pp->add(10, 20);        
47     
48     cout << "p.mi: " << p.mi <<endl;                           // p.mi: 0; 
49     cout << "p1.mi: " << p1.mi <<endl;                       // p1.mi: 0;  
50     cout << "c.Parent::mi: " << c.Parent::mi <<endl;    // c.Parent::mi: 35
51     cout << "rp.mi: " << rp.mi <<endl;                        // rp.mi: 35
52     cout << "pp->mi: " << pp->mi <<endl;                 // pp->mi: 35
53     
54     return 0;
55 }
賦值相容測試

  在面向物件的繼承關係中,我們瞭解到子類可以擁有父類中的所有屬性與行為;但是,有時父類中提供的方法並不能滿足現有的需求,所以,我們必須在子類中重寫父類中已有的方法,來滿足當前的需求。此時儘管我們已經實現了函式重寫(這裡是非虛擬函式重寫),但是在型別相容性原則中也不能出現我們期待的結果(不能根據指標/引用所指向的實際物件型別去調到對應的重寫函式)。接下來我們用程式碼來複現這個情景:

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class Parent
 7 {
 8 public:    
 9     void print()
10     {
11         cout << "I'm Parent." << endl;
12     }
13 };
14 
15 class Child : public Parent
16 {
17 public:    
18     void print()
19     {
20         cout << "I'm Child." << endl;
21     }
22 };
23 
24 void how_to_print(Parent* p)
25 {
26     p->print();
27 }
28 
29 int main()
30 {
31     Parent p;
32     Child c;
33     
34     how_to_print(&p);   // I'm Parent    // Expected to print: I'm Parent.
35     how_to_print(&c);   // I'm Parent    // Expected to print: I'm Child.
36     
37     return 0;
38 }
非虛擬函式重寫與賦值相容的問題

  為什麼會出現上述現象呢?(在賦值相容中,父類指標/引用指向子類物件時為何不能呼叫子類重寫函式?)

  問題分析:在編譯期間,編譯器只能根據指標的型別判斷所指向的物件;根據賦值相容,編譯器認為父類指標指向的是父類物件;因此,編譯結果只可能是呼叫父類中定義的同名函式。

       

  在編譯這個函式的時候,編譯器不可能知道指標p究竟指向了什麼。但是編譯器沒有理由報錯,於是,編譯器認為最安全的做法是呼叫父類的print函式。因為父類和子類肯定都有相同的print函式。

  要想解決這個問題,就需要使用c++中的多型。那麼如何實現c++中的多型呢?請看下面詳解:

多型介紹

1、 什麼是多型

  在現實生活中,多型是同一個事物在不同場景下的多種形態。

  在面向物件中,多型是指通過基類的指標或者引用,在執行時動態呼叫實際繫結物件函式的行為。與之相對應的編譯時繫結函式稱為靜態繫結。

  多型是設計模式的基礎,多型是框架的基礎。

2、 多型的分類

  

  靜態多型是編譯器在編譯期間完成的,編譯器會根據實參型別來選擇呼叫合適的函式,如果有合適的函式就呼叫,沒有的話就會發出警告或者報錯;

  動態多型是在程式執行時根據基類的引用(指標)指向的物件來確定自己具體該呼叫哪一個類的虛擬函式。

3、動態多型成立的條件

    由之前出現的問題可知,編譯器的做法並不符合我們的期望(因為編譯器是根據父類指標的型別去父類中呼叫被重寫的函式);但是,在面向物件的多型中,我們期望的行為是 根據實際的物件型別來判斷如何呼叫重寫函式(虛擬函式);

  1. 即當父類指標(引用)指向 父類物件時,就呼叫父類中定義的虛擬函式;

  2. 即當父類指標(引用)指向 子類物件時,就呼叫子類中定義的虛擬函式;

       

  這種多型行為的表現效果為:同樣的呼叫語句在實際執行時有多種不同的表現形態。

  那麼在c++中,如何實現這種表現效果呢?(實現多型的條件)

  1.  要有繼承

  2.  要有虛擬函式重寫(被 virtual 宣告的函式叫虛擬函式)

  3.  要有父類指標(父類引用)指向子類物件

4、靜態聯編和動態聯編

  靜態聯編:在程式的編譯期間就能確定具體的函式呼叫;如函式過載,非虛擬函式重寫;

  動態聯編:在程式實際執行後才能確定具體的函式呼叫;如虛擬函式重寫,switch 語句和 if 語句;

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class Parent
 7 {
 8 public:
 9     virtual void func()
10     {
11         cout << "Parent::void func()" << endl;
12     }
13     
14     virtual void func(int i)
15     {
16         cout << "Parent::void func(int i) : " << i << endl;
17     }
18     
19     virtual void func(int i, int j)
20     {
21         cout << "Parent::void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
22     }
23 };
24 
25 class Child : public Parent
26 {
27 public:
28     void func(int i, int j)
29     {
30         cout << "Child::void func(int i, int j) : " << i + j << endl;
31     }
32     
33     void func(int i, int j, int k)
34     {
35         cout << "Child::void func(int i, int j, int k) : " << i + j + k << endl;
36     }
37 };
38 
39 void run(Parent* p)
40 {
41     p->func(1, 2);     // 展現多型的特性
42                        // 動態聯編
43 }
44 
45 
46 int main()
47 {
48     Parent p;
49     
50     p.func();         // 靜態聯編
51     p.func(1);        // 靜態聯編
52     p.func(1, 2);     // 靜態聯編
53     
54     cout << endl;
55     
56     Child c;
57     
58     c.func(1, 2);     // 靜態聯編
59     
60     cout << endl;
61     
62     run(&p);
63     run(&c);
64     
65     return 0;
66 }
67 /*
68     Parent::void func()
69     Parent::void func(int i) : 1
70     Parent::void func(int i, int j) : (1, 2)
71 
72     Child::void func(int i, int j) : 3
73 
74     Parent::void func(int i, int j) : (1, 2)
75     Child::void func(int i, int j) : 3
76 */
靜態聯編與動態聯編的案列

5、動態多型的實現原理

  虛擬函式表與vptr指標

  1. 當類中宣告虛擬函式時,編譯器會在類中生成一個虛擬函式表;

  2. 虛擬函式表是一個儲存類成員函式指標的資料結構;

  3. 虛擬函式表是由編譯器自動生成與維護的;

  4. virtual成員函式會被編譯器放入虛擬函式表中;

  5. 存在虛擬函式時,每個物件中都有一個指向虛擬函式表的指標(vptr指標)。

  多型執行過程:

  1.  在類中,用 virtual 宣告一個函式時,就會在這個類中對應產生一張 虛擬函式表,將虛擬函式存放到該表中;

       2.  用這個類建立物件時,就會產生一個 vptr指標,這個vptr指標會指向對應的虛擬函式表;

       3.  在多型呼叫時, vptr指標 就會根據這個物件 在對應類的虛擬函式表中 查詢被呼叫的函式,從而找到函式的入口地址;

            》 如果這個物件是 子類的物件,那麼vptr指標就會在 子類的 虛擬函式表中查詢被呼叫的函式

            》 如果這個物件是 父類的物件,那麼vptr指標就會在 父類的 虛擬函式表中查詢被呼叫的函式

  

  

 

 

 

  注:出於效率考慮,沒有必要將所有成員函式都宣告為虛擬函式。

  如何證明vptr指標的存在?

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class Demo1
 7 {
 8 private:
 9     int mi; // 4 bytes
10     int mj; // 4 bytes
11 public:
12     virtual void print(){}  // 由於虛擬函式的存在,在例項化類物件時,就會產生1個 vptr指標
13 };
14 
15 class Demo2
16 {
17 private:
18     int mi; // 4 bytes
19     int mj; // 4 bytes
20 public:
21     void print(){}
22 };
23 
24 int main()
25 {
26     cout << "sizeof(Demo1) = " << sizeof(Demo1) << " bytes" << endl; // sizeof(Demo1) = 16 bytes
27     cout << "sizeof(Demo2) = " << sizeof(Demo2) << " bytes" << endl; // sizeof(Demo2) = 8 bytes
28     
29     return 0;
30 }
31 
32 // 64bit(OS) 指標佔 8 bytes
33 // 32bit(OS) 指標佔 4 bytes
vptr指標的證明

  顯然,在普通的類中,類的大小 == 成員變數的大小;在有虛擬函式的類中,類的大小 == 成員變數的大小 + vptr指標大小。

6、 虛解構函式

  定義:用 virtual 關鍵字修飾解構函式,稱為虛解構函式;

  格式:virtual ~ClassName(){ ... }

  意義:虛解構函式用於指引 delete 運算子正確析構動態物件;(當父類指標指向子類物件時,通過父類指標去釋放所有子類的記憶體空間)

  應用場景:在賦值相容性原則中(父類指標指向子類物件),通過  delete 父類指標   去釋放所有子類的記憶體空間。(動態多型呼叫:通過父類指標所指向的實際物件去判斷如何呼叫 delete 運算子)

  !!:建議在設計基類時將解構函式宣告為虛擬函式,為的是避免記憶體洩漏,否則有可能會造成派生類記憶體洩漏問題。
  案列分析
 1 #include <iostream>
 2 #include <cstring>
 3 
 4 using namespace std;
 5 
 6 class Base
 7 {
 8 protected:
 9     char *name;
10 public:
11     Base()
12     {
13         name = new char[20];
14         strcpy(name, "Base()");
15         cout <<this << "  " << name << endl;
16     }
17     
18     ~Base()
19     {
20         cout << this << "  ~Base()" << endl;
21         delete[] name;
22     }
23 };
24 
25 
26 class Derived : public Base
27 {
28 private:
29     int *value;
30 public:
31     Derived()
32     {
33         strcpy(name, "Derived()");
34         value = new int(strlen(name));
35         cout << this << "  " << name << "  " << *value <<  endl;
36     }
37     
38     ~Derived()
39     {
40         cout << this << "  ~Derived()" << endl;
41         delete value;
42     }
43 };
44 
45 
46 int main()
47 {    
48     cout << "在賦值相容中,關於 子類物件存在記憶體洩漏的測試" << endl;
49     
50     Base* bp = new Derived();
51     cout << bp << endl;
52     // ...
53     delete bp;  // 雖然是父類指標,但析構的是子類資源
54 
55     return 0;
56 }
57 
58 /**
59  * 在賦值相容中,關於 子類物件存在記憶體洩漏的測試
60  * 0x7a1030  Base()
61  * 0x7a1030  Derived()  9
62  * 0x7a1030
63  * 0x7a1030  ~Base()
64  */
賦值相容中,子類記憶體洩漏案列
 1 #include <iostream>
 2 #include <cstring>
 3 
 4 using namespace std;
 5 
 6 class Base
 7 {
 8 protected:
 9     char *name;
10 public:
11     Base()
12     {
13         name = new char[20];
14         strcpy(name, "Base()");
15         cout <<this << "  " << name << endl;
16     }
17     
18     virtual ~Base()
19     {
20         cout << this << "  ~Base()" << endl;
21         delete[] name;
22     }
23 };
24 
25 
26 class Derived : public Base
27 {
28 private:
29     int *value;
30 public:
31     Derived()
32     {
33         strcpy(name, "Derived()");
34         value = new int(strlen(name));
35         cout << this << "  " << name << "  " << *value <<  endl;
36     }
37     
38     virtual ~Derived()
39     {
40         cout << this << "  ~Derived()" << endl;
41         delete value;
42     }
43 };
44 
45 
46 int main()
47 {
48     //Derived *dp = new Derived();
49     //delete dp; // 直接通過子類物件釋放資源不需要 virtual 關鍵字
50     
51     cout << "在賦值相容中,虛解構函式的測試" << endl;
52     
53     Base* bp = new Derived();
54     cout << bp << endl;
55     // ...
56     delete bp;  // 動態多型發生
57 
58     return 0;
59 }
60 
61 /**
62  * 在賦值相容中,虛解構函式的測試
63  * 0x19b1030  Base()
64  * 0x19b1030  Derived()  9
65  * 0x19b1030
66  * 0x19b1030  ~Derived()
67  * 0x19b1030  ~Base()
68  */
虛解構函式解決子類記憶體洩漏案列

  兩個案列的區別:第1個案列只是普通的解構函式;第2個案列是虛解構函式。

7、 關於虛擬函式的思考題

  1. 建構函式可以成為虛擬函式嗎?--- 不可以

      不可以。因為在建構函式執行結束後,虛擬函式表指標才會被正確的初始化。

   在c++的多型中,虛擬函式表是由編譯器自動生成與維護的,虛擬函式表指標是由建構函式初始化完成的,即建構函式相當於是虛擬函式的入口點,負責呼叫虛擬函式的前期工作;在建構函式執行的過程中,虛擬函式表指標有可能未被正確的初始化;由於在不同的c++編譯器中,虛擬函式表 與 虛擬函式表指標的實現有所不同,所以禁止將建構函式宣告為虛擬函式。

          

  2. 析造函式可以成為虛擬函式嗎?--- 虛擬函式,且發生多型

      可以,並且產生動態多型。因為解構函式是在物件銷燬之前被呼叫,即在物件銷燬前  虛擬函式表指標是正確指向對應的虛擬函式表。

  3. 建構函式中可以呼叫虛擬函式發生多型嗎?--- 不能發生多型

   建構函式中可以呼叫虛擬函式,但是不可能發生多型行為,因為在建構函式執行時,虛擬函式表指標未被正確初始化。

  4. 解構函式中可以呼叫虛擬函式發生多型嗎?--- 不能發生多型

      解構函式中可以呼叫虛擬函式,但是不可能發生多型行為,因為在解構函式執行時,虛擬函式表指標已經被銷燬。   

 1 #include <iostream>
 2 #include <string>
 3 
 4 using namespace std;
 5 
 6 class Base
 7 {
 8 public:
 9     Base()
10     {
11         cout << "Base()" << endl;
12         
13         func();
14     }
15     
16     virtual void func() 
17     {
18         cout << "Base::func()" << endl;
19     }
20     
21     virtual ~Base()
22     {
23         func();
24         
25         cout << "~Base()" << endl;
26     }
27 };
28 
29 
30 class Derived : public Base
31 {
32 public:
33     Derived()
34     {
35         cout << "Derived()" << endl;
36         
37         func();
38     }
39     
40     virtual void func()
41     {
42         cout << "Derived::func()" << endl;
43     }
44     
45     virtual ~Derived()
46     {
47         func();
48         
49         cout << "~Derived()" << endl;
50     }
51 };
52 
53 void test()
54 {
55     Derived d;
56 }
57 
58 int main()
59 {   
60     //棧空間
61     test();
62 
63     // 堆空間
64     //Base* p = new Derived();    
65     //delete p; // 多型發生(指標p指向子類物件,並且又有虛擬函式重寫)
66     
67     return 0;
68 }
69 /*
70 Base()
71 Base::func()
72 Derived()
73 Derived::func()
74 Derived::func()
75 ~Derived()
76 Base::func()
77 ~Base()
78 */
構造與析構中呼叫虛擬函式案列

  結論:在建構函式與解構函式中呼叫虛擬函式不能發生多型行為,只調用當前類中定義的函式版本! !

8、純虛擬函式、抽象類、介面

  1.  定義 --- 以案例的方式說明

  想必大家很熟悉,對於任何一個普通類來說都可以例項化出多個物件,也就是每個物件都可以用對應的類來描述,並且這些物件在現實生活中都能找到各自的原型;比如現在有一個“狗類