什么是多態(tài)性操禀?
多態(tài):相同對象收到不同消息或不同對象收到相同消息時產(chǎn)生不同的動作隧枫。C++支持兩種多態(tài)性:編譯時多態(tài)性纬乍,運行時多態(tài)性。
- 編譯時多態(tài)性:通過重載函數(shù)實現(xiàn)
- 運行時多態(tài)性:通過虛函數(shù)實現(xiàn)锁孟。
早綁定 vs 晚綁定
多態(tài)與非多態(tài)的實質(zhì)區(qū)別就是函數(shù)地址是早綁定(靜態(tài)多態(tài))還是晚綁定(動態(tài)多態(tài))彬祖。如果函數(shù)的調(diào)用,在編譯器編譯期間就可以確定函數(shù)的調(diào)用地址品抽,并生產(chǎn)代碼涧至,是靜態(tài)的,就是說地址是早綁定的桑包。而如果函數(shù)調(diào)用的地址不能在編譯器期間確定,需要在運行時才確定纺非,這就屬于晚綁定哑了。
當(dāng)類之間存在層次結(jié)構(gòu)赘方,并且類之間是通過繼承關(guān)聯(lián)時,就會用到多態(tài)弱左。
C++ 多態(tài)意味著調(diào)用成員函數(shù)時窄陡,會根據(jù)調(diào)用函數(shù)的對象的類型來執(zhí)行不同的函數(shù)。簡單概括為“一個接口拆火,多個方法”跳夭。
對于同一條命名,不同對象接到后所做出的動作是不同的们镜,這就叫多態(tài)币叹。
#include<iostream>
#include<stdlib.h>
#include "Circle.h"
#include "Rect.h"
using namespace std;
int main()
{
Shape *shape1 = new Rect(3, 6);
Shape *shape2 = new Circle(5);
shape1->calcArea();
shape2->calcArea();
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
system("pause");
return 0;
}
普通虛函數(shù) vs 虛析構(gòu)函數(shù)
普通虛函數(shù):在類的成員函數(shù)前加virtual關(guān)鍵字,并在派生類中重新定義的成員函數(shù)模狭,其作用是:當(dāng)父類和子類定義有相同的成員函數(shù)颈抚,用父類的指針指向子類的對象時能夠調(diào)用子類的該成員函數(shù)。因為若不加virtual關(guān)鍵字嚼鹉,那么調(diào)用的就是父類中與之同名的成員函數(shù)贩汉。
(與之區(qū)別的是隱藏,隱藏發(fā)生在子類的對象調(diào)用子類中的成員函數(shù)時而隱藏掉其父類中同名的成員函數(shù)锚赤。)
虛析構(gòu)函數(shù):在析構(gòu)函數(shù)前加virtual關(guān)鍵字匹舞,其作用是:用父類的指針指向子類的對象并在子類中開辟一段內(nèi)存時,在銷毀內(nèi)存時由于delete后面跟的是父類的對象线脚,故只調(diào)用父類的析構(gòu)函數(shù)而不調(diào)用子類的析構(gòu)函數(shù)赐稽,從而使得子類中的內(nèi)存無法釋放。而虛析構(gòu)函數(shù)就解決了這種內(nèi)存泄漏問題酒贬。
#ifndef SHAPE_H
#define SHAPE_H
class Shape
{
public:
Shape();
virtual ~Shape();//虛析構(gòu)函數(shù)
virtual double calcArea();//加virtual關(guān)鍵字又憨,虛函數(shù)
};
#endif
多態(tài)中容易出現(xiàn)的一個問題就是:內(nèi)存泄漏
若構(gòu)造函數(shù)里沒有從堆中申請內(nèi)存,那么也就不需要在析構(gòu)函數(shù)中進行釋放內(nèi)存操作锭吨,也就是說若構(gòu)造函數(shù)什么也不做(只是打印刷存在感)的話蠢莺,調(diào)用與不調(diào)用都一樣
class Circle :public Shape
{
public:
Circle(double r );
~Circle();
virtual double calcArea();
protected:
double m_dR;
Coordinate m_pCenter;//在Circle類中定義一個指向圓心坐標的指針
};
在Circle類中定義一個指向圓心坐標的指針,則需要在Circle類的構(gòu)造函數(shù)中申請內(nèi)存零如,并且在析構(gòu)函數(shù)中釋放內(nèi)存躏将,從而保證內(nèi)存不泄漏。
當(dāng)用父類指針指向子類對象并對操作子類對象的虛函數(shù)時考蕾,是沒問題的祸憋,但想借助父類指針去銷毀子類對象的內(nèi)存時就會出現(xiàn)問題,因為delete后面如果跟的是父類的指針肖卧,則只會調(diào)用父類的析構(gòu)函數(shù)蚯窥,若跟的是子類的指針,則既調(diào)用子類的析構(gòu)函數(shù)也調(diào)用父類的析構(gòu)函數(shù)。
而對于多態(tài)問題拦赠,就是用父類的指針去指向子類對象巍沙,并對子類對象進行操作,但釋放的是父類的指針荷鼠,此時并不調(diào)用子類的析構(gòu)函數(shù)句携,也就無法釋放子類中申請的內(nèi)存,造成內(nèi)存泄漏允乐。
為解決此問題就要用到虛析構(gòu)函數(shù)
即用virtual去修飾析構(gòu)函數(shù)(注意是在父類的析構(gòu)函數(shù)前加virtual矮嫉,子類析構(gòu)函數(shù)可加可不加,不加時系統(tǒng)會自動加上牍疏,但推薦都加上蠢笋,以防子類被其他類繼承變成新的父類)
class Shape
{
public:
Shape();
virtual ~Shape();//虛析構(gòu)函數(shù)
virtual double calcArea();
};
Circle::Circle(double r)
{
m_pCenter = new Coordinate(x, y);
m_dR = r;
cout << "Circle()" << endl;
}
Circle::~Circle()
{
delete m_pCenter;
m_pCenter = NULL;
cout << "~Circle()" << endl;
}
virtual在修飾函數(shù)時的一些限制:
- 不能修飾普通函數(shù)(全局函數(shù)),也即該函數(shù)必須是某個類的成員函數(shù)麸澜,否則編譯出錯
- 不能修飾靜態(tài)函數(shù)挺尿,
- 不能修飾內(nèi)聯(lián)函數(shù),若virtual修飾inline炊邦,則系統(tǒng)會忽略掉inline關(guān)鍵字编矾,使內(nèi)聯(lián)函數(shù)變成虛函數(shù),
- 不能修飾構(gòu)造函數(shù)
函數(shù)指針
函數(shù)的本質(zhì)就是一段二進制的代碼寫進內(nèi)存中馁害≌危可以通過一個指針指向這段代碼的開頭,計算機就會從開頭一直執(zhí)行下去碘菜,直到結(jié)尾凹蜈。
先定義一個Shape類
class Shape
{
public:
virtual double calcArea()
{
return 0;
}
protected:
int m_iEdge;
};
再定義一個Circle子類,其中并沒有定義計算面積的函數(shù),而是利用Shape中的虛函數(shù)來計算面積忍啸。那么此時虛函數(shù)是如何來實現(xiàn)的呢仰坦?為了弄懂這個問題,先看一個概念:虛函數(shù)表
class Circle :public Shape
{
public:
Circle(double r);
private :
double m_dR;
};
虛函數(shù)表
C++中的虛函數(shù)的作用主要是實現(xiàn)了多態(tài)的機制计雌。關(guān)于多態(tài)悄晃,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調(diào)用實際子類的成員函數(shù)凿滤。這種技術(shù)可以讓父類的指針有“多種形態(tài)”妈橄,這是一種泛型技術(shù)。所謂泛型技術(shù)翁脆,說白了就是試圖使用不變的代碼來實現(xiàn)可變的算法眷蚓。比如:模板技術(shù),RTTI技術(shù)反番,虛函數(shù)技術(shù)等沙热。
覆蓋 vs 隱藏 vs 重載
- 隱藏:當(dāng)父類與子類出現(xiàn)同名的函數(shù)叉钥,實例化子類對象,并去調(diào)用該函數(shù)時只會調(diào)用子類中定義的函數(shù)而不會去調(diào)用父類中的同名函數(shù)篙贸,這就叫函數(shù)的隱藏(即指派生類的函數(shù)屏蔽了與其同名的基類函數(shù))沼侣。當(dāng)然,若一定要調(diào)用父類中的同名函數(shù)只需要在函數(shù)名前加上父類名就可以了歉秫。
- 覆蓋:C++多態(tài)性是通過虛函數(shù)來實現(xiàn)的,虛函數(shù)允許子類重新定義成員函數(shù)养铸,而子類重新定義父類的做法稱為覆蓋(override)雁芙,或者稱為重寫。重寫的話可以有兩種钞螟,直接重寫成員函數(shù)(比如隱藏就是這種情況)和重寫虛函數(shù)(虛函數(shù)一般定義在父類中兔甘,子類中virtual關(guān)鍵字可加可不加,一般加上為好鳞滨,以防子類被其他類繼承)洞焙,只有重寫了虛函數(shù)的才能算作是體現(xiàn)了C++多態(tài)性。
如何區(qū)分覆蓋和隱藏:
如果基類中的函數(shù)和派生類中的兩個名字一樣的函數(shù)f
滿足下面的兩個條件
(a)在基類中函數(shù)聲明的時候有virtual關(guān)鍵字
(b)基類中的函數(shù)和派生類中的函數(shù)一模一樣拯啦,函數(shù)名澡匪,參數(shù),返回類型都一樣褒链。
那么這就是叫做覆蓋(override)唁情,這也就是虛函數(shù),多態(tài)的性質(zhì)
那么其他的情況呢甫匹?甸鸟?只要名字一樣,不滿足上面覆蓋的條件兵迅,就是隱藏了抢韭。
- 重載:重載則是同一個域中允許有多個同名的函數(shù),而這些函數(shù)的參數(shù)列表不同恍箭,允許參數(shù)個數(shù)不同刻恭,參數(shù)類型不同,或者兩者都不同季惯。編譯器會根據(jù)這些函數(shù)的不同列表吠各,將同名的函數(shù)的名稱做修飾,從而生成一些不同名稱的預(yù)處理函數(shù)勉抓,來實現(xiàn)同名函數(shù)調(diào)用時的重載問題贾漏。但這并沒有體現(xiàn)多態(tài)性。
所以藕筋,相同的函數(shù)名的函數(shù)纵散,在基類和派生類中的關(guān)系只能是覆蓋或者隱藏。
RTTI
運行時類型識別(Run-Time Type Identification),它提供了運行時確定對象類型的方法伍掀。
其中有兩個重要的運算符:typeid 和 dynamic_cast
- typeid的name函數(shù)用于查看對象類型
typeid(*pHuman) == typeid(Japanese)
typeid注意事項:
1 返回一個type-info對象的引用
2 若想通過基類指針獲得派生類的數(shù)據(jù)類型掰茶,基類必須帶有虛函數(shù)
3 只能獲取對象的實際類型 - dynamic_cast 常用于從多態(tài)編程基類指針向派生類指針的向下類型轉(zhuǎn)換。它有兩個參數(shù):一個是類型名蜜笤;另一個是多態(tài)對象的指針或引用濒蒋。其功能是在運行時將對象強制轉(zhuǎn)換為目標類型并返回布爾型結(jié)果。(即編譯器無法驗證是否發(fā)生正確的轉(zhuǎn)換)
dynamic_cast<Japanese*>(pHuman)
dynamic_cast注意事項:
1 轉(zhuǎn)換的類型只能是指針或引用把兔,而不能是對象本身
2 要轉(zhuǎn)換的類型中必須包含虛函數(shù)
3 轉(zhuǎn)換成功返回子類地址沪伙,失敗返回NULL。
純虛函數(shù)
純虛函數(shù):沒有函數(shù)體县好,且在虛函數(shù)名后面加=0
抽象類:包含純虛函數(shù)的類,由于抽象類包含了沒有定義的純虛函數(shù)围橡,所以不能定義抽象類的對象。
接口類:僅包含純虛函數(shù)的類缕贡。即無數(shù)據(jù)成員只有成員函數(shù)且成員函數(shù)均為純虛函數(shù)翁授。
接口類更多的表達的是一直能力或協(xié)議。