一 多態性的分類
編譯時的多態
函數重載
運算符重載
運行時的多態
虛函數
1 運算符重載的引入
使用C++編寫程序時,我們不僅要使用基本數據類型,還要設計新的數據類型-------類類型。
一般情況下,基本數據類型的運算都是運算符來表達,這很直觀,語義也簡單。
例如:
int a,b,c;a=b+c;
對于基本數據類型,就隱含著運算符重載的概念。
如果直接將運算符作用在類類型之上,情況又如何呢?
例如:
Complex ret,c1,c2;ret=c1+c2;
編譯器將不能識別運算符的語義。
需要一種機制來重新定義運算符作用在類類型上的含義。
這種機制就是運算符重載。
二 兩種重載函數的比較
多數情況下,運算符可以重載為類的成員函數,也可以重載為友元函數。但兩種重載也有各自特點:
一般情況下,單目運算符重載為類的成員函數;雙目元素重載為類的友元函數。
有些雙目運算符不能重載為類的友元函數:=,(),[],->
類型轉換函數只能定義為類的成員函數,而不能定義為友元函數。
若一個運算符的操作需要修改對象的狀態,則重載為成員函數比較好;
若運算符所需要的操作數(尤其是第一個操作數)希望有隱式類型轉換,則只能選擇友元函數;
若運算符是成員函數,最左邊的操作數必須是運算符類的對象(或者類對象的引用)。如果左邊操作數必須是一個不同類的對象,或者是基本數據類型,則必須重載為友元函數;
當需要重載運算符的元素具有交換性時,重載為友元函數;
1 重載運算符的幾點注意事項
大多數預定義的運算符可以被重載,重載后的優先級、結合級及所需的操作數都不變。
但少數的C++運算符不能重載:
不能重載非運算符的符號,例如:;
C++ 不運行重載不存在的運算符,如"?"、“**”等。
當運算符被重載時,它是被綁定在一個特定的類類型之上的。當此運算符不作用在特定類類型上時,它將保持原有的含義。
當重載運算符時,不能創造新的運算符符號,例如不能用"**"來表示求幕運算符。
應當盡可能保持重載運算符原有的語義。試想,如果在某個程序中用"+“表示減,”*"表示除,那么這個程序讀起來將會非常別扭。
三 多態性的引入
1 虛函數和多態性
重載普通的成員函數的兩種方式:
在同一個類中重載:重載函數是以參數特征區分的。
派生類重載基類的成員函數:
由于重載函數處在不同的類中,因此它們的原型可以完全相同。調用時使用“類名::函數名”的方式加以區分。
以上兩種重載的匹配都是在編譯的時候靜態完成的。
重載是一種簡單形式的多態。
C++提供另一種更加靈活的多態機制:虛函數。虛函數運行函數調用與函數體的匹配在運行時才確定。
虛函數提供的是一種動態綁定的機制。
2 賦值兼容規則
在公有派生方式下,派生類對象可以作為基類對象來使用,具體方式如下:
派生類擁有從基類繼承過來的成員;
基類對象和派生類對象的內存布局方式;
當一個派生類對象直接賦值給基類對象時,不是所有的數據都賦給了基類對象,賦予的只是派生類對象的一部分。這部分叫做派生類對象的“切片(sliced)”。
注意
回憶一下不同的繼承方式,子類對基類中成員的訪問權限:
只有在公有派生的情況下,才有可能出現“基類的公有成員變成派生類的公有成員”的情況。
通過基類引用或指針所能看到的是一個基類對象,派生類中的成員對于基類引用或指針來說是“不可見的”。
我們能不能“通過基類引用或指針來訪問派生類的成員”呢?
為了達到上述目的,我們可以利用C++的虛函數機制,將基類的Print說明為虛函數形式。這樣就可以通過基類引用或指針來訪問派生類中的Print。
3 虛函數
在基類中用virtual關鍵字聲明的成員函數即為虛函數。
虛函數可以在一個或多個派生類中被重寫定義,但要求重定義時虛函數的原型(包括返回值類型、函數名、參數列表)必須完全相同。
3 基類中的函數具有虛特性的條件
在基類中用virtual將函數說明為虛函數。
在公有派生類中原型一致地重載該虛函數。
定義基類引用或指針,使其引用或指向派生類對象。當通過該引用或指針調用需要函數時,該函數將體現出虛特性來。
C++中,基類必須指出希望派生類重定義哪些函數。定義為virtual的函數是基類期待派生類重新定義的,基類希望派生類繼承的函數不能定義為虛函數。
注意:
在派生類中重載虛函數時必須與基類中的函數原型相同,否則該函數將丟失虛特性。
僅返回類型不同,其他相同。C++編譯器認為這種情況是不允許的。
函數原型不同,,僅函數名相同。C++編譯器認為這是一般的函數重載,此時虛特性丟失。
四 虛函數與多態性
1 提供虛函數的意義
提升軟件的重用性
基類使用虛函數提供一個接口,但派生類可以定義自己的實現版本。
虛函數調用的解釋依賴于它的對象類型,這就實現了“一個接口,多種語義”的概念。
提供軟件架構的合理性。
2 虛函數和虛指針
在編譯時,為每個有虛函數的類建立一張虛函數表VTABLE,表中存放的時每一個虛函數的指針;同時用一個虛指針VPTR指向這張表的入口。
訪問某個虛函數時,不是直接找到那個函數的地址,而是通過VPTR間接查到它的地址。
對象的內存空間除了保存數據成員外,還保存VPTR。VPTR由構造函數來初始化。
3 對虛函數的要求
虛函數必須是類的非靜態成員函數。
不能將虛函數說明為全局函數。
不能將虛函數說明為靜態成員函數。
不能將虛函數說明為友元函數。
本質的原因就是非靜態成員函數隱含傳遞this指針,而通過this指針能夠找到VPTR。
4 在成員函數中調用虛函數
在一個基類或派生類的成員函數中,可以直接調用類等級中的虛函數。此時需要根據成員函數中this指針所指向的對象來判斷調用的時哪一個函數。
5 析構函數可以定義為虛函數
構造函數不能定義為虛函數。
而析構函數可以定義為虛函數。
若析構函數為虛函數,那么當使用delete釋放基類指針所指向的派生類對象時,先調用派生類的析構函數,再調用基類的析構函數。
五 純虛函數與抽象類
基類中的這些公共接口只需要有售賣而不需要有實現,即純虛函數。純虛函數刻畫了派生類應該遵循的協議,這些協議的具體實現由派生類來決定。
將一個函數說明為純虛hasn’t,就要求任何派生類都定義自己的實現。
擁有純虛函數的類被稱為抽象類。抽象類不能被實例化,只能作為基類被使用。
抽象類的派生類需要實現純虛函數,否則該派生類也是一個抽象類。
當抽象類的所有函數成員都是純虛函數時,這個類被稱為接口類。
小結:
繼承和動態綁定在兩個方面簡化了我們的程序:
能夠容易地定義與其他類相似但又不相同的新類,能更容易地編寫忽略這些相似類型之間區別的程序。
許多應用程序的特性可以用一些相關但略有不同的概率描述。面向對象編程與這種應用非常匹配。通過繼承可以定義一些類型,可以模型不同沖類;通過動態綁定可以編寫程序,使用這些類而又忽略與具體類型相關的差異。
繼承和動態綁定的思想在概念上非常簡單,但對于如何創建應用程序以及對于程序設計語言必須支持得特性,含義深遠。
面向對象編程的關鍵思想是多態性。因為在需要情況下可以互換地使用派生類型或基類型的“許多形態”,所以稱通過繼承而相關聯的類型為多態類型。C++中,多態型僅用于通過繼承而相關性的類型的引用或指針。
我們稱因繼承而相關的類構成一個繼承層次。其中一個類稱為根,所有其他類直接或間接繼承根類。