虛函數

本篇目錄

************************************

*     什麼是虛函數                           *

*     虛函數指針以及虛函數表          *

*     虛函數表重寫的問題                *

*     成爲虛函數的條件                    *

*     虛函數的生存週期                    *

*     爲什麼要使用虛析構函數          *

*     虛函數與訪問限定                    *

************************************

一、什麼是虛函數?

虛函數就是在普通函數前面加了virtual關鍵字。

聲明時要加virtual,定義時不用加。

虛函數的作用==》用指針/引用調用時會發生動態的綁定。

二、虛函數指針

(1)有了虛函數,會產生一個虛函數指針。

(2)一個類中有無數多個虛函數都只會產生一個虛函數指針。


由上面例子可以看出,多了一個虛函數指針,導致sizeof(Base)=8;儘管有兩個虛函數,但是sizeof(Base)=8.

(3)虛函數指針的優先級最高,在內存佈局中在最前面。

(4)虛函數指針指向的是虛函數表。


查看內存佈局可以驗證。

三、虛函數表

(1)虛函數表中存放的信息。


(2)虛函數表與對象類型對應,一個類型只有一張虛函數表。

比如定義了Base b1(10),Base b2(20),Base b3(30)……,但仍然只有一張虛函數表。一個類型對應一個虛函數表!!!

(3)虛函數表在編譯期間產生,存放在只讀數據段,生存週期從程序開始到程序結束。

四、虛函數表重寫的問題

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
		clear();//調用了clear函數
	}
	void clear(){
		memset(this,0,sizeof(Base));//將對象清零
	}
	virtual ~Base(){
		cout<<"~Base()"<<endl;
	}
	virtual void show(){
		cout<<"Base::ma="<<ma<<endl;//打印一下ma的值
	}
protected:
	int ma;
};

class Derive:public Base{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void show(){
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;//打印ma和mb的值
	}
private:
	int mb;
};

(1)在堆上開闢一個基類的對象,進行打印:


程序崩潰,原因在於p1調用show函數時,因爲是虛函數,所以要去虛函數表中去找,但是在構造對象時,已經將對象進行清0,0地址訪問函數出錯。

(2)用基類指針指向派生類對象時,會發生什麼情況呢?


程序運行正確,你可能會想,在派生類構造之前需要構造基類部分,因此在派生類生成的過程中一定調用了基類的構造函數,但爲什麼程序正常呢?

原因就是在繼承過程中,每個類型對應一個虛函數表,每一層的構造剛開始之前都需要把虛函數表的地址往虛函數指針寫一遍,因爲指向什麼對象,指針就應該存誰的虛函數表。

順一下思路:生成派生類對象時,先構造基類部分,再構造派生類部分。在基類部分構造的時候,clear將對象清零,基類虛函數指針爲0了;在構造派生類的時候,派生類中也應該有一張虛函數表,虛表重寫,虛函數指針不爲0,只是在打印繼承過來的ma時,爲0.

五、生成虛函數的條件

因爲虛函數表中存放的時虛函數的地址  ==》  因此虛函數一定要能取地址

因爲虛函數指針存在於對象中 ==》  因此虛函數要用對象來調用

上面兩條必不可少,綜上,我們來判斷一下,下面哪些函數能成爲虛函數?

1)構造函數      =》不能成爲虛函數,因爲對象還未生成。

2)析構函數      =》可以成爲虛函數,滿足上面兩條。

3)靜態成員方法   =》不可以成爲虛函數,因爲不依賴於對象的調用。

4)內聯函數       =》不可以成爲虛函數,因爲不能取地址。

六、虛函數的生存周

我們寫出以下代碼:派生類繼承了基類,並且在基類和派生類中都有一個虛函數void show(){}

class Base{
public:
	Base(int a):ma(a)
	{
		cout<<"Base(int)"<<endl;
		//show();//*1
	}
	virtual ~Base()
	{
		//show();//*2
		cout<<"~Base()"<<endl;
	}
	virtual void show()
	{
		cout<<"Base::ma="<<ma<<endl;
	}
protected:
	int ma;
};

class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b)
	{
		cout<<"Derive(int)"<<endl;
		//show();//*1
	}
	~Derive()
	{
		//show();//*2
		cout<<"~Derive()"<<endl;
	}
	void show()
	{
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
	}
private:
	int mb;
};

我們分別在構造函數和析構函數中調用show()方法

