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