C++ - 類的繼承和多態

總結以下有關類的概念,用於加深對類的理解。可能有不足或錯誤之處,歡迎指出,大家共同進步。

1、引言

介紹類,繞不過去兩個概念:面向過程編程、面向對象編程(Object Oriented Programming)。其中面向過程的編程以C語言爲代表的編程方式,面向過程程序的設計方式是:確定程序需要哪些過程,然後採用最合適的算法來實現;強調的是程序實現的過程以及實現過程中採用的算法。這程序開發的前期,採用這種方式是最直接也是最簡單的,因爲當時面對的程序規模還比較小,採用面向對象的設計方式能夠滿足基本的需要。隨着程序的規模越來越大,採用面向對象的方式已不能很好滿足編程的需要,此時,提出了OOP的概念。OOP與強調過程的編程方式不同,它強調的是對象,把程序中的組成分解成一個個對象,然後通過對象之間的操作或交互來滿足程序的需要。

與面向過程相比,OOP具備以下幾個重要的特性:

1)、抽象

2)、封裝和隱藏

3)、多態

4)、繼承

5)、代碼重用性

爲了實現這些特性並把它們組合在一起,提出了類的概念。

2、類的概念

何爲類?類的英文表達爲class,直譯過來的意思就是“類型”,即,一個類是自定義的一種類型

這就不得不提及OOP中“抽象”的特性。世界是無序和複雜的,簡單來說,一個人的組成就非常複雜,到目前爲止,在醫學上還在不斷地探索人體的奧祕。而在OOP中,如果要表達一個人,我們不可能把所有與人體相關的組成或行爲都表現出來,那樣會非常複雜和繁瑣,所以我們就需要化繁爲簡,找出人具備的明顯的特徵組合起來來表達“人”,這種化繁爲簡的過程就可以稱之爲抽象。通過抽象的分析,一個“人”可能包括:具有腦袋、五官、四肢、膚色等組成屬性,具備直立行走、使用工具等行爲。經過抽象,可以把“人”分析爲一組具有多種屬性、多種行爲的描述,而在OOP中,類就是用來組合抽象出的屬性和行爲,從而形成一個用來描述“人”的一種類型。這種組合的過程其實可以理解爲對類的封裝的隱藏,通過封裝後的類可以有效地用來表達一個“人”這種實體。

3、對象的概念

那麼何爲對象呢?我們現在已經知道類是一種類型,而對象就是該類型實例化出來的一個實體,該實體可以成爲類的對象。比如“人”是一個類,那麼我們每一個人個體都可以稱之爲一個個對象,一個類型爲“人”的對象,我們都具備該類描述的屬性和行爲。對象是類的一種實例化,在OOP中,當定義好類之後,操作的主要就是對象。

4、類的繼承

繼承是類的一個很重要的概念,是實現OOP特性中代碼重用性的一種手段。正如其字面意思,通過繼承,不僅可以使用原始類中一些屬性和行爲,同時也可以對繼承的類進行擴展,形成新的屬性和行爲。

繼承,又可以稱之爲派生,當從一個類派生出另外一個類時,原始類稱之爲基類(超類),繼承類則稱之爲派生類。集成的方式有三種:Public、Protected、Private,分別對應類中的三種類型的屬性。繼承的形式如下所示:

class CChildClass : public CBaseClass
{...}

其中CChildClass屬於派生類,CBaseClass屬於基類,繼承方式爲公有繼承,即是是公有繼承也不能直接調用基類的私有成員,只能通過基類的公有方法進行調用。還有protected和private繼承,其繼承之後屬性的訪問控制如下所示:

  繼承方式 public protected private
子類訪問控制 public public protected private
子類訪問控制 protected protected protected private
子類訪問控制 private private private private

注意,不管何種繼承方式,子類都不能使用父類的private屬性,不管是內部還是外部。判斷繼承中屬性的使用是否規範需要遵守“三看”原則:一看屬性的使用是內部還是外部;而看子類是以何種方式繼承父類;三是看父類中屬性是何種方式的屬性;(Public即可內部使用也可外部使用;Protected只可內部使用;Private只可內部使用)

關於繼承有以下需要注意的幾點:

1)、構造函數和析構函數。需要注意的是,當創建派生類對象是,程序首先創建基類的對象。這也就是說,首先調用的是基類的構造函數,然後再調用派生類的構造函數。而析構函數的調用順序剛好相反,是先調用派生類的析構函數然後再調用基類的析構函數;

對於繼承和組合(即類的對象作爲其他類的屬性)同時存在的情況,先執行基類的構造函數,如果該基類還有基類,則執行基類的基類的構造函數(以此類推),然後再執行組合對象類的構造函數(執行順序和對象屬性的聲明順序一致),最後調用子類的構造函數,析構函數的調用順序與構造函數相反。

2)、構造函數的初始化列表。當採用繼承的方式時,如果基類中只有有參構造函數,可以通過初始化列表的形式初始化類的屬性,具體使用方式如下所示:

