一,多態的理論推導
1.類型兼容性原則
在上一節的C++中的繼承中介紹了什麼是類型兼容性原則。所謂的類型兼容性原則是指子類公有繼承自父類時,包含了父類的所有屬性和方法,因此父類所能完成的功能,使用子類也可以替代完成,子類是一種特殊的父類。所以可以使用子類對象初始化父類對象,可以用父類指針指向子類對象,可以用父類引用來引用子類對象。
2.函數的重寫
函數的發生在類的繼承過程中,所謂的函數的重寫是指在繼承中,子類定義了與父類函數原型相同的函數,即定義了和父類中一樣的函數。
3.類型兼容性原則遇上函數的重寫
# include<iostream>using namespace std;/* 定義父類 */class Parent
{public: /* 定義print函數 */
void print()
{
cout << "Parent print()函數" << endl;
}
};/* 定義子類繼承自父類,並重寫父類的print函數 */class Child :public Parent
{public: /* 重寫父類的print函數 */
void print()
{
cout << "Child print()函數" << endl;
}
};int main()
{
Child c; /* 調用子類對象的print函數,打印子類的print函數 */
c.print(); /* 通過使用作用域操作符調用父類的print函數,打印父類的print函數 */
c.Parent::print(); /* 當我們使用類型兼容性原則的時候,發現調用的函數是父類的print函數,這是符合編譯器規則的 */
Parent p1 = c;
p1.print();
Parent * p2 = &c;
p2->print();
Parent& p3 = c;
p3.print(); return 0;
}
輸出結果:
4.類型兼容性原則遇上函數的重寫的總結
父類中被子類重寫的函數依然存在於子類中,我們可以通過作用域操作符調用父類的函數。
子類重寫父類的函數,調用子類對象時是調用的被重寫的函數。
在C++編譯期間,編譯器不知道父類指針指向的是一個什麼對象,編譯器認爲最安全的方法就是調用父類對象的函數。
5.靜態聯編和動態聯編
所謂的聯編就是指一個程序模塊、代碼之間互相關聯的過程。
靜態聯編是程序的匹配、鏈接在編譯階段實現,也稱爲早期匹配,函數的重載使用的是靜態聯編。
動態聯編是指程序聯編推遲到運行時進行,所以又稱爲晚期聯編(遲綁定),switch和if語句是動態聯編的典型例子。
6.類型兼容性原則和函數重寫所帶來的問題
首先編譯器的這種做法是我們所不期望的。
我們需要的是根據實際的對象類型,來調用實際的函數。
如果父類指針(引用)指向(引用)的是父類對象,則調用父類對象的函數。
如果父類指針(引用)指向(引用)的是子類對象,則調用子類對象的函數。
7.針對上述問題的解決
針對上面的問題,C++提供了一套解決方案來實現上述我們的期望,新航道託福通過使用virtual關鍵字來修飾被重寫的函數後,即可以實現我們上述的問題。
8.多態的代碼示例
# include<iostream>using namespace std;/* 定義父類 */class Parent
{public: /* 定義print函數,使用virtual關鍵字修飾 */
virtual void print()
{
cout << "Parent print()函數" << endl;
}
};/* 定義子類繼承自父類,並重寫父類的print函數 */class Child :public Parent
{public: /* 重寫父類的print函數 */
virtual void print()
{
cout << "Child print()函數" << endl;
}
};int main()
{
Child c; /* 調用子類對象的print函數,打印子類的print函數 */
c.print(); /* 通過使用作用域操作符調用父類的print函數,打印父類的print函數 */
c.Parent::print(); /* 調用父類對象的函數發現當父類指針(引用)指向(引用)子類對象時,調用的是子類對象的函數,元素除外 */
Parent p1 = c;
p1.print();
Parent * p2 = &c;
p2->print();
Parent& p3 = c;
p3.print(); return 0;
}
輸出結果:
9.多態案例的分析
使用virtual關鍵字的函數被重寫後就會根據實際的對象類型指向對應的函數。
所謂的多態就是指同樣的調用語句會有多種不同的表現狀態。
父類指針指向子類對象和父類對象引用子類對象纔可以實現所謂的多態,而父類元素被子類對象初始化是不展示多態特性的。
10.多態成立的條件
存在繼承關係。
父類函數爲virtual函數,並且子類重寫父類的virtual函數。
存在父類的指針(引用)指向(引用)子類對象。
二,多態的原理探究
1.多態原理基礎知識
當類中聲明瞭虛函數(即virtual關鍵字修飾的函數)後,編譯器會根據類生成一張虛函數表。
虛函數表是一張用來存儲類中虛函數指針的表,虛函數表由編譯器自動生成和維護。
當類中存在虛函數時,每個對象都會存在一個指向虛函數表的vptr指針。雅思詞彙編譯器會給父類對象,子類對象提前佈局vptr指針,當調用對應的函數時,會根據vptr指針所指向的虛函數表來查找相應的函數並調用。
vptr指針是指向虛函數表的指針,vptr指針一般作爲類對象的第一個成員。
2.多態實現原理圖示
3.多態原理說明
通過虛函數表的指針vptr在運行時調用相應的虛函數表中的函數,因此多態是動態聯編。根據vptr尋找虛函數表是尋址操作,找到後再調用相應的函數。而普通的成員函數則是在函數編譯時就已經確定了要調用的函數,因此在執行效率上虛函數要慢許多,所以出於效率的考慮,沒有必要將類中的所有成員函數聲明爲虛函數。
C++編譯器在運行期間,不需要區分對象時子類對象還是父類對象,而只是 用相應的vptr指針來調用相應的函數而已,因此造成了這種虛假的多態現象。
4.vptr指針的存在證明
# include<iostream>using namespace std;class Test1
{public: /* 虛函數 */
virtual void test()
{
cout << "vptr指針的存在證明" << endl;
}
};class Test2
{public: /* 普通成員函數 */
void test()
{
cout << "vptr指針的存在證明" << endl;
}
};int main()
{
Test1 t1;
Test2 t2;
cout << "Test1 sizeof = " << sizeof(t1) << endl;
cout << "Test1 sizeof = " << sizeof(t2) << endl; return 0;
}
輸出結果:
我們發現含有虛函數的類的對象包含了4個字節,說明存在一個vptr指針,因爲指針大小即4個字節。
5.vptr指針的創建時機
vptr指針是在對象構造函數結束之後才創建的,然後指向虛函數表。
三,虛析構函數
1.虛析構函數的作用
當我們在開發父類的時候,通常會把父類的析構函數聲明爲虛函數,因爲在繼承中,當我們delete釋放內存的時候,子類的對象的析構函數不會去執行,我們需要將父類的析構函數顯式的聲明爲虛函數纔會讓子類的析構函數去調用執行。
2.案例演示
# include<iostream>using namespace std;class MyParent
{public:
MyParent()
{
cout << "MyParent構造函數" << endl;
} /* 父類的析構函數一般聲明爲虛析構函數 */
virtual~MyParent()
{
cout << "MyParent析構函數" << endl;
}
};class MyChild:public MyParent
{public:
MyChild()
{
cout << "MyChild構造函數" << endl;
} ~MyChild()
{
cout << "MyChild析構函數" << endl;
}
};int main()
{ /* 如果刪除父類的虛析構函數,則子類的析構函數不會被調用執行 */
MyParent * p = new MyChild; delete p; return 0;
}
四,函數的重載和重寫的區別
1.函數的重載
必須在同一個類中。
子類無法重載父類的函數,父類的同名函數會被子類名稱相同的函數覆蓋,但不會發生重載。
重載是編譯期間根據函數的參數個數和參數類型決定調用哪個函數。
2.函數的重寫
函數的重寫必須發生在繼承中。
父類和子類的函數原型必須相同。
使用virtual關鍵字的父類函數才能成爲函數的重寫,否則叫做函數的重定義。
函數的重寫調用時是在運行期間調用,屬於動態聯編,是根據實際對象的vptr指針所指向的虛函數表決定調用哪個函數。