C++虛函數總結
為什麽使用虛函數?什麽是虛函數?虛函數是為了解決什麽問題?
面向對象的三大特征:
- 封裝
- 多態
- 繼承
- 普通虛函數
- 虛析構函數
- 純虛函數
- 抽象類
- 接口類
- 隱藏 vs 覆蓋
- 隱藏與覆蓋之間的關系
- 早綁定和晚綁定
- 虛函數表
什麽是多態?
相同對象收到不同消息或不同對象收到相同消息時產生的不同的動作。
靜態多態 vs 動態多態
[-:>靜態多態也叫做早綁定
class Rect //矩形類
{
public:
int calcArea(int width);
int calcArea(int width,int height);
};
如上面的代碼,他們函數名相同,參數個數不同,一看就是互為重載的兩個函數
1 int main()
2 {
3 Rect.rect;
4 rect.calcArea(10);
5 rect.calcArea(10,20);
6 return 0;
7 }
程序在編譯階段根據參數個數確定調用哪個函數。這種情況叫做靜態多態(早綁定)
[-:>動態多態也叫做晚綁定
比如計算面積 當給圓形計算面積時使用圓形面積的計算公式,給矩形計算面積時使用矩形面積的計算公式。也就是說有一個計算面積的形狀基類,圓形和矩形類派生自形狀類,圓形與矩形的類各有自己的計算面積的方法。可見動態多態是以封裝和繼承為基礎的。
1 class Shape//形狀類
2 {
3 public:
4 double calcArea()
5 {
6 cout<<"calcArea"<<endl;
7 return 0;
8 }
9 };
10 class Circle:public Shape //公有繼承自形狀類的圓形類
11 {
12 public:
13 Circle(double r);
14 double calcArea();
15 private:
16 double m_dR;
17 };
18 double Circle::calcArea()
19 {
20 return 3.14*m_dR*m_dR;
21 }
22 class Rect:public Shape //公有繼承自形狀類的矩形類
23 {
24 public:
25 Rect(double width,double height);
26 double calArea();
27 private:
28 double m_dWidth;
29 double m_dHeight;
30 };
31 double Rect::calcArea()
32 {
33 return m_dWidth*m_dHeight;
34 }
35 int main()
36 {
37 Shape *shape1=new Circle(4.0);
38 Shape *shape2=new Rect(3.0,5.0);
39 shape1->calcArea();
40 shape2->calcArea();
41 .......
42 return 0;
43 }
如果打印結果的話,以上程序結果會打印兩行"calcArea",因為調用到的都是父類的calcArea函數,並不是我們想要的那樣去分別調用各自的計算面積的函數。如果要想實現動態多態則必須使用虛函數
關鍵字 virtual ->虛函數
用virtual去修飾成員函數使其成為虛函數
所以以上函數的修改部分如下
class Shape
{
public:
virtual double calcArea(){...}//虛函數
.... //其他部分
private:
....
};
....
class Circle:public Shape
{
public:
Circle(double r);
virtual double calcArea();//此處的virtual不是必須的,如果不加,系統會自動加
//上,如果加上則會在後續的時候看的比較明顯(推薦加上)
....
private:
....
};
....
class Rect:public Shape
{
Rect(double width,double height);
virtual double calcArea();
private
....
};
....
這樣就可以達到預期的結果了
多態中存在的問題
[-:>內存泄漏,一個很嚴重的問題
例如上面的程序中,如果在圓形的類中定義一個圓心的坐標,並且坐標是在堆中申請的內存,則在mian函數中通過父類指針操作子類對象的成員函數的時候是沒有問題的,可是在銷毀對象內存的時候則只是執行了父類的析構函數,子類的析構函數卻沒有執行,這會導致內存泄漏。部分代碼如下(想去借助父類指針去銷毀子類對象的時候去不能去銷毀子類對象)
如果delete後邊跟父類的指針則只會執行父類的析構函數,如果delete後面跟的是子類的指針,那麽它即會執行子類的析構函數,也會執行父類的析構函數
class Circle:public Shape
{
public:
Circle(int x,int y,double r);
~Circle();
virtual double calcArea();
....
private:
double m_dR;
Coordinate *m_pCenter; //坐標類指針
....
};
Circle::Circle(int x,int y,double r)
{
m_pCenter=new Coordinate(x,y);
m_dR=r;
}
Circle::~Circle()
{
delete m_pCenter;
m_pCenter-NULL;
}
....
int main()
{
Shape *shape1=new Circle(3,5,4.0);
shape1->calcArea();
delete shape1;
shape1=NULL;
return 0;
}
可見我們必須要去解決這個問題,不解決這個問題當使用的時候都會造成內存泄漏。面對這種情況則需要引入虛析構函數
虛析構函數
關鍵字 virtual ->析構函數
之前是使用virtual去修飾成員函數,這裏使用virtual去修飾析構函數,部分代碼如下
1 class Shape
2 {
3 public:
4 ....
5 virtual ~Shape();
6 private:
7 ....
8 };
9 class Circle:public Shape
10 {
11 public:
12 virtual ~Circle();//與虛函數相同,此處virtual可以不寫,系統將會自動添加,建議寫上
13 ....
14 };
15 ....
這樣父類指針指向的是哪個對象,哪個對象的構造函數就會先執行,然後執行父類的構造函數。銷毀的時候子類的析構函數也會執行。
virtual關鍵字可以修飾普通的成員函數,也可以修飾析構函數,但並不是沒有限制
virtual在函數中的使用限制
- 普通函數不能是虛函數,也就是說這個函數必須是某一個類的成員函數,不可以是一個全局函數,否則會導致編譯錯誤。
- 靜態成員函數不能是虛函數 static成員函數是和類同生共處的,他不屬於任何對象,使用virtual也將導致錯誤。
- 內聯函數不能是虛函數 如果修飾內聯函數 如果內聯函數被virtual修飾,計算機會忽略inline使它變成存粹的虛函數。
- 構造函數不能是虛函數,否則會出現編譯錯誤。
虛函數實現原理
【:-》首先:什麽是函數指針?
指針指向對象稱為對象指針,指針除了指向對象還可以指向函數,函數的本質就是一段二進制代碼,我們可以通過指針指向這段代碼的開頭,計算機就會從這個開頭一直往下執行,直到函數結束,並且通過指令返回回來。函數的指針與普通的指針本質上是一樣的,也是由四個基本的內存單元組成,存儲著內存的地址,這個地址就是函數的首地址。
【:-》多態的實現原理
虛函數表指針:類中除了定義的函數成員,還有一個成員是虛函數表指針(占四個基本內存單元),這個指針指向一個虛函數表的起始位置,這個表會與類的定義同時出現,這個表存放著該類的虛函數指針,調用的時候可以找到該類的虛函數表指針,通過虛函數表指針找到虛函數表,通過虛函數表的偏移找到函數的入口地址,從而找到要使用的虛函數。
當實例化一個該類的子類對象的時候,(如果)該類的子類並沒有定義虛函數,但是卻從父類中繼承了虛函數,所以在實例化該類子類對象的時候也會產生一個虛函數表,這個虛函數表是子類的虛函數表,但是記錄的子類的虛函數地址卻是與父類的是一樣的。所以通過子類對象的虛函數表指針找到自己的虛函數表,在自己的虛函數表找到的要執行的函數指針也是父類的相應函數入口的地址。
如果我們在子類中定義了從父類繼承來的虛函數,對於父類來說情況是不變的,對於子類來說它的虛函數表與之前的虛函數表是一樣的,但是此時子類定義了自己的(從父類那繼承來的)相應函數,所以它的虛函數表當中管於這個函數的指針就會覆蓋掉原有的指向父類函數的指針的值,換句話說就是指向了自己定義的相應函數,這樣如果用父類的指針,指向子類的對象,就會通過子類對象當中的虛函數表指針找到子類的虛函數表,從而通過子類的虛函數表找到子類的相應虛函數地址,而此時的地址已經是該函數自己定義的虛函數入口地址,而不是父類的相應虛函數入口地址,所以執行的將會是子類當中的虛函數。這就是多態的原理。
函數的覆蓋和隱藏
父類和子類出現同名函數稱為隱藏。
- 父類對象.函數函數名(...); //調用父類的函數
- 子類對象.函數名(...); //調用子類的函數
- 子類對象.父類名::函數名(...);//子類調用從父類繼承來的函數。
父類和子類出現同名虛函數稱為覆蓋
- 父類指針=new 子類名(...);父類指針->函數名(...);//調用子類的虛函數。
虛析構函數的實現原理
[:->虛析構函數的特點:
- 當我們在父類中通過virtual修飾析構函數之後,通過父類指針指向子類對象,通過delete接父類指針就可以釋放掉子類對象
[:->理論前提:
- 執行完子類的析構函數就會執行父類的析構函數
原理:
如果父類當中定義了虛析構函數,那麽父類的虛函數表當中就會有一個父類的虛析構函數的入口指針,指向的是父類的虛析構函數,子類虛函數表當中也會產生一個子類的虛析構函數的入口指針,指向的是子類的虛析構函數,這個時候使用父類的指針指向子類的對象,delete接父類指針,就會通過指向的子類的對象找到子類的虛函數表指針,從而找到虛函數表,再虛函數表中找到子類的虛析構函數,從而使得子類的析構函數得以執行,子類的析構函數執行之後系統會自動執行父類的虛析構函數。這個是虛析構函數的實現原理。
純虛函數:
純虛函數的定義
1 class Shape
2 {
3 public:
4 virtual double calcArea()//虛函數
5 {....}
6 virtual double calcPerimeter()=0;//純虛函數
7 ....
8 };
純虛函數沒有函數體,同時在定義的時候函數名後面要加“=0”。
純虛函數的實現原理:
在虛函數原理的基礎上,虛函數表中,虛函數的地址是一個有意義的值,如果是純虛函數就實實在在的寫一個0。
含有純虛函數的類被稱為抽象類
含有純虛函數的類被稱為抽象類,比如上面代碼中的類就是一個抽象類,包含一個計算周長的純虛函數。哪怕只有一個純虛函數,那麽這個類也是一個抽象類,純虛函數沒有函數體,所以抽象類不允許實例化對象,抽象類的子類也可以是一個抽象類。抽象類子類只有把抽象類當中的所有的純虛函數都做了實現才可以實例化對象。
對於抽象的類來說,我們往往不希望它能實例化,因為實例化之後也沒什麽用,而對於一些具體的類來說,我們要求必須實現那些要求(純虛函數),使之成為有具體動作的類。
近含有純虛函數的類稱為接口類
如果在抽象類當中僅含有純虛函數而不含其他任何東西,我們稱之為接口類。
- 沒有任何數據成員
- 僅有成員函數
- 成員函數都是純虛函數
class Shape
{
virtual double calcArea()=0//計算面積
virtual double calcPerimeter()=0//計算周長
};
實際的工作中接口類更多的表達一種能力或協議
比如
1 class Flyable//會飛
2 {
3 public:
4 virtual void takeoff()=0;//起飛
5 virtual void land()=0;//降落
6 };
7 class Bird:public Flyable
8 {
9 public:
10 ....
11 virtual void takeoff(){....}
12 virtual void land(){....}
13 private:
14 ....
15 };
16 void flyMatch(Flyable *a,Flyable *b)//飛行比賽
17 //要求傳入一個會飛對象的指針,此時鳥類的對象指針可以傳入進來
18 {
19 ....
20 a->takeoff();
21 b->takeoff();
22 a->land();
23 b->land();
24 }
例如上面的代碼,定義一個會飛的接口,凡是實現這個接口的都是會飛的,飛行比賽要求會飛的來參加,鳥實現了會飛的接口,所以鳥可以參加飛行比賽,如果復雜點定義一個能夠射擊的接口,那麽實現射擊接口的類就可以參加戰爭之類需要會射擊的對象,有一個戰鬥機類通過多繼承實現會飛的接口和射擊的接口還可以參加空中作戰的函數呢
C++虛函數總結