C++ :多態(類型轉換及問題、虛函數、C++如何實現動態綁定、多態成立的條件、抽象基類和純虛函數、虛析構函數)

多態是面向對象程序設計語言中數據抽象和繼承之外的第三個基本特徵。


目錄

一、類型轉換及問題

1.2 問題解決思路

1.3 問題解決方案(虛函數,vitual function)

二、C++如何實現動態綁定

三、多態成立的條件

四、抽象基類和純虛函數(pure virtual function)

五、虛析構函數


 

c++支持編譯時多態(靜態多態)運行時多態(動態多態)運算符重載和函數重載就是編譯時多態派生類和虛函數實現運行時多態

靜態多態和動態多態的區別就是函數地址是早綁定(靜態聯編)還是晚綁定(動態聯編)。如果函數的調用,在編譯階段就可以確定函數的調用地址,併產生代碼,就是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數的調用地址不能編譯不能在編譯期間確定,而需要在運行時才能決定,這這就屬於晚綁定(動態多態,運行時多態)。

 

一、類型轉換及問題

1.1

對象可以作爲自己的類或者作爲它的基類的對象來使用。還能通過基類的地址來操作它。取一個對象的地址(指針或引用),並將其作爲基類的地址來處理,這種稱爲向上類型轉換。

也就是說:父類引用或指針可以指向子類對象,通過父類指針或引用來操作子類對象。

class Animal{
public:
	void speak(){
	cout << "動物在唱歌..." << endl;
	}
};

class Dog : public Animal{
public:
	void speak(){
	cout << "小狗在唱歌..." << endl;
	}
};

void DoBussiness(Animal& animal){
	animal.speak();
}

void test(){
	Dog dog;
	DoBussiness(dog);
}
  • 運行結果: 動物在唱歌
  • 問題拋出: 我們給DoBussiness傳入的對象是dog,而不是animal對象,輸出的結果應該是  小狗在唱歌。

 

1.2 問題解決思路

解決這個問題,我們需要了解下綁定(捆綁,binding)概念。

  • 把函數體與函數調用相聯繫稱爲綁定(捆綁,binding)

當綁定在程序運行之前(由編譯器和連接器)完成時,稱爲早綁定(early binding).C語言中只有一種函數調用方式,就是早綁定。

上面的問題就是由於早綁定引起的,因爲編譯器在只有Animal地址時並不知道要調用的正確函數。編譯是根據指向對象的指針或引用的類型來選擇函數調用。這個時候由於DoBussiness的參數類型是Animal&,編譯器確定了應該調用的speak是Animal::speak的,而不是真正傳入的對象Dog::speak。

解決方法就是遲綁定(遲捆綁,動態綁定,運行時綁定,late binding),意味着綁定要根據對象的實際類型,發生在運行。

 

1.3 問題解決方案(虛函數,vitual function)

C++動態多態性是通過虛函數來實現的,虛函數允許子類(派生類)重新定義父類(基類)成員函數,而子類(派生類)重新定義父類(基類)虛函數的做法稱爲覆蓋(override),或者稱爲重寫

對於特定的函數進行動態綁定,c++要求在基類中聲明這個函數的時候使用virtual關鍵字,動態綁定也就對virtual函數起作用.

注意幾點:

  1. 爲創建一個需要動態綁定的虛成員函數,可以簡單在這個函數聲明前面加上virtual關鍵字,定義時候不需要.
  2. 如果一個函數在基類中被聲明爲virtual,那麼在所有派生類中它都是virtual的.
  3. 在派生類中virtual函數的重定義稱爲重寫(override).
  4. Virtual關鍵字只能修飾成員函數.
  5. 構造函數不能爲虛函數

 

二、C++如何實現動態綁定

