derived class

49
Derived Class • 前前 • 前前前前前前前 •前 前前 • public, protected, 前 privated 前前前前 • virtual 前前前前 • RTTI (Run-time Type Information)

Upload: bruno-solomon

Post on 01-Jan-2016

15 views

Category:

Documents


1 download

DESCRIPTION

Derived Class. 前言 衍生類別的定義 單一繼承 public, protected, 和 privated 基底類別 virtual 成員函式 RTTI (Run-time Type Information). 前言. C++ 提供類別繼承的機制來擴充或更改現有類別的功能。我們可以利用此機制來達到以下兩個目的: 程式碼的再利用( code reuse ) 物件導向的設計( object-oriented design ). class B. class D. 類別繼承的圖示. 衍生類別的定義. - PowerPoint PPT Presentation

TRANSCRIPT

Derived Class

• 前言• 衍生類別的定義• 單一繼承• public, protected, 和 privated 基底類別• virtual 成員函式• RTTI (Run-time Type Information)

前言C++ 提供類別繼承的機制來擴充或更改現有類別的功能。我們可以利用此機制來達到以下兩個目的:

程式碼的再利用( code reuse )

物件導向的設計( object-oriented design )

假定 B 是一個類別。我們可以用底下的格式來定義一個 B

的衍生類別 D :

class D : public B

{

// members of D

};

我們稱: D 繼承 B 、 B 是 D 的 base class (基底類別)或 superclass (父類別)、以及 D 是 B 的 derived class

(衍生類別)或 subclass (子類別)。

衍生類別的定義

class B

class D

類別繼承的圖示

類別繼承通常是用來表達 kind-of 關係(或稱 is-a 關係):衍生類別是基底類別的一種。譬如: Manager (經理)也是 Employee (員工),所以我們先規劃好 Employee 類別,然後把 Manager 定義成 Employee 的一個衍生類別:

class Employee {

// other members

private:

string first_name, family_name;

char middle_initial;

Date hiring_date;

short department;

};

class Manager : public Employee {

// other members

private:

set<Employee *> group;

short level;

};

Manager 的資料成員:string first_name, family_name;

char middle_initial;

Date hiring_date;

short department;

set<Employee *> group;

short level;

衍生類別除了本身的資料成員以外,也具有基底類別的資料成員。譬如:

Employee 的資料成員:string first_name, family_name;

char middle_initial;

Date hiring_date;

short department;

由於衍生類別是基底類別的子類別,因此衍生類別的物件可視為基底類別的物件;基底類別型態的指標也可用於衍生類別的物件,而不須要經過型態轉換。但反過來就不成立了。譬如:

void foo (Manager mm, Employee ee)

{

Employee *pe = &mm; // ok

Manager *pm = &ee; // error

pe->level = 2; // error: Employee doesn‘t have

// data member:level

pm = static_cast<Manager *> (pe); // ok: explicit type casting

pm->level = 2; // ok: since pe points to a

// Manager object

}

衍生類別及其朋友( friends )可以直接使用基底類別的 publ

ic 或 protected 成員,但是不能使用基底類別的 private 成員。譬如:

class B {

public:

void f();

protected:

void g();

int _x;

private:

void h();

int _y;

};

class D : public B {friend void k(D);void a() { f(); … } // okvoid b() { g(); … } // okvoid c() { _x = 0; } // ok void d() { h(); … } // errorvoid e() { _y = 0; } // error

};

void k (D obj){

obj.g(); // okobj.f(); // okobj._x = 0; // okobj._y = 0; // error

}

存取控制

本身與其朋友 衍生類別與其朋友 外界

public

protected

private

x

xx

我們用下表來總結類別成員的存取控制:

如果基底類別的成員函式不符合所需的話,我們可以在衍生類別中重新改寫( override )。譬如:

class Employee {

public:

void print();

// other members

};

void Employee::print()

{

cout << first_name << ‘ ’

<< middle_initial << ‘ ’

<< family_name;

}

class Manager : public Employee {

public:

void print();

// other members

};

void Manager ::print()

{

Employee::print();

cout << ‘ ’ << level;

}

這項改寫的機制讓 code reuse 可以很容易地達成。