CChildClass: :CChildClass(int p1, char p2, string p3): CBaseClass(p2, p3)
{attr1 = p1; } 

當定義一個CChildClass類的對象時如:CChildClass *cc = new CChildClass(1, 'a', "Hello");就會按照如上方式進行屬性的初始化,先將p2和p3傳遞給基類的構造函數進行初始化操作,然後在執行派生類的構造函數進行屬性的初始化。

當然,還有另外一種形式的初始化列表,即所有的屬性都通過初始化列表的形式進行初始化,如下所示:

CChildClass: :CChildClass(int p1, char p2, string p3): CBaseClass(p2, p3),attr1(p1)
{...}

3)、類型的兼容性原則:當子類繼承自父類時,可以當成一個特殊的父類

                 ①、子類對象可以當做父類對象使用

                 ②、子類對象可以直接賦值給父類對象

                 ③、子類對象可以直接初始化父類對象

                 ④、父類指針可以直接指向子類指針

                 ⑤、父類引用可以直接引用子類對象

4)、子類與父類存在同名成員變量和同名成員函數:在處理這種情況之前,需要先明白幾個概念:函數重載、函數重寫、函數重定義。

名稱覆蓋:如果子類中的函數名稱與父類中的相同,則會產生名稱覆蓋的情況,對於子類來說,會默認調用子類的同名函數,如果想要調用父類的同名函數,只能顯示調用:子類對象.父類名稱::成員函數(成員變量)

函數重載:函數名稱相同,參數不同;必須是在同一個類中;重載是在編譯期間根據參數類型和個數決定函數調用;子類無法重載父類的函數,如果函數名稱相同,則子類函數將會覆蓋掉父類的同名函數;

函數重寫:重寫發生在子類和父類之間;並且父類與子類中的函數必須有完全相同的函數原型(函數名稱、參數);分爲兩種形式:虛函數重寫和非虛函數重寫,虛函數重寫就是在函數有virtual關鍵字修飾,使用virtual修飾後將會產生多態,非虛函數重寫就是下面將的函數重定義;

函數重定義:一種特殊的函數重寫,無virtual關鍵字修飾。

5)、虛繼承

在繼承時,存在這樣一種情況,即C類繼承自B1和B2類,而B1和B2類繼承都繼承自B類,如下所示:

class B
{
public: int b;
};
class B1:public B
{
public: int b1;
};
class B2:public B
{
public: int b2;
};
class C: public B1, public B2
{
public: int c;
};
void play11()
{
	C c1;
	c1.c = 10;        //ok
	c1.b1 = 20;       //ok
	c1.b2 = 30;       //ok
	c1.b = 40;//err, 因爲B1和B2都從B類繼承了成員b,而C又分別從B1和B2繼承了b,所以不知調用哪一個b,即多繼承的二義性
}

這種存在有共同基類的情況會發生多繼承的二義性的問題,即繼承的變量不知是從哪個父類繼承下來的,而且在這種情況下,基類B的構造函數也會調用兩次。此時的剞劂辦法就是使用虛繼承, 即B1和B2分別虛繼承子B:

class B
{
public:
	int b;
	B(){cout << ">>> B() 構造函數:"<< endl;}
	~B(){cout << ">>> ~B() 析構函數:" << endl;}
};
//virtual
class B1: virtual public B
{public:int b1;
};
class B2: virtual public B
{public:int b2;
};
class C: public B1, public B2
{public:int c;
};
//有vitual,則只執行一次B的構造函數
//沒有virtual,則執行兩次B的構造函數
//虛繼承解決的是多個父類還有一個共同父類的情況
void play11()
{
	C c1;
	c1.c = 10;
	c1.b1 = 20;
	c1.b2 = 30; 
	c1.b = 40;	//多繼承的二義性,爲了解決多繼承的二義性,可以使用virtual關鍵字修飾繼承關係,這樣,即使是多繼承,也只會執行一次B的構造函數
				//同時,B中的屬性b也就不具備二義性了

	//在加了virtual關鍵字之後,編譯器會給類添加屬性(不可見)
	//cout << ">> sizeof(B): " << sizeof(B) << endl;
	//cout << ">> sizeof(B1): " << sizeof(B1) << endl;
	//cout << ">> sizeof(B2): " << sizeof(B2) << endl;
	//cout << ">> sizeof(C): " << sizeof(C) << endl;
}
/*虛繼承:
1、在多繼承情況下,只能解決有共同基類的情況
2、不能解決多個基類中存在變量重名的情況
*/
void main11()
{
	play11();
	return;
}

但是需要注意的是,在實際的項目開發中基本上不會出現上述情形。 

5、類的多態

在繼承時,存在這樣一種情況:Child類繼承自Parent類,Parent中有一個打印函數print(),如下所示:

