C++之virtual(虛)關鍵字:虛基類(虛繼承);虛函數和純虛函數

@著作權歸作者所有:來自CSDN博客作者大鬍子的艾娃的原創作品,如需轉載,請註明出處https://blog.csdn.net/qq_43148810,否則將追究法律責任。
如有錯誤的地方歡迎指正,謝謝!

一、虛基類

1、爲什麼要虛基類或者說虛繼承:
a、直接二義性可以用作用域與同名覆蓋的方法來消除(看程序註釋),但是間接二義性(同名數據成員在內存中同時擁有多個拷貝,同一個成員函數會有多個映射,菱形繼承或稱鑽石繼承)只能通過虛繼承來消除。

#include<iostream>
using   namespace std;
class Automobile              //汽車類
{
private:
	int power;   //動力
public:
	Automobile(int power);
	void show();
};
class Car : public Automobile      //小客車類
{
	private:
	int seat;     //座位
public:
	Car(int power, int seat) :Automobile(power);
	void show();
};
class Wagon : public Automobile //小貨車類
{
	private:
	int load;     //裝載量
public:
	Wagon(int power, int load) :Automobile(power);
	void show();
};
class StationWagon :public Car, public Wagon  //客貨兩用車類
{
public:
	StationWagon(int CPower, int WPower, int seat, int load)
		:Wagon(WPower, load), Car(CPower, seat);
	void show()     //3,同名覆蓋基類中的show()函數
	{ Car::show(); //2,作用域限定,若不限定會不斷遞歸調用StationWagon中的show()函數本身
		Wagon::show();}
};
int main()
{
	StationWagon SW(105, 108, 3, 8);
	SW.show();    //1,若StationWagon類中沒有show()函數將會產生直接二義性
	return 0;
}

b、以上代碼中 StationWagon SW(105, 108, 3, 8);時,調用構造函數過程,StationWagon對象SW回溯自己的構造函數;發現有繼承, 回溯Car構造函數,發現有繼承,回溯Automobile構造函數,沒有繼承了,依次運行Automobile構造函數、Car構造函數;回溯Wagon構造函數,發現有繼承,回溯Automobile構造函數,沒有繼承了,依次運行Automobile構造函數、Wagon構造函數;最後調用StationWagon構造函數。析構和構造順序相反。
提示:“”“”的依據是繼承被聲明的先後順序。
c、仔細分析後,我們知道:Ca::power爲105,Wagon::power爲108,但是不知道StationWagon::power爲多少。StationWagon類對象中,具有多個從不同途徑繼承來的同名的數據成員power。 佔據了內存空間,由於在內存中有不同的拷貝而可能造成數據不一致。這就產生了間接二義性。

2、虛基類的作用
a、不同的路徑繼承過來的同名數據成員在內存中就只有一個拷貝,同一個函數名也只有一個映射,解決了二義性問題,也節省了內存,避免了數據不一致的問題。
b、聲明瞭虛基類之後,虛基類在進一步派生過程中始終和派生類一起,維護同一個基類子對象的拷貝。

3、虛基類的定義
虛基類的定義是在融合在派生類的定義過程中的,其定義格式如下:
class 派生類名:virtual 繼承方式 基類名

4、虛基類的構造和虛構
a、規定,在初始化列表中同時出現對虛基類和非虛基類構造函數的調用,虛基類的構造函數先於非虛基類的構造函數的執行

b、虛基類的構造函數調用分三種情況:
(1) 虛基類沒有定義構造函數
程序自動調用系統缺省的構造函數來初始化派生類對象中的虛基類子對象。
(2) 虛基類定義了缺省構造函數
程序自動調用自定義的缺省構造函數和析構函數。
(3) 虛基類定義了帶參數的構造函數
這種情況下,虛基類的構造函數調用相對比較複雜。因爲虛基類定義了帶參數的構造函數,所以在整個繼承結構中,直接或間接繼承虛基類的所有派生類,都必須在構造函數的初始化表中列出對虛基類的初始化。但是,只有用於建立派生類對象的那個最遠派生類的構造函數才調用虛基類的構造函數,而派生類的其它非虛基類中所列出的對這個虛基類的構造函數的調用被忽略,從而保證對公共虛基類子對象只初始化一次。

c、虛基類的構造和虛構舉例
對以上程序做以下修改

class Car: virtual public Automobile      //小客車類
class Wagon: virtual public Automobile //小貨車類
StationWagon(int CPower,int WPower, int seat,int load) 
	:Automobile(CPower),Wagon(WPower,load), Car(CPower,seat)
                 {}

(1)調用構造函數過程,StationWagon對象SW回溯自己的構造函數;發現虛繼承, 回溯Automobile構造函數,沒有繼承直接運行,回溯Car構造函數,發現虛繼承,不在回溯,運行Car構造函數;回溯Wagon構造函數,發現虛繼承,不在回溯,運行Wagon構造函數;最後調用StationWagon構造函數。析構和構造順序相反。
(2)最遠派生類StationWagon的直接基類Car和Wagon的構造函數對虛基類構造函數的嵌套調用將自動被忽略,這樣,power只會被初始化一次。

二、虛函數

1、virtual關鍵字說明該成員函數爲虛函數。在定義虛函數時要注意:
a、 虛函數不能是靜態成員函數,也不能是友元函數。因爲靜態成員函數和友元函數不屬於某個對象。
b 、內聯函數是不能在運行中動態確定其位置的,即使虛函數在類的內部定義,編譯時,仍將其看作非內聯的。
c 、構造函數不能是虛函數(對象無法實例化),析構函數可以是虛函數,而且通常聲明爲虛函數。
d、只有類的成員函數才能聲明爲虛函數,虛函數的聲明只能出現在類的定義中。因爲虛函數僅適用於有繼承關係的類對象,普通函數不能說明爲虛函數。
e、虛函數有函數體。
f、虛函數可以派生,如果在派生類中沒有重新定義虛函數,虛函數就充當了派生類的虛函數。

