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