class Parent
{
public:
	Parent(int a)
	{
		this->a = a;
		cout << ">>> Parent()構造函數 a: " << this->a << endl;
	}
	void print(){cout << ">>> Parent 打印 a: " << this->a << endl;}
private:
	int a;
};
class Child: public Parent
{
public:
	Child(int b): Parent(10)
	{
		this->b = b;
		cout << ">>> Child()構造函數 b: " << this->b << endl;
	}
	virtual void print(){cout << ">>> Child 打印 b: " << this->b << endl;}
private:
	int b;
};
void paly13()
{
	Parent *base = NULL;
	Parent p1(20);
	Child c1(30);
	//Test1
	c1.print();			//執行的是子類的打印函數
	//Test2
	base = &p1;
	base->print();				//執行父類的打印函數
	//Test3
	base = &c1;			//類型兼容性原則,子類對象可以賦值給父類指針
	base->print();		//執行的仍然是父類的打印函數 
}

對於play13()函數中的Test1,很顯然執行的是子類的打印函數,對於Test2,很顯然執行的是父類的打印函數,對於Test3來說,儘管將子類對象賦值給了父類指針,但調用的依然是父類的打印函數,這是因爲:在編譯時,編譯器自動根據指針類型判斷指向的是一個什麼樣的對象,所以編譯器會認爲父類指針指向的是父類對象,如果不寫virtual關鍵字,則當子類對象賦值給父類對象時(指針、引用、傳參),仍然會調用父類對象的打印函數(靜態聯編)。此時就要引入多態的思想。

多態是類的一種很重要的特性。從字面意思上就可以理解,類可以有多種形態,同樣的調用語句有多種不同的表示方式,這樣就會調用子類的打印函數(自己調用自己的函數)。那麼實現多態的手段是什麼呢?虛函數。通過virtual來聲明成員函數,如下所示:

class HeroFighter
{
public:
	virtual int power(){return 10;}
};
class AdvHeroFighter: public HeroFighter
{
public:
	virtual int power(){return 20;}
};
class EnemyFighter
{
public:
	int attack(){return 15;}
};
//可以體現多態的概念
void play14(HeroFighter *hf, EnemyFighter *ef)
{
	hf->power()>ef->attack() ? printf("主角win\n"): printf("敵人win\n");	
}
void main()
{
	HeroFighter hf;
	AdvHeroFighter advHf;
	EnemyFighter ef;

	play14(&hf, &ef);
	play14(&advHf, &ef);
	return;
}

如上示例就是一個典型的多態的應用場景,play14(HeroFighter *hf,  EnemyFighter *ef)函數有一個父類HeroFighter類型的額參數,這樣他同時也可以處理類型爲AdvHeroFighter的子類對象,體現了C++代碼重用性的原則,可以用一個函數同時處理父類和子類,並且分別調用自己的power()函數,就是因爲virtual關鍵字修飾了power()函數。

多態的實現效果:多態,同樣的調用語句有多種不同的表現形式

面向對象的3大概念

①、封裝,突破c語言函數的概念:用類做函數參數,可以使用對象的屬性和方法

②、繼承,A B 代碼複用

③、多態,面向未來

多態實現的3個條件

①、要有繼承

②、要有虛函數重寫

③、用父類指針(父類引用)指向子類對象

多態的C++實現:virtual關鍵字,告訴編譯器這個函數要支持多態;不是根據指針類型判斷如何調用,而是要根據指針所指向的實際對象類型來判斷如何調用

多態的理論基礎:動態聯編 和 靜態聯編。動態聯編:根據實際的對象類型來判斷重寫函數的調用

多態的重要意義:多態是設計模式的基礎,多態是框架的基礎。

關於虛函數的使用有以下需要注意的地方:

1)、虛析構函數。解決的是想通過基類指針把所有子類對象的析構函數都執行一遍,即通過基類指針釋放所有的子類資源,需要在基類指針的析構函數加上virtual關鍵字。

析構函數的調用事發生當delete釋放由new創建的對象時,對於派生類來說,當delete時,如果基類中的析構函數沒有虛化,則將只會調用基類中的析構函數。而如果基類的析構函數虛化了,則將會先調用派生類的析構函數,然後再調用基類的析構函數。虛化基類的析構函數就是爲了保證析構的順序是正確的。如果直接用delete刪除掉子類對象也是可以順序調用基類的析構函數的。

2)、純虛函數。純虛函數是更加高級一點的虛函數。虛函數在基類中也是要有實現的,而純虛函數在基類中是不需要實現的,因爲純虛函數實際上是提供了一種接口,所有的派生類都可以從接口中繼承對應的函數,對應有很多共性的對象而言,採用純虛函數還是比較合適的。純虛函數的聲明方式如下所示:

class CCar

{...

public:

virtual void Drive() const=0;

}

class CManualCar : public CCar

{...

public:

virtual void Drive() const;   //在派生類中則不需要加 =0

}

需要注意的是,擁有純虛函數的類成爲抽象類,是不能創建該類的對象的,只能用作基類來使用。

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