有一種普遍的說法是把封裝、繼承和多態並稱為面向對象的三大特征。如果你很熟悉C++並且對面向對象思想有過一些思考,那麼很可能對這個說法有過懷疑,面向對象思想在本質上認為世界是由對象構成的,和面向過程是世界觀的不同,而所謂的三大特征實際和面向對象的思想本質沒有半毛錢的關系,准確的表述應該是封裝、繼承和多態是C++相對於C的三大特征。如果你碰巧了解一點C++編譯器可能會發現封裝也好,繼承、多態也好都只是語法糖,技巧層面的東西而已,和思想無關。
以上為廢話。
本文主要就C++的繼承機制進行一些討論。很多C++教材在講到繼承時喜歡利用幾何上的一些概念,比如對如下的集合關系進行建模:
在一次內部技術培訓的時候我提出這個問題,結果大家都沉默不語,於是我特意找了個新手程序員回答,他給出了我預料之中的答案:以四邊形作為基類,矩形和正方形依次繼承下來。沒人表示同意也沒人表示反對,可能所有的人第一反應得出的都是這個方案,但老鳥程序員會馬上察覺出其中的不妥,即使他給不出更好的方案。
實際上在沒有給定需求場景的情況下你永遠無法設計出一個類,更不要說設計一組類和這些類的層次關系。很多教材都有這個毛病,上來就設計Student、Teacher而沒說要完成的功能是什麼,即使是做了許多年C++之後再回過頭來看那些例子還是暈忽忽的,何況初學者,——當然這也許僅僅是因為我自己太笨。
我們設定兩個簡單的需求取邊長和計算面積,暫時不考慮繼承關系而分別實現三個類,那麼它們是下面這個樣子的:
1 class Quadrangle 2 { 3 public: 4 int GetSideLength(int index); 5 int GetArea(void); 6 7 private: 8 int m_arrSlide[4]; 9 }; 10 11 class Rectangle 12 { 13 public: 14 int GetWidth(void); 15 int GetHeight(void); 16 int GetArea(void); 17 18 private: 19 int m_nWidth; 20 int m_nHeight; 21 }; 22 23 class Square 24 { 25 public: 26 int GetWidth(void); 27 int GetArea(void); 28 29 private: 30 int m_nWidth; 31 };
顯然,正方形四邊形Square 的實現最簡單,四邊形Quadrangle的實現最復雜(知道四條邊長能確定唯一的四邊形嗎?)。繼承機制有一個特點:派生類總是比基類更復雜,因為派生類是在完整的繼承了基類實現的基礎上增加新的成員、方法。由四邊形到矩形再到正方形卻是越來越簡單,這就形成了一個悖論,導致我們無法按照繼承的層次描述三者的關系。
一個老到的程序員會告訴你最好分別實現三個類,不考慮三個者之間的關系,這在大部分場景中是可行的。如果確實需要描述三者之間的層次關系,我能想到的最好的方式是使用接口:
接口用來描述層次關系,各個類獨立實現。由此也可以看出,雖然C++中的接口是用純虛類繼承實現的,但實際上接口機制和繼承機制是兩種完全不同的東西。
到現在為止,我們可以得出結論:繼承機制實際上很難描述現實概念的層次關系,這是它的局限性。對繼承的應用很多情況下並不是為了描述真實概念的層次關系,而只是組織代碼的一種形式。比如可以嘗試下用繼承關系描述一棵進化樹,你會發現這個基本上很難。
在C++中引入繼承機制的目的是什麼,大部分資料對此都語焉不詳,但是無論如何至少有一半的目的是為了組織和復用代碼,繼承擴展是很常用的手法,在MFC、WTL等框架中到處可以看到這樣的代碼。但是在實際的應用中一定要避免單純為了復用代碼而使用繼承機制,典型的案例比如窗口和控件。
窗口和控件是兩種完全不同的東西,微軟為了復用消息機制把兩個概念硬是揉在了一起,所有的控件都從窗口繼承下來,這直接導致了GUI框架的高復雜度和難以擴展。
代碼復用只是良好設計的副產品而不應該是設計本身的目的。
最後總結一下本文的核心觀點:
1. 繼承機制有很大的局限性,難於描述現實概念的層次關系;
2. 使用繼承時避免生搬硬套現實概念的層次關系;
3. 避免單純以代碼復用為目的使用繼承
微博:@飛舞的煙灰缸