一、一個簡單的基類
面向對象程式設計的主要目的之一是提供可重用的代碼。傳統的C函數庫通過預定義、預編譯的函數提供了可重用性。C++類提供了更高層次的重用性,類庫由類聲明和實作構成。因為類組合了資料表示和類方法,是以提供了比函數庫更加完整的程式包。類繼承,C++提供了比修改代碼更好的方法來擴充和修改類,能夠從已有的類派生出新的類,而派生類繼承了原有類的特征,包括方法。通過繼承派生出的類通常比設計新類要容易得多,下面是可以通過繼承完成的一些工作:
- 可以在已有類的基礎上添加功能;
- 可以給類添加資料;
-
可以修改類方法的行為。
繼承機制隻需要提供新特性,甚至不需要通路源代碼就可以派生出類。
從一個類派生出另一個類時,原始類稱為基類,繼承類稱為派生類,為了說明繼承,首先需要一個基類。
#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
#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;
}
#include <iostream>
#include "tabtenn0.h"
int main ( void )
{
using std::cout;
TableTennisPlayer player1("Chuck", "Blizzard", true);
TableTennisPlayer player2("Tara", "Boomdea", false);
player1.Name();
if (player1.HasTable())
cout << ": has a table.\n";
else
cout << ": hasn't a table.\n";
player2.Name();
if (player2.HasTable())
cout << ": has a table";
else
cout << ": hasn't a table.\n";
// std::cin.get();
return 0;
}

