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