衍生類別的建構函式必須呼叫基底類別的建構函式(如果它存在的話),而且前者的參數必須包含後者的參數。譬如:

class Employee {

public:

Employee (const string & n, int d)

: family_name(n), department(d) { }

// other members

};

class Manager : public Employee {

public:

Manager (const string & n, int d, int lvl)

: Employee(n, d), level(lvl) { }

// other members

};

衍生類別的物件建構順序如下:

1. 執行基底類別的建構函式。

2. 執行衍生類別資料成員的建構函式。

3. 執行衍生類別的建構函式。

衍生類別的物件解構順序則恰恰相反,即:

1. 執行衍生類別的解構函式。

2. 執行衍生類別資料成員的解構函式。

3. 執行基底類別的解構函式。

資料成員和基底類別是按照宣告的順序來建構,解構則按照相反的順序。

拷貝衍生類別物件至基底類別物件時,只拷貝基底類別的資料成員。

class Employee {

Employee (const Employee &);

Employee& operator=(const Employee &);

// …

};

void f (const Manager &m)

{

Employee e = m; // construct e from Employee part of m

e = m; // assign Employee part of m to e

}

範例

class CPoint2D {

public:

CPoint2D (int x = 0, int y = 0) : _x(x), _y(y) { }

int x () { return _x; }

int y () { return _y; }

void setX (int x) { _x = x; }

void setY (int y) { _y = y; }

void set (int x, int y) { _x = x; _y = y; }

bool isZero () { return _x == 0 && _y == 0; }

double distance () { return sqrt(_x * _x + _y * _y); }

protected:

int _x, _y;

};

我們利用 CPoint2D 類別來定義 CPoint3D 類別。首先我們把 CPoint2D 的 private 成員改成 protected 成員,讓 CPoint3D 可以使用它們。

class CPoint3D : public CPoint2D {

public:

CPoint3D (int x = 0, int y = 0, int _z = 0)

: CPoint2D(x, y), _z(z) { }

int z() { return _z; }

void setZ (int z) { _z = x; }

void set (int x, int y, int z) { set(x, y); setZ(z); }

bool isZero () { return _x == 0 && _y == 0 && _z == 0; }

double distance () { return sqrt(_x * _x + _y * _y + _z * _z); }

private:

int _z;

};

CPoint3D 改寫( override ) CPoint2D 的 isZero() 和 dis

tance() 函式。

#include <iostream>

using namespace std;

#include “CPoint3D.h”

ostream& operator<< (ostream &os, CPoint3D p)

{

os << ‘(‘ << p.x() << “, “ << p.y() << “, “ << p.z() << ‘)’;

return os;

}

我們可以為 CPoint3D 類別定義輸出運算子 << 如下:

測試程式

#include “CPoint3D.h”

int main ()

{

CPoint3D p(1, 2, 3);

cout << p << endl;

CPoint2D q(5, 6);

q = p;

cout << q << endl;

p = q; // error

}

輸出結果

(1, 2, 3)

(1, 2)

範例

typedef unsigned short big5char;class big5string : public string {public:

bool is_big5char (int idx);big5char next_char(int idx);int big5_length();

private:bool in (char c, int min, int max) { return min <= c && c <= max; }bool is_big5hiByte (char c) { return in(c, 0x81, 0xFE); }bool is_big5loByte (char c)

{ return in(c, 0x40, 0x7E) || in(c, 0xA1, 0xFE); }};

我們利用標準的 string 類別來定義 big5string 類別。 big5string 類別提供了一些專門用來處理中文字串的成員函式。註:中文 Big5 碼中的中文字元是雙位元組,其中高位元組的範圍是 0x81 0xFE 、低位元組的範圍是 0x40 0x7E 與 0xA1 0xFE 。

bool big5string::is_big5char (int idx)

