c++ --- 多態

多態

1.概念:多態就是多種形態。具體來說就是完成某個行爲,當不同的對象去完成的時候會產生不同的狀態。

2.構成條件

  • (1)被調用的對象必須是指針或者是引用。

  • (2)被調用的對象必須是虛函數,且完成了虛函數的重寫。

  • (3)虛函數:就是在類的成員函數上面加上virtual關鍵字

  • (4)虛函數的重寫:派生類中有一個基類完全相同的虛函數,我們就稱子類的虛函數重寫了基類的虛函數,完全相同是指:函數名,參數,返回值都相同。另外,虛函數的重寫也叫做虛函數的覆蓋。

class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "買票 - 全價" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		  cout << "買票 - 半價" << endl;
	}
};

void Fun(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person pe;
	Student st;

	Fun(pe);
	Fun(st);
	system("pause");
	return 0;
}
  • (5)虛函數重寫的例外:協變 — 重寫的虛函數的返回值可以不同,但是必須分別是基類指針和派生類指針或者基類引用和派生類引用。
class A{};
 
class B : public A 
{};
 
class Person 
{ 
public:    
	virtual A* f() 
	{
		return new A;
	} 
};
 
class Student : public Person 
{ 
public:    
	virtual B* f() 
	{
		return new B;
	} 
};

  • (6)不規範的重寫行爲:在派生類中重寫的成員函數可以不加上virtual關鍵字,也是構成重寫的,因爲繼承後基類的虛函數被繼承下來,在派生類中依然保持虛函數的特性,我們只是重寫了它。
lass Person 
{ 
public:    
	virtual void BuyTicket() 
	{
		cout << "買票-全價" << endl;
	} 
};
 
class Student : public Person 
{ 
public:    
	void BuyTicket() 
	{
		cout << "買票-半價" << endl;
	} 
}; 

(7)析構函數的重寫行爲:基類中的析構函數如果是虛函數,那麼派生類的析構函數就重寫了基類的析構函數。這裏他們的函數名不同,看起來違背了重寫的規則。其實不然,這裏可以理解爲編譯器對析構函數的名稱做了特殊的處理,編譯後析構函數的名稱統一處理爲destructor,這也說明基類的析構函數最好寫成虛函數。

二.重載,覆蓋(重寫),隱藏(重定義)的對比

在這裏插入圖片描述

三.抽象類

在虛函數的後面寫上 = 0,則這個函數爲純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承之後也不能實例化出對象,只有重寫純虛函數才能實例化出對象。純虛函數規定了派生類必須重寫,另外純虛函數更體現了接口繼承。

四.c++11中override和final

實際中我們建議多用純虛函數 + override的方式強制重寫虛函數,因爲虛函數的意義就是實現多態,如果沒有重寫,虛函數就沒有意義。

//final 修飾的基類的虛函數不能被派生類重寫
class Car
{
public:
	virtual void Driver()final
	{}
};
class Benz : public Car
{
public:
	virtual void Driver()
	{
		cout <<"Benz - 舒適” <<endl;
	}
};
class Car
{
public:
	virtual void Drive()
	{}
};
//2.override 修飾派生類虛函數強制完成重寫,如果沒有重寫就會強制報錯。
class Benz : public Car
{
public:
	virtual void Drive() override
	{
		cout << "Benz - 舒適" << endl;
	}
};

五.多態的原理

1.虛函數表

//這裏常考的一道筆試題:sizeof(Base)的大小是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func()" << endl;
	}
private:
	int _b = 1;
}

通過觀察調試我們發現b對象是8比特,除了_b成員,還多一個_vfptr放在對象的前面,對象中的指針我們叫做虛函數表指針,一個含有虛函數的類中都至少有一個虛函數表指針,因爲虛函數的地址要被放到虛函數表中,虛函數表簡稱虛表

 // 針對上面的代碼我們做出以下改造 
 // 1.我們增加一個派生類Derive去繼承Base 
 // 2.Derive中重寫Func1 
 // 3.Base再增加一個虛函數Func2和一個普通函數Func3 
