iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Software Development

三十天內用C++寫出一個小遊戲系列 第 24

Day 24 - 繼承家業

Outline & Intro

  • Inheritance
  • An example
  • Polymorphism

人稱 OOP 的特色有三點,且缺一不可

  • Encapsulation: packaging and data hiding
  • Inheritance 繼承
  • Polymorphism 多型

Inheritance 繼承

簡介:

簡單來說,就是透過已經產生的 classes 再去做新的 classes。

特性:

  • 子類別: derived(child) class ↔ 母類別 : base(parent) class
  • 在子類別裡面會有一些母類別 define 的 class

Example

這是之前我們寫的 MyVector 的 class (不包含 function)

class MyVector
{
protected: // to be explained
	int n;
	double* m;
public:
	MyVector();
	MyVector(int n, double m[]);
	MyVector(const MyVector& v);
	~MyVector();
	void print() const;
	// == != < [] = +=
};

在這個時候,我們突然想起來,2D 中的 vector 也是 vector,這時候你舉起手,想要在重新打造一個新的 class,叫做 MyVector2D,聽起來很潮。但是老師會站在你後面很火,他說 : 明明就教過你 inheritance 了,怎麼還要重新寫一個幾乎一模一樣的 class?

不對阿,老師你還沒講欸。

class MyVector2D : public MyVector
{
public:
	MyVector2D();
	MyVector2D(double m[]);
};

MyVector2D::MyVector2D()
{
	this->n = 2;
}

MyVector2D::MyVector2D(double m[]) : MyVector(2, m)
{
}

這時候指需要做這些事情就可以做出一個 新的 class 了。在class 和 function 名字後面 冒號 public MyVector 就是先前說過的 initializer

既然MyVector2D已經繼承了 MyVector的能力了,理所當然的我們可以使用 MyVector 裡面的 member function ,像是 print() ,或是[] 等已經 overloaded 過的運算元。

int main()
{
	double i[2] = { 1, 2 };
	MyVector2D v(i);
	v.print();
	cout << v[1] << endl;
	return 0;
}

我們可以網上看一下我們的母 class,其中原本 private 的地方被我們改成 protected: ,這是因為在 inheritance 的時候,母 class 的 private member 不會被繼承,他只會存在於母 class 中,但我們改成 protected: 就可以使用了 ! 而這些member + function 就可以只被母與子 class 使用了。

不會被 child class 繼承的東西除了有

  • private member
  • 還有 constructor

因此,在 child class 的 constructor 被呼叫之前,會先呼叫 parent class 的 constructor (背後的意思也就是一定要先創造完 parent constructor 才可以宣告 child class 的 constructor)。

且如果沒有特別指定,會被呼叫的是 default constructor。

MyVector::MyVector() : n(0), m(nullptr)
{
}

MyVector2D::MyVector2D()
{
	this->n = 2;
	// this->m = nullptr is redundant
}

int main()
{
	MyVector2D v; // 呼叫 My2D 的 constructor -> 先呼叫 My 的 default constructor
	return 0;
}

那要怎麼指定 parent class 的 constructor 呢?

MyVector::MyVector(int n, double m[])
{
	this->n = n;
	this->m = new double[n];
	for (int i = 0; i < n; i++)
		this->m[i] = m[i];
}

MyVector2D::MyVector2D(double m[]) : MyVector(2, m)
{
	// not MyVector(2, m) here
}

int main()
{
	double i[2] = { 1, 2 };
	MyVector2D v(i);
	v.print();
	cout << v[1] << endl;
	return 0;
}

MyVector2D::MyVector2D(ddouble m[]) 後面那一段就是指定要呼叫哪一個 parent 的 constructor。

同理,如果我們沒有指定的話,copy constructor 也是一樣會呼叫 default 的 copy constructor。

另外,parent class 沒有權力拿 child class 的 member,但是因為 parent class 已經把遺產給 child 了,所以 child 可以拿 parent 的 member 使用 (感覺很不孝阿)。

我們可以設置 setValue()

class MyVector2D : public MyVector
{
public:
	MyVector2D();
	MyVector2D(double m[]);
	void setValue(double i1, double i2);
};

