目錄
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 = https://www.cnblogs.com/nbk-zyc/p/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 = https://www.cnblogs.com/nbk-zyc/p/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. 定義 --- 以案例的方式說明
想必大家很熟悉,對于任何一個普通類來說都可以實體化出多個物件,也就是每個物件都可以用對應的類來描述,并且這些物件在現實生活中都能找到各自的原型;比如現在有一個“狗類??”,我們就可以用這個“狗類??”實體化出很多只“狗??”,但是,在面向物件分析時,還會發現一些抽象的概念,它描述的是一類事物,并不能反映一個具體的實物,我們把這種包含抽象概念的現象稱為 抽象類,關于抽象類的例子有很多,比如:如何計算一個“圖形”的面積;什么“寵物”最可愛 等等,了解了抽象類之后,那么什么是純虛函式呢?我們現在就以 如何計算一個“圖形”的面積 這個抽象類案列說明問題;在這個例子中有2個抽象概念,分別是 “圖形” 與 “面積”,即什么樣“圖形” --- 不知道,如何”求面積“或者“面積公式”是什么 --- 也不知道;在這里,我們可以把”圖形“看成是抽象類的類名,”面積“看成是抽象類的成員函式,因為這個成員函式無法實作,只是讓外界知道有這么一回事,此處的成員函式就可以看成 純虛函式,同時,此處的抽象類也可以看成是 介面,
2. 特點
純虛函式:
(1)只在基類中宣告虛函式,并不需要在基類中定義函式體,語法格式:virtual void funtion1()=0;
(2)“=0”是告訴編譯器當前是宣告純虛函式,因此并不需要定義函式體,
(3)純虛函式被實作后成為虛函式;
(4)基類中的純虛函式就是個介面,純虛函式不能被呼叫,它的存在只是為了在派生類中重新實作該方法;
(5)c++ 規定虛解構式必須包含宣告與實作(在物件銷毀前,基類中的解構式最后一個被呼叫,若此時沒有對應的函式實作,顯然是不行的);
抽象類:
(1)用于表示現實世界中的抽象概念;
(2)是一種只能定義型別,而不能創建物件的類;但是,可以有抽象類指標 或 介面類指標,當它指向子類物件時就會發生多型;
(3)抽象類只能用作父類被繼承,子類必須實作純虛函式的具體功能;
(4)c++語言中沒有抽象類的概念,但是可以通過純虛函式實作抽象類;
(5)一個c++類中存在純虛函式就成為了抽象類;(判斷條件)
(6)如果子類沒有實作純虛函式,則子類成為抽象類,
介面:
(1)類中沒有定義任何的成員變數;
(2)所有的成員函式都是公有的純虛函式;(判斷條件 1 + 2)
(3)介面是一種特殊的抽象類;
一個類全是純虛函式就是介面;
一個類部分是純虛函式就是抽象類;
3. 引入原因
(1)為了方便使用多型特性,我們常常需要在基類中宣告純虛函式,
(2)在很多情況下,基類本身生成物件是不合情理的,例如,動物作為一個基類可以派生出老虎、孔雀等子類,但動物本身生成物件明顯不合常理,
所以,為了解決上述問題,引入了純虛函式的概念;將基類的成員函式宣告為純虛函式,則編譯器要求必須在派生類中重寫該成員函式以實作多型性,
1 #include <iostream> 2 #include <typeinfo> 3 4 using namespace std; 5 6 class Shape 7 { 8 public: 9 virtual double area() = 0; 10 }; 11 12 class Rect : public Shape 13 { 14 int ma; 15 int mb; 16 public: 17 Rect(int a, int b) 18 { 19 ma = a; 20 mb = b; 21 } 22 double area() 23 { 24 return ma * mb; 25 } 26 }; 27 28 class Circle : public Shape 29 { 30 int mr; 31 public: 32 Circle(int r) 33 { 34 mr = r; 35 } 36 double area() 37 { 38 return 3.14 * mr * mr; 39 } 40 }; 41 42 void area(Shape* p) 43 { 44 const type_info &tis = typeid(*p); 45 46 if( tis == typeid(Rect) ) 47 { 48 Rect *rect = dynamic_cast<Rect*>(p); 49 50 cout << "the area of the Rect : " << rect->area() << endl; 51 } 52 53 if( tis == typeid(Circle) ) 54 { 55 Circle *circle = dynamic_cast<Circle*>(p); 56 57 cout << "the area of the Circle : " << circle->area() << endl; 58 } 59 60 } 61 62 int main() 63 { 64 Rect rect(1, 2); 65 Circle circle(10); 66 67 area(&rect); 68 area(&circle); 69 70 return 0; 71 } 72 /** 73 * 運行結果: 74 * the area of the Rect : 2 75 * the area of the Circle : 314 76 */如何使用抽象類計算一個"圖形"的面積
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/42991.html
標籤:C++
上一篇:第十章 C++11新特性
下一篇:C語言中的宏定義
