1. 程式人生 > 程式設計 >C++中的封裝、繼承、多型理解

C++中的封裝、繼承、多型理解

封裝(encapsulation):就是將抽象得到的資料和行為(或功能)相結合,形成一個有機的整體,也就是將資料與操作資料的原始碼進行有機的結合,形成”類”,其中資料和函式都是類的成員。封裝的目的是增強安全性和簡化程式設計,使用者不必瞭解具體的實現細節,而只是要通過外部介面,特定的訪問許可權來使用類的成員。封裝可以隱藏實現細節,使得程式碼模組化。

繼承(inheritance):C++通過類派生機制來支援繼承。被繼承的型別稱為基類或超類,新產生的類為派生類或子類。保持已有類的特性而構造新類的過程稱為繼承。在已有類的基礎上新增自己的特性而產生新類的過程稱為派生。繼承和派生的目的是保持已有類的特性並構造新類。繼承的目的:實現程式碼重用。派生的目的:實現程式碼擴充。三種繼承方式:public、protected、private。

繼承時的建構函式:(1)、基類的建構函式不能被繼承,派生類中需要宣告自己的建構函式;(2)、宣告建構函式時,只需要對本類中新增成員進行初始化,對繼承來的基類成員的初始化,自動呼叫基類建構函式完成;(3)、派生類的建構函式需要給基類的建構函式傳遞引數;(4)、單一繼承時的建構函式:派生類名::派生類名(基類所需的形參,本類成員所需的形參):基類名(引數表) {本類成員初始化賦值語句;};(5)、當基類中宣告有預設形式的建構函式或未宣告建構函式時,派生類建構函式可以不向基類建構函式傳遞引數;(6)、若基類中未宣告建構函式,派生類中也可以不宣告,全採用預設形式建構函式;(7)、當基類宣告有帶形參的建構函式時,派生類也應宣告帶形參的建構函式,並將引數傳遞給基類建構函式;(8)、建構函式的呼叫次序:A、呼叫基類建構函式,呼叫順序按照它們被繼承時宣告的順序(從左向右);B、呼叫成員物件的建構函式,呼叫順序按照它們在類中的宣告的順序;C、派生類的建構函式體中的內容。

繼承時的解構函式:(1)、解構函式也不被繼承,派生類自行宣告;(2)、宣告方法與一般(無繼承關係時)類的解構函式相同;(3)、不需要顯示地呼叫基類的解構函式,系統會自動隱式呼叫;(4)、解構函式的呼叫次序與建構函式相反。

同名隱藏規則:當派生類與基類中有相同成員時:(1)、若未強行指名,則通過派生類物件使用的是派生類中的同名成員;(2)、如要通過派生類物件訪問基類中被覆蓋的同名成員,應使用基類名限定:基類名::資料成員名。

虛基類:作用:(1)、主要用來解決多繼承時可能發生的對同一基類繼承多次而產生的二義性問題;(2)、為最遠的派生類提供唯一的基類成員,而不重複產生多次拷貝。

繼承、組合:組合是將其它類的物件作為成員使用,繼承是子類可以使用父類的成員方法。(1)、A繼承B,說明A是B的一種,並且B的所有行為對A都有意義;(2)、若在邏輯上A是B的“一部分”,則不允許B從A派生,而是要用A和其它東西組合出B;(3)、繼承屬於”白盒”複用,組合屬於”黑盒”複用。

多型(Polymorphic)性可以簡單地概括為“一個介面,多種方法”程式在執行時才決定呼叫的函式C++多型性是通過虛擬函式來實現的,虛擬函式允許子類重新定義成員函式,而子類重新定義父類的做法稱為覆蓋或者稱為重寫。而過載則是允許有多個同名的函式,而這些函式的引數列表不同,允許引數個數不同,引數型別不同,或者兩者都不同。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式

多型與非多型的實質區別就是函式地址是早繫結還是晚繫結。如果函式的呼叫,在編譯器編譯期間就可以確定函式的呼叫地址,併產生程式碼,是靜態的,就是說地址是早繫結的。而如果函式呼叫的地址不能在編譯期間確定,需要在執行時才確定,這就是屬於晚繫結。

封裝可以使得程式碼模組化,繼承可以擴充套件已存在的程式碼,它們的目的都是為了程式碼重用。而多型的目的則是為了介面重用。也就是說不論傳遞過來的究竟是哪個類的物件,函式都能夠通過同一個介面呼叫到適應各自物件的實現方法。

最常見的用法就是宣告基類的指標,利用該指標指向任意一個子類物件,呼叫相應的虛擬函式,可以根據指向的子類的不同而實現不同的方法。如果沒有使用虛擬函式的話,即沒有利用C++多型性,則利用基類指標呼叫相應的函式的時候,將總被限制在基類函式本身,而無法呼叫到子類中被重寫過的函式。因為沒有多型性,函式呼叫的地址將是一定的,而固定的地址將始終呼叫到同一個函式,這就無法實現一個介面,多種方法的目的了。

