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

