目錄
背景介紹
虛函數重寫:子類重新定義父類中有相同返回值、名稱和參數的虛函數;
非虛函重寫:子類重新定義父類中有相同名稱和參數的非虛函數;
父子間的賦值兼容:子類對象可以當作父類對象使用(兼容性);具體表現為:
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指針大小。
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. 定義 --- 以案例的方式說明
想必大家很熟悉,對於任何一個普通類來說都可以實例化出多個對象,也就是每個對象都可以用對應的類來描述,並且這些對象在現實生活中都能找到各自的原型;比如現在有一個“狗類🐶”,我們就可以用這個“狗類🐶”實例化出很多只“狗🐶”。但是,在面向對象分析時,還會發現一些抽象的概念,它描述的是一類事物,並不能反映一個具體的實物,我們把這種包含抽象概念的現象稱為 抽象類。關於抽象類的例子有很多,比如:如何計算一個“圖形”的面積;什么“寵物”最可愛 等等。了解了抽象類之后,那么什么是純虛函數呢?我們現在就以 如何計算一個“圖形”的面積 這個抽象類案列說明問題;在這個例子中有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 */