首頁 > 軟體

C++深入講解類與物件之OOP物件導向程式設計與封裝

2022-05-16 16:01:16

程式導向程式設計也叫結構化程式設計。雖然結構化程式設計的理念提高了程式的清晰度,可靠性,並且方便維護。但它再編寫大型的程式時,仍然面臨這巨大的挑戰,OOP(物件導向程式設計)提供了一種新的方法。與強調演演算法的過程性程式設計不同的是,OOP強調的是資料。--引自《C++ Primer Plus(第六版)》

1.物件導向程式設計

C++ 是 基於物件導向 的, 關注 的是 物件 ,將一件事情拆分成不同的物件,靠物件之間的互動完成。

在C++中,類是一種規範,它描述了這種新型資料格式,物件是根據這種規範構造的特定資料結構。這裡有小夥伴會問,類是什麼?這個問題會在(3.類的引入) 中重點介紹。

2.程式導向性程式設計和物件導向程式設計

通過下面這個例子,可以更加清晰的揭示OOP的觀點和過程性程式設計的差別。

此舉例改變自《C++ Primer Plus(第六版)》:

曼聯足球俱樂部的一名新成員被要求記錄球隊的統計資料。很自然他會藉助計算機來完成這項任務。

如果這個新成員是過程性程式設計師,可能會這樣考慮:

我要輸入每名運動員的姓名,進球數,助攻數,登場數等其他重要的基本統計資料。之所以使用計算機,是為了簡化統計工作,因此讓他來計算某些資料。另外,我還希望程式能夠顯示這些結果。應該如何組織呢?我讓main()呼叫一個函數來獲取輸入,呼叫另外一個函數來進行計算,然後呼叫第三個函數來顯示結果。那麼,獲得下一場比賽的資料後,又改怎麼做呢?當然不想從頭開始,可以新增一個函數來更新統計資料。可能需要在main函數中新增一個選單,選擇是輸入,計算,更新還是顯示資料等。則如何表示這些資料呢。可以使用一個字串來儲存選手的姓名,用另外一個陣列來儲存每位球員的進球數,再用一個陣列儲存助攻數等等。這種方法太不靈活了。因此可以設計一個結構體來儲存每位球員的所有資訊,然後用這種結構組成的陣列來表示整個球隊。

總之,採用過程性程式設計時,首先要考慮遵守的步驟,然後考慮如何表示這些資料。

如果換成一個OOP程式設計師,又將如何考慮呢?

首先要考慮資料——不僅要考慮如何表示資料,還要考慮如何使用資料:

OOP程式設計師會想,我要跟蹤的是什麼?當然是球員。因此要有一個物件表示整個球員的各個方面(不僅僅是進球數或助攻數)。因此這將是基本資料單元——一個表示球員的姓名和統計資料的物件。我需要一些處理該物件的方法。首先需要一種將基本資訊加入到該單元中的方法;其次,計算機應計算一些東西,如進球率。因此要新增一些執行計算的方法。程式應自動完成這些計算,而無需使用者的干擾。另外,還需要一些更新和顯示資訊的方法。所以,使用者與資料互動的方式有三種:初始化,更新和報告——這就是使用者介面。

總之,採用OOP方法時,首先從使用者的角度考慮物件——描述物件所需的資料以及描述使用者與資料互動所需的操作。完成對介面的描述之後,需要確定如何實現介面和資料儲存。最後,使用尋得設計方案建立出程式。

3.類的引入

在過程化程式設計中我們用結構體來描述一個複雜物件(這裡用C語言舉例)。在C語言中,結構體中只能定義變數。結構體關鍵字是struct。在C++中,結構體內不僅可以定義變數,還可以定義函數

struct Student
{
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
	char _name[20];
	char _gender[3];
	int _age;
};
int main()
{
 Student s;
 s.SetStudentInfo("Peter", "男", 18);
 return 0; 
}

上面結構體的定義, 在 C++ 中更喜歡用 class 來代替

