語法說明
基類說明:在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 << endl ;
}
class Book:public Document
{
public:
Book(char *name,long pagecount);
private:
long PageCount;
};
Book::Book (char *name,long pagecount)
{
Name=new char [strlen(name)+1];
strcpy (Name,name);
PageCount=pagecount;
};
注意,Book的
構造函式(Book::Book)具有對數據成員Name的訪問權。在程式中可以按如下方式創建Book類對象並使用之。
//創建一個Book類的新對象,這將激活構造函式Book:BookBook
Book 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();
}
只要有一個可訪問的、無二義性的基類,派生類的指針和引用可以隱含地轉換為它們基類的指針和引用。下面的例子證實了這種使用指針的概念(同樣也適用於引用):
void main()
{
Document * DocLib[10]; //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;
{
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的函式,通常不是一個很好的設計,本章後面的“
虛擬函式”中提供了一個可替換的設計方法。
多重繼承
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;
...
};
基類
如前面討論的,繼承過程創建的新的派生類是由基類的成員加上由派生類新加的成員組成。在多重繼承中,可以構造層次圖,其中同一基類可以是多個派生類的一部分。圖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)
二義性
實例
多重繼承使得從不同的路徑繼承成員名稱成為可能。沿著這些路徑的成員名稱並不必然是唯一的。這些名稱的衝突稱為“二義性”。
任何引用類成員的
表達式必須使用一個無二義性的引用。下面的例子顯示了二義性是如何發生的。//說明兩個
基類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;
};
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".
<<endl;
}
兩個派生類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 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是作為
抽象類的一個很好的候選:
實例1
class Account
{
public:
Account(double d); //構造函式
virtual double GetBalance();//獲得平衡
virtual void PrintBalance()=0; //純
虛擬函式Private:
double _balance;
};
分析1
這裡的說明同前一次的說明的唯一不同是PrintBalance是用純說明符說明的。
* 參量類型
* 函式的返回類型
* 明確的轉換類型
實例2
在設計基類中含有純虛擬析構函式的類層次時,這一點很有用。因為在銷毀一個對象的過程中通常都要調用基類的析構函式,考慮下面的例子:#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;
}
分析2
當一個由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關鍵字。
概念描述
這一節補充一些有關類的新的概念:
* 二義性
* 全局名稱
* 名稱和限定名
* 函式的參量名稱
* 構造函式初始化器
二義性
名稱的使用在其範圍中必須是無二義性的(直到名稱的重載點)。如果這個名稱表示了一個函式,那么這個函式必須是關於參量的個數和類型是無二義性的。如果名稱存在著二義性,則要運用成員訪問規則。
全局名稱
一個對象、函式或枚舉的名稱如果在任何函式、類之外引入或前綴有全局單目範圍操作符(::),並同時沒有同任何下述的雙目操作符連用。
* 範圍分辨符(::)
* 對象和引用的成員選擇符(.)
* 指針的成員選擇符(->)
名稱及限定名
同雙目的範圍分辨符(::)一起使用的名稱叫“限定名”。在雙目範圍分辨符之後說明的名稱必須是在該說明符左邊所說明的類的成員或其
基類的成員。
在成員選擇符(.或->;)後說明的名稱必須是在該說明符左邊所說明的類類型對象的成員或其基類的成員。在成員選擇符的右邊所說明的名稱可以是任何類類型對象,只要該說明符的左邊是一個類類型對象,而且該對象的類定義了一個重載的成員選擇符(->;),它把指針所指的對象變為特殊的類類型。
編譯器按下面的順序搜尋一個名稱,發現以後便停止:
⒈ 如果名稱是在函式中使用,則在當前塊範圍中搜尋,否則在全局範圍中搜 索。
⒉ 向外到每一個封閉塊範圍中搜尋,包括最外面函式範圍(這將包括函式的參量)。
⒊ 如果名稱在一個成員函式中使用,則在該類的範圍中搜尋該名稱。
⒋ 在該類的基類中搜尋該名稱。
⒌ 在外圍嵌套類範圍(如果有)或其基類中搜尋,這一搜尋一直到最外層包裹的類的範圍搜尋之後。
⒍ 在全局範圍中搜尋。
然而你可以按如下方式改變搜尋順序:
⒎ 如果名稱的前面有::,則強制搜尋在全局範圍之中。
⒏ 如果名稱的前面有class、struct和union關鍵字,將強制編譯器僅搜尋 class,struct或union名稱。
⒐ 在範圍分辨符的左邊的名稱,只能是class,struct和union的名稱。如果在一個靜態成員函式中引用了一個非靜態的成員名,將會產生一條錯誤訊息。同樣地,任何引用包圍類中的非靜態組員會產生一條錯誤訊息,因為被包圍的類沒有包圍類的this指針。
函式參量名稱
函式的參量名稱在函式的定義中視為在函式的最外層塊的範圍中。因此,它們是局部名稱並且在函式結束之後,範圍就消失了。
函式的參量名稱是在函式說明(原型)的局部範圍中,並且在說明結束以後的範圍中消失。
預設的參量名稱是在參量(它們是預設的)範圍中,如前面兩段描述的,然而它們不能訪問
局部變數和非靜態類成員。預設參量值的確定是在函式調用的時候,但它們的給定是在函式說明的原始範圍中。因此成員函式的預設參量總是在類範圍中的。