近日周立功教授公開了數年的心血之作《程序設計與數據結構》,電子版已無償性分享到電子工程師與高校群體下載,經周立功教授授權,特對本書內容進行連載。
>>>>1.1類與對象
亞里士多德可能是第一個研究類型概念的人,他提到了“魚類和鳥類”。將具有共同的行為和特征的所有對象歸為一個類的思想,在第一個面向對象語言Simula-67中得到了直接應用,其目的是為了解決模擬問題。比如,銀行的出納業務,包括出納部門、顧客、業務、貨幣的單位等大量的對象,將具有相同數據結構(屬性)和行為(操作)的對象歸在一起為一個類,屬于類的任何對象都共享該類的所有屬性,這就是類的來源。
創建抽象數據類型是OOP的基本思想,幾乎能象完全內建類型一樣使用。程序員可以創建類型的變量和操作這些變量。每個類的成員都有共,每個賬戶有余額,每個出納員都能接收存款等。同時每個成員都有自己的狀態,每個賬戶有不同的余額,每個出納員都有名字。通常在計算機中出納員、客戶、賬戶和交易等都被描述為唯一的實體,這個實體就是對象,每個對象都屬于一個定義了它的行為和特性的特定類。
由此可見,類和類的對象不是相同的概念,與圖紙和建筑的關系類似,對象的描述依賴描述它的類。因此可以通過創建類的實例創建對象,即定義類的變量,這個過程叫做實例化。
>>>1.1.1 對象
從人類認知的抽象角度來看,對象可以是下列事物之一:
● 一個可以觸摸或可以看見的東西;
● 在智力上可以理解的東西;
● 可以指導思考或行動的東西。
顯而易見,一個對象反映了某一部分的真實存在,因此對象是在時間和空間中存在的某種東西。軟件中的“對象”術語首先出現在Simula語言中,對象存在于Simula程序中,用于模擬現實世界的某個方面。
某些對象可能有明確的概念邊界,但代表的是不可觸摸的事件或過程。比如,一個立方體和一個球相交,它們的相交線是一條不規則的曲線。雖然它離開了球體或立方體就不存在了,但這條線仍然是一個對象,因為它有明確定義的概念邊界。
某些對象可能是可觸摸的,但物理邊界不太清晰。比如,河流、霧和人群等就屬于這種類型的對象。雖然類似于美和色彩這樣的屬性不是對象,愛和恨這樣的感情也不是對象,但這些東西有可能成為其它對象的屬性,比如,一個男人(一個對象)愛他的妻子(另一個對象),或者說某只貓(又一個對象)是灰色的。由此可見,屬性表示對象記憶的信息,且只能通過對象的操作來訪問和修改。
當傳統的過程模塊或函數返回調用者時,不會帶來任何副作用,模塊運行結束,只將其結果返回。當同一模塊再次被調用時,就象是第一次誕生一樣。模塊對以前的存在沒有任何記憶,就象人類一樣對以前的存在一無所知。
就對象而言,對象的一個重要特征是它們充當數據的容器,因此對象具有記憶功能,對象知道它的過去,通常也將包含在對象屬性中的數據值稱為對象的狀態。當一個對象的調用者給該對象一個信息后,如果該調用者或其它調用者要求該對象再次提供這一信息,則該對象執行結束后并沒有死,因此對象具有如何保持其狀態(狀態即對象擁有值的集合)的能力。
假設你在看一個人,肯定會將這個人當作一個對象。顯然,每個人都有數據,比如,name、birthdate和weight等;一個人還有行為,比如,走路、說話和呼吸等,因此可以說對象是由“數據和行為”構成的。在現實世界里,由于每個對象的狀態不一樣,因此可以用存儲在一個對象中的數據表示對象的狀態,數據包含了能夠區分不同對象的信息。
在OO程序設計中,每個對象都有唯一的標識,標識是一個對象的屬性,用于區分這個對象與其它所有對象。而這個唯一的標識可以通過句柄機制提供,因此可以借助這個句柄引用對象。不同的語言實現句柄的方式不一樣,比如,地址、數組下標或人為編號。
在現實世界中一些對象有對等物,比如,ZLG公司,另一些對象則是概念實體,比如,解一元一次方程,還有一些其它的對象,比如,棧、數組變量名a,都是為了實現而引入的,沒有對應的物理實體。
許多開發人員可能會認為“一個包含了另一個對象的對象”,其本質上與“一個具有純數據成員的對象”是完全不同的,但是那些看起來不是對象的數據成員實際上也是對象,比如,整數和雙精度數。在真正的面向對象語言中,萬事萬物都是對象,甚至內置數據也是對象,即使其行為只是運算。
由此可見,雖然對象是具有明確定義的邊界的東西,但還不足以區分不同的對象。因為同一個類的每個對象具有不同的句柄,在任何特定時刻,每個對象可能有不同的狀態(指存儲在變量中的不同的“值”),因此對象是一個具有狀態、行為和標識的實體。
對象又分持久對象和主動對象,持久對象是指生存期可以超越程序的執行時間,而長期存在的且所有的操作都是被動執行的對象。在主動對象概念出現之前,人們所理解的對象概念只是被動對象,即對象的每個操作都是被動地響應從外部發來的消息才可以執行。
在開發一個具有多任務并發執行的系統時,如果僅有被動對象的概念,則很難描述系統中的多個任務。其實并發不僅僅存在操作系統中,如今多個任務并發可以說無處不在。每個任務在實現時應該成為一個可以并發執行的主動程序單位,那么如何描述呢?
如果用被動對象將無法描述那些不接收任何消息也要主動工作的對象,比如,交通燈控制系統中的信號燈,溫控器中的傳感器,它們的行為都是主動發起的,即主動對象至少有一個操作不需要接收消息就就能主動執行的對象。
盡管發現對象的活動是從具體事物出發分析和認識問題的,但人們在進行這種活動時實際上并不局限于對個別事物的認識,而是尋找一類事物的共同特征,將對象抽象為類。
>>>1.1.2 類
類的概念早在柏拉圖之前就出現了,面向對象編程就像柏拉圖之后的西方哲學家一樣延續了這種思維。類的概念與對象的概念是緊密交織在一起,因為在討論一個對象時不得不提到它的類,但是這兩個術語又存在重要的差別。對象是存在于時間和空間中的具體實體,而類僅代表一種抽象,因此可以說Validator類代表了所有校驗器的共同特征。要確定這個類中的某個具體的校驗器,則必須說“范圍值校驗器”或“奇偶校驗器”。
在面向對象分析與設計的上下文中,將類定義為——類是對現實世界中事物的描述,類描述了擁有相同屬性、行為和關系類別的一組對象,一個對象就是類的一個實例,因此沒有共同的屬性和行為的對象不能劃分為一個類。比如,一個相當高層的抽象,一個GUI框架,一個數據庫和整個系統在概念上都是獨立的對象,因此不能將它們表示為一個單獨的類。相反應該將這些抽象表示為一組類,通過這些類的實例互相協作,提供我們期望的功能。
通常將這樣的一組類稱為一個組件,而組件是預先創建好的程序模塊,可與其它模塊一起構成一個程序。通常組件以二進制形式發布,其實現對使用者來說是隱藏的。如果組件設計良好,使用者甚至不需要知道這個組件使用什么語言編寫的。但組件必須至少暴露一個接口才能使用,通常組件會有暴露多個接口。從使用者的角度來看,一個組件是一些前端接口的后端服務者,程序員通過組件接口所暴露的函數操作該組件。由此可見,組件擴展了面向對象中對象作為服務提供者通過高層接口提供服務的概念。
在現實世界里,餅干也是對象,必須先有模子(類),才能做出你想要的形狀的餅干,因此可以認為類是對象的模板。比如,只有符合一定條件的數值才能push到棧中,那么Validator校驗器類就是由RangeValidator范圍值校驗器類、OddEvenValidator奇偶校驗器類和PrimerValidator質數校驗器類等具體校驗器類的對象構成的一個集合體。屬于類的任何對象都共享該類的所有屬性,比如,所有的具體校驗器都有這樣的屬性——校驗參數。
在OO程序設計中,一個類就是一種抽象數據類型,用戶也可以創建一個自己的類,而且可以將這個類當做數據類型使用。一旦有了類,就可以象使用普通的數據類型那樣用類定義變量,如果定義了RangeValidator類,即可用它定義變量rangeValidator。RangeValidator類的變量rangeValidator可以擁有成員變量或域,代表不同校驗器的屬性或特性,通常將這些成員變量稱為數據成員。
(1)值和屬性
值是一段數據,屬性描述了類的每個對象都擁有的一個值,可以這樣類比——對象之于類如同值之于屬性。比如,name、birthdate和weight都是Person對象的屬性,color、modelYear和weight都是Car對象的屬性。對于每個對象,每個屬性都有一個值,比如,對象ZhangSan的屬性birthdate的值是“21 October 1983”,也就是說,ZhangSan生于1983年10月21日。對于一個特定的屬性,不同的對象可能會有相同或不同的取值。在一個類中,雖然每個屬性的名字都是唯一的,但在所有的類中不一定是唯一的,比如,類Person和類Car都可能有一個名為weight的屬性。
下面將介紹一種通過屬性詳細描述類的UML建模語言,一種用于可視化表示、指定、構造和描述軟件密集系統中部件的圖形化語言,它提供了一種以圖形化方式表示和管理面向對象軟件系統的方法。其不僅是系統設計的表示,而且是一種有助于完成系統設計的工具。類圖定義了3個不同的部分,即類名、屬性和方法,用于解釋所構建的類。當用UML創建對象模型時,盡可能不要在類圖中包含太多的信息,這樣就能集中注意力于整體設計,而不會將重點放在細節上。
如圖 4.1所示展示了類建模表示法,顯示了一個類(左圖)和它所描述的對象(右圖),對象ZhangSan和LiMing都是類Person的實例。對象的UML表示法是一個方框,方框里面是對象名后加冒號和類名,對象名和類名都有下劃線,并約定用黑體字表示對象名和類名。類的UML表示法也是一個方框,也約定用黑體字表示類名,將名字放在方框的正中央,首字符大寫,且用單數名詞表示類名。類Person有屬性name和birthdate,name是string(字符串),birthdate是date(日期)。類Person中一個對象的名字取值是"Zhang San",生日取值是“21 October 1983”;另一個對象的名字取值是"Li Ming",生日取值是“16 March 1950”。
圖 4.1 屬性和值的UML表示法
UML表示法會在框的第二格里列舉屬性,每個屬性后面都可以有可選項,比如,類型和默認值。在類之前有一個冒號,在默認值之前有一個等號。約定以常規字體顯示屬性名,方框中的名稱左對齊,首字母使用小寫。在對象方框的第二格里,也可能會包含屬性值,其表示法是列出每個屬性名,之后跟著等號和取值,同樣屬性值也是左對齊,使用常規字體。雖然有些實現要求對象有唯一的標識符,但這些標識符在類模型中是隱含的,即不需要也不應該顯式地將它們列舉出來,比如,PersonID:ID。因為大多數OO開發語言會自動生成標識符,可以使用這些標識符來引用對象;反之,則可能需要顯式地列舉出來,否則無法引用對象。但是不要將內部標識符和現實世界的屬性混淆了,內部標識符純粹是一種便于實現的做法,沒有應用意義。相反,納稅人編號、汽車牌照號碼和電話號碼都不是內部標識符,因為它們在實現世界有真實的意義,屬于合法的屬性。
(2)操作與方法
操作是一個函數或過程,比如,open和close都是Windows類的操作,類中所有的對象都共享相同的操作,因此將對象能夠做什么的行為稱為操作,通常將相同的操作應用于許多不同的類稱為多態。
方法是對操作的實現,其表現為OOP某個類的成員函數。比如,類Validator有一個操作validate,其校驗過程是通過validate調用不同的函數實現的。比如,范圍值校驗和奇偶校驗。雖然這些方法在邏輯上都執行相同的任務——數據校驗,但是每種方法的實現代碼會有所不同。
如圖 4.2所示RangeValidator類有min和max屬性,以及validate操作,min、max和validate都是RangeValidator的特征。特征是描述屬性或操作的類屬詞匯,類似地OddEvenValidator有isEven屬性和validate操作。
圖 4.2 操作的UML表示
注意,validate()省略了括號中的輸入參數,即“validate(pThis:void *, value:int):bool”。validate的一個參數是pThis,其類型是void *;它的另一個參數value,其類型是int。當一項操作在幾個類上都有方法時,這些方法都要有相同的簽名,即相同的參數數量和類型,以及返回值的類型。
UML的方框表示類,最多有三格,從上到下每個格里分別包含了類名、屬性列表和操作列表。類方框中的屬性和操作的框格可以選擇顯示或隱藏,缺少屬性說明沒有指定屬性,缺少操作框說明沒有指定操作。相反,空框格意味著屬性是指定的,只是沒有顯示屬性而已。
操作列表約定用常規字體列出操作名,左對齊,首字母小寫。比如,參數列表和操作結果的類型,用括號將參數列表括起來,并用逗號分隔參數。結果類型之前有一個冒號,除非括號中空的參數列表明確表示沒有參數,否則就不能下結論。
(3)客戶和服務器模式
在OOP中,如果一個類公開了一些方法供其它類調用,那么這個類被稱為服務器,公開的這些方法被稱為服務,而調用這些服務的類就是客戶。理論上客戶類調用服務器類的服務,即客戶向服務器發送了一條消息。而客戶和服務器的概念是相對而言的,當A類向B類提供了功能接口時,則類A是服務器,B類是客戶;如果類B也同時為類A提供了功能接口,則類B是服務器,類A是客戶。
設計良好的服務器應該將其實現細節隱藏起來,客戶僅需知道服務器提供的接口即可。接口就是客戶所能調用的那些函數,這些函數將消息發給服務器,那么服務器就知道客戶需要什么樣的服務,服務器會返回一些數據給客戶,或執行客戶所需的任務等。
(4)消息傳遞和方法調用
在OOP中,類和對象表現為服務器,使用類和對象的模塊表現為客戶。客戶通過特殊的方式請求服務。那么到底如何讓對象為我們做有用的事情呢?必須有一種方法能向對象做出請求,使得它能做某件事情,比如,完成交易、在屏幕上畫圖或打開開關。可以向對象發出的請求是由它的接口定義的,而接口是由類型定義的。
雖然接口規定了我們能向特定的對象發出什么請求,但必須有代碼滿足這種請求,再加上隱藏的數據就組成了實現。類型對每個可能的請求都有一個相關的函數,當向對象發出請求時,就調用這個函數。這個過程被概括為向對象“發送消息”(提出請求),對象根據這個消息確定做什么(執行代碼)。
對象之間的邏輯接口通過消息傳遞實現,消息是對對象之間通信的的抽象。常見的消息傳遞方法是直接調用定義于接收方對象中的操作,比如,當對象A調用對象B的一個方法時,對象A就是在向對象B發送一個消息,對象B的響應由其返回值定義,但只有對象的公共方法才能由另一個對象調用。
使用消息傳遞可以實現松耦合,特別在分析階段,不用指定接口的細節,比如,同步、函數調用格式和超時等。當全面理解了所有的問題后,接下來就可以決定設計和實現的細節了。通常對象接口可以看成對象與外部世界之間制定的契約,契約是由一組協議定義的,對象參與到這些協議中。接口協議包括前置條件、后置條件和不變量。
前置條件是當該操作被調用時,必須成立的條件。即在調用之前應該校驗傳入參數是否正確,只有正確才能執行該方法。也就是說,必須在消息發送或接收之前保證為真的條件,這是消息發送者的職責。一旦通過前置條件的校驗方法必須執行,且必須保證執行結果符合契約,這就是后置條件。也就是說,后置條件是當該操作完成時,必須成立的條件。即在處理消息時必須保證為真,這是消息接收者的職責。不變量是指在任何時刻都必須成立的條件,包括操作執行前、執行時和執行后。
(5)屬性抽象與行為抽象
OO程序設計思想可以采用抽象的方法,對現實世界中的多個具體對象進行概括分析,得到這類對象所具有的共同屬性和行為,加以描述就形成了類。雖然都是同一個類的對象,但每個對象的屬性不同,于是就形成了不同的具體對象實體。
抽象一般分為屬性抽象和行為抽象兩種,屬性抽象是尋找一類對象共有的屬性,比如,在范圍值校驗器RangeValidator類中,使用整型變量min和max來描述push到棧中的數值范圍,然后將min和max變量作為類的成員變量描述對象的屬性,即“屬性是包含在對象中的變量”。而行為抽象則是尋找這類對象所具有的共同行為特征,比如,對push到棧中的值進行范圍值校驗,同樣,也可以為這個類添加相應的函數,最終將該函數作為類的成員函數描述對象的行為,即“方法是包含在對象中的函數”。
在面向過程的編程中,程序是由模塊組成的,一個模塊就是一個過程,通常采用自頂而下的設計方法。而面向對象的編程與設計著眼于解決面向過程的編程和自頂而下設計中出現的一些問題,由于在面向對象的編程中構成模塊的基本單元是類,而不是過程,因此面向對象設計是面向對象編程的設計方法,它著重于類的設計,通過類的設計完成對實體的建模任務,類建模的目的是描述對象。
在面向過程的編程中,描述一個物體時,數據和方法是分開的。比如,當通過網絡發送信息時,則只會發送相關的數據,并認為網絡另一端的程序知道如何進行處理。也就是說,如果兩者之間沒有握手協議,則網絡另一端的程序不知道如何處理。而對象可以定義為“同時包含”數據和行為的一個實例,即通過封裝機制將數據和行為捆綁在一起,形成一個完整的、具有屬性和行為的對象。比如,當通過網絡傳送對象時,則傳送的是整個對象。因此使用OO技術的程序實際上就是多個對象的集合,這里的“同時包含”正是OO程序設計與面向過程程序設計方法的重要區別。
由此可見,以后在分析新的對象時,都要從屬性和行為兩個方面進行抽象和概括,提取對象的共同特征,而整個抽象過程是一個從具體到一般的過程。如果說抽象是將很多對象的共有特征提取出來成為類的成員屬性和成員函數,那么封裝機制則是將這些特征進行有機地結合形成一個完整的類。
>>>1.1.3 封裝
類和對象既是獨立的概念,又密切相關。每個對象都是某個類的一個實例,每個類都有0或多個實例。對于所有的應用來說,類幾乎都是靜態的。這就意味著,對象一旦被創建,它的類就確定了。
雖然最具挑戰的是如何確定類和對象,但只要正確使用面向對象分析(Ogject Oriented Analysis,OOA)和面向對象設計(Object Oriented Design,OOD)就能得到具有價值的領域模型和設計模型。OOA、OOD與OOP到底是什么關系?OOA的結果可以作為OOD開始的模型,OOD的結果可以作為藍圖,利用OOP方法實現一個系統。
在OOA和OOD中,不需要考慮特定的語言機制,“關鍵是尋找并解決業務問題,完成概念分析和設計。在OOA和OOD的早期,開發者的主要任務有兩項:
● 從需求的詞匯表中確定類;
● 創建一些結構,讓多組對象一起工作,提供滿足需求的行為。
通常我們將這樣的類和對象統稱為問題域的關鍵抽象,即關鍵抽象反映了問題域的詞匯表,可以從問題域中發現,也可以作為設計的一部分發明;將這些協作結構稱為實現的機制,其考慮的是許多不同類型的對象之間的協作活動。
確定關鍵抽象包括兩個過程:發現和發明,通過與領域專家(用戶)交流,將會發現領域專家所使用的抽象。如果領域專家提及它,那么這個抽象通常是很重要的,比如,范圍值校驗器RangeValidator。而發明就是創造新的類和對象的過程,雖然它們不一定是問題域的組成部分,但在設計或實現中也是很重要的。比如,微型數據庫、鏈表、棧、隊列等。這些關鍵抽象是具體設計的結果,不屬于問題域。因此在設計過程中,開發者不僅需要考慮單個類的設計,還要考慮這些類的實例如何一起工作,并使用場景驅動分析過程。由此可見,關鍵抽象反映了業務領域的抽象,機制是設計的靈魂。
假設希望對push到棧中的值,既可以進行范圍值校驗,也可以進行偶校驗。從面向對象的角度來看,首先要從問題的描述中發現對象,當找到對象后,接著開始通過共性和差異性分析這些對象所具有的屬性和行為,然后利用面向對象的封裝機制將其封裝成類。
根據問題的描述,范圍值校驗器就是一個RangeValidator具體類,其屬性是范圍值校驗參數min和max,其行為就是將符合范圍要求的數值push到棧中。因此只要將RangeValidator的屬性和行為作為成員封裝到結構體中,就形成了RangeValidator類,這是面向過程編程的C程序員最容易想到,也最容易理解的方法。
為了支持這種風格,C允許將方法作為某個結構體的一部分來聲明,那么操作存儲在結構體中的數據就很容易了,詳見程序清單 2.18。
程序清單4.1范圍值校驗器類接口
1 typedef struct _RangeValidator{
2 bool (*const RangeValidate)(struct _RangeValidator *pThis, int value);
3 const int min;
4 const int max;
5 }RangeValidator;
6
7 RangeValidator rangeValidator;
其中,類名字的首字母為大寫,對象名字的首字母為小寫。由此可見,通過擴展已有結構體的概念創造了一個全新的概念——類,類如同種類一樣,定義一個類就是在創造一個新的數據類型。雖然聲明一個類的變量如同聲明一個結構體的變量一樣,但聲明一個類的變量被稱為對象,因此有了類即可聲明一個RangeValidator類的對象rangeValidator。通常也稱rangeValidator對象是RangeValidator類的一個實例,就是創建類的一個實例的過程。
在進行范圍值校驗時,首先需要判斷value值是否符合要求?validateRange()函數接口的實現詳見程序清單 2.19。
程序清單 4.2 范圍值校驗器接口函數的實現
1 bool validateRange(RangeValidator *pThis, int value)
2 {
3 return pThis -> min <= value && value <= pThis -> max;
4 }
偶校驗器OddEvenValidator具體類和對象oddEvenValidator的定義詳見程序清單 2.20。
程序清單 4.3 偶校驗器類接口
1 typedef struct _OddEvenValidator{
2 bool (*const OddEvenValidate)(struct _OddEvenValidator *pThis, int value);
3 bool isEven;
4 }OddEvenValidator;
5
6 OddEvenValidator oddEvenValidator;
在進行偶校驗時,同樣需要判斷value值是否符合要求?validateOddEven()函數接口的實現詳見程序清單 2.21。
程序清單 4.4 偶校驗器接口函數的實現
1 bool validateOddEven(OddEvenValidator *pThis, int value)
2 {
3 return (!pThis -> isEven && (value % 2)) || (pThis -> isEven && !(value % 2));
4 }
顯然,無論是什么校驗器,其共性是value值合法性判斷,因此可以共用一個函數指針,即特殊的函數指針類型RangeValidate和OddEvenValidate被泛化成了一般的函數指針類型Validate。其次,由于每個函數都有一個指向當前對象的pThis指針,因此特殊的結構體類型RangeValidator *和OddEvenValidator *被泛化成了void *類型,即可接受任何類型的數據:
1 typedef bool(*const Validate)(void *pThis, int value);
2 typedef struct{
3 Validate validate;
4 const int min;
5 const int max;
6 }RangeValidator;
7
8 typedef struct{
9 Validate validate;
10 bool isEven;
11 }OddEvenValidator;
校驗器泛化接口的實現詳見程序清單 2.22。
程序清單 4.5 通用校驗器接口的實現(validator.c)
1 #include "validator.h"
2
3 bool validateRange(void *pThis, int value)
4 {
5 RangeValidator *pRangeValidator = (RangeValidator *)pThis;
6 return pRangeValidator -> min <= value && value <= pRangeValidator -> max;
7 }
8
9 bool validateOddEven(void *pThis, int value)
10 {
11 OddEvenValidator *pOddEvenValidator = (OddEvenValidator *)pThis;
12 return (!pOddEvenValidator -> isEven && (value % 2)) ||
13 (pOddEvenValidator -> isEven && !(value % 2));
14 }
為了便于閱讀,程序清單 4.6展示了范圍值校驗器和奇偶校驗器的接口。
程序清單 4.6 通用校驗器接口(validator.h)
1 #pragma once;
2 #include
3
4 typedef bool(*const Validate)(void *pThis, int value);
5 typedef struct{
6 Validate validate;
7 const int min;
8 const int max;
9 }RangeValidator;
10
11 typedef struct{
12 Validate validate;
13 bool isEven;
14 }OddEvenValidator;
15
16 bool validateRange(void *pThis, int value);
17 bool validateRange(void *pThis, int value);
18 #define newRangeValidator(min, max) {(validateRange), (min), (max)} // 初始化RangeValidator
19 #define newOddEvenValidator(isEven) {{validateOddEven}, (isEven)} // 初始化OddEvenValidator
這個接口主要由所有的操作聲明構成,這些操作適用于這個類的所有對象,詳見圖 4.3。
圖 4.3 類圖
以范圍值校驗器為例,假設min=0,max=9,使用名為newRangeValidator的宏將結構體初始化的使用方法如下:
RangeValidator rangeValidator = newRangeValidator(0, 9);
注意,RangeValidator類是在編譯時定義的,而rangeValidator對象是在運行時作為類的實例創建的。宏展開后如下:
RangeValidator rangeValidator = {{validateRange}, (0), (9)};
其相當于:
rangeValidator.validate = validateRange;
rangeValidator.min = 0;
rangeValidator.max = 9;
如果有以下定義:
void * pValidator = &rangeValidator;
即可通過pValidator引用RangeValidator的min和max。校驗函數的調用方式如下:
(RangeValidator *)pValidator -> validate(pValidator, 8);
以上調用形式的前提是已知pValidator指向了確定的結構體類型,如果pValidator將指向未知的校驗器,顯然以上調用形式無法做到通用,那么如何調用?
雖然pValidator與&rangeValidator.validate的類型不一樣,但它們的值相等,因此可以利用這一特性獲取validateRange()函數的地址。即:
Validate validate = *((Validate *)pValidator);
其調用形式如下:
validate(pValidator, 8);
根據OCP開閉原則,由于不允許修改push()函數,因此需要編寫一個通用的擴展push功能的pushWithValidate()函數,詳見程序清單 4.7。
程序清單 4.7 pushWithValidate()
1 bool pushWithValidate(stackADT stack, void *pValidator, int value)
2 {
3 Validate validate = *((Validate*)pValidator);
4 if (pValidator && !validate(pValidator, value)){
5 return false;
6 }
7 return push(stack, value);
8 }
其中,stack是指向當前對象(棧)的指針,用于請求對象對自身執行某些操作,而結構體的成員變量就是通過stack指針找到自己所屬的對象的。pValidator為指向校驗器的指針,如果無需校驗,則將pValidator置NULL并返回true。
使用validator.h接口的通用校驗器范例程序詳見程序清單 4.8。
程序清單 4.8 通用校驗器使用范例程序
1 #include
2 #include"Stack.h"
3 #include"validator.h"
4 // 添加pushWithValidate()函數
5 int main(int argc, int *argv[])
6 {
7 stackADT stack;
8 int temp;
9
10 stack = newStack();
11 RangeValidator rangeValidator = newRangeValidator(0, 9);
12 for (int i = 0; i < 16; i ++){
13 pushWithValidate(stack, &rangeValidator, i);
14 }
15 while (!stackIsEmpty(stack)) {
16 pop(stack, &temp);
17 printf("%d ", temp);
18 }
19 printf("\n");
20 OddEvenValidator oddEvenValidator = newOddEvenValidator(true);
21 for (int i = 0; i<16; i++){
22 pushWithValidate(stack, &oddEvenValidator, i);
23 }
24 while (!stackIsEmpty(stack)) {
25 pop(stack, &temp);
26 printf("%d ", temp);
27 }
28 freeStack(stack);
29 return 0;
30 }
由此可見,雖然在結構體內置函數指針也可以創建類,但其中的每個類都是一個獨立的單元,每個都要從頭開始。且不同類之間沒有任何關系,因為每個類的開發者都根據自己的選擇提供方法。
-
周立功
+關注
關注
38文章
130瀏覽量
37642
原文標題:周立功:類是具有共同的行為和特征的所有對象的集合
文章出處:【微信號:Zlgmcu7890,微信公眾號:周立功單片機】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論