類繼承
整理自《C++ Primer Plus》
1. 一個簡單的基類
- 從一個類派生出另一個類時,原始類稱為基類,繼承類稱為派生類。
- 公有派生,基類的公有成員將成為派生類的公有成員;基類的私有部分也將成為派生類的一部分,但只能通過基類的公有和保護方法訪問。
- 需要在繼承特性中添加什么呢?
派生類需要自己的構造函數。
派生類可以根據需要添加額外的數據成員和成員函數。 - 構造函數:訪問權限的考慮 派生類不能直接訪問基類的私有成員,而必須通過基類方法進行訪問。具體地說,派生類構造函數必須使用基類構造函數。
派生類構造函數的要點如下:
- 首先創建基類對象
- 派生類構造函數應通過成員初始化列表將基類信息傳遞給基類構造函數
- 派生類構造函數應初始化派生類新增的數據成員。
注意:創建派生類對象時,程序首先調用基類構造函數,然后再調用派生類構造函數。基類構造函數負責初始化繼承的數據成員;派生類構造函數主要用於初始化新增的數據成員。派生類的構造函數總是調用一個基類構造函數。可以使用初始化列表語法指明要使用的基類構造函數,否則將使用默認的基類構造函數。
派生類對象過期時,程序將首先調用派生類析構函數,然后再調用基類析構函數
2.多態公有繼承
當希望同一個方法在派生類和基類中的行為是不同的。換句話說,方法的行為應取決於調用該方法的對象。這種較復雜的行為稱為多態——具有多種形態,即同一個方法的行為隨上下文而異。有兩種重要的機制可用於實現多態公有繼承:
-
在派生類中重新定義基類的方法
-
使用虛方法
virtual關鍵字。如果方法是通過引用或指針而不是對象調用的,它將確定使用哪一種方法。如果沒有使用關鍵字virtual,程序將根據引用類型或指針類型選擇方法;如果使用了virtual,程序將根據引用或指針指向的對象的類型來選擇方法。
基類聲明了一個虛析構函數。這樣做是為了確保釋放對象時,按正確的順序調用析構函數。
注意:如果要在派生類中重新定義基類的方法,通常應將基類方法聲明為虛的。這樣,程序將根據對象類型而不是引用或指針的類型來選擇方法版本。為基類聲明一個虛析構函數也是一種慣例。 -
為何需要虛析構函數
如果析構函數不是虛的,則將只調用對應於指針類型的析構函數。如果析構函數是虛的,將調用相應對象類型的析構函數。因此,虛析構函數可以確保正確的析構函數序列被調用。
3.靜態聯編和動態聯編
在編譯過程中進行聯編稱為靜態聯編,又稱為早期聯編。然而,虛函數使這項工作變得更困難。因為編譯器不知道用戶將選擇哪種類型的對象。所以,編譯器必須生成能夠在程序運行時選擇正確的虛方法的代碼,這被稱為動態聯編,又稱為晚期聯編。
在C++中,動態聯編與通過指針和引用調用方法相關,從某種程度上說,這是由繼承控制的。指向基類的引用或指針
- 虛成員函數和動態聯編
BrassPlus ophelia; // derived-class object
Brass * bp; // base-class pointer
bp = &ophelia; // Brass pointer to BrassPlus object
bp->ViewAcct(); // which version
// 編譯器對虛方法使用動態聯編
為什么有兩種類型的聯編以及為什么默認為靜態聯編?
如果動態聯編讓您能夠重新定義類方法,而靜態聯編在這方面很差,為何不摒棄靜態聯編呢?原因有兩個——效率和概念模型。
提示:如果要在派生類中重新定義基類的方法,則將它設置為虛方法;否則,設置為非虛方法。
虛函數的工作原理
通常,編譯器處理虛函數的方法是:給每個對象添加一個隱藏成員。隱藏成員中保存了一個指向函數地址數組的指針。這種數組稱為虛函數表(virtual function table,vtbl)。虛函數表中存儲了為類對象進行聲明的虛函數的地址。
調用虛函數時,程序將查看存儲在對象中的vtbl地址,然后轉向對應的函數地址表。如果使用類聲明中定義的第一個虛函數,則程序將使用數組中的第一個函數地址,並執行具有該地址的函數。如果使用類聲明中第三個虛函數,程序將使用地址為數組中第三個元素的函數。
總之,使用虛函數時,在內存和執行速度方面有一定的成本,包括:
每個對象都將增大,增大量為存儲地址的空間
對於每個類,編譯器都創建一個虛函數地址表(數組);
對於每個函數調用,都需要執行一項額外的操作,即到表中查找地址
- 有關虛函數注意事項
-
在基類方法的聲明中使用關鍵字virtual可使該方法在基類以及所有的派生類(包括從派生類派生出來的類)中是虛的。
-
如果使用指向對象的引用或指針來調用虛方法,程序將使用為對象類型定義的方法,而不使用為引用或指針類型定義的方法。這稱為動態聯編或晚期聯編。這種行為非常重要,因為這樣基類指針或引用可以指向派生類對象。
-
如果定義的類被用作基類,則應將那些要在派生類中重新定義的類方法聲明為虛的。
-
構造函數。構造函數不能是虛函數。創建派生類對象時,將調用派生類的構造函數,而不是基類的構造函數,派生類的構造函數將使用基類的一個構造函數,這種順序不同於繼承機制。因此,派生類不繼承基類的構造函數,所以將類構造函數聲明為虛的沒什么意義。
-
析構函數。析構函數應當是虛函數,除非類不用做基類。
-
友元。友元不能是虛函數,因為友元不是類成員,而只有成員才能是虛函數。
-
4. 訪問控制:protected
關鍵字protected和private相似,在類外只能用公有類成員來訪問protected部分中的類成員。private和protected之間的區別只有在基類派生的類中才會表現出來。派生類的成員可以直接訪問基類的保護成員,但不能直接訪問基類的私有成員。
5. 抽象基類(abstract base class,ABC)
C++通過使用純虛函數(pure virtual function)提供未實現的函數。純虛函數的結尾處為=0。
當類聲明中包含純虛函數時,則不能創建該類的對象。這里的理念是,包含純虛函數的類只用作基類。要成為真正的ABC,必須至少包括一個純虛函數。
6. 繼承和動態內存分配
-
第一種情況:派生類不適用new
不需要為派生類定義析構函數,復制構造函數和賦值運算符。 -
第二種情況:派生類使用new
在這種情況下,必須為派生類定義顯示析構函數,復制構造函數和賦值運算符。
派生類析構函數自動調用基類的構造函數,故其自身的職責是對派生類構造函數執行工作進行清理
7. 類設計回顧
-
編譯器生成的成員函數
-
默認構造函數
默認構造函數要么沒有參數,要么所有參數都有默認值。如果沒有定義任何構造函數,編譯器將定義默認構造函數,讓您能夠創建對象。
自動生成的默認構造函數的另一項功能是,調用基類的默認構造函數以及調用本身是對象的成員所屬類的默認構造函數。
另外,如果派生類構造函數的成員初始化列表中沒有顯示調用基類構造函數,則編譯器將使用基類的默認構造函數來構造派生類對象的基類部分。在這種情況下,如果基類沒有構造函數,將導致編譯錯誤。
如果定義了某種構造函數,編譯器將不會定義默認構造函數。在這種情況下,如果需要默認構造函數,則必須自己提供。
提供構造函數的動機之一是確保對象總能正確地初始化。另外,如果類包含指針成員,則必須初始化這些成員。因此,最好提供一個顯示默認構造函數,將所有的類數據成員都初始化為合理的值。 -
復制構造函數
復制構造函數接受其所屬類的對象作為參數。
在下述情況下,將使用復制構造函數:
將新對象初始化為一個同類對象
按值將對象傳遞給函數
函數按值返回對象
編譯器生成臨時對象。如果程序沒有使用(顯式或隱式)復制構造函數,編譯器將提供原型,但不提供函數定義;否則,此程序將定義一個執行成員初始化的復制構造函數。也就是說,新對象的每個成員都被初始化原始對象相應成員的值。如果成員為類對象,則初始化該成員時,將使用相應類的復制構造函數。
-
賦值運算符
默認的賦值運算符用於處理同類對象之間的賦值。不要將賦值與初始化混淆了。如果語句創建新的對象,則使用初始化;如果語句修改已有對象的值,則是賦值:
-
Star sirius;
Star alpha = sirius; // initialization
Star dogstar;
dogstar = sirius; // assignment
-
其他的類方法
-
構造函數
構造函數不同於其他類方法,因為它創建新的對象,而其他類方法只是被現有的對象調用。這是構造函數不被繼承的原因之一。繼承意味着派生類對象可以使用基類的方法,然而,構造函數在完成其工作之前,對象並不存在。 -
析構函數
一定要定義顯示析構函數來釋放類構造函數使用new分配的所有內存,並完成類對象所需的任何特殊的清理工作。對於基類,即使它不需要析構函數,也應提供一個虛析構函數。 -
轉換
使用一個參數就可以調用的構造函數定義了從參數類型到類類型的轉換。
-
Star(const char *); // convert char* to Star
Star(const Spectral &, int members = 1); // convert Spectral to Star
-
按值傳遞對象與傳遞引用
通常,編寫使用對象作為參數的函數時,應按引用而不是按值來傳遞對象。
按引用傳遞對象的另外一個原因是,在繼承使用虛函數時,被定義為接受基類引用參數的函數可以接受派生類。 -
返回對象和返回引用
有時方法必須返回對象,但如果可以不返回對象了,則應返回引用。
首先,在編碼方面,直接返回對象和返回引用之間的唯一區別在於函數原型和函數頭。
其次,應返回引用而不是返回對象的原因在於,返回對象涉及生成返回對象的臨時副本,這是調用函數的程序可以使用的副本。因此,返回對象的時間成本包括調用復制構造函數來生成副本所需的時間和調用析構函數刪除副本所需的時間。返回引用可節省時間和內存。
然而,並不總是可以返回引用。函數不能返回在函數中創建的臨時對象的引用,因為當函數結束時,臨時對象將消失,因此這種引用將是非法的。在這種情況下,應返回對象,以生成一個調用程序可以使用的副本。
通用的規則是,如果函數返回在函數中創建的臨時對象,則不要使用引用。 -
使用const
使用const可以確保方法不修改參數:
Star::Star( const char * s) {......} // won't change the string to which s points
使用const來確保方法不修改調用它的對象:
void Star::show() const {......} // won't change invoking object
這里const表示const Star * this,而this指向調用的對象。
通常,可以將返回引用的函數放在賦值語句的左側,這實際意味着可以將值賦給引用的對象。但可以使用const來確保引用或指針返回的值不能用於修改對象中的數據:
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s; // argument object
else
return *this; // involking object
}
該方法返回對this或s的引用。因為this和s都被聲明為const,所以函數不能對它們進行修改,這意味着返回的引用也必須被聲明為const。
注意,如果函數將參數聲明為指向const的引用或指針,則不能將該參數傳遞給另一個函數,除非后者也確保了參數不會被修改。