C++之多态
1.問題引出
子類定義了與父類中原型相同的函數會發生什么?
- 父類指針/引用指向父類對象和子類對象
結論:當父類指針/引用指向子類對象的時候,如果有同名函數,默認調用父類的成員函數
- 父類指針/引用指向父類對象且子類指針/引用指向子類對象
結論:子類指針/引用指向子類對象的時候,如果有同名函數,子類函數會將父類函數覆蓋
要通過子類對象調用被覆蓋的同名父類成員函數需要顯示的加上父類名和作用域解析符。c1.Parent::func();
引出了一個矛盾:當賦值兼容性原則(父類指針/引用指向子類對象)和函數重寫(父類和子類有相同函數原型的成員函數)發生在一起,父類指針/引用只會調用父類成員函數。
C/C++是靜態編譯型語言
在編譯時,編譯器自動根據指針的類型判斷指向的是一個什么樣的對象
現象產生的原因
賦值兼容性原則遇上函數重寫 出現的一個現象
1 沒有理由報錯
2 對被調用函數來講,在編譯器編譯期間,我就確定了,這個函數的參數是p,是Parent類型的。。。
3 靜態鏈編
1、在編譯此函數的時,編譯器不可能知道指針 p 究竟指向了什么。
2、編譯器沒有理由報錯。
3、于是,編譯器認為最安全的做法是編譯到父類的print函數,因為父類和子類肯定都有相同的print函數。
面向對象新需求
- 根據實際的對象類型來判斷重寫函數的調用
- 如果父類指針指向的是父類對象則調用父類中定義的函數
2.解決方案
- C++中通過virtual關鍵字對多態進行支持
- 使用virtual聲明的函數被重寫后即可展現多態特性
實際案例
#include <iostream> using namespace std;//HeroFighter AdvHeroFighter EnemyFighterclass HeroFighter { public:virtual int power() //C++會對這個函數特殊處理{return 10;} };class EnemyFighter { public:int attack(){return 15;} };class AdvHeroFighter : public HeroFighter { public:virtual int power(){return 20;} };class AdvAdvHeroFighter : public HeroFighter { public:virtual int power(){return 30;} };//多態威力 //1 PlayObj給對象搭建舞臺 看成一個框架 //15:20 void PlayObj(HeroFighter *hf, EnemyFighter *ef) {//不寫virtual關鍵字 是靜態聯編 C++編譯器根據HeroFighter類型,去執行 這個類型的power函數 在編譯器編譯階段就已經決定了函數的調用//動態聯編: 遲綁定: //在運行的時候,根據具體對象(具體的類型),執行不同對象的函數 ,表現成多態.if (hf->power() > ef->attack()) //hf->power()函數調用會有多態發生{printf("主角win\n");}else{printf("主角掛掉\n");} }//多態的思想 //面向對象3大概念 //封裝: 突破c函數的概念....用類做函數參數的時候,可以使用對象的屬性 和對象的方法 //繼承: A B 代碼復用 //多態 : 可以使用未來...//多態很重要 //實現多態的三個條件 //C語言 間接賦值 是指針存在的最大意義 //是c語言的特有的現象 (1 定義兩個變量 2 建立關聯 3 *p在被調用函數中去間接的修改實參的值)//實現多態的三個條件 //1 要有繼承 //2 要有虛函數重寫 //3 用父類指針(父類引用)指向子類對象....void main() {HeroFighter hf;AdvHeroFighter Advhf;EnemyFighter ef;AdvAdvHeroFighter advadvhf;PlayObj(&hf, &ef);PlayObj(&Advhf, &ef);PlayObj(&advadvhf, &ef) ; //這個框架 能把我們后來人寫的代碼,給調用起來cout<<"hello..."<<endl;system("pause");} void main1401() {HeroFighter hf;AdvHeroFighter Advhf;EnemyFighter ef;if (hf.power() > ef.attack()){printf("主角win\n");}else{printf("主角掛掉\n");}if (Advhf.power() > ef.attack()){printf("Adv 主角win\n");}else{printf("Adv 主角掛掉\n");}cout<<"hello..."<<endl;system("pause");return ; }3.工程意義
多態的思想
面向對象3大概念
封裝: 突破c函數的概念….用類做函數參數的時候,可以使用對象的屬性 和對象的方法
繼承: A B 代碼復用
多態: 可以使用未來…
4.成立條件
C語言 間接賦值 是指針存在的最大意義
是c語言的特有的現象 (1 定義兩個變量 2 建立關聯 3 *p在被調用函數中去間接的修改實參的值)
實現多態的三個條件
- 1 要有繼承
- 2 要有虛函數重寫
- 3 用父類指針(父類引用)指向子類對象….
多態是設計模式的基礎,多態是框架的基礎
5.理論基礎
- 聯編是指一個程序模塊、代碼之間互相關聯的過程。
- 靜態聯編(static binding),是程序的匹配、連接在編譯階段實現, 也稱為早期匹配。
- 重載函數使用靜態聯編。
- 動態聯編是指程序聯編推遲到運行時進行,所以又稱為晚期聯編(遲綁定)。
- switch 語句和 if 語句是動態聯編的例子。
理論聯系實際
2、在編譯時,編譯器自動根據指針的類型判斷指向的是一個什么樣的對象;所以編譯器認為父類指針指向的是父類對象。
3、由于程序沒有運行,所以不可能知道父類指針指向的具體是父類對象還是子類對象
從程序安全的角度,編譯器假設父類指針只指向父類對象,因此編譯的結果為調用父類的成員函數。這種特性就是靜態聯編。
6.本質剖析
6.1 多態實現原理
- 當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
- 虛函數表是一個存儲類成員函數指針的數據結構
- 虛函數表是由編譯器自動生成與維護的
- virtual成員函數會被編譯器放入虛函數表中
- 當存在虛函數時,每個對象中都有一個指向虛函數表的指針(C++編譯器給父類對象、子類對象提前布局vptr指針;當進行howToPrint(Parent *base)函數時,C++編譯器不需要區分子類對象或者父類對象,只需要再base指針中,找vptr指針即可。)
- VPTR一般作為類對象的第一個成員
說明1:
通過虛函數表指針VPTR調用重寫函數是在程序運行時進行的,因此需要通過尋址操作才能確定真正應該調用的函數。而普通成員函數是在編譯時就確定了調用的函數。在效率上,虛函數的效率要低很多。
說明2:
出于效率考慮,沒有必要將所有成員函數都聲明為虛函數
說明3 :C++編譯器,執行HowToPrint函數,不需要區分是子類對象還是父類對象.只需要根據父類對象指針找到VPTR成員指針,再通過虛函數表找到實際對應的成員函數即可。
#include <iostream> using namespace std;//多態成立的三個條件 //要有繼承 虛函數重寫 父類指針指向子類對象 class Parent { public:Parent(int a=0){this->a = a;}virtual void print() //1 動手腳 寫virtal關鍵字 會特殊處理 //虛函數表{cout<<"我是爹"<<endl;}virtual void print2() //1 動手腳 寫virtal關鍵字 會特殊處理 //虛函數表{cout<<"我是爹"<<endl;} private:int a; };class Child : public Parent { public:Child(int a = 0, int b=0):Parent(a){this->b = b;}virtual void print(){cout<<"我是兒子"<<endl;} private:int b; };void HowToPlay(Parent *base) {base->print(); //有多態發生 //2 動手腳 //效果:傳來子類對 執行子類的print函數 傳來父類對執行父類的print函數 //C++編譯器根本不需要區分是子類對象 還是父類對象//父類對象和子類對象分步有vptr指針 , ==>虛函數表===>函數的入口地址//遲綁定 (運行時的時候,c++編譯器才去判斷) }void main01() {Parent p1; //3 動手腳 提前布局 //用類定義對象的時候 C++編譯器會在對象中添加一個vptr指針 Child c1; //子類里面也有一個vptr指針HowToPlay(&p1);HowToPlay(&c1);cout<<"hello..."<<endl;system("pause");return ; }6.2 證明VPTR的存在
利用sizeof運算符判斷有無virtual關鍵字的類的大小。
#include <iostream> using namespace std;class A { public:void printf(){cout<<"aaa"<<endl;} protected: private:int a; };class B { public:virtual void printf(){cout<<"aaa"<<endl;} protected: private:int a; };void main() {//加上virtual關鍵字 c++編譯器會增加一個指向虛函數表的指針 。。。printf("sizeof(a):%d, sizeof(b):%d \n", sizeof(A), sizeof(B));cout<<"hello..."<<endl;system("pause");return ; }6.3 構造函數中調用虛函數
這個問題實際上就是VPTR指針的分步初始化問題。
- 對象在創建的時,由編譯器對VPTR指針進行初始化
- 只有當對象的構造完全結束后VPTR的指向才最終確定
- 父類對象的VPTR指向父類虛函數表
- 子類對象的VPTR指向子類虛函數表
7.面試題集錦
7.1 關于函數重載、重寫、重定義
函數重載
- 必須在同一個類中進行
- 子類無法重載父類的函數,父類同名函數將被名稱覆蓋
- 重載是在編譯期間根據參數類型和個數決定函數調用
- 靜態聯編
函數重寫
- 必須發生于父類與子類之間
- 并且父類與子類中的函數必須有完全相同的原型
- 使用virtual聲明之后能夠產生多態(如果不使用virtual,那叫重定義)
- 多態是在運行期間根據具體對象的類型決定函數調用
父類和子類有相同的函數名、變量名出現,發生名稱覆蓋(子類的函數名,覆蓋了父類的函數名。)
子類和父類的同名函數絕對不可能重載,如果原型不是完全相同則不屬于重寫和重定義,他們之間的關系只能說是函數覆蓋。
#include <iostream> using namespace std;//重寫 重載 重定義 //重寫發生在2個類之間 //重載必須在一個類之間//重寫分為2類 //1 虛函數重寫 將發生多態 //2 非虛函數重寫 (重定義)class Parent {//這個三個函數都是重載關系 public: void abc(){printf("abc");}virtual void func() {cout<<"func() do..."<<endl;}virtual void func(int i){cout<<"func() do..."<<i<<endl;}virtual void func(int i, int j){cout<<"func() do..."<<i<< " "<<j<<endl;}virtual void func(int i, int j, int m , int n){cout<<"func() do..."<<i<< " "<<j<<endl;} protected: private: };class Child : public Parent {public: void abc(){printf("child abc");}/*void abc(int a){printf("child abc");}*/virtual void func(int i, int j){cout<<"func(int i, int j) do..."<<i<< " "<<j<<endl;}virtual void func(int i, int j, int k){cout<<"func(int i, int j) do.."<< endl; } protected: private: };//重載重寫和重定義 void main() {//: error C2661: “Child::func”: 沒有重載函數接受 0 個參數Child c1;//c1.func();//子類無法重載父類的函數,父類同名函數將被名稱覆蓋c1.Parent::func();//1 C++編譯器 看到func名字 ,因子類中func名字已經存在了(名稱覆蓋).所以c++編譯器不會去找父類的4個參數的func函數//2 c++編譯器只會在子類中,查找func函數,找到了兩個func,一個是2個參數的,一個是3個參數的.//3 C++編譯器開始報錯..... error C2661: “Child::func”: 沒有重載函數接受 4 個參數//4 若想調用父類的func,只能加上父類的域名..這樣去調用..c1.func(1, 3, 4, 5);//c1.func();//func函數的名字,在子類中發生了名稱覆蓋;子類的函數的名字,占用了父類的函數的名字的位置//因為子類中已經有了func名字的重載形式。。。。//編譯器開始在子類中找func函數。。。。但是沒有0個參數的func函數 cout<<"hello..."<<endl;system("pause");return ; }7.2 為什么定義虛析構函數
在什么情況下應當聲明虛函數
- 構造函數不能是虛函數。建立一個派生類對象時,必須從類層次的根開始,沿著繼承路徑逐個調用基類的構造函數
- 析構函數可以是虛的。虛析構函數用于指引 delete 運算符正確析構動態對象
7.3 父類和子類指針的步長
1) 鐵律1:指針也只一種數據類型,C++類對象的指針p++/–,仍然可用。
2) 指針運算是按照指針所指的類型進行的。
p++《=》p=p+1 //p = (unsigned int)basep + sizeof(*p) 步長。
3) 結論:父類p++與子類p++步長不同;不要混搭,不要用父類指針++方式操作數組。
7.4 關于多態的理解
- 多態的實現效果
多態:同樣的調用語句有多種不同的表現形態; - 多態實現的三個條件
有繼承、有virtual重寫、有父類指針(引用)指向子類對象。 - 多態的C++實現
virtual關鍵字,告訴編譯器這個函數要支持多態;不是根據指針類型判斷如何調用;而是要根據指針所指向的實際對象類型來判斷如何調用 - 多態的理論基礎
動態聯編PK靜態聯編。根據實際的對象類型來判斷重寫函數的調用。 - 多態的重要意義
設計模式的基礎 是框架的基石。可以將未來的代碼適用于以前開發的框架。 - 實現多態的本質
函數指針(虛函數表指針VPTR)做函數參數
C函數指針是C++至高無上的榮耀。C函數指針一般有兩種用法(正、反)。
7.5 C++編譯器是如何實現多態
- 當類中聲明虛函數時,編譯器會在類中生成一個虛函數表
- 虛函數表是一個存儲類成員函數指針的數據結構
- 虛函數表是由編譯器自動生成與維護的
- virtual成員函數會被編譯器放入虛函數表中
- 當存在虛函數時,每個對象中都有一個指向虛函數表的指針(C++編譯器給父類對象、子類對象提前布局vptr指針;當進行howToPrint(Parent *base)函數是,C++編譯器不需要區分子類對象或者父類對象,只需要再base指針中,找vptr指針即可。)
- VPTR一般作為類對象的第一個成員
7.6 類的每個成員函數是否都可以聲明為虛函數,為什么?
通過虛函數表指針VPTR調用重寫函數是在程序運行時進行的,因此需要通過尋址操作才能確定真正應該調用的函數。而普通成員函數是在編譯時就確定了調用的函數。在效率上,虛函數的效率要低很多。
出于效率考慮,沒有必要將所有成員函數都聲明為虛函數
7.7 構造函數中調用虛函數能實現多態嗎?為什么?
vptr指針的初始化是分步驟完成的,所以不能實現多態。
7.8 虛函數表指針(VPTR)被編譯器初始化的過程,你是如何理解的?
1.對象在創建的時,如果對象所屬的類中有虛函數,則編譯器會自動為該對象創建VPTR指針,并對VPTR指針進行初始化
2.只有當對象的構造完全結束后VPTR的指向才最終確定
3.父類對象的VPTR指向父類虛函數表
4.子類對象的VPTR指向子類虛函數表
虛函數表是在編譯期間就創建了的!編譯器一旦檢測到類里面聲明了虛函數,則為該類創建一個屬于該類的虛函數表。
當定義一個父類對象的時候比較簡單,因為父類對象的VPTR指針直接指向父類虛函數表。
但是當定義一個子類對象的時候就比較麻煩了,因為構造子類對象的時候會首先調用父類的構造函數然后再調用子類的構造函數。當調用父類的構造函數的時候,此時會創建Vptr指針(也可以認為Vptr指針是屬于父類的成員,所以在子類中重寫虛函數的時候virtual關鍵字可以省略,因為編譯器會識別父類有虛函數,然后就會生成Vptr指針變量),該指針會指向父類的虛函數表;然后再調用子類的構造函數,此時Vptr又被賦值指向子類的虛函數表。
上面的過程是Vptr指針初始化的過程。
這是因為這個原因,在構造函數中調用虛函數不能實現多態。
總結
- 上一篇: shownews.php,newssho
- 下一篇: C++之构造函数和析构函数强化