- 總部(bu):福州市八(ba)一七中路(lu)茶亭國際(ji)
- 電話:0591-83275886
- E-mail:[email protected]
- http://qigi.cc

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