國立屏東大學 資訊工程學系 物件導向程式設計
Inheritance(繼承性)是指讓某一類別的物件繼承來自其它類別的屬性與行為。C++支援多重繼承,讓一個類別可以繼承多個類別。
「A is a kind of B.」
繼承性其實就是所謂的ISA關係 – IS-A relationship,是指兩個類別A與B間存在著:A類別就是B類別的一種特殊化。所謂的「特殊化(specialization)」是指A類別其實就是B類別,但A類別比B類別特殊些。我們將A類別稱為衍生類別(derived class)或子類別(child class),B類別則稱為基礎類別(base class)或父類別(parent class)。
回顧我們在前一章開始介紹的Person類別,每個屬於這個類別的物件都會具有first name, last name等資料成員,以及showInfo()、set_firstname()、get_firstname()、set_lastname()、get_lastname()等成員函式:
#include <iostream> using namespace std; #ifndef _PERSON_ #define _PERSON_ class Person { private: string firstname; string lastname; public: Person(); Person(string, string); void showInfo(); void set_firstname(string fn); string get_firstname(); void set_lastname(string ln); string get_lastname(); }; #endif
#include "person.h" Person::Person() { } Person::Person(string fn, string ln) { firstname=fn; lastname=ln; } void Person::showInfo() { cout << "Name: " << firstname << " " << lastname << endl; } void Person::set_firstname(string fn) { firstname=fn; } void Person::set_lastname(string ln) { lastname=ln; } string Person::get_firstname() { return firstname; } string Person::get_lastname() { return lastname; }
如果我們要設計一個學生成績管理系統(Student Score Management System, STUScoreMan),在我們所要建構的物件導向世界中,必然會存在有「學生」這種類別的物件。那要如何開始設計學生這個類別呢?先考慮學生只需具備firstname、lastname與學號三個資料成員,那麼我們可以撰寫以下的程式碼:
using namespace std; #include <iostream> #ifndef _STUDENT_ #define _STUDENT_ class Student { private: string firstname; string lastname; string ID; }; #endif
就如同前一章所介紹的,我們還應該為這個類別加上建構子、顯示學生資料的method、設定存取權限、設計setters與getters…。
#include <iostream> using namespace std; #ifndef _STUDENT_ #define _STUDENT_ class Student { private: string firstname; string lastname; string ID; public: Student(); Student(string, string); void showInfo(); void set_firstname(string fn); string get_firstname(); void set_lastname(string ln); string get_lastname(); void set_ID(string id); string get_ID(); }; #endif
我們發現這個新設計的Student類別,與Person類別很相似….
「Student也是一種Person,只是比較特別!比起Person,Student還多了ID(目前為止)。」
以這樣的角度看問題時,我們可以說「Student is a kind of Person!」。我們可以用以下的程式碼,告訴電腦這件事:
#include "person.h" class Student : public Person { };
「: public Person」是用以表示Student類別是「衍生自(derived from)」Person類別,用物件導向的術語來說:Student類別繼承了Person類別。在這個ISA的繼承關係中,我們將Person類別稱為「基礎類別(base class)或父類別(parent class)」,Student類別則稱為「衍生類別(derived class)或子類別(child class)」。
其中的「public」修飾字是用以說明此繼承為public繼承,稱為「public derivation」,所有在父類別中的public成員都會變成是在子類別中的public成員。但在父類別中的private成員,則僅能從繼承自父類別中的public(或protected)成員函式來存取。除此之外,除了預設的建構函式外,其餘的建構函式並不會被繼承。
<note>
</note>
一旦你完成這樣的設計,儘管現在在Student類別中,一行程式碼都還沒寫,但Person類別該有的,Student類別也都會有,包含Person類別的屬性與行為。因此,我們可以在物件導向的世界中,把Student類別的物件視為是Person類別的物件,並且使用它所公開(public)的屬性與行為。請參考下面的程式碼:
#include <iostream> using namespace std; #include "student.h" int main() { Student *amy = new Student; amy->set_firstname("Amy"); amy->set_lastname("Chang"); amy->showInfo(); return 0; }
沒有意外地,其執行結果如下:
junwu@ws2 oop % ./a.out Name: Amy Chang junwu@ws2 oop %
但這不是我們要的結果,雖然Student已經是a kind of Person,但Student不夠特殊,它跟Person根本是一樣的。讓我們將新的屬性與行為加到Student類別中,請參考下面的程式碼:
#ifndef _STUDENT_ #define _STUDENT_ #include "person.h" class Student : public Person { private: string ID; public: void set_ID(string id); string get_ID(); }; #endif
#include "student.h" void Student::set_ID(string id) { ID=id; } string Student::get_ID() { return ID; }
#include <iostream> using namespace std; #include "student.h" int main() { Student *amy = new Student(); amy->set_firstname("Amy"); amy->set_lastname("Chang"); amy->set_ID("s111418099"); amy->showInfo(); return 0; }
<note> 在Person類別中,firstname與lastname是定義為private,所以連其子類別都不能使用,必須透過public的setters與getters才能存取。 </note>
讓我們為這個小節做個總結:
「Student IS A (kind of) Person」
在C++語言裡,子類別(subclass)可以繼承父類別(super class)所有的屬性與行為,除了那些被定義為私有的(private)。
在本章的範例中,我們使用public derivation來讓Student類別繼承了Person類別。在Person類別中,提供了Person(string, string)型式的建構函式,可以將firstname與lastname的初始值加以設定。現在,讓我們試著使用下列程式碼,來產生Student類別的物件並加以初始化其值:
#include <iostream> using namespace std; #include "student.h" int main() { Student *stu = new Student("Yo-Yo", "Ma"); return 0; }
讓將之加以編譯,會得到以下的錯誤:
junwu@ws2 oop % c++ main.cpp main.cpp: In function 'int main()': main.cpp:14:35: error: no matching function for call to ‘Student::Student(const char [4], const char [6])’ main.cpp:8:44: note: candidates are: In file included from main.cpp:4:0: student.h:5:7: note: Student::Student() student.h:5:7: note: candidate expects 0 arguments, 2 provided student.h:5:7: note: Student::Student(const Student&) student.h:5:7: note: candidate expects 1 argument, 2 provided junwu@ws2 oop %
其中,「error: no matching function for call to ‘Student::Student(const char [5], const char [2])’」含有兩點資訊:
另外「candidates are」則列示了,可能的建構函式包含哪些,其中:
student.h:5:7: note: Student::Student() student.h:5:7: note: candidate expects 0 arguments, 2 provided
表示存在一個Student::Student()的建構函式,當然它是從Person類別繼承下來的,試著將程式修改如下:
Person::Person() { cout << "A Person is created." << endl; }
並且使用無參數的Student()建構一個物件看看,你應該會看到以下的輸出。
A Person is created.
其實,在前面的討論中,我們已經說明過使用public derivation時,除此之外,除了預設的建構函式外,其餘的建構函式並不會被繼承(表示預設的無參數建構函式會被繼承)。但下面卻又告訴我們還有一個預設的建構函式存在:
student.h:5:7: note: Student::Student(const Student&) student.h:5:7: note: candidate expects 1 argument, 2 provided
這是一個單一參數的建構函式「Student::Student(const Student &)」,讓我們試試以下的程式碼:
#include <iostream> using namesapce std; #include "student.h" int main() { Student *amy = new Student(); amy->set_firstname("Amy"); amy->set_lastname("Chang"); amy->set_ID("s111418099"); Student *tony = new Student(*amy); tony->set_firstname("Tony"); amy->showInfo(); tony->showInfo(); return 0; }
其執行結果為:
junwu@ws2 oop % ./a.out Name: Amy Chang Name: Tony Chang junwu@ws2 oop %
其實,這個並不是預設的建構函式,而是預設的List Initializer,也就是說,這是預設的初始值給定。這會以所傳入的物件參考的值,來設定新物件的值。
在解構函式部份,則與建構函式類似。衍生類別也會繼承基礎類別的解構函式:
在預設的情況下,一個子類別的物件建立時,必先呼叫父類別的建構函數,再呼叫本身的建構函數;且在解構時,必先呼叫本身的解構函數,再呼叫父類別的解構函數。例如:
Person::~Person() { cout << "A Person is removed." << endl; } Person::Person() { cout << "A Person is created." << endl; } Student::Student() { cout << "A Student is created." << endl; } Student::~Student() { cout << "A Student is removed." << endl; }
若執行下列程式:
int main() { Student *amy = new Student(); amy->set_firstname("Amy"); amy->set_lastname("Chang"); amy->set_ID("s111418099"); Student *tony = new Student(*amy); tony->set_firstname("Tony"); amy->showInfo(); tony->showInfo(); delete amy; delete tony; return 0; }
其執行結果為:
A Person is created. A Student is created. Name: Amy Chang Name: Tony Chang A Student is removed. A Person is removed. A Student is removed. A Person is removed.
除了預設型式的建構函式外,我們也可以設計新的建構函式。
Student::Student(string fn, string ln, string id) { firstname=fn; lastname=ln; ID=id; }
但編譯時會得到以下的錯誤:
In file included from student.h:3:0, from student.cpp:1: person.h: In constructor ‘Student::Student(std::string, std::string, std::string)’: person.h:10:10: error: ‘std::string Person::firstname’ is private student.cpp:10:3: error: within this context In file included from student.h:3:0, from student.cpp:1: person.h:11:10: error: ‘std::string Person::lastname’ is private student.cpp:11:3: error: within this context make: *** [student.o] Error 1
從上面的訊息中,你可以瞭解問題在哪嗎?讓我們改寫這個建構函式如下:
Student::Student(string fn, string ln, string id) { set_firstname(fn); set_lastname(ln); ID=id; }
若執行下列程式:
int main() { Student *amy = new Student("Amy", "Chang", "s111418099"); amy->showInfo(); delete amy; return 0; }
可以得到以下的結果:
A Person is created. Name: Amy Chang A Student is removed. A Person is removed.
我們也可以在衍生類別的建構函式中呼叫其基礎類別的建構函式:
Student::Student(string fn, string ln, string id):Person(fn, ln) { ID=id; }
由於在衍生類別的建構過程中,已呼叫了一個基礎類別的建構函式(兩個字串參數版本),因此將不會再呼叫預設版本。我們也可以將上述建構式修改如下:
Student::Student(string fn, string ln, string id):Person(fn, ln), ID(id) { }
所謂的覆寫(overriding)是指,衍生類別將所繼承下來的成員函式改寫,提供自己的實作。例如,在我們前述的例子中,「Student」類別繼承了來自「Person」類別的「showInfo()」函式,但其不符合需求,因此,我們將其改寫如下:
#ifndef _STUDENT_ #define _STUDENT_ #include "person.h" class Student : public Person { private: string ID; public: Student(); Student(string fn, string ln, string id); ~Student(); void set_lastname(string ln); void set_firstname(string fn); void set_ID(string id); string get_ID(); void showInfo(); }; #endif
void Student::showInfo() { cout << "Name: " << get_firstname() << "" << get_lastname() << endl; cout << "ID: " << ID << endl; } void Student::set_firstname(string fn) { for(int i=0;i<fn.size();i++) { fn[i]=toupper(fn[i]); } Person::set_firstname(fn); } void Student::set_lastname(string ln) { for(int i=0;i<ln.size();i++) { ln[i]=toupper(ln[i]); } Person::set_lastname(ln); }
當我們改寫了成員函式後,還是可以用基礎類別的名稱加上「:」來呼叫原本的版本,例如「Person::set_lastname(ln);」。
所謂的重載(overloading)(又被稱為多載或覆載)是指在類別中,擁有一個以上相同名稱的成員函式。由於名稱相同,在呼叫時會以其參數來決定執行的版本。前面所提到的,一個類別可以有多個不同版本的建構函式,就是多載的例子。請參考下面的例子:
#ifndef _STUDENT_ #define _STUDENT_ #include "person.h" class Student : public Person { private: string ID; public: Student(); Student(string fn, string ln, string id); ~Student(); void set_ID(string id); void set_name(string fn, string ln); void set_name(string n); string get_ID(); void showInfo(); }; #endif
我們打算提供新的設定學生名字的方法,採用「set_name()」來完成其名稱的設定,並且提供兩種方法,首先是:
void Student::set_name(string fn, string ln) { set_firstname(fn); set_lastname(ln); } void Student::set_name(string n) { unsigned pos=n.find(" "); set_firstname( n.substr(0,pos)); set_lastname( n.substr( pos+1, n.size()-(get_firstname()).size())); }
將一個衍生類別的參考或指標轉換為其基礎類別的參考或指標,就稱為「upcasting」(向上轉型),同時只要是public derivation的類別都允許這樣做。