iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Software Development

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

Day 25 - 模板

Outline:

  • Templates
  • The standard library <vector>
  • Exception Handling

Templates

我們上次寫的 Character 的 class

class Character
{
protected:
	static const int EXP_LV = 100; 
	string name;
	int level;
	int exp;
	int power;
	int knowledge; 
	int luck; 
//...
}

在以前,我們在寫完 class 之後,只能改變值的大小,但是卻不能改變 data type。例如,name 就只能用 string ,但不能途中把它改成 int

現在我們可以用 Template 實現這個願望了,他有幾個特性:

  • 可以用在 function 和 classes 上
  • 不只限定在 OOP
  • 也被叫做 generic programming (general implementation)

C++ Template 允許你傳入一個 data-type argument

而只有在這兩個時候允許:

  • 呼叫 【用 templates defined 過的 function】時
  • 建立【用 templates defined 過後的 class 中的 object】時
- Warrior<string> w1("Alice", 10);
- Wizard<int>  w2(16, 5); 

雖然我們可能會像上面傳入兩種不同的 argument,但是我們不需要寫兩種 implementations。

Template 怎麼做?

要宣告一個 type parameter,我們需要使用 template 還有 type name

template<typename T>
class TheClassName
{
	// T can be treated as a type inside the class definition block
}

如果要 implement 到 member function 上

template<typename T>
T TheClassName<T>::f(Tt)
{
	// t 是一個型態為 T 的變數
}

templat<typename T>
void TheClassName<T>::f(int i)
{
	// 當不用 T 的時候用這邊的函式
}

這時候就可以這樣用

int main()
{
	TheClassName<int> a;
	TheClassName<double> b;
	TheClassName<AnotherClassName> c;
}

例子:

#include<iostream>
using namespace std;

template<typename T>
void f(T t)
{
	cout << t;
}

int main()
{
	f<double>(1.2); // 1.2
	f<int>(1.2); // 1
	return 0;
}

例子:

複數個 parameter

#include<iostream>
using namespace std;

template<typename A, typename B>
void g(A a, B b)
{
	cout << a + b << endl;
}

int main()
{
	g<double, int>(1.2, 1.7); // 1.2 + 1 = 2.2
	return 0;
}

例子:

用在 class 中

template<typename T>
class C
{
public:
	T f(T i);
};

template<typename T>
T C<T>::f(T i)
{
	return i * 2;
}

int main()
{
	C<int> c;
	cout << c.f(10) << endl;
	return 0;
}

所以回到 Character,我們要怎麼寫?

template <typename KeyType>
class Character
{
protected:
	static const int EXP_LV = 100;
	KeyType name;
	int level;
	int	exp;
	int	power;
	int	knowledge;
	int	luck;
	void levelUp(int pInc, int kInc, int lInc);
public:
	Character(KeyType n, int lv, int po, int kn, int lu);
	virtual void beatMonster (int exp) = 0;
	virtual void print();
	KeyType getName
};

在這邊,因為我們只想要改變 name 的 type,所以就只有改 name 的 data type。

template<typename KeyType>
Character<KeyType>::Character(KeyType 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)
{
}

// 因為 beatmonster 是 pure virtual function -> 所以不用寫

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

template<typename KeyType>
void Character<KeyType>::levelUp(int pInc, int kInc, int lInc)
{
	this->level++;
	this->power += pInc;
	this->knowledge += kInc;
	this->luck += lInc;
}

template<typename KeyType>
KeyType Character<KeyType>::getName()
{
	return this->name;
}

這時候就可以對 Warrior 還有 Wizard 做更改

Warrior:

template <typename KeyType>
class Warrior : public Character<KeyType>
{
private:
  static const int PO_LV = 10;
  static const int KN_LV = 5;
  static const int LU_LV = 5;
public:
  Warrior(KeyType n, int lv = 0); 
  void print();
  void beatMonster(int exp);
};

