派生類
派生類
利用繼承機制,新的類可以從已有的類中派生。那些用於派生的類稱為這些特別派生出的類的“基類”。
如前面討論的,繼承過程創建的新的派生類是由基類的成員加上由派生類新加的成員組成。在多重繼承中,可以構造層次圖,其中同一基類可以是多個派生類的一部分。圖9.4顯示了這種圖。
多重基類
如同多重繼承中所描述的,一個類可以從多個基類中派生出來。在派生類由多個基類派生出來的多重繼承模式中,基類是用基類表語法成份來說明的。
class CollectionOfBook:public Book,public Collection
{
//新成員
};
由構造函數引起的初始化發生的順序。如果你的代碼依賴於CollectionOfBook的Book部分要在Collection部分之前初始化,則此說明順序將很重要。初始化是按基類表中的說明順序進行初始化的。
激活析構函數以作清除工作的順序。同樣,當類的其它部分正在被清除時,如果某些特別部分要保留,則該順序也很重要。析構函數的調用是按基類表說明順序的反向進行調用的。
注意:基類的說明順序會影響類的存儲器分佈。不要對基類成員在存儲器中的順序作出任何編程的決定。
在你說明基類表時,不能把同一類名稱說明多次。但是對於一個派生類而言,其非直接基類可以有多個相同的。
虛擬基類
因為一個類可以多次作為一個派生類的非直接基類。C++提供了一個辦法去優化這種基類的工作。
注意,在LunchCashierQueue對象中,有兩個Queue子對象。下面的代碼說明Queue為虛擬基類:
class Queue
{
//成員表
};
class CashierQueue:virtual public Queue
{
//成員表
};
class LunchQueue: virtual public Queue
{
//成員表
};
class LunchCashierQueue:public LunchQueue,public CashierQueue
{
//成員表
};
一個類對於給定的類型既可以有虛擬的組成部分,也可以有非虛擬的組成部分。
如果一個派生類重載了一個從虛擬基類中繼承的虛擬函數,而且該派生類以指向虛擬基類的指針調用這些構造函數和析構函數時,編譯器會引入一個附加的隱含的“vtordisp”域到帶有虛擬基類的類中。/vd0編譯器選項禁止了這個增加的隱含vtordisp構造/析構位置成員。/vd1選項(預設),使得在需要時可以解除禁止。只有在你確信所有類的構造函數或析構函數都虛擬地調用了虛擬函數,vtordisp才可以關掉。
/vd編譯器選項會影響全局編譯模式。使用vtordisp編譯指示可以在基於類方式上打開或禁止vtordisp域:
#pragma vtordisp(off)
class GetReal:virtual public{...};
#pragma vtordisp(on)
C++的後期的一些版本為繼承引入了“多重繼承”模式。在一個多重繼承的圖中,派生類可以有多個直接基類。
對於一個特定的程序如果每個類的屬性並不是全部要求使用,則每個類可以單獨使用或者同別的類聯合在一起使用。
虛基類層次 有一些類層次很龐大,但有很多東西很普遍。這些普遍的代碼在基類中實現了,然而在派生類中又實現了特殊的代碼。
對於基類來說重要的是建立一種機制,通過這種機制派生類能夠完成大量的函數機能。
這種機制通常是用虛函數來實現的。有時,基類為這些函數提供了一個預設的實現。
了解到所有的Identify和WhereIs的函數實現返回的是同種類型的信息,這一點很重要。在這個例子中,恰好是一種描述性字元串。
這些函數可以作為虛擬函數來實現,然後用指向基類的指針來調用,對於實際代碼的聯結將在運行時決定,以選擇正確的Identify和WhereIs函數。
類協議的實現
類可以實現為要強制使用某些協議。這些類稱為“抽象類”,因為不能為這種類類型創建對象。它們僅僅是為了派生別的類而存在。
當一個類中含有純虛擬函數或當他們繼承了某些純虛擬函數卻又沒有為它們提供一個實現時,該類稱為抽象類。純虛擬函數是用純說明符定義的虛擬函數。如下:
virtual char *Identify()=0;
基類Document把如下一些協議強加給派生類。
* 為Identify函數提供一個合適的實現
* 為WhereIs函數提供一個合適的實現
在設計Document類時,通過說明這種協議,類設計者可以確保如不提供Identify和WhereIs函數則不能實現非抽象類。因而Document類含有如下說明:
class Document
{
public:
...
//對派生類的要求,它們必須實現下面這些函數
virtual char *Identify()=0;
virtual char *WhereIs()=0;
...
};
基類說明:在C++中要定義的新的數據類型不僅擁有新定義的成員,而且還同時擁有舊的成員,我們稱已存在的用來派生新類的類為C++基類,又稱為父類。
基類表:基類表中存放各個基類名稱
基類說明符:基類類體中類成員的訪問說明符
在“單一繼承”這種最普通的形式中,派生類僅有一個基類。
在類的層次設計中,可以發現一些普遍的特性,即派生類總是同基類有“kind of”關係。
另一個值得注意點是Book既是派生類(從PrintedDocument中派生),也是基類(PaperbackBook是從Book派生的)。下面的例子是這種類層次的一個輪廓性的說明。
class PrintedDocument
{
//成員表
};
//Book是從PrintedDocument中派生的
class Book:public PrintedDocument
{
//成員表
};
//PaperbackBook是從Book中派生
class PaperbackBook: public Book
{
//成員表
};
PrintedDocument作為Book的直接基類,它同時也是PaperbackBook的非直接 基類。直接基類和非直接基類的區別在於直接基類出現在類說明的基類表中,而非直接基類不出現在基類表中。
每個派生類的說明是在基類的說明之後說明的,因此對於基類僅只給出一個前向引用的說明是不夠的,必須是完全的說明。
一個類可以作為很多特別類的基類。
在繼承中,派生類含有基類的成員加上任何你新增的成員。結果派生類可以引用基類的成員(除非這些成員在派生類中重定義了)。當在派生類中重定義直接基類或間接基類的成員時,可以使用範圍分辨符(::)引用這些成員。考慮下面的代碼:
class Document
{
public:
char * Name;//文檔名稱
void PrintNameOf(); //列印名稱
};
//實現類Document的PrintNameOf函數
void Document::PrintNameOf()
{
cout << Name << end ;
}
class Book:public Document
{
public:
Book(char *name,long pagecount);
long PageCount;
};
//class Book 構造函數
Book::Book (char *name,long pagecount)
{
Name=mew char [strlen(name)+1];
strcpy (Name,name);
PageCount=pagecount;
};
注意,Book的構造函數(Book::Book)具有對數據成員Name的訪問權。在程序中可以按如下方式創建Book類對象並使用之。
//創建一個Book類的新對象,這將激活構造函數Book:BookBook
LibraryBook ("Programming Windows,2nd Ed",994);
...
//使用從Document中繼承的函數PrintNameOf.
LibraryBook.PrintNameOf();如前面例子所示,類成員和繼承的數據與函數以一致的方式引用。如果類Book所調用的PrintNameOf是由類Book重新定義實現的,則原來屬於類Document的PrintNameOf函數只能用範圍分辯符(::)才能使用:
class Book:public Document
{
Book(char *name,long pagecount);
void PrintNameOf();
long PageCount;
};
void Book::PrintNameOf()
{
cout<<"Name of Book:";
Document::PrintNameOf();
}
只要有一個可訪問的、無二義性的基類,派生類的指針和引用可以隱含地轉換為它們基類的指針和引用。下面的例子證實了這種使用指針的概念(同樣也適用於引用):
#include < iostream.h>
void main()
{
Document * DocLib; //10個文檔的庫
for (int i=0; i<10; ++i)
{
cout<<"Type of document:"
<<"P)aperback,M)agazine,H)elp File,C)BT"
<< endl;
char CDocType;
cin >>CDocType;
switch(tolower(CDocType))
{
case 'p':
DocLib=new PaperbackBook;
break;
case 'm':
DocLib=new Magazine;
break;
case 'h':
DocLib=new HelpFile;
break;
case 'c':
DocLib=new ComputerBasedTraining;
break;
default:
--i;
break;
}
}
for (i=0; i<10; ++i)
DocLib->PrintNameOf();
}
在前面例子的SWITCH語句中,創建了不同類型的對象。這一點依賴於用戶對CDocType對象所作出的說明。然而這些類型都是從類Document中派生出來的,故可以隱含地轉換為Document*。結果是DocLib成為一個“相似鏈表”(heterogeneous list)。此鏈表所包含的是不同種類的對象,其中的所有對象並不是有相同的類型。
因為Document類有一個PrintNameOf函數。因此它能夠列印圖書館中每本書的名稱,但對於Document類型來說有一些信息會省略掉了(如:Book的總頁數,HelpFile的位元組數等)。
注意:強制基類去實現一個如PrintNameOf的函數,通常不是一個很好的設計,本章後面的“虛擬函數”中提供了一個可替換的設計方法。
實例
多重繼承使得從不同的路徑繼承成員名稱成為可能。沿著這些路徑的成員名稱並不必然是唯一的。這些名稱的衝突稱為“二義性”。
任何引用類成員的表達式必須使用一個無二義性的引用。下面的例子顯示了二義性是如何發生的。//說明兩個基類A和B
class A
{
public:
unsigned a;
unsigned b();
};
class B
{
public:
unsigned a(); //注意類A也有一個成員"a"和一個成員"b"
int b();
char c;
};
//定義從類A和類B中派生出的類C
class C : public A,public B
{
};
分析
按上面所給出的類說明,如下的代碼就會引出二義性,因為不清楚是引用類A的b呢,還是引用類B的b:
C *pc=new C;
pc->b();
考慮一下上面的代碼,因為名稱a既是類A又是類B的成員,因而編譯器並不能區分到底調用哪一個a所指明的函數。訪問一個成員,如果它能代表多個函數、對象、類型或枚舉則會引起二義性。
編譯器通過下面的順序執行以檢測出二義性:
⒈ 如果訪問的名稱是有二義性的(如前述),則產生一條錯誤信息。
⒉ 如果重載函數是無二義性的,它們就沒有什麼問題了
⒊ 如果訪問的名稱破壞了成員訪問許可,則產生一條錯誤信息
在一個表達式產生了一個通過繼承產生的二義性時,通過用類名稱限制發生問題的名稱即可人工解決二義性,要使前面的代碼以無二義性地正確編譯,要按如下使用代碼:
C *pc = new C;
pc->B::a();
注意:在類C說明之後,在C的範圍中引用B就會潛在地引起錯誤。但是,直到在C的範圍中實際使用了一個對B的無限定性的引用,才會產生錯誤。
二義性和虛擬基類
如果使用了虛擬基類、函數、對象、類型以及枚舉可以通過多重繼承的路徑到達,但因為只有一個虛擬基類的實例,因而訪問這些名稱時,不會引起二義性。
訪問任何類A的成員,通過非虛擬基類訪問則會引起二義性;因為編譯器沒有任何信息以解釋是使用同類B聯繫在一起的子對象,還是使用同類C聯繫在一起的子對象,然而當A說明為虛擬基類時,則對於訪問哪一個子對象不存在問題了。
通過繼承圖可能有多個名稱(函數的、對象的、枚舉的)可以達到。這種情況視為非虛擬基類引起的二義性。但虛擬基類也可以引起二義性,除非一個名稱“支配”(dominate)了其它的名稱。一個名稱支配其它的名稱發生在該名稱定義在兩個類中,其中一個是由另一個派生的,占支配地位的名稱是派生類中的名稱,在此名稱被使用的時候,相反不會產生二義性,如下面的代碼所示:
class A
{
public:
int a;
};
class B: public virtual A
{
public:
int a();
};
class C: public virtual A
{
...
};
class D: public B,public C
{
public:
D() {a();} //不會產生二義性,B::a()支配了A::a
};
轉換的二義性
顯式地或隱含地對指向類類型的指針或引用的轉換也可引起二義性。
實例1
虛擬函數可以確保在一個對象中調用正確的函數,而不管用於調用函數的表達式。
假設一個基類含有一個說明為虛擬函數同時一個派生類定義了同名的函數。派生類中的函數是由派生類中的對象調用的,甚至它可以用指向基類的指針和引用來調用。下面的例子顯示了一個基類提供了一個PrintBalance函數的實現:
class Account
{
public:
Account(double d); //構造函數
virtual double GetBalance(); //獲得平衡
virtual void PrintBalance(); //預設實現
private:
double _balance;
};
//構造函數Account的實現
double Account::Account(double d)
{
_balance=d;
}
//Account的GetBalance的實現
double Account::GetBalance()
{
return _balance;
}
//PrintBalance的預設實現
void Account::PrintBalance()
{
cerr<<"Error.Balance not available for base type".
<
}
兩個派生類CheckingAccount和SavingsAccount按如下方式創建:
class CheckingAccount:public Account
{
public:void
PrintBalance();
};
//CheckingAccount的PrintBalance的實0現
void CheckingAccount::PrintBalance()
{
cout<<"Checking account balance:"
<< GetBalance();
}
class SavingsAccount:public Account
{
public:
void PrintBalance();
};
//SavingsAccount中的PrintBalance的實
現void SavingsAccout::PrintBalance()
{
cout<<"Savings account balance:"
<< GetBalance();
}
函數PrintBalance在派生類中是虛擬的,因為在基類Account中它是說明為虛擬的,要調用如PrintBalance的虛擬函數,可以使用如下的代碼:
//創建類型CheckingAccount和SavingsAccount的對象
SavingsAccount *pSavings=new SavingsAccount(1000.00);
//用指向Account的指針調用PrintBalance
Account *pAccount=pChecking;
pAccount->PrintBalance();
//使用指向Account的指針調用PrintBalance
pAccount=pSavings;
pAccount->PrintBalance();
分析1
在前面的代碼中,除了pAccount所指的對象不同,調用PrintBalance的代碼是相同的。
因為PrintBalance是虛擬的,將會調用為每個對象所定義的函數版本,在派生類CheckingAccount和SavingsAccount中的函數“復蓋”了基類中的同名函數。如果一個類的說明中沒有提供一個對PrintBalance的復蓋的實現,則將採用基類Account中的預設實現。
實例2
派生類中的函數重載基類中的虛擬函數,僅在它們的類型完全相同時才如此。派生類中的函數不能僅在返回值上同基類中的虛擬函數不同;參量表也必須不同。當指針或引用調用函數時,要遵循如下規則:
* 對虛擬函數調用的解釋取決於調用它們的對象所基於的類型。
* 對非虛函數調用的解釋取決於調用它們的指針或引用的類型。
下面例子顯示了在使用指針調用虛擬或非虛擬函數時它們的行為:#include
//說明一個基類
class Base
{
public:
virtual void NameOf(); //虛擬函數
void InvokingClass(); //非虛擬函數
};
//兩個函數的實現
void Base::NameOf()
{
cout<<"Base::NameOf\n";
}
void Base::InvokingClass()
{
cout<<"Invoked by Base\n";
}
//說明一個派生類
class Derived:public Base
{
public:
void NameOf(); //虛擬函數
void InvokingClass(); //非虛擬函數
};
//兩個函數的實現
void Derived::NameOf()
{
cout<<"Derived::NameOf\n";
}
void Derived::InvokingClass()
{
cout<<"Invoked by Derived\n";
}
void main()
{
//說明一個Derived類型的對象
Derived aDerived;
//說明兩個指針,一個是Derived*型的,另一個是Base*型的,並用 //aDerived初始化它們。
Derived *pDerived=&aDerived;
Base *pBase =&aDerived;
//調用這個函數
pBase->NameOf(); //調用虛擬函數
pBase->InvokingClass();//調用非虛擬函數
pDerived->NameOf();//調用虛擬函數
pDerived->InvokingClass(); //調用非虛擬函數
}
分析2
該程序的輸出是:
Derived::NameOf
Invoked by Base
Derived::NameOf
Invoked by Derived
注意,不管調用NameOf函數的指針是通過指向基類的指針還是指向派生類的指針,它調用的函數是派生類的。因為NameOf是虛擬函數,而且pBase和pDerived指向的對象都是派生類的,故而調用函數是派生類的。
因為虛擬函數只能為類類型的對象所調用,所以你不能把一個全局的或靜態函數說明為虛擬的。
在派生類中說明一個重載函數時可以用virtual關鍵字,但是這並不是必須的,因為重載一個虛擬函數,此函數就必然是虛擬函數。
基類中的虛擬函數必須有定義,除非它們被說明為純的。
虛擬函數調用機制可以用範圍分辨符(::)明確地限定函數名稱的方法來加以限制。考慮前面的代碼,用下面的代碼調用基類的PrintBalance。
pChecking->Account::PrintBalance(); //明確限定
Account *pAccount=pChecking; //調用Account::PrintBalance
pAccount->Account::PrintBalance();//明確限定
上面例子中的兩個對PrintBalance的調用都限制了虛擬函數的調用機制。
抽象類就像一個一段意義上的說明,通過它可以派生出特有的類。你不能為抽象類創建一個對象,但你可以用抽象類的指針或引用。
至少含有一個純虛擬函數的類就是抽象類。從抽象類中派生出的類必須為純虛擬函數提供實現,否則它們也是抽象類。
把一個虛擬函數說明為純的,只要通過純說明符語法,考慮一下本章早些時候在“虛擬函數”中提供的例子。類Account的意圖是提供一個通常意義的函數功能,Account類型的對象太簡單而沒有太多用處。因此Account是作為抽象類的一個很好的候選:
class Account
{
public:
Account(double d); //構造函數
virtual double GetBalance();//獲得平衡
virtual void PrintBalance()=0; //純虛擬函數
double _balance;
};
這裡的說明同前一次的說明的唯一不同是PrintBalance是用純說明符說明的。
使用抽象類的限制
抽象類不能用於如下用途:
*變數或成員數據
* 參量類型
* 函數的返回類型
* 明確的轉換類型
另外一個限制是如果一個抽象類的構造函數調用了一個純虛擬函數,無論是直接還是間接的,結果都是不確定的。但抽象類的構造函數的析構函數可以調用其它成員函數。
抽象類的純虛擬函數可以有定義,但它們不能用下面語法直接調用:
抽象類名稱::函數名稱()
在設計基類中含有純虛擬析構函數的類層次時,這一點很有用。因為在銷毀一個對象的過程中通常都要調用基類的析構函數,考慮下面的例子:#include
//說明一個帶有純虛擬析構函數的抽象類
class base
{
public:
base() { }
virtual ~base()=0;
};
//提供一個析構函數的定義
base::~base()
{
};
class derived:public base
{
public: derived(){ };
~derived() { };
};
void main()
{
derived *pDerived=new derived;
delete pDerived;
}
當一個由pDerived所指的對象銷毀的時候,會調用類derived的析構函數,進而調用基類base中的析構函數。純虛擬函數的空的實現保證了該函數至少存在著一些操作。注意:在前面例子中,純虛擬函數base::~base是在derived::~derived中隱含調用的。當然明確地用全限定成員函數名稱去調用純虛擬函數是可能的。
public | 基類的public和protected的成員被派生類繼承后,保持原來的狀態 |
private | 基類的public和protected的成員被派生類繼承后,變成派生類的private成員 |
protected | 基類的public和protected的成員被派生類繼承后,變成派生類的protected成員 |
註:無論何種繼承方式,基類的private成員都不能被派生類訪問。從上面的表中可以看出,聲明為public的方法和屬性可以被隨意訪問;聲明為protected的方法和屬性只能被類本身和其子類訪問;而聲明為private的方法和屬性只能被當前類的對象訪問。
1. 友元函數必須在類中進行聲明而在類外定義,聲明時須在函數返回類型前面加上關鍵字friend。友元函數雖不是類的成員函數,但它可以訪問類中的私有和保護類型數據成員。
2. 虛函數在重新定義時參數的個數和類型必須和基類中的虛函數完全匹配,這一點和函數重載完全不同。
3. #include <文件名>和#include "文件名"
文件包含的兩種格式中,第一種格式用來包含那些由系統提供的並放在指定子目錄中的頭文件;而第二種格式用來包含那些由用戶自己定義的放在當前目錄或其他目錄下的頭文件或其它源文件。
4. 數組也可以作為函數的實參和形參,若數組元素作為函數的實參,則其用法與變數相同。當數組名作為函數的實參和形參時,傳遞的是數組的地址。當進行按值傳遞的時候,所進行的值傳送是單向的,即只能從實參傳向形參,而不能從形參傳回實參。形參的初值和實參相同,而形參的值發生改變后,實參並不變化,兩者的終值是不同的。而當用數組名作為函數參數進行傳遞時,由於實際上實參和形參為同一數組,因此當形參數組發生變化時,實參數組也隨之發生變化。
註:實參數組與形參數組類型應一致,如不一致,結果將出錯;形參數組也可以不指定大小,在定義數組時數組名後面跟一個空的方括弧,為了在被調用函數中處理數組元素的需要,可以另設一個參數,傳遞數組元素的個數。如:int sum(int array[],int n);
5. 重載、復蓋和隱藏的區別?
函數的重載是指C++允許多個同名的函數存在,但同名的各個函數的形參必須有區別:形參的個數不同,或者形參的個數相同,但參數類型有所不同。
復蓋(Override)是指派生類中存在重新定義的函數,其函數名、參數列、返回值類型必須同父類中的相對應被複蓋的函數嚴格一致,復蓋函數和被複蓋函數只有函數體(花括弧中的部分)不同,當派生類對象調用子類中該同名函數時會自動調用子類中的復蓋版本,而不是父類中的被複蓋函數版本,這種機制就叫做復蓋。
下面我們從成員函數的角度來講述重載和復蓋的區別。
成員函數被重載的特徵有:1) 相同的範圍(在同一個類中);2) 函數名字相同;3) 參數不同;4) virtual關鍵字可有可無。
復蓋的特徵有:1) 不同的範圍(分別位於派生類與基類);2) 函數名字相同;3) 參數相同;4) 基類函數必須有virtual關鍵字。
這一節補充一些有關類的新的概念:
* 二義性
* 全局名稱
* 名稱和限定名
* 函數的參量名稱
* 構造函數初始化器
二義性
名稱的使用在其範圍中必須是無二義性的(直到名稱的重載點)。如果這個名稱表示了一個函數,那麼這個函數必須是關於參量的個數和類型是無二義性的。如果名稱存在著二義性,則要運用成員訪問規則。
全局名稱
一個對象、函數或枚舉的名稱如果在任何函數、類之外引入或前綴有全局單目範圍操作符(::),並同時沒有同任何下述的雙目操作符連用。
* 範圍分辨符(::)
* 對象和引用的成員選擇符(.)
* 指針的成員選擇符(->)
名稱及限定名
同雙目的範圍分辨符(::)一起使用的名稱叫“限定名”。在雙目範圍分辨符之後說明的名稱必須是在該說明符左邊所說明的類的成員或其基類的成員。
在成員選擇符(.或->)后說明的名稱必須是在該說明符左邊所說明的類類型對象的成員或其基類的成員。在成員選擇符的右邊所說明的名稱可以是任何類類型對象,只要該說明符的左邊是一個類類型對象,而且該對象的類定義了一個重載的成員選擇符(->),它把指針所指的對象變為特殊的類類型。
編譯器按下面的順序搜索一個名稱,發現以後便停止:
1. 如果名稱是在函數中使用,則在當前塊範圍中搜索,否則在全局範圍中搜 索。
2. 向外到每一個封閉塊範圍中搜索,包括最外面函數範圍(這將包括函數的參量)。
3. 如果名稱在一個成員函數中使用,則在該類的範圍中搜索該名稱。
4. 在該類的基類中搜索該名稱。
5. 在外圍嵌套類範圍(如果有)或其基類中搜索,這一搜索一直到最外層包裹的類的範圍搜索之後。
6. 在全局範圍中搜索。
然而你可以按如下方式改變搜索順序:
7. 如果名稱的前面有::,則強制搜索在全局範圍之中。
8. 如果名稱的前面有class、struct和union關鍵字,將強制編譯器僅搜索 class,struct或union名稱。
9. 在範圍分辨符的左邊的名稱,只能是class,struct和union的名稱。如果在一個靜態成員函數中引用了一個非靜態的成員名,將會產生一條錯誤消息。同樣地,任何引用包圍類中的非靜態組員會產生一條錯誤消息,因為被包圍的類沒有包圍類的this指針。
函數參量名稱
函數的參量名稱在函數的定義中視為在函數的最外層塊的範圍中。因此,它們是局部名稱並且在函數結束之後,範圍就消失了。
函數的參量名稱是在函數說明(原型)的局部範圍中,並且在說明結束以後的範圍中消失。
派生類