{

assert(idx >= 0 && idx < length() );

char *cp = c_str();

if (cp[idx] < 128 || idx == length() -1)

return false;

if (is_big5hiByte(cp[idx]) && is_big5loByte(cp[idx+1])

return true;

else

return false;

}

big5char big5string::next_char (int idx){

char *cp = c_str();return is_big5char(idx)? cp[idx]*256+cp[idx] : cp[idx];

}

int big5string::big5_length (){

int len = 0, k = 0;while (k < length()) {

k = is_big5char(k) ? k+2 : k+1;len++;

}return len;

}

測試程式

#include <iostream>

#include <string>

#include “big5string.h”

int main ()

{

string s1(“Hi, “);

big5string s2;

s2 = s1 + “ 好久不見” ;

cout << s2 << endl;

cout << “# of characters is: “ << s2.big5_length() << endl;

return 0;

}

輸出結果

Hi, 好久不見# of characters is: 8

big5string 物件除了可使用 big5string 的功能以外,也可使用 string 所提供的各項功能。

單一繼承類別可隨需要而建立層層的繼承關係。譬如:

class Employee { … };

class Manager : public Employee { … };

class Director : public Manager { … };

Director 是 Manager 的衍生類別、 Manager

又是 Employee 的衍生類別。因此 Director 是 Manager

的子類別,也是 Employee 的子類別。之前所說 Manager

和 Employee 間的關係同樣適用於 Director 和 Employee

之間。譬如: Director 包含 Employee 所有的資料成員、也可以直接使用 Employee 的 public 和 protected 成員、等等。

Employee

Manager

Director

B

D1

D2

Dn

衍生類別也可以作為其他類別的基底類別。如此一來,類別的繼承就形成如右圖所示的線性結構。 D1, D2, …, Dn

都是基底類別 B 的衍生類別(或子類別),其中 D1 稱為 B 的「直接衍生類別」、 D2, …, Dn 稱為 B 的「間接衍生類別」。此外, D1, D2, …,

Dn 型態的指標都可以轉換成 B 型態的指標。

root class有些時候,單一繼承的類別會形成如圖所示的階層狀(樹狀)的結構。其中最上層的類別稱為「根類別( root class )」。其他的類別都是根類別的子類別。

public, protected, 和 privated 基底類別

基底類別可以指定成 public 、 protected 、或 private 。譬如:

class X : public B { … };

class Y : protected B { … };

class Z : privated B { … };

如果省略這些指定,則 class 的基底類別預設為 private 、而 struct 的基底類別預設為 public 。譬如:

class X : B { … }; // B is a private base

struct X : B { … }; // B is a public base

這三種的差別在於以下的限制對間接衍生類別有所不同:• 基底類別 public 和 protected 成員的存取;• 把指標和參照從衍生類別的型態轉換成基底類別的型態。

假定 D 是 B 的一個直接衍生類別。

B 是一個 private 基底類別B 的 protected 和 public 成員在 D 中變成 private 成員。這使得只有 D 的成員和朋友可以使用它們,而其他函式(包含 D 的衍生類別之成員和朋友)則不能使用它們。

此外,只有 D 的成員和朋友可以把 D* 轉換成 B* 。

class B {

public:

void foo ();

protected:

void bar ();

};

class D1 : private B {

void f () { bar(); … } // ok

};

class D2 : public D1 {

void g () { bar(); … } // error

void h () { foo(); … } // error

};

void func (){

B b;D1 d1;D2 d2;

b.foo(); // okb.bar(); // error

d1.foo(); // errord1.bar(); // error

d2.foo(); // errord2.bar(); // error

B *bp = &D1; // error}

B 是一個 protected 基底類別B 的 protected 和 public 成員在 D 中變成 protected 成員,使得只有 D 及其衍生類別的成員和朋友可以使用它們,而其他函式則不能使用它們。

此外,只有 D 及其衍生類別的成員和朋友可以把 D* 轉換成 B* 。

B 是一個 public 基底類別B 的 protected 和 public 成員在 D 中仍維持相同的存取模式。此外,任何函式都可以把 D* 轉換成 B* 。

class B {

public:

void foo ();

protected:

void bar ();

};

class D1 : protected B {

void f () { bar(); … } // ok

};

class D2 : public B {

void g () { bar(); … } // ok

void h () { foo(); … } // ok

};

void func (){

B b;D1 d1;D2 d2;

b.foo(); // okb.bar(); // error

d1.foo(); // errord1.bar(); // error

d2.foo(); // errord2.bar(); // error

B *bp = &D1; // error}

class B {

public:

void foo ();

protected:

void bar ();

};

class D1 : public B {

void f () { bar(); … } // ok

};

class D2 : public B {

void g () { bar(); … } // ok

void h () { foo(); … } // ok

};

void func (){

B b;D1 d1;D2 d2;

b.foo(); // okb.bar(); // error

d1.foo(); // okd1.bar(); // error

d2.foo(); // okd2.bar(); // error

B *bp = &D1; // ok}

使用 B 的 public 和 protected 成員

D 的成員 D 及其衍生類別 其他函式與朋友 的成員與朋友

private B

protected B

public B

x public butnot protected

public butnot protected

public butnot protected

把 D* 轉換成 B*

D 的成員 D 及其衍生類別 其他函式與朋友 的成員與朋友

private B

protected B

public B

x

x x

從以上的比較我們得知:對基底類別成員的存取,以 public

的限制最少、 protected 次之、而 private 最多。此外,只有 public 的繼承方式允許在非成員的函式中,把衍生類別型態的指標轉換成基底類別型態的指標。由於這些原因, public 的繼承方式是最常用來定義基底類別的子類型( subtype )。如果我們想把基底類別當成 implementation 內部的一個類別,不希望外界直接地使用它,最好使用 protected 和 private 的繼承方式,其中又以 private 的隔離效果比 protected 來得大。

Virtual Functions

假定我們有如右邊所示的類別繼承關係。由於資料成員多寡不一,每個類別因此各自定義了一個 print() 成員函式,用來列印相關的員工資料。

class Employee {public:

void print ();// …

};

class Manager : public Employee {public:

void print ();// …

};

class Director : Manager {public:

void print ();// …

};

假定你想寫一個函式能夠列印任何一類員工的資料。由於 Manager 和 Director 都屬於 Employee 類別,因此你可能認為以下的函式就可以達到這個目的:

void print_emp (Employee *e)

{

e->print();

}

其實不然。原因是: e 是 Employee 型態的指標,所以不論傳進來的引數型態是 Employee 、 Manager 、或 D

irector , e->print() 永遠是呼叫 Employee 的成員函式 p

rint() 。

你可以在 Employee 類別中,加入一個儲存員工類型的資料成員來解決前述的問題。譬如:

class Employee {public:

enum emp_type {EMPLOYEE, MANAGER, DIRECTOR};void print ();emp_type type() { return _type; }// …

private:emp_type _type;

};

並在三個類別的建構函式中,加入資料成員 type 的設定。經過這些加工之後,你就可以寫出下一頁的列印函式。

void print_emp (Employee *e)

{

switch (e->type()) {

case Employee::EMPLOYEE:

e->print();

break;

case Employee::MANAGER:

static_cast<Manager *>(e)->print(); // Manager’s print()

break;

case Employee::DIRECTOR:

static_cast<Director *>(e)->print(); // Director’s print()

break;

}

}

上述的解決方案有下面兩個缺點:

就如同 print_emp() 函式所示,程式設計師必須判斷物件的類別,然後採取適當的型態轉換。這種作法不僅增加程式撰寫的負擔,也容易造成錯誤。

print_emp() 函式只能列印 Employee 、 Manager 、 和 Director 三種類別的物件。如果其他人用繼承的方式定義另一種員工的類別,如 S

ecretary ,則已經寫死的 pri

nt_emp() 函式將無法用來列印這個新類別的物件。

class Secretary : public Employee {public:

void print ();// …

};

為了解決上述的問題, C++ 提供一種稱為 virtual 函式的特別成員函式。你只要在成員函式的宣告之前加上關鍵字 virtu

al ,就可以把它變成 virtual 函式,即

virtual return_type func_name (parameter list)

當類別含有 virtual 成員函式時, C++ 編譯器會為它產生一個 virtual function table ,其中包含此類別所有 virtual 成員函式的位址。此外,屬於此類別的物件,除了儲存資料成員外,會另外儲存一個指向此 virtual function table 的指標。

宣告 virtual 函式

舉例來說,假定類別 X 的宣告如下:

class X {

public:

virtual void vf1 ();

virtual void vf2 ();

void f ();

private:

int _x, _y;

};

X a, b;

則 X 類別的物件結構將如右圖所示。

_x

_y

_ _vptr_ _X

X::vf1()

X::vf1()

virtual tablefor class X

_x

_y

_ _vptr_ _X

a

b

衍生類別的 virtual 函式會覆蓋( override )基底類別的同名同參數列的 virtual 函式。

class Base {

virtual void foo (int);

// other members

};

class Derived : public Base

{

virtual void foo (int);

// other members

};

override

若我們把前述 Employee

類別和它衍生類別中的 pri

nt() 成員函式都改成 virtua

l (如左圖所示),則函式 print_emp() 就變得簡單多了,而且也克服前述的一些缺點。

class Employee {public:

virtual void print ();// …

};

class Manager : public Employee {public:

virtual void print ();// …

};

class Director : Manager {public:

virtual void print ();// …

};

利用 virtual 函式的 print_emp() 定義如下:

void print_emp (Employee *e)

{

e->print();

}

則 e->print() 會呼叫傳進來物件所定義的 print() 成員函式。譬如:若傳進來 Emploee 型態的物件時, e->print() 等同於e->Emploee::print() ;若是 Manager 型態的物件時, e->print

() 等同於 e-> Manager ::print() 。

型態相同的物件(都是 Employee )卻具有不同的行為(不同的 print() 功能),稱之為 polymorphism (多型)。具有 virtual 成員函式的類別稱為多型型態( polymorphic type )。

在 C++ 中,若要使用多型,你必須:

• 把一些成員函式定義成 virtual 。

• 透過物件指標或參照來呼叫這些 virtual 成員函式。

若透過物件直接呼叫 virtual 成員函式,因為編譯時會固定呼叫的對像,所以會達不到多型的效果。譬如:

Employee e;

e.print(); // 一定呼叫 Employee::print()

Pure Virtual Functions

如果基底類別的 virtual 成員函式只是用來規定衍生類別應該具備的使用介面( interface ),而且基底類別也不用來定義物件的話,我們可以用以下的格式把 virtual 成員函式設定無定義的函式:

virtual return_type fucn_name (parameter_list) = 0

這樣的函式稱為 pure virtual function 。

Abstract Classes本身擁有或繼承但不改變 pure virtual functions 的類別稱為抽象類別( abstract class )。由於 pure virtual functions 是沒有定義的函式,因此抽象類別不可用來定義物件。譬如底下的 Abst

ract_Base 和 Abstract_Derived 是抽象類別,而 Concrete_Deriv

ed 就不再是了:

class Abstract_Base {public:

virtual void foo () = 0;};class Abstract_Derived : public Abstract_Base { /* … */ }class Concrete_Derived : public Abstract_Base {public:

virtual void foo () {…}; // no longer pure}

抽象類別通常製訂介面( interface )用來規範衍生類別的基本功能。譬如:

class Shape {

public:

// 所有 Shape 類別物件都必須提供下列的功能virtual void rotate (int) = 0;

virtual void draw () = 0;

virtual void is_closed (int) = 0;

};

class Circle : public Shape {

public:

virtual void rotate (int) { /* function definition */ }

virtual void draw () { /* function definition */ }

virtual void is_closed (int) { /* function definition */ }

// other members

}

RTTI (Run-time Type Information)

C++ 的 RTTI 的機制提供下面兩個功能:• dynamic_cast

• typeid

dynamic_cast

dynamic_cast<T *>(p) =

p

0

如果指標 p 所指的物件型態是 T 或其基底類別是 T 。

所有其他情形

void f (Derived *p){

Base *q0 = p; // okBase *q1 = dynamic_cast<Base *>( p); // okOther *q2 = p; // compile errorOther *q3 = dynamic_cast<Other *>( p); // ok: q3 is 0

}

假定 Base 是 Derived 的基底類別, Other 是與它們無任何繼續關係的類別

typeid

typeid 運算子可用來取得物件的型態資訊:

typeid(obj); // 傳回物件 obj 的型態資訊

typeid(*objptr); // 傳回物件指標 objptr 所指物件的型態資訊

我們可以從型態資訊中取得型態的名稱,如:

typeid(obj).name(); // 物件 obj 所屬類別的名稱

也可以比較兩個物件的型態是否相同,如

typeid(obj1) == typeid(obj2) // true 若兩物件的類別相同

typeid(obj1) != typeid(obj2) // true 若兩物件的類別不相同