template <typename KeyType>
Warrior<KeyType>::Warrior(KeyType n, int lv) : Character<KeyType>(n, lv, lv * PO_LV, lv * KN_LV, lv * LU_LV) 
{
}

template <typename KeyType>
void Warrior<KeyType>::print()
{
  cout << "Warrior ";
  Character<KeyType>::print();
}

template <typename KeyType>
void Warrior<KeyType>::beatMonster(int exp)
{
  this->exp += exp;
  while(this->exp >= pow(this->level, 2) * Character<KeyType>::EXP_LV) // Why?
    this->levelUp(PO_LV, KN_LV, LU_LV);
}

Wizard:

template <typename KeyType>
class Wizard : public Character<KeyType>
{
private:
  static const int PO_LV = 4;
  static const int KN_LV = 9;
  static const int LU_LV = 7;
public:
  Wizard(KeyType n, int lv = 0); 
  void print();
  void beatMonster(int exp);
};

template <typename KeyType>
Wizard<KeyType>::Wizard(KeyType n, int lv) : Character<KeyType>(n, lv, lv * PO_LV, lv * KN_LV, lv * LU_LV) 
{
}

template <typename KeyType>
void Wizard<KeyType>::print()
{
  cout << "Wizard ";
  Character<KeyType>::print();
}

template <typename KeyType>
void Wizard<KeyType>::beatMonster(int exp)
{
  this->exp += exp;
  while(this->exp >= pow(this->level, 2) * Character<KeyType>::EXP_LV) // Why?
    this->levelUp(PO_LV, KN_LV, LU_LV);
}

值得注意的一個點是 EXP_LV 我們需要使用 <KeyType> 的原因是因為,他是一個 static variable,我們需要知道他是怎樣的 parent 才能呼叫他(電腦才知道要呼叫誰)。

Team:

template <typename KeyType>
class Team
{
private:
  int memberCount;
  Character<KeyType>* member[10];
public:
  Team();
  ~Team();
  void addWarrior(KeyType name, int lv);
  void addWizard(KeyType name, int lv);
  void memberBeatMonster(KeyType name, int exp);
  void printMember(KeyType name);
};

template <typename KeyType>
Team<KeyType>::Team()
{
  this->memberCount = 0;
  for(int i = 0; i < 10; i++)
    member[i] = nullptr;
}

template <typename KeyType>
Team<KeyType>::~Team()
{
  for(int i = 0; i < this->memberCount; i++)
    delete this->member[i];
}

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

template <typename KeyType>
void Team<KeyType>::addWizard(KeyType name, int lv)
{
  if(memberCount < 10)
  {
    member[memberCount] = new Wizard<KeyType>(name, lv);
    memberCount++;
  }
}

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

template <typename KeyType>
void Team<KeyType>::printMember(KeyType name)
{
  for(int i = 0; i < this->memberCount; i++)
  {
    if(this->member[i]->getName() == name)
    {
      this->member[i]->print();
      break;
    }
  }
}

使用:

int main()
{
  Team<string> t; // 用名字稱呼的 team
  
  t.addWarrior("Alice", 1);
  t.memberBeatMonster("Alice", 10000);
  t.addWizard("Bob", 2);
  t.printMember("Alice");

  Team<int> t2; // 用編號稱呼的 team
  
  t2.addWarrior(1, 1);
  t2.memberBeatMonster(1, 10000);
  t2.addWizard(2, 2);
  t2.printMember(1);
  
  return 0;
}

注意注意!

如果當今天我們使用的 typename 是一個 class,這時候我們可能就要自己寫 operator overloading,才可以做比較。

Vector

前情提要

在 C string ,我們使用的是 char array

而 C++ string 則是用 class 來做 string

我們可以簡單地說, C++ string 就是把 C string 放進一個 class 中,並加入一些好用的 function。

所以同樣的,我們可能也希望把 integer 或是 double 裝入一個 class 中,並加入一些好用的 function。這件事情,可以說是使用 template 的大好時機。

