第6章
當C++愛上面向對象
很多第一次進入C++世界的人都會問:C++中的那兩個加號到底是什麼意思啊?
C++是由C語言發展而來的,它比C語言多出的兩個加號,實際上是C語言的自增操作符,表示C++語言是在C語言的基礎上添加了新的內容而發展形成的。如果其中一個加號代表C++在C語言的基礎上增加了模板、異常處理等現代程序設計語言的新特性的話,那麼另外一個加號則代表C++在C語言的基礎上增加了對面向對象程序設計思想的支持。正是這兩個加號所代表的新增內容,讓C++在C語言的根基之上,完成了從傳統到現代,從面向過程到面向對象的進化,讓C++與C語言有了本質的區別。
而在C++所新增的這兩個內容中,最核心的是對面向對象程序設計思想的支持,正是它的加入,才完成了從C語言到C++的華麗蛻變。那麼,到底什麼是面向對象?C++為什麼要添加對它的支持?C++又是如何對面向對象進行支持的?別著急,且聽我一一道來。
面向對象程序設計的雛形早在1960年的Simula語言中就出現過。當時程序設計領域正面臨一種危機:面對越來越復雜的軟硬件系統,傳統的以C語言為代表的面向過程程序設計思想已越來越無法滿足現實的需要——面向過程的設計無法很好地描述整個系統,同時設計結果也讓人難以理解,因而給軟件的實現以及後期的維護帶來了巨大的挑戰,項目越大越難以實現,越到項目後期越難以實現,人們正陷入一場前所未有的軟件危機中。為了化解這場軟件危機,人們開始尋找能夠消滅“軟件危機”這頭怪獸的“銀彈(silver bullet)”。面向對象程序設計思想正是在這種背景下應運而生,它通過強調設計與實現的可擴展性和可重復性,成功地化解了這場“軟件危機”。至此以後,面向對象的程序設計思想開始在業界大行其道,逐漸成為主流。而從C語言向C++的進化剛好就發生在這個時期,自然而然地,C++也就選擇了支持面向對象程序設計思想。
《人月神話》與銀彈
《人月神話》是軟件領域裡一本具有深遠影響的著作。它誕生於軟件危機的背景之下,而正是這本書提出了“銀彈”的概念。
在西方的神話傳說中,只有被銀彈擊中心髒,才可以殺死怪獸。而在這本書中,作者把那些規模越來越大的、管理與維護越來越困難的軟件開發項目比作傳說中無法控制的怪獸,並希望有一項技術能夠像銀彈殺死怪獸那樣,徹底地解決這場軟件危機。
其實,面向對象程序設計思想並不是完全意義上的銀彈,它不可能解決所有大型軟件項目所遇到的問題,但是它提出了一種描述軟件的更加自然的方式,在一定程度上解決了這場軟件危機。
要想了解新的面對對象思想有什麼優點,最簡單直接的方式就是先看看舊的面向過程思想有什麼缺點。回顧前面章節中曾經學過的例子,我們在解決問題時總是按照這樣的流程:先提出問題;然後分析問題的處理流程;接著根據流程需要把一個大問題劃分為幾個小問題;如果細分後的小問題仍然比較復雜,則進一步細分,直到小問題可以簡單解決為止;實現每個子模塊,解決每個小問題;最後通過主函數按照業務流程次序調用這些子模塊,最終解決整個大問題,如圖6-1所示。像這樣從問題出發,自頂向下、逐步求精的開發思想我們稱為“面向過程程序設計思想”,它描述的主要是解決問題的“過程”。
圖6-1 面向過程程序設計的流程
面向過程程序設計思想誕生於20世紀60年代,鼎盛於20世紀80年代,是當時最為流行的程序設計思想。它的流行有其內在原因,跟當時其他程序設計思想相比,它有著明顯的優勢。
正如第4章中所介紹的程序流程控制結構一樣,面向過程程序設計思想限定程序只有順序、選擇和循環這三種基本控制結構。任何程序邏輯,無論是簡單的還是復雜的,都可以用這三種基本的控制結構經過不同的組合或嵌套來實現。這就使得程序的結構相對比較簡單,易於實現和維護。
人們在解決復雜問題時,總是采用“分而治之”的策略,把大問題分解為多個小問題後,再“各個擊破”並最終讓大問題得到解決。面向過程程序設計思想也采取這種“分而治之”的策略,把較大的程序按照業務邏輯劃分為多個子模塊,然後分工逐個完成這些子模塊,最後再按照業務流程把它們組織起來,最終使得整個問題得到解決。按照一定的原則,把大問題細分為小問題“各個擊破”,符合人們思考問題的一般規律,其設計結果更易於理解,同時這種方法也更易於人們掌握。通過分解問題,降低了問題的復雜度,使得程序易於實現和維護。另外,部分分解後的小問題(子模塊)可以重復使用,從而避免了重復開發。而多個子模塊也可由多人分工協作完成,提高了開發效率。
面向過程程序設計思想倡導的方法是“自頂向下,逐步求精”。所謂“自頂向下,逐步求精”,就是先從宏觀角度考慮,按照功能或者業務邏輯劃分程序的子模塊,定義程序的整體結構,然後再對各個子模塊逐步細化,最終分解到程序語句為止。這種方法使得程序員能夠全面考慮問題,使程序的邏輯關系清晰明了。它讓整個開發過程從原來的考慮“怎麼做”變成考慮“先做什麼,再做什麼”,流程也更加清晰。
隨著時代的發展,軟件開發項目也越來越復雜。雖然面向過程程序設計思想有諸多優點,但在利用它解決復雜問題的時候,其缺點也逐漸暴露出來:在面向過程程序設計中,數據和操作是相互分離的,這就導致如果數據的結構發生變化,相應的操作函數就不得不重新改寫;如果遇到需求變化或者新的需求,還可能涉及模塊的重新劃分,這就要修改大量原先寫好的功能模塊。面向過程程序設計中數據和操作相互分離的特點,使得一些模塊跟具體的應用環境結合緊密,舊有的程序模塊很難在新的程序中得到復用。這些面向過程程序設計思想的固有缺點使得它越來越無法適應大型的軟件項目的開發,這真是“成也面向過程,敗也面向過程”。於是,人們開始尋找一種新的程序設計思想。正是在這種情況下,一些新的程序設計思想開始不斷湧現並逐漸取代面向過程程序設計思想,而面向對象程序設計思想就是其中的“帶頭大哥”。
面向對象程序設計(Object Oriented Programming, OOP)是對面向過程程序設計的繼承和發展,它不僅汲取了後者的精華,而且以一種更接近人類思維的方式來分析和解決問題:程序是對現實世界的抽象和描述,現實世界的基本單元是物體,與之對應的,程序中的基本單元就是對象。
面向對象思想認為:現實世界是由很多彼此相關並互通信息的實體——對象組成的。大到一個星球、一個國家,小到一個人、一個分子,無論是有生命的,還是沒有生命的,都可以看成是一個對象。通過分析這些對象,發現每個對象都由兩部分組成:描述對象狀態或屬性的數據(變量)和描述對象行為或功能的方法(函數)。與面向過程將數據和對數據進行操作的函數相分開不同的是,面向對象將數據和操作數據的函數緊密結合,共同構成對象來更加准確地描述現實世界。這可以說是面向過程與面向對象兩者最本質的區別。
跟現實世界相對應的,在面向對象中,我們用某個對象來代表現實世界中的某個實體,每個對象都有自己的屬性和行為,而整個程序則由一系列相互作用的對象構成,對象之間通過互相操作來完成復雜的業務邏輯。比如在一個班中,有一位陳老師和50名學生,那麼我們就可以用一個老師對象和50個學生對象來抽象和描述這一個班級。對這51個對象而言,有些屬性是它們所共有的,比如姓名、年齡等,每個對象都有;而有部分屬性則是某類對象特有的,比如老師對象有職務這個屬性,而學生對象則沒有。另外,老師和學生這兩種對象還有各自不同的行為;比如老師對象有備課、上課、批改作業的行為;而學生對象則有聽課、完成作業等行為。老師對象和學生對象各自負責自己的行為和職責,同時又相互發生聯系,比如老師上課的動作需要以學生作為動作對象。通過對象之間的相互作用,整個班級就可以正常運作。整個面向對象分析設計的結果,跟我們的現實世界非常接近,自然也就更容易理解和實現了。老師對象如圖6-2所示。
圖6-2 用面向對象思想將老師抽象成對象
知道更多:面向對象編程的重要性在哪?
這一點,也許可以從面向對象的誕生說起。
在面向對象出生之前,有一個叫做面向過程的人,它將整個待解決的問題,抽象為描述事物的數據以及描述對數據進行處理的函數,或者說數據處理過程。當問題規模比較小,需求變化不大的時候,面向過程工作得很好。
可是(任何事物都怕“可是”二字),當問題的規模越來越大越來越復雜,需求變化越來越快的時候,面向過程就顯得力不從心了。想象一下,當我們根據需求變化修改了某個結構體,就不得不修改與之相關的所有過程函數,而一個過程函數的修改,往往又會涉及到其他數據結構,在系統規模較小的時候,這還比較容易解決,可是當系統規模越來越大,特別是涉及到多人協作開發的時候,這簡直就是一場噩夢。這就是那場著名的軟件危機。
為了解決這場軟件危機,面向對象應運而生了(有問題的出現,必然就有解決問題的方法的出現,英雄人物大都是這樣誕生的)。
我們知道,面向對象的三板斧分別是封裝、繼承和多態:它用封裝將問題中的數據和對數據進行處理的函數結合在了一起,形成了一個整體的類的概念,這樣更加符合人的思維習慣,更利於理解,自然在理解和抽象一些復雜系統的時候也更加容易;它用繼承來應對系統的擴展,在原有系統的基礎上,只要簡單繼承,就可以完成系統的擴展,而無需重起爐灶;它用多態來應對需求的變化,統一的借口,卻可以有不同的實現。
可以說,面向對象思想用它的三板斧,在一定程度上解決了軟件危機,這就是它重要性的根本體現。
我們知道,面向對象是為了解決面向過程所無法解決的“軟件危機”而誕生的,那麼,它又是如何解決“軟件危機”的呢?封裝、繼承與多態是面向對象思想的三座基石,正是它們的共同作用,才使得“軟件危機”得到了一定程度的解決,如圖6-3所示。
1. 封裝程序是用來抽象和描述現實世界的。那麼先來看看在現實世界中我們又是如何描述周圍的事物的。我們總是從數據和操作兩個方面來描述某個事物:這個事物是什麼和這個事物能做什麼。比如我們要描述一位老師,我們會說:他身高178厘米,體重72公斤,年齡32歲,同時他能給學生上課,能批改作業。這樣,一個活生生的老師形象就會在我們頭腦中建立起來。
在傳統的面向過程思想中,程序中的數據和操作是相互分離的。也就是說,在描述一個事物的時候,事物是什麼(數據)和事物能做什麼(操作)是相互分離的。但在面向對象思想中,我們通過封裝機制將數據和相應的操作捆綁到了一起,以形成一個完整的、具有屬性(數據)和行為(操作)的數據類型。在C++中,我們把這種數據類型稱為類(class),而用這種數據類型所定義的變量,就被稱為對象(object)。這樣就使得程序中的數據和對這些數據的操作結合在了一起,更加符合人們描述某個事物的思維習慣,因此也更加容易理解和實現。簡單來說,對象就是封裝了數據和操作這些數據的動作的邏輯實體,也是現實世界中事物在程序中的反映,如圖6-4所示。
圖6-4 將屬性和行為封裝成對象
封裝機制還帶來了另外一個好處,那就是對數據的保護。在面向過程思想中,因為數據和操作是相互分離的,某些操作有可能錯誤地修改了不屬於它的數據,從而造成程序錯誤。而在面向對象思想中,數據和操作被捆綁在一起成了對象,而數據可以是某個對象所私有的,只能由與它捆綁在一起的操作訪問,這樣就避免了數據被其他操作意外地訪問。這就如同錢包裡的錢是我們的私有財產,只有我們自己可以訪問,別人是不可以訪問的。當然,小偷除外。
在創造某個新事物的時候,我們總是希望可以在某個舊事物的基礎之上開始,毫無疑問這樣會提高效率。可是對於面向過程的C語言而言,這一點卻很難做到。在C語言中,如果已經寫好了一個“上課”的函數,而要想再寫一個“上數學課”的函數,很多時候我們都不得不另起爐灶全部重新開始。如果我們每次都另起爐灶,那樣開發效率就太低了。顯然,這無法滿足大型的復雜系統的開發需要。
正是為了解決這個問題,面向對象思想提出了繼承的機制。繼承是可以讓某個類型獲得另一個類型的屬性(成員變量)和行為(成員函數)的簡單方法。繼承就如同現實世界中的進化一樣,繼承得到的子類型,既可以擁有父類型的屬性和行為,又可以新增加子類型所特有的屬性和行為。比如我們已經用封裝機制將姓名屬性和說話行為封裝成了“人”這個類,再此基礎之上,我們可以很容易地通過繼承“人”這個類,同時添加職務屬性和上課行為而得到一個新的“老師”類。而這個新的“老師”類,不僅擁有它的父類“人”的姓名屬性和說話行為,同時還擁有它自己的職務屬性和上課行為。如果需要,我們還可以在“老師”類的基礎之上繼承得到“數學老師”、“語文老師”類等等。在這個過程中,我們直接復用了父類已有的屬性和行為,這就避免了面向過程的另起爐灶重新開始,很大程度地提高了開發效率。如圖6-5所示。
圖6-5 繼承
“見領導阿谀奉承,見下屬飛揚跋扈”,是說一個人兩面三刀,不是什麼好人。可在C++世界中,這種在不同情況下做不同事情的現象,卻被冠以一個冠冕堂皇的名字——多態,成為面向對象思想的一個重要特性。
多態是繼承的直接結果。由於繼承,在同一個繼承體系中的多個類型的對象往往擁有相同的行為能干同樣的事情,但是因為類型的不同,這些行為往往又需要有不同的實現方式。比如,“大學老師”和“小學老師”都是從“老師”這個父類繼承而來的,它們兩者都同樣從“老師”父類中繼承得到了“上課”這個行為,然而兩者“上課”的具體方式又是不同的:“小學老師”是拿著課本上課,而“大學老師”是拿著鼠標上課。多態就是讓一個對象在做某件事(調用某個接口函數)時,該對象能夠搞清楚到底怎麼完成(采用何種實現)這件事。還是上面的例子,有了多態機制,同樣是對“上課”函數的調用,如果這個對象是“小學老師”類型的,就使用“小學老師”類的實現,而如果這個對象是“大學老師”類型的,就使用“大學老師”類的實現。這就是C++世界中的“見領導阿谀奉承,見下屬飛揚跋扈”。如圖6-6所示。
圖6-6 多態
最佳實踐:多態與重載的區別
多態跟之前我們學習的函數重載相比,兩者都是根據不同的情況而決定調用函數的不同實現。但是,無論是內在機制還是外在形式,兩者卻有著很大的區別。
首先,在內在機制上,兩者發生的時間不同。重載是一個編譯時概念,它發生在程序的編譯時期,編譯器根據代碼中調用函數的實際參數的個數和類型,來決定調用這個重載函數的某個具體實現。而多態是一個運行時概念,它發生在程序的運行時期,程序會根據調用這個函數的真實對象類型,來決定調用這個函數在某個特定類中的實現。
其次,在外在形式上,兩者的層次關系不同。對於函數重載,同名的重載函數都在同一個作用域內,要麼同為全劇函數,要麼同為某個局部作用域的局部函數。而多態則是伴隨著繼承而生的,它只能發生在某個繼承體系的不同層次的類之間。
多態機制使得不同類型的不同內部實現可以擁有相同的函數聲明,共享相同的外部接口。這意味著雖然針對不同對象的具體操作不同,但通過一個公共的父類,它們(成員函數)能夠以相同的方式被調用。簡單來說,多態機制允許通過相同的接口引發一組相關但不相同的動作。通過這種方式,保持了代碼的一致性減少了代碼的復雜度。相同的函數調用形式,但在某種特定的情況下應該做出怎樣的動作由編譯器決定,而不需要程序員手工進行干預,為程序員省了很多事。
縱觀面向對象思想的三大特征,它們是緊密相關、不可分割的。通過封裝,我們將現實世界中的數據和對數據進行操作的動作捆綁到一起成了類,然後再通過類定義對象,很好地實現了對現實世界事物的抽象和描述;通過繼承,可以在舊類型的基礎上快速派生得到新的類型,很好地實現了設計和代碼的復用;同時,多態機制保證了在繼承的同時,還有機會對已有行為進行重新定義,滿足了不斷出現的新需求的需要。
正是因為面向對象思想的封裝、繼承和多態這三大特性,使得面向對象思想在程序設計中有著不可替代的優勢。
(1) 容易設計和實現。
面向對象思想強調從客觀存在的事物(對象)出發來認識問題和分析解決問題,因為這種方式更加符合我們認識事物的規律,所以大大降低了問題的理解難度。面向對象思想所運用的封裝、繼承與多態等基本原則,符合人類日常的思維習慣,使得采用面向對象思想設計的程序結構清晰、更容易設計和實現。
(2) 復用設計和代碼,開發效率和系統質量都得到提高。
面向對象思想的繼承和多態,強調了程序設計和代碼的重用,這在設計新系統的時候,可以最大限度地重用已有的、經過大量實踐檢驗的設計和代碼,使系統能夠滿足新的業務需求並具有較高的質量。同時,因為可以復用以前的設計和代碼,大大提高了開發效率。
(3) 容易擴展。
開發大型系統的時候,最擔心的就是需求的變更以及對系統進行擴展。利用面向對象思想繼承、封裝和多態的特性,可以設計出“高內聚、低耦合”的系統結構,可讓系統更靈活、更易擴展,從而輕松應對系統的擴展需求,降低維護成本。
最佳實踐:高內聚,低耦合
高內聚,低耦合是軟件工程中的一個概念,通常用以判斷一個軟件設計的好壞。所謂的高內聚,是指一個軟件模塊是由相關性很強的代碼組成,只負責某項單一任務,也就是常說的“單一責任原則”。而低耦合指的是在一個完整的系統中,模塊與模塊之間,盡可能地保持相互獨立。換句話說,也就是讓每個模塊盡可能的獨立完成某個特定的子功能。模塊與模塊之間的接口,盡量的少而簡單。
高內聚低耦合的系統具有更好的重用性、可維護性和擴展性,可以更高效的完成系統的開發、維護和擴展,持續地支持業務的發展。因而,它可以作為判斷一個軟件設計好壞的標准,自然也是我們軟件設計的目標。
面向對象程序設計思想在軟件開發中的這些優勢,使其成為當前最為流行的程序設計思想之一,是每個進入C++世界的程序員都需要理解和掌握的。它就像程序設計中的《易筋經》般博大精深,而這裡所介紹的只是面向對象思想最基礎的入門知識,要完全領會和靈活運用面向對象思想,還需要在實踐中不斷學習和總結。在理解概念的同時,更要著重體會如何利用面向對象思想來分析問題設計程序,只有這樣才能增加軟件設計和開發的功力,成為真正的高手。
(我還會回來的)