周立功(gōng)教授數年之(zhi)心血之作《程(chéng)序設計與數(shu)據結構》,電子(zǐ)版已無償性(xing)分享到電子(zǐ)工程師與高(gao)校群體。書本(běn)内容公⛷️開後(hòu),在電子行業(yè)掀起一片學(xue)習熱潮。經周(zhōu)立功教授授(shou)權,特對本書(shū)内容進行連(lián)載,願共勉之(zhī)。
第一章爲程(cheng)序設計基礎(chu),本文爲1.5.2/1.5.3共性(xìng)與可變性分(fèn)析:建👄立抽象(xiàng)📱和建立接口(kǒu)。
>>>> 1.5.2 建立抽象
抽(chou)象化的目的(de)是使調用者(zhě)無需知道模(mo)塊的内部細(xi)節,隻🌈需🆚要🔆知(zhi)道模塊或函(hán)數的名字,因(yin)此将其稱爲(wei)黑❄️盒化。調用(yòng)者隻需要知(zhi)道黑盒子的(de)輸入和輸出(chu),而過程的細(xì)節是隐藏的(de)。由🏃🏻♂️于建立了(le)一個由黑盒(he)子組成✌️的系(xì)統,因此複雜(zá)的結構就被(bèi)黑盒子隐藏(cang)起來了,則理(li)解系統的✂️整(zhěng)體結構就變(bian)得更容易了(le)。
從概念的視(shi)角來看,建立(li)抽象關注的(de)不是如何實(shi)現,而是函數(shù)要做什麽,過(guo)早地關注實(shi)現細節,将實(shí)🈚現細🔱節隐藏(cang)起來,進而幫(bang)🍓助我們構建(jiàn)更易于修改(gai)的軟件。因此(ci),我們首先📱應(yīng)該選擇一個(ge)具有描述性(xing)的符合需😄求(qiú)的名字,雖然(rán)可以選擇的(de)名字有swapByte、swapWord和swap,但(dan)swap更簡潔更貼(tie)切。其次,可以(yi)用一句話概(gai)念性地描述(shu)🐪swap的數據抽象(xiang)——swap是實現兩個(ge)數據交換的(de)函數。
顯然,調(diào)用者僅需一(yi)般性地在概(gai)念層次上與(yu)實現者交流(liu),因爲調用者(zhě)的意圖是如(ru)何使用swap()實現(xian)兩個⭐數據的(de)交換,所以無(wú)🎯需準确地知(zhi)道實現的細(xì)節。而🐅具體如(ru)何完成數據(ju)的交換,這是(shì)在實現層次(ci)進行的。由此(cǐ)可見,将模塊(kuài)的目的與🏃🏻實(shí)現分離的抽(chou)象揭🐆示了問(wèn)題的本質,并(bìng)沒有提供解(jiě)決方案。隻說(shuo)明需要做什(shí)麽,并不會指(zhǐ)出如何實現(xian)某個模塊。隻(zhī)🛀🏻要概念不變(bian),調用者與實(shí)現細節的變(bian)化就徹底隔(gé)離了。當某個(gè)模塊完成編(bian)碼後,隻要說(shuō)明該🚩模塊的(de)目的和參數(shu)就可以使用(yòng)它🔴,無需知道(dao)具體的實現(xian)。
函數抽象對(dui)團隊項目非(fēi)常重要,因爲(wei)在團隊中必(bi)須使✨用其他(tā)成員編寫的(de)模塊。比如,編(bian)程語言本身(shēn)自帶的庫函(han)數,由于已經(jīng)被預編譯,因(yīn)此無法訪問(wen)🈲它的源代碼(mǎ)。同時庫函數(shu)不一定是用(yong)🔆C編寫的,因此(ci)隻要知道其(qí)調用規範,就(jiu)可以在程序(xu)中毫無顧忌(jì)地使用這個(gè)函數。實際上(shang),在使用scanf()函數(shù)的過程中,我(wo)🏒們考慮過scanf()是(shì)如何實現的(de)嗎?無關緊要(yao)。盡管不同系(xì)統實現scanf()的☁️方(fāng)法可能不一(yī)樣,但其中的(de)不同對于程(chéng)🌈序員來說是(shi)透明的。
>>>> 1.5.3 建立(lì)接口
接口是(shì)由公開訪問(wèn)的方法和數(shù)據組成的,接(jie)口描述了與(yu)模塊交互的(de)唯一途徑。最(zui)小化的接口(kou)隻包含對于(yu)接口的任務(wu)非常重要的(de)參數,最小化(hua)的接口便于(yú)學習如何與(yǔ)之交互,且隻(zhī)需要理解少(shǎo)量的參數,同(tong)時易于擴展(zhan)和維護,因🐪此(ci)設計良好的(de)接口是一項(xiàng)重要的技能(neng)。
>>> 1. 函數調用
(1)傳(chuan)值調用
如何(hé)調用swap()函數呢(ne)?實參将值從(cong)主調函數傳(chuán)遞給被調函(hán)數,也許其調(diào)用形式是下(xià)面這樣的:
swap(a, b);
從(cong)黑盒視角來(lái)看,形參和其(qi)它局部變量(liàng)都是函數私(sī)有的💋,聲明在(zài)不同函數中(zhong)的同名變量(liang)是完全不同(tóng)的變量,而且(qie)函數無法直(zhi)接訪問其它(ta)函數中的變(biàn)量,這種限制(zhi)❗訪問保護了(le)數據的完整(zheng)性,黑盒發生(sheng)了什麽對主(zhu)調函數是不(bú)可見的。
一個(gè)變量的有效(xiào)範圍稱作它(ta)的作用域,變(bian)量的作用域(yu)指可以通過(guo)變量名稱引(yin)用變量的區(qū)域,在函數内(nei)部聲明的變(biàn)量隻在該函(han)數内部有效(xiao)。當主調函數(shù)調用子函🎯數(shù)時,主函數内(nèi)聲明的變量(liang)在子函數内(nèi)無效,子函數(shù)内聲明的變(bian)量也隻在該(gai)子函數内部(bu)有效。
由于傳(chuan)遞給函數的(de)是變量的替(tì)身,因此改變(bian)函數參數對(duì)原始變量沒(mei)有影響。當變(biàn)量傳遞給函(han)數時,變量的(de)值被複制給(gei)函數參數。由(you)此可見,通過(guo)“傳值調用”方(fāng)式交換a、b的值(zhi),無法改變主(zhǔ)調函數相應(yīng)變量的值。
(2)傳(chuan)址調用
如果(guo)希望通過被(bèi)調函數将更(gèng)多的值傳回(huí)主調函數而(ér)改變主❄️調函(hán)數中的變量(liàng),則使用“傳址(zhi)調用”——将&a、&b作爲(wèi)實參傳遞🈲給(gěi)形參。其調用(yong)形式如下:
swap(&a, &b);
利(lì)用指針作爲(wèi)函數參數傳(chuán)遞數據的本(běn)質,就是在💞主(zhu)調函數✊和🌈被(bei)調函數中,通(tōng)過不同的指(zhǐ)針指向同一(yi)内存地👣址訪(fang)問相同的内(nèi)存區域,即它(ta)們背後共享(xiǎng)相同的内存(cun),從而實現數(shù)據的傳遞和(he)交換。
>>> 2. 函數原(yuán)型
函數原型(xing)是C語言的一(yi)個強有力的(de)工具,它讓編(biān)譯💋器📞捕獲在(zài)📧使用函數時(shí)可能出現的(de)許多錯誤或(huò)疏漏。如果編(biān)譯器沒有發(fā)現這些問題(tí),就很難察覺(jiao)出來。函數原(yuan)型包括函數(shu)返回值的類(lei)型、函數名和(hé)形參列表(參(can)數的數量和(hé)每個參數的(de)類型),有了這(zhe)些信息,編譯(yi)器‼️就可以檢(jian)查函數調用(yòng)與函數原型(xíng)是🎯否匹配?比(bǐ)如,參數的數(shù)量是否正确(què)?參數的類型(xing)是否匹配?如(rú)果類型不匹(pǐ)配💘,編譯器會(huì)将實參的類(lèi)型轉換成形(xing)參的類型。
(1)函(hán)數形參
通過(guò)程序清單 1.15可(kě)以看出,其相(xiàng)同的處理部(bù)分是2個int類值(zhi)的交換代碼(ma),因此可以将(jiāng)數據交換代(dài)碼移到swap()函數(shu)的實現中,其(qi)可變的數據(jù)由外部傳進(jin)來的參數應(yīng)對❄️。由于&a是指(zhǐ)向int類型變量(liàng)a的指針,&b是指(zhi)向int類型變量(liang)b的指針,因此(cǐ)必須将p1、p2形參(can)聲明爲指向(xiang)int *類型的指針(zhen)變量,即必須(xū)将存儲int類型(xíng)值變量的地(dì)址作爲🌈實參(cān)賦給指針形(xíng)參,實參與形(xing)參才能匹配(pei)。其函數原型(xíng)進化如下:
swap(int *p1, int *p2);
(2)返(fan)回值的類型(xíng)
聲明函數時(shi)必須聲明函(hán)數的類型,帶(dai)返回值的函(hán)數類♋型應該(gai)與其返回值(zhi)類型相同,而(er)沒有返回值(zhí)的函數應✉️該(gai)聲👅明爲void。類型(xíng)聲明是函數(shù)定義的一部(bu)分,函數類型(xíng)指的是返回(hui)值的類型,不(bú)是函數參數(shù)的類型。
雖然(rán)可以使用return返(fan)回值,但return隻能(neng)返回一個值(zhí)給主調函數(shù)。比🔴如,如果返(fan)回值爲整數(shu),則函數返回(hui)值的類型爲(wèi)int。當返回值爲(wèi)int類型時,如果(guo)返回值爲負(fù)數,則表示失(shī)敗;如果返回(hui)值爲非負數(shu),則表示成功(gong)。當返回🧡值爲(wei)bool類型時,如果(guǒ)返回值爲false,則(zé)表示失敗,如(rú)果返回🐇值爲(wèi)true,則表示成功(gong)。當返回值爲(wei)指針類型時(shí),如果返回值(zhi)爲NULL,則表示失(shi)敗,否則返回(huí)一個有效的(de)指🥵針。
如果利(li)用指針作爲(wèi)參數傳遞給(gei)函數,不僅可(ke)以向函🔞數傳(chuán)入數據,而且(qiě)還可以從函(han)數返回多個(ge)值。因爲函數(shù)的調用者和(hé)函數都可以(yǐ)使用指向同(tóng)一内存地址(zhǐ)❗的指針,即使(shǐ)用同一塊内(nèi)存,所以使用(yong)指針作爲函(han)數參數時就(jiù)是對同一數(shu)據進行讀寫(xie)操作。這樣不(bú)僅可以傳入(ru)數據,還可以(yǐ)通過💰在函數(shù)内部修改這(zhe)些數據,将函(han)數的結果傳(chuan)出給調用者(zhě)。
當函數的實(shi)參是指針變(biàn)量時,有時希(xi)望函數能通(tong)過指針👈指向(xiang)别處的方式(shì)改變此變量(liang),則需要使用(yòng)指向指針的(de)指針作🌈爲形(xíng)🙇🏻參。
由于swap()無返(fan)回值,因此swap()返(fan)回值的類型(xíng)爲void,其函數原(yuán)型🔴如下:
void swap(int *p1, int *p2);
其被(bei)解釋爲swap是返(fan)回void的函數(參(can)數是int *p1,int *p2)。
這是一(yi)個不斷叠代(dai)優化的過程(chéng),用戶隻需要(yao)知道“函數名(ming)、傳入🌈函數的(de)參數和函數(shu)返回值的類(lèi)型”,就♉知道如(ru)何有效地調(diao)用相📧應的函(han)數。
>>> 3. 依賴倒置(zhi)原則
在面向(xiang)過程編程中(zhōng),通常的做法(fǎ)是高層模塊(kuài)調用低層模(mo)塊💋,其目的之(zhī)一就是要定(dìng)義子程序層(ceng)次結構。當🏃♂️高(gao)層模塊依賴(lai)于低🤞層模塊(kuai)時,對低層模(mo)塊的改動會(huì)直接影響高(gāo)層模塊,從而(er)迫使它們依(yi)次做出修改(gǎi)。如果高層模(mo)🌍塊獨立于低(dī)層模塊,則高(gāo)層模塊更容(rong)易重♌用,這就(jiù)是分層架構(gou)設計的核心(xin)原則,即依賴(lai)倒置原則(Dependence Inversion Principle,DIP):
● 高(gao)層模塊不應(ying)該依賴低層(céng)模塊,兩者都(dōu)應該依賴于(yu)抽象接⛱️口;
● 抽(chōu)象接口不應(ying)該依賴于細(xi)節,細節應該(gāi)依賴抽象接(jie)口。
當在分層(ceng)架構中使用(yòng)依賴倒置原(yuan)則時,将會發(fa)現“不⛹🏻♀️再存在(zài)分層”的概念(nian)了。無論是高(gao)層還是低層(ceng),它😍們都依賴(lai)于抽象接口(kǒu),好像将整個(gè)分層架構推(tuī)平一樣。
其實(shi)從“Hello World”程序開始(shǐ),我們就已經(jing)在使用stdio.h包含(han)的“抽象接口(kou)”了,即以後凡(fán)是用#include文件的(de)擴展名叫.h(頭(tóu)文件)。如果源(yuan)代碼⛹🏻♀️中要用(yòng)到stdio标準輸入(ru)輸出函數時(shi),那麽就要包(bāo)含這個頭文(wén)件,比如,“scanf("%d",&i);”函數(shu)⚽,其目的是告(gao)訴編譯器要(yào)使用stdio庫。庫是(shì)一種工具的(de)集合,這些工(gong)具是由其它(ta)程序員編寫(xiě)的,用于實現(xiàn)特定的🐉功能(neng)。盡管實現者(zhe)無需關♌心用(yòng)戶将如何使(shǐ)用庫,且不會(hui)直接開放源(yuan)代碼給用戶(hù)使用,但必須(xū)給用戶提供(gòng)調用函🧑🏽🤝🧑🏻數所(suo)需要的信息(xī)。顯然隻要将(jiāng)頭文件㊙️開放(fang)給用戶,即可(ke)讓用戶了解(jie)接口的所有(you)細節,詳見程(cheng)序清單🌍 1.16。
程序(xù)清單 1.16 swap數據交(jiao)換接口(swap.h)
1 #ifndef _SWAP_H
2 #define _SWAP_H
3 // 前置(zhì)條件:實參必(bì)須是int類型變(bian)量的地址
4 // 後(hòu)置條件:p1、p2作爲(wei)輸出參數,改(gǎi)變主調函數(shu)中相應的變(bian)量
5 void swap(int *p1, int *p2);
6 // 調用形式(shì):swap(&a, &b)
7 #endif
其中,每個頭(tóu)文件都指出(chū)了一個用戶(hù)可見的外部(bu)函數接口,主(zhǔ)要包括函數(shu)名、所需的參(cān)數、參數的類(lèi)型和返回結(jié)果的類型。其(qí)中,swap是庫的名(míng)字,程序清單(dān) 1.16(1~2)與(8)是幫助編(bian)譯器記錄它(tā)所讀取的接(jie)口,當寫一個(gè)👌接口時,必須(xu)包含#ifndef、#define和#ednif。#include行部(bù)分僅當接口(kou)本身需要其(qí)它庫時才使(shǐ)用,它由标準(zhǔn)的#include行組成。程(cheng)序清單 1.16(6)接口(kou)項表示庫輸(shu)出☀️的函數的(de)原型、常量和(he)類型等。不管(guan)你是否理解(jie),這些行是接(jiē)口的模闆文(wén)件,這就是信(xìn)息隐藏。
>>> 4. 前/後(hou)置條件
處理(lǐ)信息隐藏還(hai)涉及到另一(yī)個技術,那就(jiu)是使用前置(zhì)條件和❄️後置(zhì)條件描述函(hán)數的行爲。在(zài)編寫一個完(wán)整的🤩函數🤞定(ding)義時,需要描(miáo)述該函數是(shì)如何執行計(ji)算的。但在使(shǐ)用函數時,隻(zhī)👅需考慮該函(han)數能做什麽(me),無需🈲知道是(shì)如🚶何完成的(de)。當不知道💞函(han)數是如👈何實(shi)現時,就是在(zài)使用一種名(ming)爲過程抽象(xiang)的信息隐藏(cang)形式,它抽象(xiang)掉的是函數(shu)如何工作的(de)細節。計算機(jī)科學家使用(yòng)“過程”表示任(ren)意指令集,因(yin)此使用術📧語(yu)過程抽象。過(guo)程抽象是一(yī)種強大的工(gong)具,使得我們(men)一次隻考慮(lü)一個而不是(shì)🏃♂️所有的函數(shu),從而使✉️問題(ti)求解簡單化(hua)。
爲了使描述(shu)更準确,則需(xū)要遵循固定(ding)的格式,它包(bao)🚶♀️含兩部分♌信(xin)息:函數的前(qián)置條件和後(hòu)置條件。前置(zhi)條件就是調(diao)用該函數必(bì)須成立的條(tiáo)件,當函數被(bèi)調用時,該語(yǔ)句給出要求(qiu)爲真的條件(jian)。除非前置條(tiao)件爲真,否則(zé)無法保⁉️證函(han)數能正确執(zhi)行☀️。在調用swap()函(han)數時,實參必(bi)須是int類型變(bian)量的地址,這(zhe)是調用者的(de)職責。通常在(zài)函數開始處(chù)檢查📧是否滿(mǎn)足?如果不滿(man)足,說明調用(yòng)代碼有問題(ti),抛出一個異(yì)常。
後置條件(jiàn)就是該操作(zuò)完成後必須(xū)成立的條件(jian),當函🌈數調用(yòng)❗時♈,如果函數(shu)是正确的,而(er)且前置條件(jiàn)爲真,那麽該(gai)函數調用将(jiāng)可以執行完(wán)成。當函數調(diào)用完成後,後(hòu)置條🏃🏻件爲真(zhen)。如果不滿足(zu)後置條件,則(zé)說♌明業務邏(luó)輯有問題。
當(dang)滿足調用swap()函(hán)數的前置條(tiáo)件時,必須同(tong)時确保其結(jié)束時滿足它(tā)的後置條件(jian),其後置條件(jian)是被調函數(shu)将返回☔值傳(chuan)回主♈調函數(shù),改變主調函(hán)數中變量的(de)值。
前後置條(tiáo)件不隻是概(gai)括地描述函(han)數的行爲,聲(shēng)明這些🌈條件(jian)應該是設計(jì)任何函數的(de)第一步。在開(kāi)始考慮🍓某個(gè)函數的算🈲法(fa)和代碼之前(qián),應該寫出該(gāi)函數的原型(xing),其中包括函(hán)數的返回類(lèi)型、名稱和參(can)數列表,最後(hou)緊跟一個分(fen)号。直接來自(zi)于用戶的輸(shu)入不能作爲(wei)前置條件,通(tōng)常前/後置條(tiáo)件都可以轉(zhuǎn)化爲assert語句。編(bian)寫函數原型(xing)時,應該以注(zhu)釋的形式描(miáo)述該函數的(de)前置條件和(he)後置條件。
事(shì)實上,前置條(tiao)件和後置條(tiáo)件在使用函(han)數的程序員(yuán)和編寫函數(shù)的程序員之(zhī)間形成了一(yi)個契約,也就(jiù)是爲什麽需(xu)要這個函數(shù)?接口通過前(qian)置條件和後(hòu)置條👈件以契(qì)約的形式表(biǎo)達需求,承🧑🏽🤝🧑🏻諾(nuò)在滿足前📐置(zhi)條件時開始(shǐ),按照💰程序的(de)流程運行,系(xì)統就能到達(da)後置條件。
雖(suī)然注釋是一(yī)種很好的溝(gou)通形式,但在(zài)代碼可以傳(chuan)📱遞🔆意圖📱的地(di)方不要寫注(zhù)釋。因爲代碼(mǎ)解釋做了什(shi)麽,再注釋也(yě)沒有什麽用(yong)處,相反注釋(shi)要說明爲什(shi)麽會這樣寫(xie)代碼?
>>> 5. 開閉原(yuán)則
接口僅需(xu)指明用戶調(diao)用程序可能(néng)調用的标識(shí)符♈,應盡可🌂能(neng)地将算法以(yi)及一些與具(jù)體的實現細(xì)節無關的信(xìn)🈲息隐藏起來(lái),這樣用戶在(zai)調用程序時(shi)也👅就不必依(yi)賴特定的實(shí)現細節了。當(dāng)接口一旦發(fa)布後,也就不(bu)能改變了♌,因(yin)爲改變接口(kǒu)勢必引起用(yòng)戶程序的改(gai)變♉。如果此前(qian)定義的接口(kǒu)滿足不了需(xū)求,怎麽辦?隻(zhi)能擴🧑🏽🤝🧑🏻展新的(de)接口,但不能(néng)修改或廢除(chu)原有的接口(kǒu)🧡,這就是“對修(xiu)改關閉,對擴(kuo)♻️展開放”的開(kai)閉😄原則(Open-Closed Princple,OCP)。顯然(ran),依賴倒置原(yuan)則更加精确(que)的定義就是(shì)面🐪向接口的(de)編程,它是實(shi)現開閉原則(ze)的重要途♍徑(jing)。如果DIP依賴倒(dǎo)置原則🔞沒有(yǒu)實現,就别想(xiang)實現對擴展(zhǎn)開放,對修改(gǎi)關閉。