在 C++ standard library (STL, standard template library) 有一個 class 叫做 <vector>

他可以做甚麼事情?

  • 可以做動態 array
  • 可以做 operator overloading

Creation

vector<int> v1; // integer vector
vector<double> v2;
vector<Warrior> v3;
  • member function that modifies a vector:
    • push_back(), pop_back(), insert(), erase(), swap, =, etc.
  • member function that access a vector element
    • [ ], front(), back(), etc.
  • member function related to the capacity
    • size(), max_size(), resize(), etc

Use

#include<iostream>
#include<vector>
using namespace std;

void printVector(vector<int> v)
{
	for (int i = 0; i < v.size(); i++)
		cout << v[i] << " ";
	cout << endl;
}

int main()
{
	vector<int> v;
	cout << v.size() << endl;
	cout << v.max_size() << endl;
	v.push_back(10);
	v.push_back(9);
	v.push_back(8);
	printVector(v); // 10 9 8
	v.pop_back();
	v.push_back(5);
	printVector(v); // 10 9 5

	return 0;
}

Rewrite Team

#include <iostream>
#include <string>
#include <cmath>
using namespace std;

template <typename KeyType>
class Team
{
private:
  vector<Character<KeyType>*> member;  // 原本最多只能 10 人
public:
  Team();
  ~Team();
  void addWarrior(KeyType name, int lv);
  void addWizard(KeyType name, int lv);
  void memberBeatMonster(KeyType name, int exp);
  void printMember(KeyType name);
};

template <typename KeyType>
Team<KeyType>::Team()
{
}

template <typename KeyType> // 非常重要,因為我們要刪除空間,否則會 memory leak!
Team<KeyType>::~Team()
{
  while(this->member.size() > 0)
  {
    delete this->member.back();
    this->member.pop_back();
  }
}

template <typename KeyType>
void Team<KeyType>::addWarrior(KeyType name, int lv) 
{
  Warrior<KeyType>* wPtr = new Warrior<KeyType>(name, lv); // 必須使用動態記憶體配置!
  this->member.push_back(wPtr);
}

template <typename KeyType>
void Team<KeyType>::addWizard(KeyType name, int lv)
{
  Wizard<KeyType>* wPtr = new Wizard<KeyType>(name, lv); 
  this->member.push_back(wPtr);
}

template <typename KeyType>
void Team<KeyType>::memberBeatMonster(KeyType name, int exp)
{
  for(int i = 0; i < this->member.size(); i++) // 不用記 member 有多少人了
  {
    if(this->member[i]->getName() == name)
    {
      this->member[i]->beatMonster(exp);
      break;
    }
  }  
}

template <typename KeyType>
void Team<KeyType>::printMember(KeyType name)
{
  for(int i = 0; i < this->member.size(); i++)
  {
    if(this->member[i]->getName() == name)
    {
      this->member[i]->print();
      break;
    }
  }
}

但是老師給了一個建議:

如果你寫不出 vector,就先不要用它吧!

主要是因為這是比較高階的用法,但是因為我們現在還算是新手階段,所以要盡量的使用基礎一點的用法解決問題,等到我們融會貫通後,vector 的想法也自然可以使用了。

Exceptions 例外

在寫程式的時候,常會發生 run-time error 等問題,像是這樣

#include<iostream>
using namespace std;

void f(int a[], int n)
{
	int i = 0;
	cin >> i;
	a[i] = 1; // run-time error
}

int main()
{
	int a[5] = {0};
	f(a, 5);
	for (int i = 0; i < 5; i++)
		cout << a[i] << " ";
	return 0;
}

因為很有可能會產生 run-time error ,但是我們不知道何時會發生,這時候可以 check 看看就知道了

#include<iostream>
using namespace std;

bool f(int a[], int n)
{
	int i = 0;
	cin >> i;
	if (i < 0 || i > n)
		return false;	
	a[i] = 1;
	return true;
}