int main()
{
	Derive* p=new Derive(20);
	p->show();
	delete p;

	return 0;
}

(1)去掉代碼中 *1 的注射,查看反彙編

Base下,構造函數中調用虛函數show:


Derive下,構造函數中調用虛函數show:


可以看出在構造函數中虛函數的調用不是動態的綁定,而是靜態的綁定。因爲在構造函數中對象正在生成,因此沒有發生多態。

(2)去掉代碼中的 *2 註釋,查看反彙編

Base下析構函數中的調用虛函數show


Derive下析構函數中的調用虛函數show


可以看出在析構函數中虛函數 的調用步數動態的綁定,而是靜態的綁定。因爲在析構函數中對象正在死亡,邏輯意義上對象已經不完整,因此調用虛函數沒發生多態。

綜上:虛函數的生存週期:從構造函數開始,到析構結束。

七、爲什麼要使用虛析構函數?

(1)如果一個基類指針指向派生類時,並且派生類對象在堆上構造時,調用delete時,只看指針的類型,那麼就會造成指針只會析構基類的對象,而派生類的對象並未析構,將會導致內存泄漏。

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;
	return 0;
}

打印結果:


可以看出:代碼運行正常,但是對於析構只是析構了基類的一部分,並沒有調用派生類的析構,造成內存泄漏。

(2)接着,當派生類中有虛函數時,調用delete會使程序奔潰。因爲生成對象時,指針永遠保存的是基類的起始地址。
class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	virtual void show(){
		cout<<"mb="<<mb<<endl;
	}//只有派生類有虛函數
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;//程序會崩潰
	return 0;
}


程序出現奔潰,從打印的地址上可以看出,派生類在生成時,是從0x01344560處開始開闢,因此指針也是從該地址處指向,但是在delete指針時,只是看指針的類型,而指針的類型是Base*,所以它就會調用基類的析構函數,基類部分和對象開闢的起始位置不一樣,所以程序奔潰。

派生類的內存佈局:


(3)寫成虛析構

class Base{
public:
	Base(int a):ma(a){
		cout<<"Base(int)"<<endl;
	}
	virtual ~Base(){
		cout<<"~Base()"<<endl;
	}
	void operator delete(void *p){
		cout<<"free start addr:"<<p<<endl;
		free(p);
	}
private:
	int ma;
};
class Derive:public Base
{
public:
	Derive(int b):Base(b),mb(b){
		cout<<"Object start addr:"<<this<<endl;
		cout<<"Derive(int)"<<endl;
	}
	~Derive(){
		cout<<"~Derive()"<<endl;
	}
	void* operator new(size_t size){
		void* p=malloc(size);
		cout<<"malloc start addr:"<<p<<endl;
		return p;
	}
private:
	int mb;
};
int main(){
	Base* pb=new Derive(20);
	delete pb;
	return 0;
}

運行成功並且沒有內存泄漏:



總結:將析構函數寫成虛析構的前提條件是什麼?===》在堆上生成對象時。

八、虛函數與訪問限定

寫出如下代碼:

class Base{
public:
	Base(int a):ma(a){}
	virtual ~Base(){}
	//virtual void show(){
	//	cout<<"Base::ma="<<ma<<endl;
	//}
	//*1
protected:
	//virtual void show(){
	//	cout<<"Base::ma="<<ma<<endl;
	//}
	//*2
	int ma;
};

class Derive:public Base{
public:
	Derive(int b):Base(b),mb(b){}
	~Derive(){}
private:
	void show()
	{
		cout<<"Base::ma="<<ma<<" Derive::mb="<<mb<<endl;
	}
	int mb;
};
int main()
{
	Base* p=new Derive(20);
	p->show();//動態的綁定
	delete p;

	return 0;
}

在上述代碼中,

1)派生類中的虛函數不論寫在private/protected/public下,只要基類的虛函數寫成private/protected下時,會出現編譯錯誤。

2)派生類中的虛函數不論寫在private/protected/public下,只要基類的虛函數寫在public下時,運行成功。

原因:

1)訪問限定符只在編譯期間起作用。

2)p爲Base*類型(基類指針),編譯期間調用基類的show方法(放在private/protected下時,編譯錯誤),發現show方法爲虛函數,動態的綁定,p指向的是derive類型的對象,運行的時候從派生的的虛表中找虛函數,運行時訪問限定不起作用。

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