天天看點

第十一章 C++ 封裝/繼承/多态

面向對象程式設計(OOP)是一種計算機程式設計思想,主要目标是為了實作代碼的重用性、靈活性和擴充性。面向對象程式設計以對象為核心,程式由一系列對象組成。對象間通過消息傳遞(一個對象調用了另一個對象的函數)互相通信,來模拟現實世界中不同僚物間的關系。

面向對象程式設計有三大特性:封裝,繼承,多态。

封裝的基本展現就是對象,封裝使得程式的實作“高内聚、低耦合”的目标。封裝就是把資料和資料處理包圍起來,對資料的處理隻能通過已定義的函數來實作。封裝可以隐藏實作細節,使得代碼子產品化。繼承允許我們依據一個類來定義另一個類。當建立一個類時,不需要重新編寫新的資料成員和函數成員,隻需繼承一個已有類即可。這個已有類稱為基類(父類),建立的類稱為派生類(子類)。派生類就自然擁有了基類的資料成員和函數成員。這樣做可以重用代碼功能和提高執行效率。多态性是指不同的對象接收到同一個的消息傳遞(函數調用)之後,所表現出來的行為是各不相同的。多态是建構在封裝和繼承的基礎之上的。多态就是允許不同類的對象對同一函數名的調用後的結果是不一樣的。多态雖然概念有些複雜,但是隻要了解了繼承,多态就非常簡單了。

我們建立一個怪物的父類,建立Monster.h和Monster.cpp檔案,用于該類的聲明和實作。以下是Monster.h 内容:

#pragma once
#include <iostream>
#include <string>
using namespace std;

// 定義一個怪物類
class Monster {

protected:
	int id;		    // ID
	string name;	// 名稱
	int attack;		// 攻擊值

public:
	
	// 聲明預設構造方法
	Monster();

	// 聲明有參構造方法
	Monster(int _id, string _name, int _attack);

	// 聲明戰鬥方法
	void battle();
};
           

以下是Monster.cpp内容:

#include "Monster.h"

// 預設構造方法
Monster::Monster() {
	this->id = 1;
	this->name = "monster";
	this->attack = 0;
}

// 有參構造方法
Monster::Monster(int _id, string _name, int _attack) {
	this->id = _id;
	this->name = _name;
	this->attack = _attack;
}

// 定義戰鬥方法
void Monster::battle() {
	cout << name << " attack " << attack << " !" << endl;
}
           

類的聲明和定義,其實就是函數的聲明和定義的差別,當然我們也可以在頭檔案中定義函數的實作。Monster類主要定義了怪物的攻擊特征,因為遊戲中的怪物都會具備這樣的共性。然後我們再聲明蜘蛛Spider類和蛇Snake類,分别繼承這個怪物Monster父類,這樣他們就擁有了父類的攻擊特征。為了簡單友善,我們直接在Monster.h中定義這兩個類,代碼如下:

// 定義個蜘蛛(繼承怪物)類
class Spider : public Monster {};

// 定義個蛇(繼承怪物)類
class Snake : public Monster {};
           

注意,C++預設繼承是private,也就是說子類不能通路父類的資料和函數,是以我們這裡改用public,這樣我們就能通路父類的資料和函數了。在我們的主檔案ConsoleApplication.cpp中,我們可以這樣使用這兩個新類:

// 執行個體化一個Spider對象
Spider spider;
spider.battle();

// 執行個體化一個Snake對象
Snake snake;
snake.battle();
           

我們一般使用類去聲明一個變量(對象)的時候,稱之為執行個體化,這個變量(對象)也稱之為類的一個執行個體。繼承既簡化了我們的代碼,又能實作對應功能。另外一點,構造方法是不能被繼承的。是以,在建立子類對象時,為了初始化從父類繼承來的資料成員,我們需要手動完成子類的構造方法,并在其中也可以調用父類的構造方法。調用方式就是在Monster類的構造函數後,加一個冒号(:),然後加上父類的帶參數的構造函數。這樣,在子類的構造函數被調用時,系統就會去調用父類的帶參數的構造函數去完成資料的初始化。這裡面比較特殊的是父類預設構造方法,它是自動被子類的預設構造方法調用的,當然這個影響在實際開發中影響不大。我們給子類添加構造方法:

// 定義個蜘蛛(繼承怪物)類
class Spider : public Monster {

public:
	// 調用父類的構造函數
	Spider() : Monster() {}
	Spider(int _id, string _name, int _attack) : Monster(_id, _name,_attack){}
};

// 定義個蛇(繼承怪物)類
class Snake : public Monster {

public:
	// 調用父類的構造函數
	Snake() : Monster() {}
	Snake(int _id, string _name, int _attack) : Monster(_id, _name, _attack) {}
};
           

在我們的主檔案ConsoleApplication.cpp中,我們可以使用新類的構造函數了:

// 執行個體化一個Spider對象
Spider spider2(1, "Spider", 100);
spider2.battle();