int main()
{
	int a[5] = {0};
	f(a, 5);
	for (int i = 0; i < 5; i++)
		cout << a[i] << " ";
	return 0;
}

但是這樣的 check 方式其實有很多缺點:

  • 使用者會亂輸入值
  • 我們好像只能回傳 false
  • 我們很難回傳 error message

因此 C++ 提供一個方式叫做 exception handling,他是

  • a mechanism for handling logic & run-time error
  • a function can report the occurrence of an error by Throwing an exception
  • One catch an exception and then respond accordingly (你丟我撿)

使用方法

use try & catch

try
{
	// statement that may throw exceptions
}

catch(ExceptionClass identifier) // this kind? 
{
	responses
}

catch(AnotherExceptionClass identifier) // that kind?
{
	// other responses
}

程式跑到這邊的時候會 try 你說的 exception,這時候如果他有讀到 exception 就會跳到 catch 那裏面,並做出反應,最後會直接 shut down 程式。

因此這時候要記得宣告 destructor ,否則前面的動態配置不會被 release 掉。

例子

replace()

#include<iostream>
#include<string>
#include<stdexcept>
using namespace std;

void g (string& s, int i)
{
	s.replace(i, 1, ".");
}
int main()
{
	string s = "12345";
	int i = 0;
	cin >> i;
	g(s, i);
	cout << s << endl;
	return 0;
}

這時候如果 i 打入 -8,這樣會使程式 error。

所以就必須使用 exception

我們可以在 function 就使用 try & catch

void g(string& s, int i)
{
	try
	{
		s.replace(i, 1, ".");
	}
	catch (out_of_range e)
	{
		cout << "...\n";
	}
	
}

也可以在 caller 使用

int main()
{
	string s = "12345";
	int i = 0;
	cin >> i;
	try
	{
		g(s, i);
	}
	catch (out_of_range e)
	{
		cout << "...\n";
	} 
	cout << s << endl;
	return 0;
}

在 exception 中的 classes 有:

exception
		logic_error
				domain_error
				invalid_argument
				length_error
				out_of_range
		runtime_error
				range_error
				overflow_error
				underflow_error

層級階層大概是長這樣。

Throwing exception

#include<iostream>
#include<stdexcept>
using namespace std;

void f(int a[], int n) throw (logic_error) // 寫了這個就代表只能傳出這個 exception
{
	int i = 0;
	cin >> i;
	if (i < 0 || i > n)
		throw logic_error("..."); // 丟一個 exception -> 就像是做了一個 object 後往外丟
	a[i] = 1; 
}

int main()
{
	int a[5] = { 0 };
	f(a, 5);
	for (int i = 0; i < 5; i++)
		cout << a[i] << " ";
	return 0;
}

在函數後面寫 throw (...error) ,寫了這個就代表只能傳出這個 exception,也就是設計這個 function 的人跟其他人說,他可能傳出甚麼exception。那如果你確定他不會跑出 exception ,這時候就可以在 function 後面加上noexcept

Our own exception

我們也可以自己定義自己的 exception

#include<stdexcept>
using namespace std;

class MyException : public exception
{
public:
	MyException(const string& msg = "") : exception(msg.c_str()) {}
};

使用!

template <typename KeyType>
void Team<KeyType>::addWarrior(KeyType name, int lv) throw (MyException)
{
	if (memberCnt < 10)
	{  
		member[memberCnt] = new Warrior<KeyType>(name, lv);
		memberCnt++;
	}
	else
		throw MyException("...");
}

心得

今天的內容是小傑老師上的最後一堂課程!

有一種畢業的感覺(並沒有)

但是真的很進階,可能還是要多看一些 code 才知道未來遇到的時候要怎麼辦


之後的內容就會是我自己開發的小遊戲!

敬啟期待!


上一篇
Day 24 - 繼承家業
下一篇
Day 26 - 你有沒有玩過LOL、特、CS、SF、楓之谷、APEX?
系列文
三十天內用C++寫出一個小遊戲30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言