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

聯系(xì)我們 丨 站點地(di)圖 丨 友情鏈接(jie) 丨 工作機會
聯(lian)系地址:福州市(shì)八一七中路茶(chá)亭國際 郵編:350004 電(dian)話:0591- 83275886
Copyright © 2011-2012 All Right Reserved 京ICP證000000号
