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 的副作用)。一次解决了两个问题! 好爽

心得

感觉可以把这次用的加入我的小游戏里面喔!!


<<:  Day21 URLSession 01 - POST

>>:  Day 24. 事件处理 – v-on

[Day26]用Canvas打造自己的游乐场-labyrinth 迷雾效果

昨天是把整张地图绘制出来,不过这样一下子就能看清长张地图的路线,缺乏了挑战性,这边要将地图可视范围缩...

Data layer implementation (2)

上一篇的 repository 还欠一个 mapper 把 EtaResponse 转成 EtaRe...

DAY 13- 《公钥密码》-RSA(1)

第一个要来看的公钥加密演算法是 RSA。 记得我们在 DAY6 的时候介绍到 RC4 时提到一个人吗...

晚上的空教室补课:名字赋予存在之变数 Variable

「今天要正式开始补课了。」诗忆相当紧张,趁着午休时间,拿着课堂讲义在图书馆试图预习,可惜一个字也读不...

铁人赛 Day6 -- PHP SQL基本语法(一)资料库连线 & require_once 引入档案

前言 昨天把资料库建立好之後,我们就要来试着连线我们的资料库 档案名称 : db.php (不罗嗦,...