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