// 執行個體化一個Snake對象
Snake snake2(2, "Snake", 200);
snake2.battle();
           

函數重寫(override):在基類中定義了一個普通函數,然後在派生類中又定義了一個同名同參數同傳回類型的函數,這就是重寫了。在派生類對象上直接調用這個函數名,隻會調用派生類中的那個。例如我們重寫父類的戰鬥方法,先在Monster.h做聲明,然後在Monster.cpp檔案中完成即可:

// 子類重寫戰鬥方法(Monster.h)
void battle();

// 子類重寫Spider類的戰鬥方法(Monster.cpp)
void Spider::battle() {

	attack += 100;
	cout << "Spider attack " << attack << "!" << endl;
}
           

在我們的主檔案ConsoleApplication.cpp中,我們可以使用重寫函數了:

// 執行個體化一個Spider對象
Spider spider;
spider.battle();
           

函數重載(overload):在基類中定義了一個普通函數,然後在派生類中定義一個同名,但是具有不同的形參表的函數,這就是重載。在派生類對象上調用這幾個函數時,用不同的參數會調用到不同的函數,如果沒有則仍然去父類尋找。例如我們重載父類的戰鬥方法,先在Monster.h做聲明,然後在Monster.cpp檔案中完成即可:

// 子類重載Spider類的戰鬥方法(Monster.h)
void battle(int _attack);

// 子類重載Spider類的戰鬥方法(Monster.cpp)
void Spider::battle(int _attack) {

	cout << "Spider attack " << _attack << "!" << endl;
}
           

在我們的主檔案ConsoleApplication.cpp中,我們可以使用重載函數了:

// 執行個體化一個Spider對象
Spider spider;
spider.battle(500);
           

備注:派生類可以通路基類中所有的非私有成員。是以基類成員如果不想被派生類的成員函數通路,則應在基類中聲明為 private。多繼承即一個子類可以有多個父類,它繼承了多個父類的特性。C++中一個派生類中允許有兩個及以上的基類,我們稱這種情況為多繼承。使用多繼承可以描述事物之間的組合關系,但是如此一來也可能會增加命名沖突的可能性。是以,在其他進階語言中,C#和Java是不允許多繼承的!

C++ 多态意味着調用成員函數時,會根據調用函數的對象的類型來執行不同的函數。簡單的說,重寫是父類與子類之間多态性的展現,而重載是同一個類的行為的多态性的展現。C++支援兩種多态性:編譯時多态性和運作時多态性(也稱為靜态多态和動态多态)。一般情況下,我們所說的多态都是指的運作時多态,也就是動态多态。

C++編譯器在編譯的時候,要确定每個對象調用的函數的位址。這種綁定關系是根據對象的資料類型來決定的。如果想要系統在運作時再去确定對象的類型以及正确的調用函數,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數我們稱為虛函數。我們說多态是在程式進行動态綁定得以實作的,而不是編譯時就确定對象的調用方法的靜态綁定。程式運作到動态綁定時,通過基類的指針所指向的對象類型,然後調用其相應的方法,即可實作多态。

構成多态還有兩個條件:

1. 調用函數的對象必須是指針或者引用。

2. 被調用的函數必須是虛函數,且完成了虛函數的重寫。

多态一般的用法,就是用父類的指針指向子類,然後用父類的指針去調用子類中被重寫的虛函數。通過父類指針調用子類的虛函數,可以讓父類指針有多種形态。在我們的執行個體中,我們首先需要在父類Monster中使用 virtual 來修飾戰鬥函數,如下所示:

// 父類聲明戰鬥方法
virtual void battle();
           

如果父類Monster中的battle方法是一個普通的方法,即沒有使用virtual修飾的話,那麼雖然spiderPointer和snakePointer裡面存儲的的确是Spider類和Snake類的指針,但是他們依然隻會執行父類Monster的battle方法。隻有我們使用virtual修飾父類battle方法後,才能得到我們想要的正确執行結果。在我們的主檔案ConsoleApplication.cpp中,我們可以使用多态了:

// 執行個體化一個Spider對象
Spider spider3(1, "Spider", 100);
// 執行個體化一個Snake對象
Snake snake3(2, "Snake", 200);

// 定義怪物對象指針
Monster* spiderPointer = &spider3;
spiderPointer->battle();	// 執行子類函數
Monster* snakePointer = &snake3;
snakePointer->battle();		// 執行父類函數
           

因為Snake類并沒有重寫battle函數,是以它隻能調用父類的battle函數了。但是Spider類重新了battle函數,在重寫的函數中,我們在原來的攻擊值上增加了100。

備注:封裝性是基礎 ,繼承性是關鍵 ,多态性是補充 ,多态性又存在于繼承的環境之中 ,是以這三大特征是互相關聯的 ,互相補充的。