純虛擬函式是在基類中宣告的虛擬函式,它在基類中沒有定義,但要求任何派生類都要定義自己的實現方法。在基類中實現純虛擬函式的方法是在函式原型後加“= 0”。為了方便使用多型特性,常常需要在基類中定義虛擬函式,在很多情況下,基類本身生成物件是不合情理的。為了解決這些問題,引入了純虛擬函式的概念,將函式定義為純虛擬函式,則編譯器要求在派生類中必須予以重寫以實現多型性。同時含有純虛擬函式的類稱為抽象類,它不能生成物件。由於純虛擬函式所在的類中沒有它的定義,在該類的建構函式和解構函式中不允許呼叫純虛擬函式,否則會導致程式執行錯誤,但其它成員函式可以呼叫純虛擬函式。

C++支援兩種多型性:(1)、編譯時多型性(靜態多型,在編譯時就可以確定物件使用的形式):通過過載函式實現;(2)、執行時多型性(動態多型,其具體引用的物件在執行時才能確定):通過虛擬函式實現。

C++中,實現多型有以下方法:虛擬函式、抽象類、過載、覆蓋、模板。

函式過載(Overload):指在相同作用域裡(如同一類中),函式同名不同參,返回值則不用理會,不同參可以是不同個數,也可以是不同型別。效果:根據實參的個數和型別呼叫對應的函式體。

函式覆蓋(Override)(函式重寫):指派生類中的函式覆蓋基類中的同名同參虛擬函式,因此作用域不同。效果:基類指標或引用訪問虛擬函式時會根據例項的型別呼叫對應的函式。

函式隱藏(Hide):對於子類中與基類同名的函式,如果不是覆蓋那就成了隱藏。兩種情況:(1)、同名不同參;(2)、同名同參但基類不是virtual函式。

派生類的建構函式使用說明:(1)、在派生類建構函式中,只要基類不是僅使用無參的預設建構函式,都要顯示的給出基類名稱引數表;(2)、基類沒有定義建構函式,派生類也可以不定義,使用預設建構函式;(3)、基類有帶參建構函式,派生類必須定義建構函式。

虛擬函式的過載函式仍是虛擬函式。在派生類重定義虛擬函式時必須有相同的函式原型,包括返回型別、函式名、引數個數、引數型別的順序必須相同。虛擬函式必須是類的成員函式,不能為全域性函式,也不能為靜態函式。不能將友員說明為虛擬函式,但虛擬函式可以是另一個類的友員。解構函式可以是虛擬函式,但建構函式不能為虛擬函式。一般地講,若某類中定義有虛擬函式,則其解構函式也應當說明為虛擬函式。特別是在解構函式需要完成一些有意義的操作,比如釋放記憶體時,尤其應當如此。在類系統中訪問一個虛擬函式時,應使用指向基類型別的指標或對基類型別的引用,以滿足執行時多型性的要求。當然也可以像呼叫普通成員函式那樣利用物件名來呼叫一個函式。若在派生類中沒有重新定義虛擬函式,則該類的物件將使用其基類中的虛擬函式程式碼。

抽象類:如果一個類中至少有一個純虛擬函式,那麼這個類被稱為抽象類。抽象類不僅包括純虛擬函式,也可包括虛擬函式。抽象類中的純虛擬函式可能是在抽象類中定義的,也可能是從它的抽象基類中繼承下來且重定義的。抽象類有一個重要特點,即抽象類必須用作派生其它類的基類,而不能用於直接建立物件例項。抽象類不能直接建立物件的原因是其中有一個或多個函式沒有定義,但仍可使用指向抽象類的指標支援執行時多型性。派生類中必須過載基類中的純虛擬函式,否則它仍將被看作一個抽象類。從基類繼承來的純虛擬函式,在派生類中仍是虛擬函式。

虛擬函式表:虛擬函式是通過一張虛擬函式表來實現的。簡稱為V-Table,在這個表中,主要是一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其真實反應實際的函式。這樣,在有虛擬函式的類的例項中這個表被分配在了這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得有無重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。

一個多型的例子:

#include <iostream>
using namespace std;
 
class A
{
public:
	void foo()
	{
		printf("1\n");
	}
 
	virtual void fun()
	{
		printf("2\n");
	}
};
 
class B : public A
{
public:
	void foo()
	{
		printf("3\n");
	}
 
	void fun()
	{
		printf("4\n");
	}
};
 
int main(void)
{
	A a;
	B b;
 
	A* p = &a;
	p->foo();//1
	p->fun();//2
 
	p = &b;
	p->foo();//1
	p->fun();//4
 
	B* ptr = (B*)&a;
	ptr->foo();//3
	ptr->fun();//2
 
	return 0;
}

另一個例子:

#include <iostream>
using namespace std;
 
int main(void)
{
	class CA 
	{
	public:
		virtual ~CA() {cout<<"delete CA"<<endl;}
		virtual int GetValue() {return 1;}
	};
 
	class CB : public CA
	{
	public:
		~CB() {cout<<"delete CB"<<endl;}
		virtual int GetValue() {return 2;}
	};
 
	CA* pA = new CB;
	cout<<pA->GetValue()<<endl;
	delete pA;
 
	/* result:
		2
		delete CB
		delete CA
	*/
	/*若父類CA中沒有將解構函式定義為虛擬函式,則result:
		2
		delete CA
		由結果看出,如果不將父類CA的解構函式定義為虛擬函式,則不會呼叫到子類的解構函式
	*/
	/*若父類CA中的成員函式GetValue沒有定義為虛擬函式,則result:
		1
		delete CA
	*/
 
	return 0;
}

