天天看點

《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制

類别:部分人

在了解c++11中的final/override關鍵字之前,我們先回顧一下c++關于重載的概念。簡單地說,一個類a中聲明的虛函數fun在其派生類b中再次被定義,且b中的函數fun跟a中fun的原型一樣(函數名、參數清單等一樣),那麼我們就稱b重載(overload)了a的fun函數。對于任何b類型的變量,調用成員函數fun都是調用了b重載的版本。而如果同時有a的派生類c,卻并沒有重載a的fun函數,那麼調用成員函數fun則會調用a中的版本。這在c++中就實作多态。

在通常情況下,一旦在基類a中的成員函數fun被聲明為virtual的,那麼對于其派生類b而言,fun總是能夠被重載的(除非被重寫了)。有的時候我們并不想fun在b類型派生類中被重載,那麼,c++98沒有方法對此進行限制。我們看看下面這個具體的例子,如代碼清單2-23所示。

《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制
《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制

在代碼清單2-23中,我們的基礎類mathobject定義了兩個接口:arith和print。類printable則繼承于mathobject并實作了print接口。接下來,add2和mul3為了使用mathobject的接口和printable的print的實作,于是都繼承了printable。這樣的類派生結構,在面向對象的程式設計中非常典型。不過倘若這裡的printable和add2是由兩個程式員完成的,printable的編寫者不禁會有一些憂慮,如果add2的編寫者重載了print函數,那麼他所期望的統一風格的列印方式将不複存在。

對于java這種所有類型派生于單一進制類型(object)的語言來說,這種問題早就出現了。是以java語言使用了final關鍵字來阻止函數繼續重寫。final關鍵字的作用是使派生類不可覆寫它所修飾的虛函數。c++11也采用了類似的做法,如代碼清單2-24所示的例子。

《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制

在代碼清單2-24中,派生于object的base類重載了object的fun接口,并将本類中的fun函數聲明為final的。那麼派生于base的derived類對接口fun的重載則會導緻編譯時的錯誤。同理,在代碼清單2-23中,printable的編寫者如果要阻止派生類重載print函數,隻需要在定義時使用final進行修飾就可以了。

讀者可能注意到了,在代碼清單2-23及代碼清單2-24兩個例子當中,final關鍵字都是用于描述一個派生類的。那麼基類中的虛函數是否可以使用final關鍵字呢?答案是肯定的,不過這樣将使用該虛函數無法被重載,也就失去了虛函數的意義。如果不想成員函數被重載,程式員可以直接将該成員函數定義為非虛的。而final通常隻在繼承關系的“中途”終止派生類的重載中有意義。從接口派生的角度而言,final可以在派生過程中任意地阻止一個接口的可重載性,這就給面向對象的程式員帶來了更大的控制力。

在c++中重載還有一個特點,就是對于基類聲明為virtual的函數,之後的重載版本都不需要再聲明該重載函數為virtual。即使在派生類中聲明了virtual,該關鍵字也是編譯器可以忽略的。這帶來了一些書寫上的便利,卻帶來了一些閱讀上的困難。比如代碼清單2-23中的printable的print函數,程式員無法從printable的定義中看出print是一個虛函數還是非虛函數。另外一點就是,在c++中有的虛函數會“跨層”,沒有在父類中聲明的接口有可能是祖先的虛函數接口。比如在代碼清單2-23中,如果printable不聲明arith函數,其接口在add2和mul3中依然是可重載的,這同樣是在父類中無法讀到的資訊。這樣一來,如果類的繼承結構比較長(不斷地派生)或者比較複雜(比如偶爾多重繼承),派生類的編寫者會遇到資訊分散、難以閱讀的問題(雖然有時候編輯器會進行提示,不過編輯器不是總是那麼有效)。而自己是否在重載一個接口,以及自己重載的接口的名字是否有拼寫錯誤等,都非常不容易檢查。

在c++11中為了幫助程式員寫繼承結構複雜的類型,引入了虛函數描述符override,如果派生類在虛函數聲明時使用了override描述符,那麼該函數必須重載其基類中的同名函數,否則代碼将無法通過編譯。我們來看一下如代碼清單2-25所示的這個簡單的例子。

《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制

在代碼清單2-25中,我們在基類base中定義了一些virtual的函數(接口)以及一個非virtual的函數print。其派生類derivedmid中,基類的base的接口都沒有重載,不過通過注釋可以發現,derivedmid的作者曾經想要重載出一個“void vneumann(double g)”的版本。這行注釋顯然迷惑了編寫derivedtop的程式員,是以derivedtop的作者在重載所有base類的接口的時候,犯下了3種不同的錯誤:

《深入了解C++11:C++ 11新特性解析與應用》——2.10 final/override控制

如果沒有override修飾符,derivedtop的作者可能在編譯後都沒有意識到自己犯了這麼多錯誤。因為編譯器對以上3種錯誤不會有任何的警示。這裡override修飾符則可以保證編譯器輔助地做一些檢查。我們可以看到,在代碼清單2-25中,derivedtop作者的4處錯誤都無法通過編譯。

此外,值得指出的是,在c++中,如果一個派生類的編寫者自認為新寫了一個接口,而實際上卻重載了一個底層的接口(一些簡單的名字如get、set、print就容易出現這樣的狀況),出現這種情況編譯器還是愛莫能助的。不過這樣無意中的重載一般不會帶來太大的問題,因為派生類的變量如果調用了該接口,除了可能存在的一些虛函數開銷外,仍然會執行派生類的版本。是以編譯器也就沒有必要提供檢查“非重載”的狀況。而檢查“一定重載”的override關鍵字,對程式員的實際應用則會更有意義。

還有值得注意的是,如我們在第1章中提到的,final/override也可以定義為正常變量名,隻有在其出現在函數後時才是能夠控制繼承/派生的關鍵字。通過這樣的設計,很多含有final/override變量或者函數名的c++98代碼就能夠被c++編譯器編譯通過了。但出于安全考慮,建議讀者在c++11代碼中應該盡可能地避免這樣的變量名稱或将其定義在宏中,以防發生不必要的錯誤。

繼續閱讀