人称 OOP 的特色有三点,且缺一不可
简单来说,就是透过已经产生的 classes 再去做新的 classes。
这是之前我们写的 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 继承的东西除了有
因此,在 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。
在继承的情形下,我们有时候会 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();
除了 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
而 Destructor 则会
我们可以把 public → protected → private分成三个层级,public 是大家都可以用,protected 是只有继承的人 可以用,而 private 则是只有自己可以使用。所以透过这三个方式,可以设定你想要的 inheritance visibility。
虽然说像这样的 multiple inheritance 在 C++ 里面是可以运作的,但是非常地不推荐!
原因是因为同样继承的 n 或是 m 就会混淆。且有时候你会觉得你可以 inherit from sister, brother,但是理论上不太能这样做(真的会太容易混淆)
反正,就先不要这样做吧。
也就是俗称的角色扮演游戏(RPG),基本上就是以赚取经验值升等为主要目的(衍伸的还有装备、职业)。且这些职业会有不同的特性,像是 剑士就是个坦克,血厚但攻击力普通;刺客攻击力高但血薄;法师单体伤害低,但范围伤害高,等等。
所以他们就很适合使用 class 来做。
首先是每一个职业都一样的 characteristics
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;
}
所以我们现在要来创造职业的 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。
虽然这东西看起来没甚麽问题,但是可能还是有一点问题
class Team
{
private:
int warriorCount
int wizardCount
Warrior* warrior[10];
Wizard* wizard[10];
public:
Team();
~Team();
// some other functions
};
但是会产生一个问题
这样会整个大混乱。
上述的那些小问题,都可以透过 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;
}
}
我们解决了上面的问题,但还是有几个问题待解决
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,就必须先了解甚麽是 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 i
与 int j
。
A a = b
的时候 ,因为在 compile 的时候电脑就会分配 4 byte 给 a (也就是 a 的 data type 老早在 compile 时就决定了),所以理所当然 a 就没有空间装得下 j 了。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 才能使用
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 就行了 !
但仔细想想,我们其实没有呼叫到 parent 的 beatMonster()
。那麽我们就可以把这个函式设成
virtual beatMonster(int exp) = 0;
这时候这个函式就会变成 pure virtual function,与此同时,我们也就不能把 Character 设成一个 object 了(pure virtual function 的副作用)。一次解决了两个问题! 好爽
感觉可以把这次用的加入我的小游戏里面喔!!
<<: Day21 URLSession 01 - POST
昨天是把整张地图绘制出来,不过这样一下子就能看清长张地图的路线,缺乏了挑战性,这边要将地图可视范围缩...
上一篇的 repository 还欠一个 mapper 把 EtaResponse 转成 EtaRe...
第一个要来看的公钥加密演算法是 RSA。 记得我们在 DAY6 的时候介绍到 RC4 时提到一个人吗...
「今天要正式开始补课了。」诗忆相当紧张,趁着午休时间,拿着课堂讲义在图书馆试图预习,可惜一个字也读不...
前言 昨天把资料库建立好之後,我们就要来试着连线我们的资料库 档案名称 : db.php (不罗嗦,...