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 23 介绍 FactoryBot Rails 及设定

>>:  Vue.js介绍及开发环境准备(DAY24)

【Day 10】- 藏起来的 Process 真的看不见摸不着?(讲解找出断链後的 Process 方法)

Agenda 资安宣言 测试环境与工具 学习目标 前情提要 技术原理与程序码 References ...

# Day33 无内文 待补

Day33 Golang gRPC介绍与应用 RPC (Remote Procedure Call)...

Day9 - 期货contract及读取报价方式

今天要讲的是期货合约的相关函数。 首先是Contracts函数,就像之前文章里有使用到的一样,透过C...

LINE Bot主动推播

我现在使用django架构来做LINE Bot,後端如果有新增优惠消息到资料库里面,就会传送优惠消息...

完赛心得

  最好的投资,就是投资自己 Investing in yourself is the best ...