對C++繼承,封裝,多型的理解

用了C++一段時間,感覺對C++慢慢有了一點認識,在這和大家分享一下。
C++是一款面向物件的語言,擁有面向物件語言的三大核心特性:繼承,封裝,多型。每一個特性的良好理解與使用都會為我們的程式設計帶來莫大的幫助。下面我就這三個特性講一下我對C++的理解。

繼承

學過面嚮物件語言的人基本都可以理解什麼是繼承,但我們為什麼要使用繼承?
很多人說繼承可以使程式碼得到良好的複用,當然這個是繼承的一個優點,但程式碼複用的方法除了繼承還有很多,而且有些比繼承更好。我認為使用繼承最重要的原因是繼承可以使整個程式設計更符合人們的邏輯,從而方便的設計出想要表達的意思。比如我們要設計一堆蘋果,橘子,梨等水果類,使用面向物件的方法,我們首先會抽象出一個水果的基類,而後繼承這個基類,派生出具體的水果類。如果要設計的水果很多,我們還可以在水果基類基礎上,繼續生成新的基類,比如熱帶水果類,溫帶水果類,寒帶水果類等,而後再繼承這些基類。這樣的設計思想就相當於人類的分類思想,簡單易懂,而且設計出來的程式層次分明,容易掌握。
既然繼承這麼好,那該如何使用繼承?
繼承雖好但不能濫用,否則設計出來的程式會雜亂不堪。根據上面的介紹,可以發現繼承主要用來定義一個東西是什麼,比如熱帶水果是水果,菠蘿是熱帶水果等,即繼承主要用來設計一個程式的類的框架,將所要設計的東西用繼承來設立一個基本結構。如果想為一個類新增一個行為或格外的功能,最好是使用組合的方式。如果想了解組合的方式,可以看一下比較著名的策略模式。

封裝

封裝是什麼?
在C++中,比較狹隘的解釋就是將資料與操作資料的方法放在一個類中,而後給每個成員設定相應的許可權。從大一點的角度來說,封裝就是將完成一個功能所需要的所有東西放在一起,對外部只開放呼叫它的介面。
為什麼要封裝?
我認為模組化設計是封裝的本質原因。
對軟體設計或其他工程設計,特別是比較複雜的工程,一般都是模組化設計。模組化設計的好處就是可以將一個複雜的系統拆分成不同的模組。每一個模組又可以獨立的設計,除錯,這就讓多人一起做一個複雜的工程成為現實。如果想做到模組化設計,封裝是不可缺少的一部分。一個好的模組,比如一塊inter的CPU晶片,它有強大的功能,雖然我們不知道它內部是如何實現的,但卻可以很好的使用它。

多型

什麼是多型?
多型簡單的說就是“一個函式,多種實現”,或是“一個介面,多種方法”。多型性表現在程式執行時根據傳入的物件呼叫不同的函式。
C++的多型是通過虛擬函式來實現的,在基類中定義一個函式為虛擬函式,該函式就可以在執行時,根據傳入的物件呼叫不同的實現方法。而如果該函式不設為虛擬函式,則在呼叫的過程中呼叫的函式就是固定的。比如下面一個例子

//
//定義一個Duck基類,而後繼承Duck派生出一個RedHandDuck類。
//其中display()方法,第一次執行設為普通函式,第二次設為虛擬函式
 
#include "iostream"
 
class Duck {
 
public:
	Duck(){}
	~Duck(){}
 
	//定義一個虛擬函式display
	virtual void display(){
 
		std::cout<<" I am a Duck !"<<std::endl;
	}
};
 
class RedHandDuck:public Duck{
 
public:
	RedHandDuck(){}
	~RedHandDuck(){}
 
	//重寫display
	void display(){
 
		std::cout<<" I am a RedHandDuck !"<<std::endl;
	}
};
 
int main(){
 
	RedHandDuck* duck1 = new RedHandDuck();
	Duck* duck2 = duck1;
 
	duck1->display();
	duck2->display();
 
	std::getchar();
}

第一次執行結果(不使用虛擬函式):

第二次執行結果(使用虛擬函式):

由結果可以看到,由於虛擬函式的使用,Duck物件(可以理解為介面),呼叫的display()方法是根據傳入的物件決定的。這就實現了“一個介面,多種方法”。

從網上看到一個關於多型的介紹,非常精闢,分享給大家

  多型與非多型的實質區別就是函式地址是早繫結還是晚繫結。如果函式的呼叫,在編譯器編譯期間就可以確定函式的呼叫地址,並生產程式碼,是靜態的,就是說地址是早繫結的。而如果函式呼叫的地址不能在編譯器期間確定,需要在執行時才確定,這就屬於晚繫結。