4.類的定義

class className
{
    // 類體:由成員函數和成員變陣列成
}; // 一定要注意後面的分號

class為定義類的關鍵字,ClassName為類的名字,{}中為類的主體,注意類定義結束時後面分號。

類中的元素稱為類的成員:類中的資料稱為類的屬性或者成員變數; 類中的函數稱為類的方法或者成員函數。

4.1類的兩種定義方式

4.1.1宣告和定義全部放在類體中

需要注意:成員函數如果在類中定義 ,編譯器可能會將其當成 行內函式 處理。

class Student
{
public:
	void SetStudentInfo(const char* name, const char* gender, int age)
	{
		strcpy(_name, name);
		strcpy(_gender, gender);
		_age = age;
	}
	void PrintStudentInfo()
	{
		cout << _name << " " << _gender << " " << _age << endl;
	}
public:
	char _name[20];
	char _gender[3];
	int _age;
};

4.2.2.宣告和定義不放在類體中

宣告放在.h檔案中,類的定義放在.cpp檔案中

//student.h
//學生
class Student 
{
public:
	void SetStudentInfo(const char* name, const char* gender, int age);
	void PrintStudentInfo();
public:
	char _name[20];
	char _gender[3];
	int _age;
};
//test.cpp
#include "student.h"
void Student::SetStudentInfo(const char* name, const char* gender, int age)
{
	strcpy(_name, name);
	strcpy(_gender, gender);
	_age = age;
}
void Student::PrintStudentInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

一般情況下,更期望採用第二種方式。

5.類的存取限定符及封裝

5.1 存取限定符

在剛剛的程式碼中,細心的小夥伴可以發現在類中出現了public這個詞,那這到底有什麼用呢?這就是我們現在要說明的存取限定符。在C++中,除了public(公有)外,還有private(私有),protected(保護)限定符。

那麼為什麼要引入存取限定符呢?

C++實現封裝的方式:用類將物件的屬性與方法結合在一塊,讓物件更加完善,通過存取許可權選擇性的將其介面提供給外部的使用者使用。

那麼他們都有什麼含義呢?

【存取限定符說明】

1. public修飾的成員在類外可以直接被存取

2. protected和private修飾的成員在類外不能直接被存取(此處protected和private是類似的)

3. 存取許可權作用域從該存取限定符出現的位置開始直到下一個存取限定符出現時為止

4. class的預設存取許可權為private,struct為public(因為struct要相容C)

注意:

1.存取限定符只在編譯時有用,當資料對映到記憶體後,沒有任何存取限定符上的區別。

2.C++需要相容C語言,所以C++中struct可以當成結構體去使用。另外C++中struct還可以用來定義類。和class是定義類是一樣的,區別是struct的成員預設存取方式是public,class是的成員預設存取方式是private。

5.2封裝

什麼是封裝?

封裝:將資料和運算元據的方法進行有機結合,隱藏物件的屬性和實現細節,僅對外公開介面來和物件進行互動。封裝的本質是一種管理。 我們使用類資料和方法都封裝到一下。 不想給別人看到的,我們使用 protected/private 把成員 封裝 起來。 開放 一些共有的成員函數對成員合理的訪 問。所以封裝本質是一種管理。

在C語言中,大多數情況中呼叫者和定義結構者不是同一個人,就可能會存在呼叫者測出bug的可能。

//C語言中資料和方法是分離的
struct Stack
{
	int* _a;
	int _top;
	int _capacity;
};
void StackInit(struct Stack* ps)
{
	assert(ps);
	ps->_a = NULL;
	ps->_capacity = 0;
	ps->_top = 0;
}
void StackPush(struct Stack* ps, int x)
{
}
struct Stack StackTop(struct Stack* ps)
{
}
int main()
{
	struct Stack st;
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);
	StackPush(&st, 3);
	printf("%dn", StackTop(&st));
	printf("%dn", st._a[st._top]);     //可能就存在誤用
	printf("%dn", st._a[st._top - 1]); //可能就存在誤用
}