void MyVector2D::setValue(double i1, double i2)
{
	if (this->m == nullptr)
		this->m = new double[2];
	this->m[0] = i1;
	this->m[1] = i2;
}

像是這樣的話, 因為是放在 My2D 裡面,所以其他的 dimension 的 vector 就不能使用了。

那麼 destructor因為在 parent class 已經有了 destructor, 且 child class 也會自動的呼叫,這時候我們就不需要在 child class 裡面在宣告一個 destructor,這樣會導致我們刪了兩次空間,導致 run-time error。

Inheritance 特性:

  • 我們只需要定義 child 的 constructor 還有自己要用的 function,其他都沿用 parent class 的就好了! 省時省力 !
  • 機制要小心 呼叫 constructor
  • private member 不能被繼承 要改成 protected

Function overriding

在繼承的情形下,我們有時候會 redefine 一個 parent 那邊獲得的 member function,這時候就會被稱作 function overriding (函數負載)。

例如我們來做 print() 的 overriding:

class MyVector2D : public MyVector
{
public:
	MyVector2D();
	MyVector2D(double m[]);
	void setValue(double i1, double i2);
	void print() const;
};

void MyVector2D::print() const
{
	cout << "2D: (";
	for (int i = 0; i < n - 1; i++)
		cout << m[i] << ", ";
	cout << m[n - 1] << ")\n";
}

這時候跟 parent 一樣名字的 print 就會把 parent 的 member function 覆蓋,當你使用 MyVector2D 的時候就會只使用這一個 print()

那這時候你想要使用 parent class 裡面的就要這樣:

MyVector::print();

Cascade Inheritance

除了 parent class → child class,我們也可以做一個 grandchild class 來繼承 child class。

例如我們來做一個 (+, +) 的 vector (non-negative vector)

class NNVector2D : public MyVector2D
{
public:
	NNVector2D(); // MyVector2D's constructor??
	NNVector2D(double m[]);
	void setValue(double i1, double i2);
};
NNVector2D::NNVector2D()
{
}
NNVector2D::NNVector2D(double m[])
{
	this->m = new double[2];
	this->m[0] = m[0] >= 0 ? m[0] : 0;
	this->m[1] = m[1] >= 0 ? m[1] : 0;
}
void NNVector2D::setValue(double i1, double i2)
{
	if (this->m == nullptr)
		this->m = new double[2];
	this->m[0] = i1 >= 0 ? i1 : 0;
	this->m[1] = i2 >= 0 ? i2 : 0;
}

在NNVector 裡面的 constructor,會先呼叫 MyVector2D 的 constructor,而這時候又會呼叫 MyVector 的 default constructor。

簡單來說,這一個 grandchild class 可以擁有他前面繼承下來的人所擁有的 protected, public member(constructor 還有 destructor 例外(因為基本上是傳上一個的))。

這時候 Constructor

  • constructor 會從最老的 class 傳到 最年輕的 class
  • 每一個 constructor 會一層一層的傳

而 Destructor 則會

  • 從最年輕的傳到最老的

Inheritance visibility

我們可以把 public → protected → private分成三個層級,public 是大家都可以用,protected 是只有繼承的人 可以用,而 private 則是只有自己可以使用。所以透過這三個方式,可以設定你想要的 inheritance visibility。

Multiple inheritance

雖然說像這樣的 multiple inheritance 在 C++ 裡面是可以運作的,但是非常地不推薦!

原因是因為同樣繼承的 n 或是 m 就會混淆。且有時候你會覺得你可以 inherit from sister, brother,但是理論上不太能這樣做(真的會太容易混淆)

反正,就先不要這樣做吧。

Example

An Role Playing Game

也就是俗稱的角色扮演遊戲(RPG),基本上就是以賺取經驗值升等為主要目的(衍伸的還有裝備、職業)。且這些職業會有不同的特性,像是 劍士就是個坦克,血厚但攻擊力普通;刺客攻擊力高但血薄;法師單體傷害低,但範圍傷害高,等等。

所以他們就很適合使用 class 來做。

首先是每一個職業都一樣的 characteristics

Character

