C++基礎學習之類繼承(10)
面向物件程式設計的主要目的之一是提供可重用的程式碼。C++提供了更高層次的重用性方法來擴充套件和修改類。這種方法叫類繼承,它能夠從已有的類派生出新的類,而派生類繼承了原有類(稱為基類)的特徵,包括方法。通過繼承可完成的工作有:
- 可以在已有類的基礎上新增功能。例如,對於陣列類,可以新增數學運算。
- 可以給類新增資料。
- 可以修改類方法的行為。
一個簡單的基類(最簡單的繼承和初始化)
從一個類派生出另一個類時,原始類稱為基類,繼承類稱為派生類。先設計一個簡單的基類用於管理乒乓球會員。
// tabtenn0.h -- a table-tennis bass class
#ifndef TABTENN0_H_
#define TABTENN0_H_
#include <string>
using std::string;
// simple base class
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none",
const string & ln = "none", bool ht = false );
void Name() const;
bool HasTable() const { return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
#endif
// tabtenn0.cpp -- simple base-class method
#include "tabtenn0.h"
#include <iostream>
TableTennisPlayer::TableTennisPlayer(const string & fn,
const string & ln, bool ht) : firstname(fn), lastname(ln), hasTable(ht) {}
void TableTennisPlayer::Name() const
{
std::cout << lastname << ", " << firstname;
}
派生一個類
現在需要一個類表示參加過比賽的會員,這樣就可以在會員的基類上擴展出來。語法如下:
// RatedPlyer derives from the TableTennisPlayer base class
class RatedPlayer : public TableTennisPlayer
{
...
};
冒號指出RatedPlayer類的基類是TableTennisPlayer類。上述特殊的宣告頭表明TableTennisPlayer是一個公有基類,這被稱為公有派生。派生類物件包含基類物件。使用公有派生,基類的公有成員將成為派生類的公有成員;基類的私有部分也將成為派生類的一部分,但只能通過基類的公有和保護方法訪問。
RatedPlayer物件將具有以下特徵:
- 派生類物件儲存了基類的資料成員(派生類繼承了基類的實現)
- 派生類物件可以使用基類的方法(派生類繼承了基類的介面)
繼承特性中可以新增的部分:
- 派生類需要自己的建構函式。
- 派生類可以根據需要新增額外的資料成員和成員函式。
所以RatedPlyer類應該如下宣告:
// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating; // add a data member
public:
RatedPlayer(unsigned int r = 0, const string & fn = "none",
const string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const { return rating; } // add a method
void ResetRating(unsigned int r) { rating = r; } // add a method
};
建構函式必須給新成員(如果有)和繼承的成員提供資料。在第一個RatedPlayer建構函式中,每個成員對應一個形參;而第二個RatedPlayer建構函式使用一個TebleTennisPlayer引數,該引數包括firstname、lastname和hasTable。
建構函式:訪問許可權的問題
派生類不能直接訪問基類的私有成員,而必須通過基類方法進行訪問。具體來說就是派生類建構函式必須使用基類建構函式。建立派生類物件時,程式首先建立基類物件。從概念上說,這意味著基類物件應當在程式進入派生類建構函式之前被建立。C++使用成員初始化列表語法來完成這種工作。例如RatedPlayer建構函式:
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
其中,:TableTennisPlayer(fn, ln, ht)是成員初始化列表。它是可執行的程式碼,呼叫TableTennisPlayer建構函式。
如果省略成員初始化列表,那麼程式會使用預設基類建構函式,等同於:
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) //: TableTennisPlayer()
{
rating = r;
}
第二種建構函式將會呼叫複製建構函式,這裡沒有定義,但是由於沒有使用動態記憶體分配,所以是可以的。
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp) : TableTennisPlayer(tp)
{
rating = r;
}
所以有關派生類建構函式的要點如下:
- 首先建立基類物件;
- 派生類建構函式應通過成員初始化列表將基類資訊傳遞給基類建構函式;
- 派生類建構函式應初始化派生類新增的資料成員。
另外,釋放物件的順序與建立物件的順序相反,即首先執行派生類的解構函式,然後自動呼叫基類的解構函式。
派生類和基類之間的特殊關係
派生類與基類之間有一些特殊的關係。一是派生類物件可以使用基類的公有方法;二是基類指標可以在不進行顯式型別轉換的情況下指向派生類物件;三是基類引用可以在不進行顯式型別轉換的情況下引用派生類物件。而且基類指標或引用只能用於呼叫基類方法。
多型公有繼承
前面的例子很簡單,派生類物件使用基類的方法而未做任何修改。然而,可能會遇到這樣的情況,即希望同一個方法在派生類和基類中的行為是不同的。換句話說,方法的行為應取決於呼叫該方法的物件。這種較複雜的行為稱為多型——具有多種形態,即同一個方法的行為隨上下文而異。有兩種重要的機制可用於實現多型公有繼承:
- 在派生類中重新定義基類的方法。
- 使用虛方法。
這裡使用新的一個例子說明:
// brass.h -- bank account classes
#ifndef BRASS_H_
#define BRASS_H_
#include <string>
// Brass Account Class
class Brass
{
private:
std::string fullName;
long acctNum;
double balance;
public:
Brass(const std::string & s = "Nullbody", long an = -1, double bal = 0.0);
void Deposit(double amt);
virtual void Withdraw(double amt);
double Balance() const;
virtual void ViewAcct() const;
virtual ~Brass() {}
};
// Brass Plus Account Class
class BrassPlus : public Brass
{
private:
double maxloan;
double rate;
double owesBank;
public:
BrassPlus(const std::string & s = "Nullbody", long an = -1, double bal = 0.0,
double ml = 500, double r = 0.11125);
BrassPlus(const Brass & ba, double ml = 500, double r = 0.11125);
virtual void ViewAcct() const;
virtual void Withdraw(double amt);
void ResetMax(double m) { maxLoan = m; }
void ResetRate(double r) { rate = r; }
void ResetOwes() { owesBank = 0; }
};
#endif
這個程式需要說明幾點:
- BrassPlus類在Brass類的基礎上添加了3個私有資料成員和3個公有成員函式;
- Brass類和BrassPlus類都聲明瞭ViewAcct()和Withdraw()方法,但BrassPlus物件和Brass物件的這些方法的行為是不同的;
- Brass類在宣告ViewAcct()和Withdraw()時使用了新關鍵字virtual。這些方法被稱為虛方法(virtual method);
- Brass類還聲明瞭一個虛解構函式,雖然該解構函式不執行任何操作。
第一點和之前的一樣,沒有什麼新鮮的。
第二點介紹了宣告如何指出方法在派生類的行為的不同。兩個ViewAcct()原型表明將有2個獨立的方法定義。基類版本的限定名為Brass::ViewAcct(),派生類版本的限定名為BrassPlus::ViewAcct()。程式將使用物件型別來確定使用哪個版本:
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
dom.ViewAcct(); // use Brass::ViewAcct()
dot.ViewAcct(); // use BrassPlus::ViewAcct()
同樣,Withdraw()也有2個版本,一個供Brass物件使用,另一個供BrassPlus物件使用。對於在兩個類中行為相同的方法(如Deposit()和Balance()),則只在基類中宣告。
第三點(使用virtual)比前兩點要複雜。如果方法是通過引用或指標而不是物件呼叫的,它將確定使用哪一種方法。如果沒有使用關鍵字virtual,程式將根據引用型別或指標型別選擇方法;如果使用了virtual,程式將根據引用或指標指向的物件的型別來選擇方法。如果ViewAcct()不是虛的,則程式的行為如下:
// behavior with non-virtual ViewAcct()
// method chosen according to reference type
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use Brass::ViewAcct()
引用變數的型別為Brass,所以選擇了Brass::ViewAccount()。使用Brass指標代替引用時,行為將與此類似。
如果ViewAcct()是虛的,則行為如下:
// behavior with virtual ViewAcct()
// method chosen according to object type
Brass dom("Dominic Banker", 11224, 4183.45);
BrassPlus dot("Dorothy Banker", 12118, 2592.00);
Brass & b1_ref = dom;
Brass & b2_ref = dot;
b1_ref.ViewAcct(); // use Brass::ViewAcct()
b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()
這裡兩個引用的型別都是Brass,但b2_ref引用的是一個BrassPlus物件,所以使用的是BrassPlus::ViewAcct()。使用Brass指標代替引用時,行為將類似。
第四點是,基類聲明瞭一個虛解構函式。這樣做是為了確保釋放派生物件時,按正確的順序呼叫解構函式。基類應該包含一個虛解構函式,如果解構函式不是虛的,那麼如果用基類型別來儲存派生類型別,那麼最後將只調用基類的解構函式,而如果基類的解構函式是虛的,那麼將呼叫相應的指向型別的虛構函式,也就是說將先呼叫派生類的解構函式,然後自動呼叫基類的解構函式。
注意: 如果要在派生類中重新定義基類的方法,通常應將基類方法宣告為虛的。這樣,程式將根據物件型別而不是引用或指標的型別來選擇方法版本。為基類宣告一個虛解構函式也是一種慣例。