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 bytesvptr指標的證明
顯然,在普通的類中,類的大小 == 成員變數的大小;在有虛擬函式的類中,類的大小 == 成員變數的大小 + 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. 定義 --- 以案例的方式說明
想必大家很熟悉,對於任何一個普通類來說都可以例項化出多個物件,也就是每個物件都可以用對應的類來描述,並且這些物件在現實生活中都能找到各自的原型;比如現在有一個“狗類