當我們告訴通過創建一個virtual函數來告訴編譯器要進行動態綁定,那麼編譯器就會根據動態綁定機制來實現我們的要求, 不會再執行早綁定。

  • 問題:C++的動態捆綁機制是怎麼樣的?
  1. 當編譯器發現我們的類中有虛函數的時候,編譯器會創建一張虛函數表,把虛函數的函數入口地址放到虛函數表中,並且在類中祕密增加一個指針,這個指針就是vpointer(縮寫vptr),這個指針是指向對象的虛函數表。在多態調用的時候,根據vptr指針,找到虛函數表來實現動態綁定。
  2. 子類繼承基類,子類繼承了基類的vptr指針,這個vptr指針是指向基類虛函數表,當子類調用構造函數,使得子類的vptr指針指向了子類的虛函數表。
  • 當子類無重寫基類虛函數時:(子類無重寫基類虛函數是無意義的

(類animal中有虛函數func1、func2.  類dog中有除繼承外虛函數func3、func4)

  • 過程分析:
  •      Animal* animal = new Dog;
  •      animal->fun1();
  •      當程序執行到這裏,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,此時由於子類並沒有重寫也就是覆蓋基類的func1函數,所以調用func1時,仍然調用的是基類的func1.
  • 執行結果: 我是基類的func1
  • 測試結論: 無重寫基類的虛函數,無意義

 

  • 當子類重寫基類虛函數時:(這纔是我們討論的重寫

(上圖紅色爲重寫的函數)

  • 過程分析:
  •      Animal* animal = new Dog;
  •      animal->fun1();
  •      當程序執行到這裏,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,由於子類重寫基類的func1函數,所以調用func1時,調用的是子類的func1.
  • 執行結果: 我是子類的func1

 

三、多態成立的條件

  • 有繼承
  • 子類重寫父類虛函數函數
  •            a) 返回值,函數名字,函數參數,必須和父類完全一致(析構函數除外)
  •            b) 子類中virtual關鍵字可寫可不寫,建議寫
  • 父類指針或父類引用指向子類對象

 

四、抽象基類和純虛函數(pure virtual function)

在設計時,常常希望基類僅僅作爲其派生類的一個接口。這就是說,僅想對基類進行向上類型轉換,使用它的接口,而不希望用戶實際的創建一個基類的對象。同時創建一個純虛函數允許接口中放置成員原函數,而不一定要提供一段可能對這個函數毫無意義的代碼。

做到這點,可以在基類中加入至少一個純虛函數(pure virtual function),使得基類稱爲抽象類(abstract class).

注意點:

  • 初始化語法: virtual 返回值類型 函數名 (形參) = 0
  • 有了純虛函數的類,也稱爲抽象類,不能實例化出對象。
  • 子類必須重寫父類純虛函數,否則子類也是抽象類。
  • Virtual void fun() = 0;    告訴編譯器在vtable中爲函數保留一個位置,但在這個特定位置不放地址。

 

五、虛析構函數

利用虛析構可以解決 :不調用子類的析構函數的問題

純虛析構需要有聲明,也需要有實現,如果函數體中 有了純虛析構,那麼這個函數也屬於抽象類。

例子:

class Animal
{
public:
	Animal()
	{
		cout << "Animal構造函數調用" << endl;
	}
	virtual void speak() = 0;
	virtual ~Animal()
	{
		cout << "Animal析構函數調用" << endl;
	}
};

Animal::~Animal()
{
	cout << "Animal純虛析構函數調用" << endl;
}

class Cat :public Animal
{
public:
	Cat(char * name)
	{
		cout << "Cat構造函數調用" << endl;
		this->m_Name =  new char[strlen(name) + 1];
		strcpy(this->m_Name, name);
	}
	void speak()
	{
		cout <<  this->m_Name<< "小貓在說話" << endl;
	}

	~Cat()
	{	
		if (this->m_Name != NULL)
		{
			cout << "Cat析構函數調用" << endl;
			delete[]this->m_Name;
			this->m_Name = NULL;
		}
	}

	char * m_Name; //姓名

};

void test01()
{
	Animal * cat = new Cat("Tom");
	cat->speak();

	delete cat;
}

 

 

 

 

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