這是我們在資料結構階段用C語言實現的一個棧,在主函數中,我們想要存取棧頂的元素。在常規情況下,我們呼叫StackTop函數即可存取到棧頂元素。但是我們也可以使用存取陣列下標的方式拿到棧頂元素,此時如果呼叫者不清楚使用者的定義方式,就有可能存在誤用。例如:這段程式碼我們定義_top是棧頂元素的下一個元素的下標,因此棧頂元素的下標應該是_top-1,而呼叫者如果誤以為top就是棧頂元素的下標,即有可能存在誤用。因此這裡太過自由。

為了解決這一問題,在C++中,結構體不僅可以定義變數,還可以定義函數。我們如果把函數定義在類中,我們把成員變數封裝在類中,外界函數無法呼叫。因此如果此時我們想呼叫棧頂元素,我們只能呼叫Top函數的介面。這就避免了上述問題的發生。

class Stack
{
private:
	void Checkcapacity()
	{
	}
public:
	void Init()
	{
	}
	void Push(int x)
	{
	}
	void Top()
	{
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

6.類的作用域

類定義了一個新的作用域 ,類的所有成員都在類的作用域中 。 在類體外定義成員,需要使用 :: 作用域解析符指明成員屬於哪個類域。 就像在這段程式碼中,我們想要在類作用域外定義成員,就要使用::

7.類的範例化

用類型別建立物件的過程,稱為類的範例化

1. 類只是 一個 模型 一樣的東西,限定了類有哪些成員,定義出一個類 並沒有分配實際的記憶體空間 來儲存它

2. 一個類可以範例化出多個物件, 範例化出的物件 佔用實際的物理空間,儲存類成員變數

3. 做個比方。 類範例化出物件就像現實中使用建築設計圖建造出房子,類就像是設計圖 ,只設計出需要什麼東西,但是並沒有實體的建築存在,同樣類也只是一個設計,範例化出的物件才能實際儲存資料,佔用物理空間

我們繼續參照我們剛剛用C++所寫的棧,其中st就是一個範例化物件。

class Stack
{
private:
	void Checkcapacity()
	{
	}
public:
	void Init()
	{
	}
	void Push(int x)
	{
	}
	void Top()
	{
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st;
	st.Init();
	st.Push(1);
	st.Top();
	return 0;
}

8.類物件模型

如何計算類物件的大小

在C語言中,我們在學習結構體的時候知道,由於結構體中只定義變數,因此我們是可以計算出結構體的大小的。sizeof計算的是定義型別物件的大小。

那在C++中,由於類中不僅定義變數,還定義函數,那麼類的大小是怎麼計算的呢?

我們發現此類的大小還是12。

因此我們猜測:類物件的儲存方式只儲存成員變數,成員函數存放在公共的程式碼段。

那我們思考為什麼採用這種方式呢?

在上述中說到,類就像是一份建築圖紙,而所建造的每一個房子中的name,capacity,top應當是不一樣的。但是所呼叫的方法Init(),Top()應當是同一個方法。因此沒有必要把函數在物件中存一份。我們也可以通過組合看看不同的物件是否呼叫同一個函數。

我們能夠發現st1和st2所呼叫得Init()函數是同一份。因此如果都把函數存在類中,就會造成浪費。因此我們可以把函數放在一個公共的區域,這個區域叫做程式碼段。

結論:一個類的大小,實際就是該類中”成員變數”之和,當然也要進行記憶體對齊,注意空類的大小,空類比較特殊,編譯器給了空類一個位元組來唯一標識這個類。注意:最小記憶體單元是1.作業系統規定都要有地址記錄,就像sizeof(void) = 1。

到此這篇關於C++深入講解類與物件之OOP物件導向程式設計與封裝的文章就介紹到這了,更多相關C++類別與物件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com