class Character
{
protected:
	static const int EXP_LV = 100; // 生到 k 級 所需經驗 exp = 100(k - 1) ^ 2
	// 因為大家的升級公式都一樣,所以設 const
	string name;
	int level;
	int exp; // 經驗值
	int power; //力量
	int knowledge; //智力
	int luck; // 幸運

public:
	Character(string n, int lv, int po, int kn, int lu); 
	void print();
};

接下來就可以做這些函式:

Character::Character(string n, int lv, int po, int kn, int lu) : name(n), level(lv), exp(pow(lv - 1, 2)* EXP_LV),
power(po), knowledge(kn), luck(lu)
{

}

void Character::print()
{
	cout << this->name
		<< ": Level" << this->level << "(" << this->exp << "/" << pow(this->level, 2) * EXP_LV
		<< "), " << this->power << "-" << this->knowledge << "-" << this->luck << "\n";
}

void Character::levelUp(int pInc, int kInc, int lInc) // p: power, k:knowledge, l:luck
												                              // Inc : Increasement
{
	this->level++;
	this->power += pInc;
	this->knowledge += kInc;
	this->luck += lInc;

}

void beatMonster(int exp)
{
	this->exp += exp;
	while (this->exp >= pow(this->level, 2) * EXP_LV)
		this->levelUp(0, 0, 0); // no improvement when advancing to next level
}

string Character::getName()
{
	return this->name;
}

Warrior & Wizard

  • 由於我們剛剛建立的 character 這個 class,沒辦法讓我們創立一個職業,所以,我們不能直接以 character 創建一個 object
  • 且我們每一種職業的特性,也都還沒有決定。
  • 就目前為止,character 可以說是一個模板(abstract class),擁有一些大家都擁有的特質並且可以供大家(concrete class)取用。

所以我們現在要來創造職業的 character,這時候我們就可以用到 inheritance了。

大概就是這樣的情形。

可以簡單地說,Warrior 和 Wizard 的差別就只在升級的時候的能力值增加的幅度不同而已。

所以我們先來做一下 Warrior 的 class

class Warrior : public Character
{
private:
  static const int PO_LV = 10; //Power per level
  static const int KN_LV = 5; // knowledge 
  static const int LU_LV = 5; // luck
public:
  Warrior(string n) : Character(n, 1, PO_LV, KN_LV, LU_LV) {} // 如果只有傳入名字
  Warrior(string n, int lv) : Character(n, lv, lv * PO_LV, lv * KN_LV, lv * LU_LV) {}
  void print() //查詢職業
  {
    cout << "Warrior ";
    Character::print();
  }  
  void beatMonster(int exp)
  {
    this->exp += exp;
    while(this->exp >= pow(this->level, 2) * EXP_LV)
      this->levelUp(PO_LV, KN_LV, LU_LV);
  }
};

那 Wizard 的 class 其實就長得跟 warrior 一樣了

class Wizard : public Character
{
private:
  static const int PO_LV = 4;
  static const int KN_LV = 9;
  static const int LU_LV = 7;
public:
  Wizard(string n) : Character(n, 1, PO_LV, KN_LV, LU_LV) {}
  Wizard(string n, int lv) : Character(n, lv, lv * PO_LV, lv * KN_LV, lv * LU_LV) {}
  void print()
  {
    cout << "Wizard ";
    Character::print();
  }  
  void beatMonster(int exp)
  {
    this->exp += exp;
    while(this->exp >= pow(this->level, 2) * EXP_LV)
      this->levelUp(PO_LV, KN_LV, LU_LV);
  }
};

所以同理,如果 Wizard 之後要轉職成 祭師、火毒巫師、冰雷魔法師,這時候也可以從 Wizard 再繼承給其他 class。

雖然這東西看起來沒甚麼問題,但是可能還是有一點問題

  • 你還是無法阻止其他開發者叫出 character as a object。
  • 如果你今天要做一個組隊的 class 的話(每一組最多10人),你可能會這樣寫
class Team
{
private:
	int warriorCount
	int	wizardCount
	Warrior* warrior[10];
	Wizard* wizard[10];
public:
  Team();
  ~Team(); 
// some other functions
};

但是會產生一個問題

  1. 你宣告的這段空間其實蠻浪費的
  2. 如果加入每個角色的名字都不一樣
  3. 或是一個Team把怪打倒了,但是 warrior 和 wizard 的升級公式不太一樣