class Base
{
public:
	virtual void Fun1()
	{
		cout << "Base::Fun1()" << endl;
	}
	virtual void Fun2()
	{
		cout << "Base::Fun2()" << endl;
	}
	void Fun3()
	{
		cout << "base::Fun3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Fun1()
	{
		cout << "Derive::Fun1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	system("pause");
	return 0;
}

結論:

  • 1.派生類對象d中也有一個虛表指針,d對象是由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員。
  • 2.基類b對象和派生類d獨享虛表指針是不一樣的,這裏我們發現Fun1完成了重寫,所有d的虛表中存的是重寫的Derive::Fun1,所以虛函數的重寫也叫覆蓋,覆蓋就是指虛表中虛函數的覆蓋。
  • 3.Fun2()繼承下來後也就是虛函數,所以放進了虛表中,Fun3也繼承下來了,但是不是虛函數,所以不會放進虛表。
  • 4.虛函數表本質上是一個存虛函數指針的指針數組,這個數組最後面放了一個nullptr。
  • 派生類的虛表生成:
    (1)先將基類的虛表中的內容拷貝一份到派生類虛表中。
    (2)如果派生類重寫了基類中的某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數。
    (3)派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的後面。
  • 5.虛表存的是虛函數的指針,不是虛函數。虛函數和普通函數一樣,都是存在代碼段的,只是它的指針又存在虛表中。對象中存的不是虛表,存的是虛表指針。虛表經過實際驗證發現在vs中是存在代碼段的。

2.兩個條件:(1)虛函數覆蓋(2)對象的指針或引用調用虛函數。

六.內聯函數

在這裏插入圖片描述
面試題:
1.什麼是多態?

  • 多種形態,具體就是完成某個行爲,當不同的對象去完成時會產生出不同的狀態。

  • 多態的實現主要分爲靜態多態和動態多態,靜態多態主要是重載,在編譯的時候已經確定了;動態多態是用虛函數機制實現的,在運行期間進行動態綁定。舉個例子:一個父類類型的指針指向一個子類對象時候,使用父類的指針去調用子類中重寫了的父類的虛函數的時候。會調用子類重寫過後的函數,在父類中聲明瞭virtual關鍵字的函數,在子類中重寫不需要加上virtual也是虛函數。

  • 虛函數的實現:在有虛函數的類中,類中最開始的部分是一個虛函數表指針,這個指針指向了一個虛函數表,虛函數表中放的是虛函數的地址,實際上虛函數存在代碼段。當子類繼承父類的時候,也會繼承父類的虛函數表,當子類重寫父類的虛函數的時候,會將其繼承到的虛函數表的地址替換爲重新寫的函數地址,使用了虛函數,會增加訪問內存的開銷,降低效率。

2.什麼是重載,重寫(覆蓋),重定義(隱藏)

  • 1.重載:就是在同一個作用域中,函數名相同,參數相同就構成了重載。
  • 2.重寫(覆蓋):在基類和派生類中同時存在,且必須是虛函數,函數名相同,參數相同,返回值相同(協變除外)
  • 2.重定義(隱藏):兩個函數分別在基類和派生類中。函數名相同

3.多態的實現原理?

  • 1.虛函數覆蓋
  • 2.對象的指針或引用調用虛函數

4.inline函數可以是虛函數嗎?
不能,因爲inline函數沒有地址,無法把地址放到虛函數表中。

5.靜態成員可以是虛函數嗎?
不能,因爲靜態成員沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。

6.構造函數可以是虛函數嗎?
不能,因爲對象中的徐哈桑農戶表指針是在構造函數初始化列表階段才初始化的。

7.析構函數可以是虛函數嗎?
可以,並且最好把基類的析構函數定義爲虛函數。將可能會被繼承的父類的析構函數設置成爲虛函數。可以保證當我們new出一個子類的時候,然後使用基類指針指向該子類對象,釋放基類指針時可以釋放掉子類的空間,防止內存泄漏。

8.對象訪問普通函數快還是虛函數快?
如果時普通函數的話是一樣快的。如果是指針對象或者是引用對象,則調用普通函數快,因爲構成多態,運行時調用虛函數需要到虛函數表裏面去查找。

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