人稱 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 的副作用)。一次解決了兩個問題! 好爽
感覺可以把這次用的加入我的小遊戲裡面喔!!