這樣會整個大混亂。

Polymorphism (多型)

上述的那些小問題,都可以透過 polymorphism 來解決。

我們可以想一下為什麼在上面要做兩個 array?

原因是因為在一個 array 中只能裝入一個 data type,而 Warrior 跟 Wizard 是兩種不同的形態,因此就必須要使用兩個 array 來裝。

那麼我們可以使用一個 array 來裝類型不同的 class 嗎?

要記得,他們的 base class 都是 character。

因此,我們可以做一個data type 為 character 的 array,再把 warrior 和 wizard 存進去!

而這件事情就被稱為 Polymorphism

Use a variable of parent type to store a value of child type.

例子

class Parent
{
protected:
	int x;
	int y;
public:
	Parent(int a, int b) : x(a), y(b) {}
};
class Child : public Parent
{
protected:
	int z;
public:
	Child(int a, int b, int c): Parent(a,b ){z = c; }
};

int main()
{
	Parent p1(1, 2);
	Child c1(3, 4, 5);
	Parent p2 = c1; c1;// OK: 5 is discarded
	//	Child c2 = p1; // Not OK: no v3
return 0;
}

像下面如果用 Parent 來宣告 p2,把 c1 裝進去的話,這時候就只會把 x , y 傳進去 p2 裡面。

那接下來就來 implement 在剛剛的RPG裡面。

首先我們可以確定我們做的 class (Warrior 和 Wizard) 可以運作

int main()
{
	Warrior w("Alice", 10);
	Character c = w; // Polymorphism
	cout << c.getName() << endl; // Alice
  return 0;
}

同樣的,我們也可以裡用指標來做同樣的功能

(可以用不同類型的指標指向不同類型的變數)

int main()
{
	Warrior w("Alice", 10);
	Character* c = &w; // Polymorphism
	cout << c->getName() << endl; // Alice
	return 0;
}

而有了 polymorphism,我們就不用擔心 warrior 跟 wizard 之間如果有相同函數的時候 argument 要怎麼辦。

void printInitial(Character c)
{
	string name = c.getname();
	cout << name[0];
}

int main()
{
	Warrior alice("Alice", 10);
	Wizard bob("Bob", 8);
	printInitial(alice);
	printInitial(bob);
	return 0;
}

這時候我們就只需要寫一個 void printInitial 就好了。

所以說我們宣告 array 就可以把 warrior 和 wizard 混在一起。

int main()
{
	Character* c[3]; // 不能用 Character c[3]; 因為沒有 default constructor
	c[0] = new Warrior("Alice", 10);
	c[1] = new Wizard("Bob", 8);
	c[2] = new Warrior("Amy", 12);
	for (int i = 0; i < 3; i++)
		c[i]->print();
	for (int i = 0; i < 3; i++)
		delete [i]; // 這是有三個指標指向三個不同空間
 // 不是 delete [] c (這是一個指標指向一排空間)
	return 0;
}

所以 Team 就可以被改成這樣:

class Team
{
private:
	int memberCount;
	Character* member[10]; // character 指標
public:
	Team();
	~Team();
	void addWarrior(string name, int lv);
	void addWizard(string name, int lv);
	void memberBeatMonster(string name, int exp);
	void printMember(string name);
};

Team::Team()
{
	memberCount = 0;
	for (int i = 0; i < 10; i++)
		member[i] = nullptr;
}
Team::~Team()
{
	for (int i = 0; i < memberCount; i++)
		delete member[i];
}

void Team::addWarrior(string name, int lv) 
{
	if (memberCount < 10)
	{
		member[memberCount] = new Warrior(name, lv);
		membercount++;
	}
}

void Team::addWizard(string name, int lv)
{
	if (memberCount < 10)
	{
		member[memberCount] = new Warrior(name, lv);
		membercount++;
	}
}

void Team::memberBeatMonster(string name, int exp)
{
	for (int i = 0; i < memberCount; i++)
	{
		if (member[i]->getName() == name)
		{
			member[i]->beatMonster(exp);
			break;
		}

	}
}

void Team::printMember(string name)
{
	for (int i = 0; i < memberCount; i++)
	{
		member[i]->print();
		break;
	}
}

