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