接下來要了解的抽象的特性。面向對象程式設計中一切都是對象,對象都是通過類來描述的,但并不是所有的類都可以來描述對象的。如果一個類沒有足夠的資訊來描述一個具體的對象,而需要其他具體的類來實作它,那麼這樣的類我們稱它為抽象類。比如遊戲怪物類Monster,它沒有一個具體肖像,隻是一個概念,需要一個具體的實體,如一隻蜘蛛,一條蛇來對它進行特定的描述,我們才知道它的具體呈現。抽象類就是實作多态的一種機制。它定義了一組抽象的方法,至于這組抽象方法的具體内容由派生類來實作。同時抽象類提供了繼承的概念,它的出發點就是為了繼承,否則它沒有存在的任何意義。在 C#和Java 中,可以通過兩種形式來展現面向對象程式設計的抽象:抽象類(abstract)和接口(interface)。C++語言并沒有像C#和Java那樣對抽象類和接口有顯式的支援。C++中隻能使用virtual關鍵字來聲明虛函數。C++語言中也沒有抽象類的概念,但是可以通過虛函數實作抽象類。如果類中至少有一個函數被聲明為虛函數,則這個類就是抽象類。抽象類隻能用作父類被繼承,子類必須實作虛函數的具體功能。C++ 接口則是使用抽象類來實作的。該類中沒有定義任何的成員變量,所有的成員函數都是虛函數。接口就是一種特殊的抽象類。

最後還有講一個組合的概念。類是一種構造資料類型,在類中可以其他類定義資料成員,這種資料成員稱為對象成員。類中包含對象成員的形式,稱為組合(Composition)。類之間的組合關系稱為has-a關系,是指一個類擁有另一個類的執行個體對象。含有對象成員的類在調用構造函數對其資料成員進行初始化,其中的對象成員也需要調用其構造函數賦初值,文法如下:

<類名> :: <構造函數名> ([<形參表>]) : [對象成員1](<實參表1>) , [對象成員2](<實參表2>){...}

單冒号之後用逗号分隔的是類中對象成員和傳遞的實參,稱為成員初始化清單。普通的資料成員既可以在構造函數中對其指派,也可以在成員初始化清單中完成。對象成員隻能在初始化清單中初始化,并且對象成員的構造函數的調用先于主類的構造函數。

首先我們構造兩個類Sword.h(武器)和Player.h(玩家),代碼如下:

#pragma once
#include <iostream>
#include <string>
using namespace std;

// 定義一把武器
class Sword {

protected:
	int id;		    // 唯一标示
	string name;	// 名稱
	int attack;		// 攻擊值

public:
	// 定義預設構造函數
	Sword() {
		
		id = 1;
		name = "Sword";
		attack = 10;
	}

	// 定義有參構造方法
	Sword(int _id, string _name, int _attack) {
		this->id = _id;
		this->name = _name;
		this->attack = _attack;
	}

	// 定義攻擊方法
	void battle() {
		cout << name << " Sword attack " << attack << " !" << endl;
	}
};
           

然後是Player.h(玩家):

#pragma once
#include <iostream>
#include <string>
#include "Sword.h"
using namespace std;

// 定義一個玩家
class Player {

protected:
	int id;			    // 唯一标示
	string name;		// 名稱
	Sword weapon;		// 武器類

public:

	// 預設構造函數,調用武器類的構造函數
	Player() : id(1), name("Player"), weapon() {}

	// 定義有參構造函數,調用武器類的構造函數
	Player(int pid, string pname, int sid, string sname, int sattact) : weapon(sid, sname, sattact) {
		this->id = pid;
		this->name = pname;
	}

	// 戰鬥函數,調用Sword的戰鬥函數
	void battle() {
		weapon.battle();
	}
};
           

然後在我們的主檔案ConsoleApplication.cpp中,我們可以使用類的組合了:

// 類的組合使用
Player player(1, "小菜鳥", 1, "木劍", 10);
player.battle();
           

類的組合在程式開發過程中經常使用。面向對象程式開發中,一切皆為對象。每一個對象都代表了一個封裝好的資料和功能集合。一個複雜的對象可以通過繼承,組合等多種方式來實作。在Unity中,場景中所有的物體都視為遊戲對象(GameObject),一個複雜的遊戲對象由不同的元件對象(component )構成。我們可以給一個遊戲對象添加各種不同的元件來讓該遊戲對象具有不同的功能。這其實就是類組合的應用。

本課程的所有代碼案例下載下傳位址:

C++示例源代碼(配合教學課程使用)-C/C++文檔類資源-CSDN下載下傳

備注:這是我們遊戲開發系列教程的第一個課程,主要是程式設計語言的基礎學習,優先學習C++程式設計語言,然後進行C#語言,最後才是Java語言,緊接着就是使用C++和DirectX來介紹遊戲開發中的一些基礎理論知識。我們遊戲開發系列教程的第二個課程是Unity遊戲引擎的學習。課程中如果有一些錯誤的地方,請大家留言指正,感激不盡!

繼續閱讀