我們解決了上面的問題,但還是有幾個問題待解決

  • 其他人還是可以創造一個 character object
  • 在 print 的時候,你不知道他是 warrior 還是 wiazard
  • exp 是累加的,levelup(0, 0, 0),所以能力值都還沒有上升

關於 parent 與 child 的 function,如果你像這樣子寫:

class A
{
public:
	void a() { cout << "a\n";}
	void f() { cout << "af\n";}
};

class B : public A
{
public:
	void b() { cout << "b\n";}
	void f() { cout << "bf\n";}
};

int main()
{
	B b;
	A a = b;
	A* ap = &b;
	a.a(); //a
	a.f(); // af
	ap->a(); // a
	ap->f(); // af 
	return 0;
}

你會發現在使用指標讀取 function 的時候,會以 parent 的函式為優先,因此為了解決這個問題,我們必須要使用

  • Late Binding
  • Virtual functions

Late binding

要了解 late binding,就必須先了解甚麼是 early binding

class A
{
protected:
	int i;
public:
	void a() { cout << "a\n";}
	void f() { cout << "af\n";}
};

class B : public A
{
protected:
	int j;
public:
	void b() { cout << "b\n";}
	void f() { cout << "bf\n";}
};

在這裡面,A class 會宣告 int i ,而B class 則會宣告 int iint j

  • Early binding: 當我們做 A a = b 的時候 ,因為在 compile 的時候電腦就會分配 4 byte 給 a (也就是 a 的 data type 老早在 compile 時就決定了),所以理所當然 a 就沒有空間裝得下 j 了。
  • Late binding: 反過來,當我們做A* = &b 的時候,由於 a 是一個指標,可以指向任何東西,所以可以指向 A 或是指向 B,也就是說它的 type 是在 run-time 時才知道的。

所以如果把上面的程式(Parent)改成這樣

class Parent
{
protected:
	int x;
	int y;
public:
	Parent(int a, int b) : x(a), y(b) {}
	void print(int a, int b){cout << x << " " << y;}
};
class Child : public Parent
{
protected:
	int z;
public:
	Child(int a, int b, int c): Parent(a,b ){z = c; }
	void print(int c){cout << z;}
};

int main()
{
	Child c(3, 4, 5);
	Parent p = c;
	p.print(); //(3, 4)
	return 0;
}

這時候我們就要把宣告的形式改成用指標

class Parent
{
protected:
	int x;
	int y;
public:
	Parent(int a, int b) : x(a), y(b) {}
	void print(int a, int b){cout << x << " " << y;}
};
class Child : public Parent
{
protected:
	int z;
public:
	Child(int a, int b, int c): Parent(a,b ){z = c; }
	void print(int c){cout << z;}
};

int main()
{
	Child c(3, 4, 5);
	Parent* pPtr = &c;
	pPtr->print(); 
	return 0;
}

這時候我們就不會去呼叫 parent 的 print()

但接下來還要搭配 virtual function 才能使用

Virtual functions 虛擬函數

class Parent
{
protected:
	int x;
	int y;
public:
	Parent(int a, int b) : x(a), y(b) {}
	virtual void print(int a, int b){cout << x << " " << y;}
};
class Child : public Parent
{
protected:
	int z;
public:
	Child(int a, int b, int c): Parent(a,b ){z = c; }
	void print(int c){cout << z;}
};

int main()
{
	Child c(3, 4, 5);
	Parent* pPtr = &c;
	pPtr->print(); 
	return 0;
}

所以對於 我們的 beatMonster() 還有 print() ,就可用 virtual + late binding 就行了 !

Pure Virtual function

但仔細想想,我們其實沒有呼叫到 parent 的 beatMonster()。那麼我們就可以把這個函式設成

virtual beatMonster(int exp) = 0;

這時候這個函式就會變成 pure virtual function,與此同時,我們也就不能把 Character 設成一個 object 了(pure virtual function 的副作用)。一次解決了兩個問題! 好爽

心得

感覺可以把這次用的加入我的小遊戲裡面喔!!


上一篇
Day 23 - 字串又來了,我還是沒吃到串燒
下一篇
Day 25 - 模板
系列文
三十天內用C++寫出一個小遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言