1.派生一個類
- 派生類需要自己的構造函數
-
派生類可以根據需要添加額外的資料成員和成員函數。
構造函數必須給新成員和繼承的成員提供資料。
2.構造函數:通路權限的考慮
派生類不能直接通路基類的私有成員,而必須通過基類的方法進行通路。派生類構造函數必須使用基類的構造函數。建立派生類對象時,程式首先建立基類對象。有關派生類構造函數的要點如下:
- 首先建立基類對象;
- 派生類構造函數應通過成員初始化清單将基類資訊傳遞給基類構造函數
-
派生類構造函數應初始化派生類新增的資料成員。
3.使用派生類
要使用派生類,程式必須要能夠通路基類聲明。
#ifndef TABTENN1_H_
#define TABTENN1_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; };
};
// simple derived class
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
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; }
void ResetRating (unsigned int r) {rating = r;}
};
#endif
#include "tabtenn1.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;
}
// RatedPlayer methods
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
: TableTennisPlayer(tp), rating(r)
{
}
#include <iostream>
#include "tabtenn1.h"
int main ( void )
{
using std::cout;
using std::endl;
TableTennisPlayer player1("Tara", "Boomdea", false);
RatedPlayer rplayer1(1140, "Mallory", "Duck", true);
rplayer1.Name(); // derived object uses base method
if (rplayer1.HasTable())
cout << ": has a table.\n";
else
cout << ": hasn't a table.\n";
player1.Name(); // base object uses base method
if (player1.HasTable())
cout << ": has a table";
else
cout << ": hasn't a table.\n";
cout << "Name: ";
rplayer1.Name();
cout << "; Rating: " << rplayer1.Rating() << endl;
// initialize RatedPlayer using TableTennisPlayer object
RatedPlayer rplayer2(1212, player1);
cout << "Name: ";
rplayer2.Name();
cout << "; Rating: " << rplayer2.Rating() << endl;
// std::cin.get();
return 0;
}
4.派生類和基類之間的特殊關系
派生類對象可以使用基類的方法,條件是方法不是私有的;另外兩個重要的關系是:基類指針可以在不進行顯示類型轉換的情況下指向派生類對象;基類引用可以在不進行顯示類型轉換的情況下引用派生類的對象。
二、繼承:is-a關系
派生類和基類之間的特殊關系是基于C++繼承的底層模型的。實際上,C++有3種繼承方式:公有繼承、保護繼承和私有繼承。公有繼承是最常用的方式,它建立一種is-a關系,即派生類對象也是一個基類對象,可以對基類對象執行的任何操作,也可以對派生類對象執行。新類将繼承原始類的所有資料成員,派生類可以添加特性。這種關系的通常使用術語is-a。
公有繼承不建立has-a關系,舉例:午餐可能包括水果,但通常午餐并不是水果,在午餐中加入水果的正确方法是将其做為一種has-a關系:午餐有水果。
公有繼承不能建立is-like-a關系,也就說,他不采用明喻。繼承可以在基類的基礎上添加屬性,但不能删除基類的屬性。在這些情況下,可以設計一個包含公有特性的類,然後以is-a或has-a關系,在這個類的基礎上定義相關的類。
公有繼承不建立is-implemented-as-a(作為……來實作),例如,可以使用數組來實作棧,但從Array類派生出Stack類是不合适的,因為棧不是數組。正确的做法是讓棧包含一個私有的Array對象成員來隐藏數組實作。
公有繼承不建立uses-a關系。例如:計算機可以使用雷射列印機,但從Computer類派生出Printer類(或者反過來是沒有意義的),然而可以使用友元函數或類來處理Printer對象和Computer對象之間的通信,
三、多态公有繼承
派生類對象使用基類的方法而未做任何修改。有兩種機制可用于實作多态公有繼承:
- 在派生類中重新定義基類的方法
-
使用虛方法
1.類實作
關鍵字virtual隻用于類聲明的方法原型中。派生類并不能直接通路基類的私有資料,而必須使用基類的公有方法才能通路這些資料。通路的方式取決于方法,構造函數使用一種技術,而其他成員函數使用另一種技術,
2.示範虛方法的行為
方法是通過對象(而不是指針或引用)調用的,沒有使用虛方法特性。
3.為何需要虛析構函數
如果析構函數不是虛的,則将隻調用對應于指針類型的析構函數。
四、靜态聯編和動态聯編
程式調用函數時,将使用哪個可執行代碼塊?編譯器負責回答這個問題。将源代碼中的函數調用解釋為執行特定的函數代碼塊被稱為函數名聯編。在C語言中,這非常簡單,因為每個函數名都将對應一個不同的函數。在C++中,由于重載函數的緣故,這項任務更複雜。編譯器必須檢視函數參數以及函數名才能确定使用哪個函數。然而,C/C++編譯器可以在編譯過程完成這種聯編。在編譯過程中進行來聯編被稱為靜态聯編,又稱為早期聯編。然而,虛函數使這項工作變得更困難。使用哪一個函數是不能在編譯時确定的,因為編譯器不知道使用者将選擇哪種類型的對象,是以,編譯器必須生成能夠在程式運作時選擇正确的虛方法的代碼,這被稱為動态聯編,又稱為晚期聯編。
1.指針和引用類型的相容性
在C++中,動态聯編與通過指針和引用調用方法相關,從某種程度上來說,這是由繼承控制的。公有繼承建立is-a關系的一種方法是如何處理指向對象的指針和引用。通常C++不允許将一種類型的位址賦給另一種類型的指針,也不允許一種類型的引用指向另一種類型:
double x = 2.5;
int * pi = &x ; // invalid assignment
long & rl = x ;
指向基類的引用或指針可以引用派生類對象,而不必進行顯示類型轉換。
BrassPlus dilly ("Annie Dill" , 493222,2000);
Brass * pb = &dilly ; // ok
Brass & rl = dilly ; //ok
将派生類引用或指針轉換為基類引用或指針被稱為向上強制轉換,這使公有繼承不需要及逆行顯示類型轉換,該規則使is-a關系的一部分。
相反的過程——将基類指針或引用轉換為派生類的指針或引用——稱為向下強制轉換,如果不使用顯示類型轉換,則向下強制轉換是不被允許的,原因是is-a關系通常是不可逆的。
2.虛成員函數和動态聯編
如果動态聯編讓您能夠重新定義類方法,而靜态聯編在這方面很差,但是依舊要預設靜态聯編。原因有兩個——效率和概念模型
為使程式能夠在運作階段進行決策,必須采取一些方法來跟蹤基類指針或引用指向的對象類型。同樣,如果派生類不重新定義基類的任何方法,也不需要使用動态聯編。在這些情況下,使用靜态聯編更合理,效率也更高。
在設計類時,可能包含一些不在派生類重新定義的成員函數。不該将函數設定為虛函數,有兩方面的好處:首先效率更高,其次,指出不要重新定義該函數,這表明,僅将那些預期将被重新定義的方法聲明為虛的。
虛函數的工作原理:C++規定了虛函數的行為,但将實作方法留給了編譯器作者,不需要知道實作方法就可以使用虛函數,但了解虛函數的工作原理有利于更好地了解概念。通常,編譯器處理虛函數地方法是:給每一個對象添加一個隐藏成員,隐藏成員中儲存了一個指向函數位址數組的指針,這種數組稱為虛函數表。虛函數表中存儲了為類對象進行聲明的虛函數位址。使用虛函數時,在記憶體和執行速度方面有一定的成本,包括:
- 每個對象都将增大,增大量為存儲位址的空間
- 對于每個類,編譯器都建立一個虛函數的位址表
- 對于每個函數調用,都需要執行一項額外的操作,即到表中查找位址。
五、通路控制:protected
關鍵字protected與private相似,在類外隻能用共有類成員來通路protected部分中的類成員。private與protected之間的差別隻有在基類派生的類中才會表現出來,派生類的成員可以直接通路基類的保護成員,但不能直接通路基類的私有成員。是以,對于外部世界來說,保護成員的行為與私有成員相似;但對于派生類來說,保護成員的行為與公有成員相似。對于成員函數來說,保護通路控制很有用,它讓派生類能夠通路公衆不能使用的内部函數。
六、抽象基類
圓是橢圓的一種特殊情況——長軸和短軸等長的橢圓。是以,所有的圓都是橢圓,可以從Ellipse類派生出Circle類。
考慮Ellipse類包含的内容,資料成員可以包括橢圓中心的坐标、半長軸、短半軸以及方向角。
另外,還可以包括一些移動橢圓、傳回橢圓面積、旋轉橢圓以及縮放長半軸和短半軸的方法:
class Ellipse
{
private:
double x ;
double y ;
double a ;
double b;
double angle ;
……
public :
……
void Move(int nx , ny ){x = nx;y = ny ;}
virtual double Area() const {return 3.14159 * a * b ;}
virtual void Rotate(double nang){angle += nang;}
virtual void Scale(double sa , double sb)
現在從Ellipse類派生出一個Circle類:
class Circle : public Ellipse
{
……
};
雖然圓是橢圓的一種,但是這種派生是笨拙的。圓隻需要一個半徑就能描述大小和形狀,并不需要那麼多的量。是以,總的來說,不使用繼承而直接定義Circle類更簡單:
class Circle
{
private :
double x ;
double y ;
double r ;
……
public:
……
void Move(int nx , ny){ x = nx; y = ny;}
double Area() const{return 3.14159 * r * r;}
void Scale {double sr } {r *= sr ;}
……
};
還有另外一種解決方法,就是将Ellipse和Circle類中抽象出它們的共性,将這些特性放到一個ABC中,然後從該ABC派生出Circle和Ellipse類。這樣,便可以使用基類指針數組同時管理Circle和Ellipse對象,即可以使用多态方法。
當類聲明中包含虛函數是,則不能建立該類的對象。包含純虛函數的類隻用作基類,要成為真正的ABC,必須至少包含一個純虛函數,原型中的=0使虛函數成為純虛函數。在原型中的=0指出類使一個抽象基類,在類中可以不定義該函數。
七、繼承和動态記憶體配置設定
1.派生類不使用new
假設基類使用了動态記憶體配置設定:
class baseDMA
{
private :
char * label;
int rating'
public :
baseDMA (const char * 1 = 'null', int r = 0);
baseDMA (const baseDMA & rs);
virture -bassDMA();
bassDMA & operator =(const baseDMA & rs);
……
};
聲明中包含了構造函數使用new時需要的特殊方法;析構函數、複制構造函數和重載指派運算符。
2.派生類使用new
class hasDMA : public baseDMA
{
private :
char * style ;
public :
……
};
在這種情況下,必須為派生類定義顯式析構函數、複制構造函數和指派運算符。
派生類析構函數将自動調用基類的析構函數,故其自身的職責是對派生類構造函數執行工作的進行清理。