2、虛函數的訪問與其它成員函數不同
在正常情況下,完全一樣。只有通過子類指針初始化的指向基類的指針或引用來調用虛函數時才體現虛函數與一般函數的不同。

3、要實現動態聯編,需要滿足三個條件:
a、應滿足類型兼容規則。
b、在基類中定義虛函數, 並且在派生類中要重新定義虛函數。
c、要由成員函數或者是通過指針、引用訪問虛函數。
注意:調用虛函數不一定發生動態聯編,調用虛函數在靜態聯編是當成一般成員函數使用。

舉例說明:

#include<iostream>
using namespace std;
class Point
{
private:
	int X, Y;
public:
	Point(int X = 0, int Y = 0)
	{
		this->X = X, this->Y = Y;
	}
	virtual double area()   //求面積
	{
		return 0.0;
	}
};
const double PI = 3.14159;
class Circle :public Point
{
private:
	double radius;   //半徑
public:
	Circle(int X, int Y, double R) :Point(X, Y)
	{
		radius = R;
	}
	double area()   //求面積
	{
		return PI*radius*radius;
	}
};
int main()
{
	Point P1(10, 10);
	cout << "P1.area()=" << P1.area() << endl;
	Circle C1(10, 10, 20);
	cout << "C1.area()=" << C1.area() << endl;
	Point *Pp;
	Pp = &C1;
	cout << "Pp->area()=" << Pp->area() << endl;//Pp->area()發生動態聯編
	Point & Rp = C1;
	cout << "Rp.area()=" << Rp.area() << endl;    // Rp.area()發生動態聯編
	return 0;
}
/*
運行結果:
P1.area()=0
C1.area()=1256.64
Pp->area()=1256.64
Rp.area()=1256.64
*/

程序說明:
(1)如果去除virtual對Point::area()函數說明, Pp->area()將調用Point::area()函數;不去除,發生動態聯編,調用Circle::area()函數。
(2)當在派生類中未重新定義虛函數(去除Circle::area()函數),雖然虛函數被派生類繼承,但通過基類、派生類類型指針、引用調用虛函數時,不實現動態聯編,調用的是基類的虛函數。
(3)沒有重新定義虛函數時,並且不滿足類型兼容規則(派生類中定義了虛函數的重載函數),與虛函數同名的重載函數覆蓋了派生類中的虛函數。此時試圖通過派生類對象、指針、引用調用派生類的虛函數時,不實現動態聯編,調用的是基類的虛函數。

三、純虛函數
1、定義:
a、純虛函數(pure virtual function)是一個在基類中說明的虛函數,它在該基類中沒有定義具體實現,要求各派生類根據實際需要定義函數實現。純虛函數的作用是爲派生類提供一個一致的接口。
b、 帶純虛函數的類叫虛基類也叫抽象類,這種基類不能直接生成對象,只能被繼承,重寫虛函數後才能使用,運行時動態綁定!
c、定義的形式爲:
virtual 函數類型 函數名(參數表)=0;

3、與一般的虛函數的區別
a、原型在書寫格式上的不同就在於後面加了=0。
b、純虛函數根本就沒有函數體,而虛函數一定有函數體,空虛函數的函數體爲空。
c、純虛函數所在的類是抽象類,不能直接進行實例化,空虛函數所在的類是可以實例化的。

4、共同的特點是都可以派生出新的類,然後在新類中給出新的虛函數的實現,而且這種新的實現可以具有多態特徵。

四、補充(二、1、c):虛析構函數
1、當基類的析構函數被聲明爲虛函數,則派生類的析構函數,無論是否使用virtual關鍵字進行聲明,都自動成爲虛函數。
2、析構函數聲明爲虛函數後,程序運行時採用動態聯編,因此可以確保使用基類類型的指針就能夠自動調用適當的析構函數對不同對象進行清理工作。
3、當使用delete運算符刪除一個對象時,隱含着對析構函數的一次調用,如果析構函數爲虛函數,則這個調用採用動態聯編,保證析構函數被正確執行。

class A
{
public:
   virtual ~A()            //虛析構函數
   {  
        cout<<"A::~A() is called."<<endl; 
   }
   A() 
   {
        cout<<"A::A() is called."<<endl; 
   }
};
class B: public A          //派生類
{
private:
      int  *ip;
public:
      B(int size=0)
     {	
          ip=new int[size]; 
          cout<<"B::B() is called."<<endl;
     }
     ~B()
     { 
          cout<<"B::~B() is called."<<endl; 
          delete []  ip;
     }
};
int main()
{
    A *b=new B(10);          //類型兼容
    delete b;
    return 0; 
}

運行結果:
1:A::A() is called.
2:B::B() is called.
3:B::~B() is called.
4:A::~A() is called.
由於定義基類的析構函數是虛析構函數,所以當程序運行結束時,通過基類指針刪除派生類對象時,先調用派生類析構函數,然後調用基類的析構函數。
如果基類的析構函數不是虛析構函數,則程序運行結果會少了第3行,顯然派生類對象中的動態分配的內存沒有被釋放,導致內存泄漏。所以一般將析構函數聲明爲虛函數。

更多內容請關注個人博客:https://blog.csdn.net/qq_43148810

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章